aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Utils.ts61
-rw-r--r--src/client/apis/GoogleAuthenticationManager.scss19
-rw-r--r--src/client/apis/GoogleAuthenticationManager.tsx140
-rw-r--r--src/client/apis/google_docs/GooglePhotosClientUtils.ts38
-rw-r--r--src/client/apis/youtube/YoutubeBox.tsx2
-rw-r--r--src/client/documents/DocumentTypes.ts7
-rw-r--r--src/client/documents/Documents.ts178
-rw-r--r--src/client/northstar/dash-nodes/HistogramBox.tsx2
-rw-r--r--src/client/util/DictationManager.ts27
-rw-r--r--src/client/util/DocumentManager.ts185
-rw-r--r--src/client/util/DragManager.ts44
-rw-r--r--src/client/util/DropConverter.ts46
-rw-r--r--src/client/util/Import & Export/DirectoryImportBox.tsx14
-rw-r--r--src/client/util/Import & Export/ImageUtils.ts31
-rw-r--r--src/client/util/LinkManager.ts19
-rw-r--r--src/client/util/ProsemirrorExampleTransfer.ts4
-rw-r--r--src/client/util/RichTextRules.ts78
-rw-r--r--src/client/util/RichTextSchema.tsx236
-rw-r--r--src/client/util/SearchUtil.ts35
-rw-r--r--src/client/util/SelectionManager.ts13
-rw-r--r--src/client/util/SharingManager.scss4
-rw-r--r--src/client/util/SharingManager.tsx139
-rw-r--r--src/client/util/TooltipTextMenu.tsx85
-rw-r--r--src/client/util/UndoManager.ts4
-rw-r--r--src/client/views/CollectionLinearView.scss73
-rw-r--r--src/client/views/CollectionLinearView.tsx110
-rw-r--r--src/client/views/DictationOverlay.tsx71
-rw-r--r--src/client/views/DocComponent.tsx84
-rw-r--r--src/client/views/DocumentButtonBar.tsx66
-rw-r--r--src/client/views/DocumentDecorations.tsx123
-rw-r--r--src/client/views/EditableView.tsx6
-rw-r--r--src/client/views/GlobalKeyHandler.ts38
-rw-r--r--src/client/views/InkingCanvas.scss3
-rw-r--r--src/client/views/InkingCanvas.tsx16
-rw-r--r--src/client/views/InkingControl.tsx89
-rw-r--r--src/client/views/InkingStroke.tsx18
-rw-r--r--src/client/views/Main.scss201
-rw-r--r--src/client/views/Main.tsx5
-rw-r--r--src/client/views/MainOverlayTextBox.scss29
-rw-r--r--src/client/views/MainOverlayTextBox.tsx155
-rw-r--r--src/client/views/MainView.scss96
-rw-r--r--src/client/views/MainView.tsx636
-rw-r--r--src/client/views/MainViewModal.tsx2
-rw-r--r--src/client/views/MainViewNotifs.scss18
-rw-r--r--src/client/views/MainViewNotifs.tsx32
-rw-r--r--src/client/views/MetadataEntryMenu.tsx2
-rw-r--r--src/client/views/OverlayView.tsx13
-rw-r--r--src/client/views/PreviewCursor.tsx18
-rw-r--r--src/client/views/TemplateMenu.tsx9
-rw-r--r--src/client/views/collections/CollectionBaseView.tsx196
-rw-r--r--src/client/views/collections/CollectionDockingView.scss3
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx46
-rw-r--r--src/client/views/collections/CollectionMasonryViewFieldRow.tsx367
-rw-r--r--src/client/views/collections/CollectionPDFView.scss11
-rw-r--r--src/client/views/collections/CollectionPDFView.tsx37
-rw-r--r--src/client/views/collections/CollectionSchemaCells.tsx9
-rw-r--r--src/client/views/collections/CollectionSchemaView.scss1
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx28
-rw-r--r--src/client/views/collections/CollectionStackingView.scss112
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx199
-rw-r--r--src/client/views/collections/CollectionStackingViewFieldColumn.tsx164
-rw-r--r--src/client/views/collections/CollectionSubView.tsx66
-rw-r--r--src/client/views/collections/CollectionTreeView.scss4
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx173
-rw-r--r--src/client/views/collections/CollectionVideoView.scss51
-rw-r--r--src/client/views/collections/CollectionVideoView.tsx115
-rw-r--r--src/client/views/collections/CollectionView.scss (renamed from src/client/views/collections/CollectionBaseView.scss)2
-rw-r--r--src/client/views/collections/CollectionView.tsx209
-rw-r--r--src/client/views/collections/CollectionViewChromes.tsx80
-rw-r--r--src/client/views/collections/ParentDocumentSelector.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx3
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss4
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx84
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss5
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx69
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss6
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx219
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.scss7
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx76
-rw-r--r--src/client/views/linking/LinkFollowBox.tsx91
-rw-r--r--src/client/views/nodes/Annotation.tsx117
-rw-r--r--src/client/views/nodes/AudioBox.scss136
-rw-r--r--src/client/views/nodes/AudioBox.tsx266
-rw-r--r--src/client/views/nodes/ButtonBox.scss2
-rw-r--r--src/client/views/nodes/ButtonBox.tsx30
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.scss2
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx46
-rw-r--r--src/client/views/nodes/ColorBox.scss22
-rw-r--r--src/client/views/nodes/ColorBox.tsx45
-rw-r--r--src/client/views/nodes/DocuLinkBox.scss8
-rw-r--r--src/client/views/nodes/DocuLinkBox.tsx81
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx53
-rw-r--r--src/client/views/nodes/DocumentView.scss5
-rw-r--r--src/client/views/nodes/DocumentView.tsx286
-rw-r--r--src/client/views/nodes/DragBox.tsx99
-rw-r--r--src/client/views/nodes/FieldView.tsx15
-rw-r--r--src/client/views/nodes/FontIconBox.scss (renamed from src/client/views/nodes/DragBox.scss)2
-rw-r--r--src/client/views/nodes/FontIconBox.tsx46
-rw-r--r--src/client/views/nodes/FormattedTextBox.scss44
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx690
-rw-r--r--src/client/views/nodes/FormattedTextBoxComment.tsx10
-rw-r--r--src/client/views/nodes/IconBox.tsx6
-rw-r--r--src/client/views/nodes/ImageBox.scss20
-rw-r--r--src/client/views/nodes/ImageBox.tsx292
-rw-r--r--src/client/views/nodes/KeyValueBox.scss1
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx27
-rw-r--r--src/client/views/nodes/KeyValuePair.tsx8
-rw-r--r--src/client/views/nodes/PDFBox.scss4
-rw-r--r--src/client/views/nodes/PDFBox.tsx93
-rw-r--r--src/client/views/nodes/PresBox.tsx37
-rw-r--r--src/client/views/nodes/QueryBox.scss0
-rw-r--r--src/client/views/nodes/QueryBox.tsx35
-rw-r--r--src/client/views/nodes/VideoBox.scss58
-rw-r--r--src/client/views/nodes/VideoBox.tsx200
-rw-r--r--src/client/views/nodes/WebBox.tsx82
-rw-r--r--src/client/views/pdf/Annotation.scss3
-rw-r--r--src/client/views/pdf/Annotation.tsx36
-rw-r--r--src/client/views/pdf/PDFMenu.scss2
-rw-r--r--src/client/views/pdf/PDFMenu.tsx10
-rw-r--r--src/client/views/pdf/PDFViewer.scss27
-rw-r--r--src/client/views/pdf/PDFViewer.tsx400
-rw-r--r--src/client/views/presentationview/PresElementBox.tsx163
-rw-r--r--src/client/views/search/FilterBox.tsx4
-rw-r--r--src/client/views/search/IconBar.tsx16
-rw-r--r--src/client/views/search/IconButton.scss2
-rw-r--r--src/client/views/search/SearchBox.scss22
-rw-r--r--src/client/views/search/SearchBox.tsx17
-rw-r--r--src/client/views/search/SearchItem.scss50
-rw-r--r--src/client/views/search/SearchItem.tsx42
-rw-r--r--src/client/views/webcam/DashWebCam.tsx5
-rw-r--r--src/client/views/webcam/DashWebRTCVideo.tsx3
-rw-r--r--src/extensions/ArrayExtensions.ts34
-rw-r--r--src/new_fields/Doc.ts194
-rw-r--r--src/new_fields/InkField.ts6
-rw-r--r--src/new_fields/SchemaHeaderField.ts3
-rw-r--r--src/new_fields/URLField.ts2
-rw-r--r--src/new_fields/documentSchemas.ts59
-rw-r--r--src/server/DashUploadUtils.ts39
-rw-r--r--src/server/RouteStore.ts5
-rw-r--r--src/server/RouteSubscriber.ts26
-rw-r--r--src/server/apis/google/GoogleApiServerUtils.ts122
-rw-r--r--src/server/apis/google/GooglePhotosUploadUtils.ts4
-rw-r--r--src/server/authentication/config/passport.ts10
-rw-r--r--src/server/authentication/models/current_user_utils.ts259
-rw-r--r--src/server/database.ts12
-rw-r--r--src/server/index.ts401
146 files changed, 6039 insertions, 4570 deletions
diff --git a/src/Utils.ts b/src/Utils.ts
index b481b3b47..2dd4eace4 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -62,14 +62,14 @@ export namespace Utils {
}
export function fromRGBAstr(rgba: string) {
- let rm = rgba.match(/rgb[a]?\(([0-9]+)/);
+ let rm = rgba.match(/rgb[a]?\(([ 0-9]+)/);
let r = rm ? Number(rm[1]) : 0;
- let gm = rgba.match(/rgb[a]?\([0-9]+,([0-9]+)/);
+ let gm = rgba.match(/rgb[a]?\([ 0-9]+,([ 0-9]+)/);
let g = gm ? Number(gm[1]) : 0;
- let bm = rgba.match(/rgb[a]?\([0-9]+,[0-9]+,([0-9]+)/);
+ let bm = rgba.match(/rgb[a]?\([ 0-9]+,[ 0-9]+,([ 0-9]+)/);
let b = bm ? Number(bm[1]) : 0;
- let am = rgba.match(/rgba?\([0-9]+,[0-9]+,[0-9]+,([0-9]+)/);
- let a = am ? Number(am[1]) : 0;
+ let am = rgba.match(/rgba?\([ 0-9]+,[ 0-9]+,[ 0-9]+,([ .0-9]+)/);
+ let a = am ? Number(am[1]) : 1;
return { r: r, g: g, b: b, a: a };
}
@@ -149,6 +149,29 @@ export namespace Utils {
}
+ export function clamp(n: number, lower: number, upper: number) {
+ return Math.max(lower, Math.min(upper, n));
+ }
+
+ export function getNearestPointInPerimeter(l: number, t: number, w: number, h: number, x: number, y: number) {
+ var r = l + w,
+ b = t + h;
+
+ var x = clamp(x, l, r),
+ y = clamp(y, t, b);
+
+ var dl = Math.abs(x - l),
+ dr = Math.abs(x - r),
+ dt = Math.abs(y - t),
+ db = Math.abs(y - b);
+
+ var m = Math.min(dl, dr, dt, db);
+
+ return (m === dt) ? [x, t] :
+ (m === db) ? [x, b] :
+ (m === dl) ? [l, y] : [r, y];
+ }
+
export function GetClipboardText(): string {
var textArea = document.createElement("textarea");
document.body.appendChild(textArea);
@@ -272,6 +295,8 @@ export function percent2frac(percent: string) {
export function numberRange(num: number) { return Array.from(Array(num)).map((v, i) => i); }
+export function returnTransparent() { return "transparent"; }
+
export function returnTrue() { return true; }
export function returnFalse() { return false; }
@@ -326,7 +351,7 @@ const easeInOutQuad = (currentTime: number, start: number, change: number, durat
return (-change / 2) * (newCurrentTime * (newCurrentTime - 2) - 1) + start;
};
-export default function smoothScroll(duration: number, element: HTMLElement, to: number) {
+export function smoothScroll(duration: number, element: HTMLElement, to: number) {
const start = element.scrollTop;
const change = to - start;
const startDate = new Date().getTime();
@@ -343,4 +368,28 @@ export default function smoothScroll(duration: number, element: HTMLElement, to:
}
};
animateScroll();
+}
+export function addStyleSheet(styleType: string = "text/css") {
+ let style = document.createElement("style");
+ style.type = styleType;
+ var sheets = document.head.appendChild(style);
+ return (sheets as any).sheet;
+}
+export function addStyleSheetRule(sheet: any, selector: any, css: any) {
+ var propText = typeof css === "string" ? css : Object.keys(css).map(p => p + ":" + (p === "content" ? "'" + css[p] + "'" : css[p])).join(";");
+ return sheet.insertRule("." + selector + "{" + propText + "}", sheet.cssRules.length);
+}
+export function removeStyleSheetRule(sheet: any, rule: number) {
+ if (sheet.rules.length) {
+ sheet.removeRule(rule);
+ return true;
+ }
+ return false;
+}
+export function clearStyleSheetRules(sheet: any) {
+ if (sheet.rules.length) {
+ numberRange(sheet.rules.length).map(n => sheet.removeRule(0));
+ return true;
+ }
+ return false;
} \ No newline at end of file
diff --git a/src/client/apis/GoogleAuthenticationManager.scss b/src/client/apis/GoogleAuthenticationManager.scss
new file mode 100644
index 000000000..13bde822d
--- /dev/null
+++ b/src/client/apis/GoogleAuthenticationManager.scss
@@ -0,0 +1,19 @@
+.authorize-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ .paste-target {
+ padding: 5px;
+ width: 100%;
+ }
+
+ .avatar {
+ border-radius: 50%;
+ }
+
+ .welcome {
+ font-style: italic;
+ margin-top: 15px;
+ }
+} \ No newline at end of file
diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx
new file mode 100644
index 000000000..01dac3996
--- /dev/null
+++ b/src/client/apis/GoogleAuthenticationManager.tsx
@@ -0,0 +1,140 @@
+import { observable, action, reaction, runInAction } from "mobx";
+import { observer } from "mobx-react";
+import * as React from "react";
+import MainViewModal from "../views/MainViewModal";
+import { Opt } from "../../new_fields/Doc";
+import { Identified } from "../Network";
+import { RouteStore } from "../../server/RouteStore";
+import "./GoogleAuthenticationManager.scss";
+
+const AuthenticationUrl = "https://accounts.google.com/o/oauth2/v2/auth";
+const prompt = "Paste authorization code here...";
+
+@observer
+export default class GoogleAuthenticationManager extends React.Component<{}> {
+ public static Instance: GoogleAuthenticationManager;
+ @observable private openState = false;
+ private authenticationLink: Opt<string> = undefined;
+ @observable private authenticationCode: Opt<string> = undefined;
+ @observable private clickedState = false;
+ @observable private success: Opt<boolean> = undefined;
+ @observable private displayLauncher = true;
+ @observable private avatar: Opt<string> = undefined;
+ @observable private username: Opt<string> = undefined;
+
+ private set isOpen(value: boolean) {
+ runInAction(() => this.openState = value);
+ }
+
+ private set hasBeenClicked(value: boolean) {
+ runInAction(() => this.clickedState = value);
+ }
+
+ public fetchOrGenerateAccessToken = async () => {
+ let response = await Identified.FetchFromServer(RouteStore.readGoogleAccessToken);
+ // if this is an authentication url, activate the UI to register the new access token
+ if (new RegExp(AuthenticationUrl).test(response)) {
+ this.isOpen = true;
+ this.authenticationLink = response;
+ return new Promise<string>(async resolve => {
+ const disposer = reaction(
+ () => this.authenticationCode,
+ authenticationCode => {
+ if (authenticationCode) {
+ Identified.PostToServer(RouteStore.writeGoogleAccessToken, { authenticationCode }).then(
+ ({ access_token, avatar, name }) => {
+ runInAction(() => {
+ this.avatar = avatar;
+ this.username = name;
+ });
+ this.beginFadeout();
+ disposer();
+ resolve(access_token);
+ },
+ action(() => {
+ this.hasBeenClicked = false;
+ this.success = false;
+ })
+ );
+ }
+ }
+ );
+ });
+ }
+ // otherwise, we already have a valid, stored access token
+ return response;
+ }
+
+ beginFadeout = action(() => {
+ this.success = true;
+ this.authenticationCode = undefined;
+ this.displayLauncher = false;
+ this.hasBeenClicked = false;
+ setTimeout(action(() => {
+ this.isOpen = false;
+ setTimeout(action(() => {
+ this.success = undefined;
+ this.displayLauncher = true;
+ this.avatar = undefined;
+ this.username = undefined;
+ }), 500);
+ }), 3000);
+ });
+
+ constructor(props: {}) {
+ super(props);
+ GoogleAuthenticationManager.Instance = this;
+ }
+
+ private handleClick = () => {
+ window.open(this.authenticationLink);
+ setTimeout(() => this.hasBeenClicked = true, 500);
+ }
+
+ private handlePaste = action((e: React.ChangeEvent<HTMLInputElement>) => {
+ this.authenticationCode = e.currentTarget.value;
+ });
+
+ private get renderPrompt() {
+ return (
+ <div className={'authorize-container'}>
+ {this.displayLauncher ? <button
+ className={"dispatch"}
+ onClick={this.handleClick}
+ style={{ marginBottom: this.clickedState ? 15 : 0 }}
+ >Authorize a Google account...</button> : (null)}
+ {this.clickedState ? <input
+ className={'paste-target'}
+ onChange={this.handlePaste}
+ placeholder={prompt}
+ /> : (null)}
+ {this.avatar ? <img
+ className={'avatar'}
+ src={this.avatar}
+ /> : (null)}
+ {this.username ? <span
+ className={'welcome'}
+ >Welcome to Dash, {this.username}
+ </span> : (null)}
+ </div>
+ );
+ }
+
+ private get dialogueBoxStyle() {
+ const borderColor = this.success === undefined ? "black" : this.success ? "green" : "red";
+ return { borderColor, transition: "0.2s borderColor ease" };
+ }
+
+ render() {
+ return (
+ <MainViewModal
+ isDisplayed={this.openState}
+ interactive={true}
+ contents={this.renderPrompt}
+ overlayDisplayedOpacity={0.9}
+ dialogueBoxStyle={this.dialogueBoxStyle}
+ />
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index 29cc042b6..e93fa6eb4 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -13,15 +13,12 @@ import { Docs, DocumentOptions } from "../../documents/Documents";
import { NewMediaItemResult, MediaItem } from "../../../server/apis/google/SharedTypes";
import { AssertionError } from "assert";
import { DocumentView } from "../../views/nodes/DocumentView";
-import { DocumentManager } from "../../util/DocumentManager";
import { Identified } from "../../Network";
+import GoogleAuthenticationManager from "../GoogleAuthenticationManager";
export namespace GooglePhotos {
- const endpoint = async () => {
- const accessToken = await Identified.FetchFromServer(RouteStore.googlePhotosAccessToken);
- return new Photos(accessToken);
- };
+ const endpoint = async () => new Photos(await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken());
export enum MediaType {
ALL_MEDIA = 'ALL_MEDIA',
@@ -89,9 +86,14 @@ export namespace GooglePhotos {
}
const resolved = title ? title : (StrCast(collection.title) || `Dash Collection (${collection[Id]}`);
const { id, productUrl } = await Create.Album(resolved);
- const newMediaItemResults = await Transactions.UploadImages(images, { id }, descriptionKey);
- if (newMediaItemResults) {
- const mediaItems = newMediaItemResults.map(item => item.mediaItem);
+ const response = await Transactions.UploadImages(images, { id }, descriptionKey);
+ if (response) {
+ const { results, failed } = response;
+ let index: Opt<number>;
+ while ((index = failed.pop()) !== undefined) {
+ Doc.RemoveDocFromList(dataDocument, "data", images.splice(index, 1)[0]);
+ }
+ const mediaItems: MediaItem[] = results.map(item => item.mediaItem);
if (mediaItems.length !== images.length) {
throw new AssertionError({ actual: mediaItems.length, expected: images.length });
}
@@ -99,6 +101,9 @@ export namespace GooglePhotos {
for (let i = 0; i < images.length; i++) {
const image = Doc.GetProto(images[i]);
const mediaItem = mediaItems[i];
+ if (!mediaItem) {
+ continue;
+ }
image.googlePhotosId = mediaItem.id;
image.googlePhotosAlbumUrl = productUrl;
image.googlePhotosUrl = mediaItem.productUrl || mediaItem.baseUrl;
@@ -304,17 +309,22 @@ export namespace GooglePhotos {
};
export const UploadThenFetch = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption") => {
- const newMediaItems = await UploadImages(sources, album, descriptionKey);
- if (!newMediaItems) {
+ const response = await UploadImages(sources, album, descriptionKey);
+ if (!response) {
return undefined;
}
- const baseUrls: string[] = await Promise.all(newMediaItems.map(item => {
+ const baseUrls: string[] = await Promise.all(response.results.map(item => {
return new Promise<string>(resolve => Query.GetImage(item.mediaItem.id).then(item => resolve(item.baseUrl)));
}));
return baseUrls;
};
- export const UploadImages = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption"): Promise<Opt<NewMediaItemResult[]>> => {
+ export interface ImageUploadResults {
+ results: NewMediaItemResult[];
+ failed: number[];
+ }
+
+ export const UploadImages = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption"): Promise<Opt<ImageUploadResults>> => {
if (album && "title" in album) {
album = await Create.Album(album.title);
}
@@ -331,8 +341,8 @@ export namespace GooglePhotos {
media.push({ url, description });
}
if (media.length) {
- const uploads: NewMediaItemResult[] = await Identified.PostToServer(RouteStore.googlePhotosMediaUpload, { media, album });
- return uploads;
+ const results = await Identified.PostToServer(RouteStore.googlePhotosMediaUpload, { media, album });
+ return results;
}
};
diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx
index d73988bb8..bed812852 100644
--- a/src/client/apis/youtube/YoutubeBox.tsx
+++ b/src/client/apis/youtube/YoutubeBox.tsx
@@ -40,7 +40,7 @@ export class YoutubeBox extends React.Component<FieldViewProps> {
@observable curVideoTemplates: VideoTemplate[] = [];
- public static LayoutString() { return FieldView.LayoutString(YoutubeBox); }
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(YoutubeBox, fieldKey); }
/**
* When component mounts, last search's results are laoded in based on the back up stored
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index be290862a..957836a7a 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -19,7 +19,12 @@ export enum DocumentType {
YOUTUBE = "youtube",
WEBCAM = "webcam",
DRAGBOX = "dragbox",
+ FONTICON = "fonticonbox",
PRES = "presentation",
LINKFOLLOW = "linkfollow",
- PRESELEMENT = "preselement"
+ PRESELEMENT = "preselement",
+ QUERY = "search",
+ COLOR = "color",
+ DOCULINK = "doculink",
+ PDFANNO = "pdfanno"
} \ No newline at end of file
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 3df1e0e9a..8ba24133a 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1,9 +1,8 @@
import { HistogramField } from "../northstar/dash-fields/HistogramField";
import { HistogramBox } from "../northstar/dash-nodes/HistogramBox";
import { HistogramOperation } from "../northstar/operations/HistogramOperation";
-import { CollectionVideoView } from "../views/collections/CollectionVideoView";
import { CollectionView } from "../views/collections/CollectionView";
-import { CollectionViewType } from "../views/collections/CollectionBaseView";
+import { CollectionViewType } from "../views/collections/CollectionView";
import { AudioBox } from "../views/nodes/AudioBox";
import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
import { ImageBox } from "../views/nodes/ImageBox";
@@ -38,16 +37,19 @@ import { DocumentManager } from "../util/DocumentManager";
import DirectoryImportBox from "../util/Import & Export/DirectoryImportBox";
import { Scripting, CompileScript } from "../util/Scripting";
import { ButtonBox } from "../views/nodes/ButtonBox";
-import { DragBox } from "../views/nodes/DragBox";
+import { FontIconBox } from "../views/nodes/FontIconBox";
import { SchemaHeaderField, RandomPastel } from "../../new_fields/SchemaHeaderField";
import { PresBox } from "../views/nodes/PresBox";
-import { ComputedField } from "../../new_fields/ScriptField";
+import { ComputedField, ScriptField } from "../../new_fields/ScriptField";
import { ProxyField } from "../../new_fields/Proxy";
import { DocumentType } from "./DocumentTypes";
import { LinkFollowBox } from "../views/linking/LinkFollowBox";
import { DashWebCam } from "../views/webcam/DashWebCam";
import { PresElementBox } from "../views/presentationview/PresElementBox";
import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo";
+import { QueryBox } from "../views/nodes/QueryBox";
+import { ColorBox } from "../views/nodes/ColorBox";
+import { DocuLinkBox } from "../views/nodes/DocuLinkBox";
var requestImageSize = require('../util/request-image-size');
var path = require('path');
@@ -65,23 +67,48 @@ export interface DocumentOptions {
panY?: number;
page?: number;
scale?: number;
+ fitWidth?: boolean;
+ forceActive?: boolean;
+ preventTreeViewOpen?: boolean; // ignores the treeViewOpen Doc flag which allows a treeViewItem's expande/collapse state to be independent of other views of the same document in the tree view
layout?: string | Doc;
- isTemplate?: boolean;
+ hideHeadings?: boolean; // whether stacking view column headings should be hidden
+ isTemplateField?: boolean;
+ isTemplateDoc?: boolean;
templates?: List<string>;
viewType?: number;
backgroundColor?: string;
+ ignoreClick?: boolean;
+ lockedPosition?: boolean;
opacity?: number;
defaultBackgroundColor?: string;
dropAction?: dropActionType;
- backgroundLayout?: string;
chromeStatus?: string;
+ columnWidth?: number;
+ fontSize?: number;
curPage?: number;
+ currentTimecode?: number; // the current timecode of a time-based document (e.g., current time of a video) value is in seconds
+ displayTimecode?: number; // the time that a document should be displayed (e.g., time an annotation should be displayed on a video)
documentText?: string;
borderRounding?: string;
+ boxShadow?: string;
+ sectionFilter?: string; // field key used to determine headings for sections in stacking and masonry views
schemaColumns?: List<SchemaHeaderField>;
dockingConfig?: string;
autoHeight?: boolean;
+ removeDropProperties?: List<string>; // list of properties that should be removed from a document when it is dropped. e.g., a creator button may be forceActive to allow it be dragged, but the forceActive property can be removed from the dropped document
dbDoc?: Doc;
+ unchecked?: ScriptField; // returns whether a check box is unchecked
+ activePen?: Doc; // which pen document is currently active (used as the radio button state for the 'unhecked' pen tool scripts)
+ onClick?: ScriptField;
+ dragFactory?: Doc; // document to create when dragging with a suitable onDragStart script
+ onDragStart?: ScriptField; //script to execute at start of drag operation -- e.g., when a "creator" button is dragged this script generates a different document to drop
+ icon?: string;
+ gridGap?: number; // gap between items in masonry view
+ xMargin?: number; // gap between left edge of document and start of masonry/stacking layouts
+ yMargin?: number; // gap between top edge of dcoument and start of masonry/stacking layouts
+ sourcePanel?: Doc; // panel to display in 'targetContainer' as the result of a button onClick script
+ targetContainer?: Doc; // document whose proto will be set to 'panel' as the result of a onClick click script
+ dropConverter?: ScriptField; // script to run when documents are dropped on this Document.
// [key: string]: Opt<Field>;
}
@@ -95,93 +122,97 @@ export namespace Docs {
export namespace Prototypes {
- type LayoutSource = { LayoutString: (ext?: string) => string };
- type CollectionLayoutSource = { LayoutString: (fieldStr: string, fieldExt?: string) => string };
- type CollectionViewType = [CollectionLayoutSource, string, string?];
+ type LayoutSource = { LayoutString: (key: string) => string };
type PrototypeTemplate = {
layout: {
view: LayoutSource,
- ext?: string, // optional extension field for layout source
- collectionView?: CollectionViewType
+ dataField: string
},
options?: Partial<DocumentOptions>
};
type TemplateMap = Map<DocumentType, PrototypeTemplate>;
type PrototypeMap = Map<DocumentType, Doc>;
const data = "data";
- const anno = "annotations";
const TemplateMap: TemplateMap = new Map([
[DocumentType.TEXT, {
- layout: { view: FormattedTextBox },
+ layout: { view: FormattedTextBox, dataField: data },
options: { height: 150, backgroundColor: "#f1efeb", defaultBackgroundColor: "#f1efeb" }
}],
[DocumentType.HIST, {
- layout: { view: HistogramBox, collectionView: [CollectionView, data] as CollectionViewType },
+ layout: { view: HistogramBox, dataField: data },
options: { height: 300, backgroundColor: "black" }
}],
+ [DocumentType.QUERY, {
+ layout: { view: QueryBox, dataField: data },
+ options: { width: 400 }
+ }],
+ [DocumentType.COLOR, {
+ layout: { view: ColorBox, dataField: data },
+ options: { nativeWidth: 220, nativeHeight: 300 }
+ }],
[DocumentType.IMG, {
- layout: { view: ImageBox, collectionView: [CollectionView, data, anno] as CollectionViewType },
- options: { curPage: 0 }
+ layout: { view: ImageBox, dataField: data },
+ options: {}
}],
[DocumentType.WEB, {
- layout: { view: WebBox, collectionView: [CollectionView, data, anno] as CollectionViewType },
+ layout: { view: WebBox, dataField: data },
options: { height: 300 }
}],
[DocumentType.COL, {
- layout: { view: CollectionView },
+ layout: { view: CollectionView, dataField: data },
options: { panX: 0, panY: 0, scale: 1, width: 500, height: 500 }
}],
[DocumentType.KVP, {
- layout: { view: KeyValueBox },
+ layout: { view: KeyValueBox, dataField: data },
options: { height: 150 }
}],
[DocumentType.VID, {
- layout: { view: VideoBox, collectionView: [CollectionVideoView, data, anno] as CollectionViewType },
- options: { curPage: 0 },
+ layout: { view: VideoBox, dataField: data },
+ options: { currentTimecode: 0 },
}],
[DocumentType.AUDIO, {
- layout: { view: AudioBox },
- options: { height: 32 }
+ layout: { view: AudioBox, dataField: data },
+ options: { height: 35, backgroundColor: "lightGray" }
}],
[DocumentType.PDF, {
- layout: { view: PDFBox, ext: anno },
+ layout: { view: PDFBox, dataField: data },
options: { nativeWidth: 1200, curPage: 1 }
}],
[DocumentType.ICON, {
- layout: { view: IconBox },
+ layout: { view: IconBox, dataField: data },
options: { width: Number(MINIMIZED_ICON_SIZE), height: Number(MINIMIZED_ICON_SIZE) },
}],
[DocumentType.IMPORT, {
- layout: { view: DirectoryImportBox },
+ layout: { view: DirectoryImportBox, dataField: data },
options: { height: 150 }
}],
[DocumentType.LINKDOC, {
data: new List<Doc>(),
- layout: { view: EmptyBox },
+ layout: { view: EmptyBox, dataField: data },
}],
[DocumentType.YOUTUBE, {
- layout: { view: YoutubeBox }
+ layout: { view: YoutubeBox, dataField: data }
}],
[DocumentType.BUTTON, {
- layout: { view: ButtonBox },
+ layout: { view: ButtonBox, dataField: data },
}],
[DocumentType.PRES, {
- layout: { view: PresBox },
+ layout: { view: PresBox, dataField: data },
options: {}
}],
- [DocumentType.DRAGBOX, {
- layout: { view: DragBox },
- options: { width: 40, height: 40 },
+ [DocumentType.FONTICON, {
+ layout: { view: FontIconBox, dataField: data },
+ options: { width: 40, height: 40, borderRounding: "100%" },
}],
[DocumentType.LINKFOLLOW, {
- layout: { view: LinkFollowBox }
+ layout: { view: LinkFollowBox, dataField: data }
}],
[DocumentType.WEBCAM, {
- layout: { view: DashWebRTCVideo }
+ layout: { view: DashWebRTCVideo, dataField: data }
}],
[DocumentType.PRESELEMENT, {
- layout: { view: PresElementBox }
+ layout: { view: PresElementBox, dataField: data }
}],
]);
@@ -259,16 +290,9 @@ export namespace Docs {
let title = prototypeId.toUpperCase().replace(upper, `_${upper}`);
// synthesize the default options, the type and title from computed values and
// whatever options pertain to this specific prototype
- let options = { title: title, type: type, baseProto: true, ...defaultOptions, ...(template.options || {}) };
- let primary = layout.view.LayoutString(layout.ext);
- let collectionView = layout.collectionView;
- if (collectionView) {
- options.layout = collectionView[0].LayoutString(collectionView[1], collectionView[2]);
- options.backgroundLayout = primary;
- } else {
- options.layout = primary;
- }
- return Doc.assign(new Doc(prototypeId, true), { ...options, baseLayout: primary });
+ let options = { title, type, baseProto: true, ...defaultOptions, ...(template.options || {}) };
+ options.layout = layout.view.LayoutString(layout.dataField);
+ return Doc.assign(new Doc(prototypeId, true), { ...options, baseLayout: options.layout });
}
}
@@ -279,7 +303,7 @@ export namespace Docs {
*/
export namespace Create {
- const delegateKeys = ["x", "y", "width", "height", "panX", "panY"];
+ const delegateKeys = ["x", "y", "width", "height", "panX", "panY", "nativeWidth", "nativeHeight", "dropAction", "forceActive", "fitWidth"];
/**
* This function receives the relevant document prototype and uses
@@ -315,6 +339,8 @@ export namespace Docs {
let dataDoc = MakeDataDelegate(proto, protoProps, data);
let viewDoc = Doc.MakeDelegate(dataDoc, delegId);
+ AudioBox.ActiveRecordings.map(d => DocUtils.MakeLink({ doc: viewDoc }, { doc: d }, "audio link", "link to audio: " + d.title));
+
return Doc.assign(viewDoc, delegateProps);
}
@@ -348,11 +374,11 @@ export namespace Docs {
requestImageSize(target)
.then((size: any) => {
let aspect = size.height / size.width;
- if (!inst.proto!.nativeWidth) {
- inst.proto!.nativeWidth = size.width;
+ if (!inst.nativeWidth) {
+ inst.nativeWidth = size.width;
}
- inst.proto!.nativeHeight = Number(inst.proto!.nativeWidth!) * aspect;
- inst.proto!.height = NumCast(inst.proto!.width) * aspect;
+ inst.nativeHeight = NumCast(inst.nativeWidth) * aspect;
+ inst.height = NumCast(inst.width) * aspect;
})
.catch((err: any) => console.log(err));
// }
@@ -382,6 +408,14 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.HIST), new HistogramField(histoOp), options);
}
+ export function QueryDocument(options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.QUERY), "", options);
+ }
+
+ export function ColorDocument(options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.COLOR), "", options);
+ }
+
export function TextDocument(options: DocumentOptions = {}) {
return InstanceFromProto(Prototypes.get(DocumentType.TEXT), "", options);
}
@@ -441,6 +475,10 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Freeform }, id);
}
+ export function LinearDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", backgroundColor: "black", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Linear }, id);
+ }
+
export function SchemaDocument(schemaColumns: SchemaHeaderField[], documents: Array<Doc>, options: DocumentOptions) {
return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List(schemaColumns), ...options, viewType: CollectionViewType.Schema });
}
@@ -462,8 +500,8 @@ export namespace Docs {
}
- export function DragboxDocument(options?: DocumentOptions) {
- return InstanceFromProto(Prototypes.get(DocumentType.DRAGBOX), undefined, { ...(options || {}) });
+ export function FontIconDocument(options?: DocumentOptions) {
+ return InstanceFromProto(Prototypes.get(DocumentType.FONTICON), undefined, { ...(options || {}) });
}
export function LinkFollowBoxDocument(options?: DocumentOptions) {
@@ -662,32 +700,38 @@ export namespace DocUtils {
}
});
}
- export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", sourceContext?: Doc, id?: string, anchored1?: boolean) {
- let sv = DocumentManager.Instance.getDocumentView(source);
- if (sv && sv.props.ContainingCollectionDoc === target) return;
- if (target === CurrentUserUtils.UserDocument) return undefined;
+
+ export function MakeLink(source: { doc: Doc, ctx?: Doc }, target: { doc: Doc, ctx?: Doc }, title: string = "", description: string = "", id?: string) {
+ let sv = DocumentManager.Instance.getDocumentView(source.doc);
+ if (sv && sv.props.ContainingCollectionDoc === target.doc) return;
+ if (target.doc === CurrentUserUtils.UserDocument) return undefined;
let linkDocProto = new Doc(id, true);
UndoManager.RunInBatch(() => {
linkDocProto.type = DocumentType.LINK;
- linkDocProto.targetContext = targetContext;
- linkDocProto.sourceContext = sourceContext;
- linkDocProto.title = title === "" ? source.title + " to " + target.title : title;
+ linkDocProto.title = title === "" ? source.doc.title + " to " + target.doc.title : title;
linkDocProto.linkDescription = description;
- linkDocProto.anchor1 = source;
- linkDocProto.anchor1Page = source.curPage;
+ linkDocProto.anchor1 = source.doc;
+ linkDocProto.anchor2 = target.doc;
+ linkDocProto.anchor1Context = source.ctx;
+ linkDocProto.anchor2Context = target.ctx;
linkDocProto.anchor1Groups = new List<Doc>([]);
- linkDocProto.anchor1anchored = anchored1;
- linkDocProto.anchor2 = target;
- linkDocProto.anchor2Page = target.curPage;
linkDocProto.anchor2Groups = new List<Doc>([]);
+ linkDocProto.anchor1Timecode = source.doc.currentTimecode;
+ linkDocProto.anchor2Timecode = target.doc.currentTimecode;
+ linkDocProto.layoutKey1 = DocuLinkBox.LayoutString("anchor1");
+ linkDocProto.layoutKey2 = DocuLinkBox.LayoutString("anchor2");
+ linkDocProto.borderRounding = "20";
+ linkDocProto.width = linkDocProto.height = 0;
+ linkDocProto.isBackground = true;
+ linkDocProto.isButton = true;
LinkManager.Instance.addLink(linkDocProto);
- Doc.GetProto(source).links = ComputedField.MakeFunction("links(this)");
- Doc.GetProto(target).links = ComputedField.MakeFunction("links(this)");
+ Doc.GetProto(source.doc).links = ComputedField.MakeFunction("links(this)");
+ Doc.GetProto(target.doc).links = ComputedField.MakeFunction("links(this)");
}, "make link");
return linkDocProto;
}
diff --git a/src/client/northstar/dash-nodes/HistogramBox.tsx b/src/client/northstar/dash-nodes/HistogramBox.tsx
index b81eafbee..854135648 100644
--- a/src/client/northstar/dash-nodes/HistogramBox.tsx
+++ b/src/client/northstar/dash-nodes/HistogramBox.tsx
@@ -24,7 +24,7 @@ import { Id } from "../../../new_fields/FieldSymbols";
@observer
export class HistogramBox extends React.Component<FieldViewProps> {
- public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(HistogramBox, fieldStr); }
+ public static LayoutString(fieldStr: string) { return FieldView.LayoutString(HistogramBox, fieldStr); }
private _dropXRef = React.createRef<HTMLDivElement>();
private _dropYRef = React.createRef<HTMLDivElement>();
private _dropXDisposer?: DragManager.DragDropDisposer;
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index cebb56bbe..6bbd3d0ed 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -6,7 +6,7 @@ import { DocumentType } from "../documents/DocumentTypes";
import { Doc, Opt } from "../../new_fields/Doc";
import { List } from "../../new_fields/List";
import { Docs } from "../documents/Documents";
-import { CollectionViewType } from "../views/collections/CollectionBaseView";
+import { CollectionViewType } from "../views/collections/CollectionView";
import { Cast, CastCtor } from "../../new_fields/Types";
import { listSpec } from "../../new_fields/Schema";
import { AudioField, ImageField } from "../../new_fields/URLField";
@@ -14,6 +14,7 @@ import { HistogramField } from "../northstar/dash-fields/HistogramField";
import { MainView } from "../views/MainView";
import { Utils } from "../../Utils";
import { RichTextField } from "../../new_fields/RichTextField";
+import { DictationOverlay } from "../views/DictationOverlay";
/**
* This namespace provides a singleton instance of a manager that
@@ -63,7 +64,7 @@ export namespace DictationManager {
const intraSession = ". ";
const interSession = " ... ";
- let isListening = false;
+ export let isListening = false;
let isManuallyStopped = false;
let current: string | undefined = undefined;
@@ -88,12 +89,11 @@ export namespace DictationManager {
export const listen = async (options?: Partial<ListeningOptions>) => {
let results: string | undefined;
- let main = MainView.Instance;
let overlay = options !== undefined && options.useOverlay;
if (overlay) {
- main.dictationOverlayVisible = true;
- main.isListening = { interim: false };
+ DictationOverlay.Instance.dictationOverlayVisible = true;
+ DictationOverlay.Instance.isListening = { interim: false };
}
try {
@@ -101,21 +101,21 @@ export namespace DictationManager {
if (results) {
Utils.CopyText(results);
if (overlay) {
- main.isListening = false;
+ DictationOverlay.Instance.isListening = false;
let execute = options && options.tryExecute;
- main.dictatedPhrase = execute ? results.toLowerCase() : results;
- main.dictationSuccess = execute ? await DictationManager.Commands.execute(results) : true;
+ DictationOverlay.Instance.dictatedPhrase = execute ? results.toLowerCase() : results;
+ DictationOverlay.Instance.dictationSuccess = execute ? await DictationManager.Commands.execute(results) : true;
}
options && options.tryExecute && await DictationManager.Commands.execute(results);
}
} catch (e) {
if (overlay) {
- main.isListening = false;
- main.dictatedPhrase = results = `dictation error: ${"error" in e ? e.error : "unknown error"}`;
- main.dictationSuccess = false;
+ DictationOverlay.Instance.isListening = false;
+ DictationOverlay.Instance.dictatedPhrase = results = `dictation error: ${"error" in e ? e.error : "unknown error"}`;
+ DictationOverlay.Instance.dictationSuccess = false;
}
} finally {
- overlay && main.initiateDictationFade();
+ overlay && DictationOverlay.Instance.initiateDictationFade();
}
return results;
@@ -146,7 +146,6 @@ export namespace DictationManager {
recognizer.start();
return new Promise<string>((resolve, reject) => {
-
recognizer.onerror = (e: SpeechRecognitionError) => {
if (!(indefinite && e.error === "no-speech")) {
recognizer.stop();
@@ -201,6 +200,7 @@ export namespace DictationManager {
if (!isListening || !recognizer) {
return;
}
+ isListening = false;
isManuallyStopped = true;
salvageSession ? recognizer.stop() : recognizer.abort();
// let main = MainView.Instance;
@@ -336,7 +336,6 @@ export namespace DictationManager {
let newBox = Docs.Create.TextDocument({ width: 400, height: 200, title: "My Outline" });
newBox.autoHeight = true;
let proto = newBox.proto!;
- proto.page = -1;
let prompt = "Press alt + r to start dictating here...";
let head = 3;
let anchor = head + prompt.length;
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 4e6968f08..346e88f40 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,17 +1,15 @@
import { action, computed, observable } from 'mobx';
-import { Doc, DocListCastAsync } from '../../new_fields/Doc';
+import { Doc, DocListCastAsync, DocListCast } from '../../new_fields/Doc';
import { Id } from '../../new_fields/FieldSymbols';
-import { Cast, NumCast } from '../../new_fields/Types';
+import { List } from '../../new_fields/List';
+import { Cast, NumCast, StrCast } from '../../new_fields/Types';
import { CollectionDockingView } from '../views/collections/CollectionDockingView';
-import { CollectionPDFView } from '../views/collections/CollectionPDFView';
-import { CollectionVideoView } from '../views/collections/CollectionVideoView';
import { CollectionView } from '../views/collections/CollectionView';
import { DocumentView } from '../views/nodes/DocumentView';
import { LinkManager } from './LinkManager';
-import { undoBatch, UndoManager } from './UndoManager';
import { Scripting } from './Scripting';
-import { List } from '../../new_fields/List';
import { SelectionManager } from './SelectionManager';
+import { DocumentType } from '../documents/DocumentTypes';
export class DocumentManager {
@@ -44,8 +42,8 @@ export class DocumentManager {
if (toReturn.length === 0) {
DocumentManager.Instance.DocumentViews.map(view => {
let doc = view.props.Document.proto;
- if (doc && doc[Id]) {
- if (doc[Id] === id) { toReturn.push(view); }
+ if (doc && doc[Id] && doc[Id] === id) {
+ toReturn.push(view);
}
});
}
@@ -56,9 +54,9 @@ export class DocumentManager {
return this.getDocumentViewsById(doc[Id]);
}
- public getDocumentViewById(id: string, preferredCollection?: CollectionView | CollectionPDFView | CollectionVideoView): DocumentView | null {
+ public getDocumentViewById(id: string, preferredCollection?: CollectionView): DocumentView | undefined {
- let toReturn: DocumentView | null = null;
+ let toReturn: DocumentView | undefined;
let passes = preferredCollection ? [preferredCollection, undefined] : [undefined];
for (let pass of passes) {
@@ -75,124 +73,130 @@ export class DocumentManager {
toReturn = view;
}
});
+ } else {
+ break;
}
}
return toReturn;
}
- public getDocumentView(toFind: Doc, preferredCollection?: CollectionView | CollectionPDFView | CollectionVideoView): DocumentView | null {
+ public getDocumentView(toFind: Doc, preferredCollection?: CollectionView): DocumentView | undefined {
return this.getDocumentViewById(toFind[Id], preferredCollection);
}
+ public getFirstDocumentView(toFind: Doc): DocumentView | undefined {
+ const views = this.getDocumentViews(toFind);
+ return views.length ? views[0] : undefined;
+ }
public getDocumentViews(toFind: Doc): DocumentView[] {
-
let toReturn: DocumentView[] = [];
- //gets document view that is in a freeform canvas collection
- DocumentManager.Instance.DocumentViews.map(view => {
- let doc = view.props.Document;
-
- if (doc === toFind) {
- toReturn.push(view);
- } else {
- if (Doc.AreProtosEqual(doc, toFind)) {
- toReturn.push(view);
- }
- }
- });
+ DocumentManager.Instance.DocumentViews.map(view =>
+ Doc.AreProtosEqual(view.props.Document, toFind) && toReturn.push(view));
return toReturn;
}
@computed
public get LinkedDocumentViews() {
- let pairs = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || Doc.IsBrushed(dv.props.Document)).reduce((pairs, dv) => {
+ let pairs = DocumentManager.Instance.DocumentViews.filter(dv =>
+ (dv.isSelected() || Doc.IsBrushed(dv.props.Document)) // draw links from DocumentViews that are selected or brushed OR
+ || DocumentManager.Instance.DocumentViews.some(dv2 => { // Documentviews which
+ let rest = DocListCast(dv2.props.Document.links).some(l => Doc.AreProtosEqual(l, dv.props.Document));// are link doc anchors
+ let init = (dv2.isSelected() || Doc.IsBrushed(dv2.props.Document)) && dv2.Document.type !== DocumentType.AUDIO; // on a view that is selected or brushed
+ return init && rest;
+ })
+ ).reduce((pairs, dv) => {
let linksList = LinkManager.Instance.getAllRelatedLinks(dv.props.Document);
pairs.push(...linksList.reduce((pairs, link) => {
- if (link) {
- let linkToDoc = LinkManager.Instance.getOppositeAnchor(link, dv.props.Document);
- if (linkToDoc) {
- DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => {
- pairs.push({ a: dv, b: docView1, l: link });
- });
+ let 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) {
+ pairs.push({ a: dv, b: docView1, l: link });
}
- }
+ });
return pairs;
}, [] as { a: DocumentView, b: DocumentView, l: Doc }[]));
- // }
return pairs;
}, [] as { a: DocumentView, b: DocumentView, l: Doc }[]);
return pairs;
}
-
- @undoBatch
- public jumpToDocument = async (docDelegate: Doc, willZoom: boolean, forceDockFunc: boolean = false, dockFunc?: (doc: Doc) => void, linkPage?: number, docContext?: Doc): Promise<void> => {
- let doc = Doc.GetProto(docDelegate);
- const contextDoc = await Cast(doc.annotationOn, Doc);
- if (contextDoc) {
- contextDoc.scrollY = NumCast(doc.y) - NumCast(contextDoc.height) / 2 * 72 / 96;
- }
-
- let docView: DocumentView | null;
- // using forceDockFunc as a flag for splitting linked to doc to the right...can change later if needed
- if (!forceDockFunc && (docView = DocumentManager.Instance.getDocumentView(doc))) {
- Doc.BrushDoc(docView.props.Document);
- if (linkPage !== undefined) docView.props.Document.curPage = linkPage;
- UndoManager.RunInBatch(() => docView!.props.focus(docView!.props.Document, willZoom), "focus");
+ public jumpToDocument = async (targetDoc: Doc, willZoom: boolean, dockFunc?: (doc: Doc) => void, docContext?: Doc, linkId?: string, closeContextIfNotFound: boolean = false): Promise<void> => {
+ let highlight = () => {
+ const finalDocView = DocumentManager.Instance.getFirstDocumentView(targetDoc);
+ finalDocView && (finalDocView.Document.scrollToLinkID = linkId);
+ finalDocView && Doc.linkFollowHighlight(finalDocView.props.Document);
+ };
+ const docView = DocumentManager.Instance.getFirstDocumentView(targetDoc);
+ const annotatedDoc = await Cast(targetDoc.annotationOn, Doc);
+ if (docView) { // we have a docView already and aren't forced to create a new one ... just focus on the document. TODO move into view if necessary otherwise just highlight?
+ annotatedDoc && docView.props.focus(annotatedDoc, false);
+ docView.props.focus(docView.props.Document, willZoom);
+ highlight();
} else {
- if (!contextDoc) {
- let docs = docContext ? await DocListCastAsync(docContext.data) : undefined;
- let found = false;
- // bcz: this just searches within the context for the target -- perhaps it should recursively search through all children?
- docs && docs.map(d => found = found || Doc.AreProtosEqual(d, docDelegate));
- if (docContext && found) {
- let targetContextView: DocumentView | null;
-
- if (!forceDockFunc && docContext && (targetContextView = DocumentManager.Instance.getDocumentView(docContext))) {
- docContext.panTransformType = "Ease";
- targetContextView.props.focus(docDelegate, willZoom);
- } else {
- (dockFunc || CollectionDockingView.AddRightSplit)(docContext, undefined);
- setTimeout(() => {
- let dv = DocumentManager.Instance.getDocumentView(docContext);
- dv && this.jumpToDocument(docDelegate, willZoom, forceDockFunc,
- doc => dv!.props.focus(dv!.props.Document, true, 1, () => dv!.props.addDocTab(doc, undefined, "inPlace")),
- linkPage);
- }, 1050);
- }
- } else {
- const actualDoc = Doc.MakeAlias(docDelegate);
- Doc.BrushDoc(actualDoc);
- if (linkPage !== undefined) actualDoc.curPage = linkPage;
- (dockFunc || CollectionDockingView.AddRightSplit)(actualDoc, undefined);
- }
+ const contextDocs = docContext ? await DocListCastAsync(docContext.data) : undefined;
+ const contextDoc = contextDocs && contextDocs.find(doc => Doc.AreProtosEqual(doc, targetDoc)) ? docContext : undefined;
+ const targetDocContext = (annotatedDoc ? annotatedDoc : contextDoc);
+
+ if (!targetDocContext) { // we don't have a view and there's no context specified ... create a new view of the target using the dockFunc or default
+ (dockFunc || CollectionDockingView.AddRightSplit)(Doc.BrushDoc(Doc.MakeAlias(targetDoc)), undefined);
+ highlight();
} else {
- let contextView: DocumentView | null;
- Doc.BrushDoc(docDelegate);
- let contextViews = DocumentManager.Instance.getDocumentViews(contextDoc);
- if (!forceDockFunc && contextViews.length) {
- contextView = contextViews[0];
- SelectionManager.SelectDoc(contextView, false); // force unrendered annotations to be created
- contextDoc.panTransformType = "Ease";
+ const targetDocContextView = DocumentManager.Instance.getFirstDocumentView(targetDocContext);
+ targetDocContext.scrollY = 0; // this will force PDFs to activate and load their annotations / allow scrolling
+ if (targetDocContextView) { // we have a context view and aren't forced to create a new one ... focus on the context
+ targetDocContext.panTransformType = "Ease";
+ targetDocContextView.props.focus(targetDocContextView.props.Document, willZoom);
+
+ // now find the target document within the context
setTimeout(() => {
- SelectionManager.DeselectDoc(contextView!);
- docView = DocumentManager.Instance.getDocumentView(docDelegate);
- docView && contextView!.props.focus(contextView!.props.Document, willZoom);
- docView && UndoManager.RunInBatch(() => docView!.props.focus(docView!.props.Document, willZoom), "focus");
+ const retryDocView = DocumentManager.Instance.getDocumentView(targetDoc);
+ if (retryDocView) {
+ retryDocView.props.focus(targetDoc, willZoom); // focus on the target if it now exists in the context
+ } else {
+ if (closeContextIfNotFound && targetDocContextView.props.removeDocument) targetDocContextView.props.removeDocument(targetDocContextView.props.Document);
+ targetDoc.layout && (dockFunc || CollectionDockingView.AddRightSplit)(Doc.BrushDoc(Doc.MakeAlias(targetDoc)), undefined); // otherwise create a new view of the target
+ }
+ highlight();
}, 0);
- } else {
- (dockFunc || CollectionDockingView.AddRightSplit)(contextDoc, undefined);
+ } else { // there's no context view so we need to create one first and try again
+ (dockFunc || CollectionDockingView.AddRightSplit)(targetDocContext, undefined);
setTimeout(() => {
- this.jumpToDocument(docDelegate, willZoom, forceDockFunc, dockFunc, linkPage);
- }, 1000);
+ const finalDocView = DocumentManager.Instance.getFirstDocumentView(targetDoc);
+ const finalDocContextView = DocumentManager.Instance.getFirstDocumentView(targetDocContext);
+ setTimeout(() => // if not, wait a bit to see if the context can be loaded (e.g., a PDF). wait interval heurisitic tries to guess how we're animating based on what's just become visible
+ this.jumpToDocument(targetDoc, willZoom, dockFunc, undefined, linkId, true), finalDocView ? 0 : finalDocContextView ? 250 : 2000); // so call jump to doc again and if the doc isn't found, it will be created.
+ }, 0);
}
}
}
}
+ public async FollowLink(link: Doc | undefined, doc: Doc, focus: (doc: Doc, maxLocation: string) => void, zoom: boolean = false, reverse: boolean = false, currentContext?: Doc) {
+ const linkDocs = link ? [link] : LinkManager.Instance.getAllRelatedLinks(doc);
+ SelectionManager.DeselectAll();
+ const firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor1 as Doc, doc));
+ const secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor2 as Doc, doc));
+ const firstDocWithoutView = firstDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor2 as Doc).length === 0);
+ const secondDocWithoutView = secondDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor1 as Doc).length === 0);
+ const first = firstDocWithoutView ? [firstDocWithoutView] : firstDocs;
+ const second = secondDocWithoutView ? [secondDocWithoutView] : secondDocs;
+ const linkDoc = first.length ? first[0] : second.length ? second[0] : undefined;
+ const linkFollowDocs = first.length ? [await first[0].anchor2 as Doc, await first[0].anchor1 as Doc] : second.length ? [await second[0].anchor1 as Doc, await second[0].anchor2 as Doc] : undefined;
+ const linkFollowDocContexts = first.length ? [await first[0].anchor2Context as Doc, await first[0].anchor1Context as Doc] : second.length ? [await second[0].anchor1Context as Doc, await second[0].anchor2Context as Doc] : [undefined, undefined];
+ const linkFollowTimecodes = first.length ? [NumCast(first[0].anchor2Timecode), NumCast(first[0].anchor1Timecode)] : second.length ? [NumCast(second[0].anchor1Timecode), NumCast(second[0].anchor2Timecode)] : [undefined, undefined];
+ if (linkFollowDocs && linkDoc) {
+ const maxLocation = StrCast(linkFollowDocs[0].maximizeLocation, "inTab");
+ const targetContext = !Doc.AreProtosEqual(linkFollowDocContexts[reverse ? 1 : 0], currentContext) ? linkFollowDocContexts[reverse ? 1 : 0] : undefined;
+ const target = linkFollowDocs[reverse ? 1 : 0];
+ target.currentTimecode !== undefined && (target.currentTimecode = linkFollowTimecodes[reverse ? 1 : 0]);
+ DocumentManager.Instance.jumpToDocument(linkFollowDocs[reverse ? 1 : 0], zoom, (doc: Doc) => focus(doc, maxLocation), targetContext, linkDoc[Id]);
+ }
+ }
+
@action
zoomIntoScale = (docDelegate: Doc, scale: number) => {
let docView = DocumentManager.Instance.getDocumentView(Doc.GetProto(docDelegate));
@@ -202,8 +206,7 @@ export class DocumentManager {
getScaleOfDocView = (docDelegate: Doc) => {
let doc = Doc.GetProto(docDelegate);
- let docView: DocumentView | null;
- docView = DocumentManager.Instance.getDocumentView(doc);
+ const docView = DocumentManager.Instance.getDocumentView(doc);
if (docView) {
return docView.props.getScale();
} else {
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index ddc8fb62c..bbc29585c 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -1,6 +1,6 @@
import { action, runInAction } from "mobx";
import { Doc, Field } from "../../new_fields/Doc";
-import { Cast, StrCast } from "../../new_fields/Types";
+import { Cast, StrCast, ScriptCast } from "../../new_fields/Types";
import { URLField } from "../../new_fields/URLField";
import { emptyFunction } from "../../Utils";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
@@ -10,10 +10,12 @@ import { LinkManager } from "./LinkManager";
import { SelectionManager } from "./SelectionManager";
import { SchemaHeaderField } from "../../new_fields/SchemaHeaderField";
import { Docs } from "../documents/Documents";
-import { CompileScript } from "./Scripting";
import { ScriptField } from "../../new_fields/ScriptField";
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";
export type dropActionType = "alias" | "copy" | undefined;
export function SetupDrag(
@@ -87,12 +89,12 @@ export async function DragLinkAsDocument(dragEle: HTMLElement, x: number, y: num
}
}
-export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: number, sourceDoc: Doc) {
+export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: number, sourceDoc: Doc, singleLink?: Doc) {
let srcTarg = sourceDoc.proto;
let draggedDocs: Doc[] = [];
if (srcTarg) {
- let linkDocs = LinkManager.Instance.getAllRelatedLinks(srcTarg);
+ let linkDocs = singleLink ? [singleLink] : LinkManager.Instance.getAllRelatedLinks(srcTarg);
if (linkDocs) {
draggedDocs = linkDocs.map(link => {
let opp = LinkManager.Instance.getOppositeAnchor(link, sourceDoc);
@@ -211,7 +213,9 @@ export namespace DragManager {
offset: number[];
dropAction: dropActionType;
userDropAction: dropActionType;
+ embedDoc?: boolean;
moveDocument?: MoveFunction;
+ isSelectionMove?: boolean; // indicates that an explicitly selected Document is being dragged. this will suppress onDragStart scripts
applyAsTemplate?: boolean;
[id: string]: any;
}
@@ -236,14 +240,16 @@ export namespace DragManager {
export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
runInAction(() => StartDragFunctions.map(func => func()));
+ dragData.draggedDocuments.map(d => d.dragFactory); // does this help? trying to make sure the dragFactory Doc is loaded
StartDrag(eles, dragData, downX, downY, options, options && options.finishDrag ? options.finishDrag :
(dropData: { [id: string]: any }) => {
- (dropData.droppedDocuments = dragData.userDropAction === "alias" || (!dragData.userDropAction && dragData.dropAction === "alias") ?
- dragData.draggedDocuments.map(d => Doc.MakeAlias(d)) :
- dragData.userDropAction === "copy" || (!dragData.userDropAction && dragData.dropAction === "copy") ?
- dragData.draggedDocuments.map(d => Doc.MakeCopy(d, true)) :
- dragData.draggedDocuments
+ (dropData.droppedDocuments =
+ dragData.draggedDocuments.map(d => !dragData.isSelectionMove && !dragData.userDropAction && ScriptCast(d.onDragStart) ? ScriptCast(d.onDragStart).script.run({ this: d }).result :
+ dragData.userDropAction === "alias" || (!dragData.userDropAction && dragData.dropAction === "alias") ? Doc.MakeAlias(d) :
+ dragData.userDropAction === "copy" || (!dragData.userDropAction && dragData.dropAction === "copy") ? Doc.MakeCopy(d, true) : d)
);
+ dropData.droppedDocuments.forEach((drop: Doc, i: number) =>
+ Cast(dragData.draggedDocuments[i].removeDropProperties, listSpec("string"), []).map(prop => drop[prop] = undefined));
});
}
@@ -302,16 +308,6 @@ export namespace DragManager {
[id: string]: any;
}
- export class EmbedDragData {
- constructor(embeddableSourceDoc: Doc) {
- this.embeddableSourceDoc = embeddableSourceDoc;
- this.urlField = embeddableSourceDoc.data instanceof URLField ? embeddableSourceDoc.data : undefined;
- }
- embeddableSourceDoc: Doc;
- urlField?: URLField;
- [id: string]: any;
- }
-
// for column dragging in schema view
export class ColumnDragData {
constructor(colKey: SchemaHeaderField) {
@@ -325,10 +321,6 @@ export namespace DragManager {
StartDrag([ele], dragData, downX, downY, options);
}
- export function StartEmbedDrag(ele: HTMLElement, dragData: EmbedDragData, downX: number, downY: number, options?: DragOptions) {
- StartDrag([ele], dragData, downX, downY, options);
- }
-
export function StartColumnDrag(ele: HTMLElement, dragData: ColumnDragData, downX: number, downY: number, options?: DragOptions) {
StartDrag([ele], dragData, downX, downY, options);
}
@@ -424,16 +416,17 @@ export namespace DragManager {
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 ? "alias" : undefined;
+ dragData.userDropAction = e.ctrlKey ? "alias" : undefined;
}
if (((options && !options.withoutShiftDrag) || !options) && e.shiftKey && CollectionDockingView.Instance) {
AbortDrag();
+ finishDrag && finishDrag(dragData);
CollectionDockingView.Instance.StartOtherDrag({
pageX: e.pageX,
pageY: e.pageY,
preventDefault: emptyFunction,
button: 0
- }, docs);
+ }, dragData.droppedDocuments);
}
//TODO: Why can't we use e.movementX and e.movementY?
let moveX = e.pageX - lastX;
@@ -506,3 +499,4 @@ 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
new file mode 100644
index 000000000..6b53333d7
--- /dev/null
+++ b/src/client/util/DropConverter.ts
@@ -0,0 +1,46 @@
+import { DragManager } from "./DragManager";
+import { CollectionViewType } from "../views/collections/CollectionView";
+import { Doc, DocListCast } from "../../new_fields/Doc";
+import { DocumentType } from "../documents/DocumentTypes";
+import { ObjectField } from "../../new_fields/ObjectField";
+import { StrCast } from "../../new_fields/Types";
+import { Docs } from "../documents/Documents";
+import { ScriptField } from "../../new_fields/ScriptField";
+
+
+function makeTemplate(doc: Doc): boolean {
+ let layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateField ? doc.layout : doc;
+ let layout = StrCast(layoutDoc.layout).match(/fieldKey={"[^"]*"}/)![0];
+ let fieldKey = layout.replace('fieldKey={"', "").replace(/"}$/, "");
+ let docs = DocListCast(layoutDoc[fieldKey]);
+ let any = false;
+ docs.map(d => {
+ if (!StrCast(d.title).startsWith("-")) {
+ any = true;
+ return Doc.MakeMetadataFieldTemplate(d, Doc.GetProto(layoutDoc));
+ }
+ if (d.type === DocumentType.COL) return makeTemplate(d);
+ return false;
+ });
+ return any;
+}
+export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
+ data && data.draggedDocuments.map((doc, i) => {
+ let dbox = doc;
+ if (!doc.onDragStart && !doc.onClick && doc.viewType !== CollectionViewType.Linear) {
+ let layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateField ? doc.layout : doc;
+ if (layoutDoc.type === DocumentType.COL) {
+ layoutDoc.isTemplateDoc = makeTemplate(layoutDoc);
+ } else {
+ layoutDoc.isTemplateDoc = (layoutDoc.type === DocumentType.TEXT || layoutDoc.layout instanceof Doc) && !data.userDropAction;
+ }
+ dbox = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, backgroundColor: StrCast(doc.backgroundColor), title: "Custom", icon: layoutDoc.isTemplateDoc ? "font" : "bolt" });
+ dbox.dragFactory = layoutDoc;
+ dbox.removeDropProperties = doc.removeDropProperties instanceof ObjectField ? ObjectField.MakeCopy(doc.removeDropProperties) : undefined;
+ dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)');
+ } else if (doc.viewType === CollectionViewType.Linear) {
+ dbox.ignoreClick = true;
+ }
+ data.droppedDocuments[i] = dbox;
+ });
+}
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index d3f81b992..f27d05487 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx
@@ -22,13 +22,15 @@ import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
import "./DirectoryImportBox.scss";
import { Identified } from "../../Network";
import { BatchedArray } from "array-batcher";
+import { ExifData } from "exif";
const unsupported = ["text/html", "text/plain"];
-interface FileResponse {
+interface ImageUploadResponse {
name: string;
path: string;
type: string;
+ exif: any;
}
@observer
@@ -48,7 +50,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
@observable private uploading = false;
@observable private removeHover = false;
- public static LayoutString() { return FieldView.LayoutString(DirectoryImportBox); }
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DirectoryImportBox, fieldKey); }
constructor(props: FieldViewProps) {
super(props);
@@ -117,7 +119,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
const responses = await Identified.PostFormDataToServer(RouteStore.upload, formData);
runInAction(() => this.completed += batch.length);
- return responses as FileResponse[];
+ return responses as ImageUploadResponse[];
});
await Promise.all(uploads.map(async upload => {
@@ -129,7 +131,11 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
title: upload.name
};
const document = await Docs.Get.DocumentFromType(type, path, options);
- document && docs.push(document);
+ const { data, error } = upload.exif;
+ if (document) {
+ Doc.GetProto(document).exif = error || Docs.Get.DocumentHierarchyFromJson(data);
+ docs.push(document);
+ }
}));
for (let i = 0; i < docs.length; i++) {
diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts
new file mode 100644
index 000000000..c9abf38fa
--- /dev/null
+++ b/src/client/util/Import & Export/ImageUtils.ts
@@ -0,0 +1,31 @@
+import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc";
+import { ImageField } from "../../../new_fields/URLField";
+import { Cast, StrCast } from "../../../new_fields/Types";
+import { RouteStore } from "../../../server/RouteStore";
+import { Docs } from "../../documents/Documents";
+import { Identified } from "../../Network";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { Utils } from "../../../Utils";
+
+export namespace ImageUtils {
+
+ export const ExtractExif = async (document: Doc): Promise<boolean> => {
+ const field = Cast(document.data, ImageField);
+ if (!field) {
+ return false;
+ }
+ const source = field.url.href;
+ const response = await Identified.PostToServer(RouteStore.inspectImage, { source });
+ const { error, data } = response.exifData;
+ document.exif = error || Docs.Get.DocumentHierarchyFromJson(data);
+ return data !== undefined;
+ };
+
+ export const ExportHierarchyToFileSystem = async (collection: Doc): Promise<void> => {
+ const a = document.createElement("a");
+ a.href = Utils.prepend(`${RouteStore.imageHierarchyExport}/${collection[Id]}`);
+ a.download = `Dash Export [${StrCast(collection.title)}].zip`;
+ a.click();
+ };
+
+} \ No newline at end of file
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 8a668e8d8..ee2f2dadc 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -1,10 +1,7 @@
-import { observable, action } from "mobx";
-import { StrCast, Cast, FieldValue } from "../../new_fields/Types";
import { Doc, DocListCast } from "../../new_fields/Doc";
-import { listSpec } from "../../new_fields/Schema";
import { List } from "../../new_fields/List";
-import { Id } from "../../new_fields/FieldSymbols";
-import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
+import { listSpec } from "../../new_fields/Schema";
+import { Cast, StrCast } from "../../new_fields/Types";
import { Docs } from "../documents/Documents";
import { Scripting } from "./Scripting";
@@ -79,7 +76,7 @@ export class LinkManager {
let related = LinkManager.Instance.getAllLinks().filter(link => {
let protomatch1 = Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, null));
let protomatch2 = Doc.AreProtosEqual(anchor, Cast(link.anchor2, Doc, null));
- return protomatch1 || protomatch2;
+ return protomatch1 || protomatch2 || Doc.AreProtosEqual(link, anchor);
});
return related;
}
@@ -242,11 +239,11 @@ export class LinkManager {
//TODO This should probably return undefined if there isn't an opposite anchor
//TODO This should also await the return value of the anchor so we don't filter out promises
public getOppositeAnchor(linkDoc: Doc, anchor: Doc): Doc | undefined {
- if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, null))) {
- return Cast(linkDoc.anchor2, Doc, null);
- } else {
- return Cast(linkDoc.anchor1, Doc, null);
- }
+ let a1 = Cast(linkDoc.anchor1, Doc, null);
+ let a2 = Cast(linkDoc.anchor2, Doc, null);
+ if (Doc.AreProtosEqual(anchor, a1)) return a2;
+ if (Doc.AreProtosEqual(anchor, a2)) return a1;
+ if (Doc.AreProtosEqual(anchor, linkDoc)) return linkDoc;
}
}
Scripting.addGlobal(function links(doc: any) {
diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index dd0f72af0..003ff6272 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -11,7 +11,7 @@ const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) :
export type KeyMap = { [key: string]: any };
-export let updateBullets = (tx2: Transaction, schema: Schema) => {
+export let updateBullets = (tx2: Transaction, schema: Schema, mapStyle?: string) => {
let fontSize: number | undefined = undefined;
tx2.doc.descendants((node: any, offset: any, index: any) => {
if (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item) {
@@ -20,7 +20,7 @@ export let updateBullets = (tx2: Transaction, schema: Schema) => {
if (node.type === schema.nodes.ordered_list) depth++;
fontSize = depth === 1 && node.attrs.setFontSize ? Number(node.attrs.setFontSize) : fontSize;
let fsize = fontSize && node.type === schema.nodes.ordered_list ? Math.max(6, fontSize - (depth - 1) * 4) : undefined;
- tx2.setNodeMarkup(offset, node.type, { ...node.attrs, mapStyle: node.attrs.mapStyle, bulletStyle: depth, inheritedFontSize: fsize }, node.marks);
+ tx2.setNodeMarkup(offset, node.type, { ...node.attrs, mapStyle: mapStyle ? mapStyle : node.attrs.mapStyle, bulletStyle: depth, inheritedFontSize: fsize }, node.marks);
}
});
return tx2;
diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts
index cd37ea0bb..ebb9bda8a 100644
--- a/src/client/util/RichTextRules.ts
+++ b/src/client/util/RichTextRules.ts
@@ -1,10 +1,12 @@
import { textblockTypeInputRule, smartQuotes, emDash, ellipsis, InputRule } from "prosemirror-inputrules";
import { schema } from "./RichTextSchema";
import { wrappingInputRule } from "./prosemirrorPatches";
-import { NodeSelection } from "prosemirror-state";
+import { NodeSelection, TextSelection } from "prosemirror-state";
import { NumCast, Cast } from "../../new_fields/Types";
import { Doc } from "../../new_fields/Doc";
import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
+import { Docs } from "../documents/Documents";
+import { Id } from "../../new_fields/FieldSymbols";
export const inpRules = {
rules: [
@@ -70,6 +72,34 @@ export const inpRules = {
return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: Number(match[1]) }));
}),
new InputRule(
+ new RegExp(/t/),
+ (state, match, start, end) => {
+ if (state.selection.to === state.selection.from) return null;
+ let node = (state.doc.resolve(start) as any).nodeAfter;
+ return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "todo", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
+ }),
+ new InputRule(
+ new RegExp(/i/),
+ (state, match, start, end) => {
+ if (state.selection.to === state.selection.from) return null;
+ let node = (state.doc.resolve(start) as any).nodeAfter;
+ return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "ignore", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
+ }),
+ new InputRule(
+ new RegExp(/\!/),
+ (state, match, start, end) => {
+ if (state.selection.to === state.selection.from) return null;
+ let node = (state.doc.resolve(start) as any).nodeAfter;
+ return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "important", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
+ }),
+ new InputRule(
+ new RegExp(/\x/),
+ (state, match, start, end) => {
+ if (state.selection.to === state.selection.from) return null;
+ let node = (state.doc.resolve(start) as any).nodeAfter;
+ return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "disagree", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
+ }),
+ new InputRule(
new RegExp(/^\^\^\s$/),
(state, match, start, end) => {
let node = (state.doc.resolve(start) as any).nodeAfter;
@@ -80,8 +110,9 @@ export const inpRules = {
ruleProvider["ruleAlign_" + heading] = "center";
return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
}
- return node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "center" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+ let replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "center" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
state.tr;
+ return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
}),
new InputRule(
new RegExp(/^\[\[\s$/),
@@ -92,8 +123,11 @@ export const inpRules = {
let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
if (ruleProvider && heading) {
ruleProvider["ruleAlign_" + heading] = "left";
+ return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
}
- return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
+ let replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "left" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+ state.tr;
+ return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
}),
new InputRule(
new RegExp(/^\]\]\s$/),
@@ -104,8 +138,44 @@ export const inpRules = {
let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
if (ruleProvider && heading) {
ruleProvider["ruleAlign_" + heading] = "right";
+ return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
}
- return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
+ let replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "right" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+ state.tr;
+ return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
+ }),
+ new InputRule(
+ new RegExp(/##\s$/),
+ (state, match, start, end) => {
+ let node = (state.doc.resolve(start) as any).nodeAfter;
+ let sm = state.storedMarks || undefined;
+ let target = Docs.Create.TextDocument({ width: 75, height: 35, autoHeight: true, fontSize: 9, title: "inline comment" });
+ let replaced = node ? state.tr.insertText("←", start).replaceRangeWith(start + 1, end + 1, schema.nodes.dashDoc.create({
+ width: 75, height: 35,
+ title: "dashDoc", docid: target[Id],
+ float: "right"
+ })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+ state.tr;
+ return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 1)));
+ }),
+ new InputRule(
+ new RegExp(/\(\(/),
+ (state, match, start, end) => {
+ let node = (state.doc.resolve(start) as any).nodeAfter;
+ let sm = state.storedMarks || undefined;
+ let mark = state.schema.marks.highlight.create();
+ let selected = state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).addMark(start, end, mark);
+ let content = selected.selection.content();
+ let replaced = node ? selected.replaceRangeWith(start, start,
+ schema.nodes.star.create({ visibility: true, text: content, textslice: content.toJSON() })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+ state.tr;
+ return replaced.setSelection(new TextSelection(replaced.doc.resolve(end + 1)));
+ }),
+ new InputRule(
+ new RegExp(/\)\)/),
+ (state, match, start, end) => {
+ let mark = state.schema.marks.highlight.create();
+ return state.tr.removeStoredMark(mark);
}),
new InputRule(
new RegExp(/\^f\s$/),
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 64821d8db..cc3548e1a 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -1,18 +1,24 @@
+import { action, observable, runInAction, reaction, IReactionDisposer } 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, TextSelection } from "prosemirror-state";
+import { EditorState, NodeSelection, TextSelection, Plugin } from "prosemirror-state";
import { StepMap } from "prosemirror-transform";
import { EditorView } from "prosemirror-view";
-import { Doc } from "../../new_fields/Doc";
-import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
+import * as ReactDOM from 'react-dom';
+import { Doc, WidthSym, HeightSym } from "../../new_fields/Doc";
+import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils } from "../../Utils";
import { DocServer } from "../DocServer";
-import { Cast, NumCast } from "../../new_fields/Types";
+import { DocumentView } from "../views/nodes/DocumentView";
import { DocumentManager } from "./DocumentManager";
import ParagraphNodeSpec from "./ParagraphNodeSpec";
-import { times } from "async";
+import { Transform } from "./Transform";
+import React = require("react");
+import { BoolCast, NumCast } from "../../new_fields/Types";
+import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
+import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
const pDOM: DOMOutputSpecArray = ["p", 0], blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"],
preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0];
@@ -157,6 +163,35 @@ export const nodes: { [index: string]: NodeSpec } = {
}
},
+ dashDoc: {
+ inline: true,
+ attrs: {
+ width: { default: 200 },
+ height: { default: 100 },
+ title: { default: null },
+ float: { default: "right" },
+ 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}, height: ${node.attrs.height}` };
+ return ["div", { ...node.attrs, ...attrs }];
+ }
+ },
+
video: {
inline: true,
attrs: {
@@ -200,6 +235,7 @@ export const nodes: { [index: string]: NodeSpec } = {
bulletStyle: { default: 0 },
mapStyle: { default: "decimal" },
setFontSize: { default: undefined },
+ setFontFamily: { default: undefined },
inheritedFontSize: { default: undefined },
visibility: { default: true }
},
@@ -209,8 +245,9 @@ export const nodes: { [index: string]: NodeSpec } = {
const multiMap = bs === 1 ? "decimal1" : bs === 2 ? "upper-alpha" : bs === 3 ? "lower-roman" : bs === 4 ? "lower-alpha" : "";
let map = node.attrs.mapStyle === "decimal" ? decMap : multiMap;
let fsize = node.attrs.setFontSize ? node.attrs.setFontSize : node.attrs.inheritedFontSize;
- return node.attrs.visibility ? ['ol', { class: `${map}-ol`, style: `list-style: none;font-size: ${fsize}` }, 0] :
- ['ol', { class: `${map}-ol`, style: `list-style: none; font-size: ${fsize}` }];
+ let ffam = node.attrs.setFontFamily;
+ return node.attrs.visibility ? ['ol', { class: `${map}-ol`, style: `list-style: none; font-size: ${fsize}; font-family: ${ffam}` }, 0] :
+ ['ol', { class: `${map}-ol`, style: `list-style: none; font-size: ${fsize}; font-family: ${ffam}` }];
}
},
@@ -391,10 +428,13 @@ export const marks: { [index: string]: MarkSpec } = {
},
search_highlight: {
+ attrs: {
+ selected: { default: false }
+ },
parseDOM: [{ style: 'background: yellow' }],
- toDOM() {
+ toDOM(node: any) {
return ['span', {
- style: 'background: yellow'
+ style: `background: ${node.attrs.selected ? "orange" : "yellow"}`
}];
}
},
@@ -403,20 +443,35 @@ export const marks: { [index: string]: MarkSpec } = {
user_mark: {
attrs: {
userid: { default: "" },
- hide_users: { default: [] },
opened: { default: true },
- modified: { default: "when?" }
+ modified: { default: "when?" }, // 5 second intervals since 1970
+ },
+ group: "inline",
+ toDOM(node: any) {
+ let uid = node.attrs.userid.replace(".", "").replace("@", "");
+ let min = Math.round(node.attrs.modified / 12);
+ let hr = Math.round(min / 60);
+ let day = Math.round(hr / 60 / 24);
+ let remote = node.attrs.userid !== Doc.CurrentUserEmail ? " userMark-remote" : "";
+ return node.attrs.opened ?
+ ['span', { class: "userMark-" + uid + remote + " userMark-min-" + min + " userMark-hr-" + hr + " userMark-day-" + day }, 0] :
+ ['span', { class: "userMark-" + uid + remote + " userMark-min-" + min + " userMark-hr-" + hr + " userMark-day-" + day }, ['span', 0]];
+ }
+ },
+ // the id of the user who entered the text
+ user_tag: {
+ attrs: {
+ userid: { default: "" },
+ opened: { default: true },
+ modified: { default: "when?" }, // 5 second intervals since 1970
+ tag: { default: "" }
},
group: "inline",
toDOM(node: any) {
- let hideUsers = node.attrs.hide_users;
- let hidden = hideUsers.indexOf(node.attrs.userid) !== -1 || (hideUsers.length === 0 && node.attrs.userid !== Doc.CurrentUserEmail);
- return hidden ?
- (node.attrs.opened ?
- ['span', { class: "userMarkOpen" }, 0] :
- ['span', { class: "userMark" }, ['span', 0]]
- ) :
- ['span', 0];
+ let uid = node.attrs.userid.replace(".", "").replace("@", "");
+ return node.attrs.opened ?
+ ['span', { class: "userTag-" + uid + " userTag-" + node.attrs.tag }, 0] :
+ ['span', { class: "userTag-" + uid + " userTag-" + node.attrs.tag }, ['span', 0]];
}
},
@@ -600,6 +655,7 @@ export class ImageResizeView {
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;
@@ -615,63 +671,51 @@ export class ImageResizeView {
this._handle.style.bottom = "-10px";
this._handle.style.right = "-10px";
let 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 (!view.isOverlay || e.ctrlKey) {
+ if (e.ctrlKey) {
e.preventDefault();
e.stopPropagation();
- DocServer.GetRefField(node.attrs.docid).then(async linkDoc => {
- const location = node.attrs.location;
- if (linkDoc instanceof Doc) {
- let proto = Doc.GetProto(linkDoc);
- let targetContext = await Cast(proto.targetContext, Doc);
- let jumpToDoc = await Cast(linkDoc.anchor2, Doc);
- if (jumpToDoc) {
- if (DocumentManager.Instance.getDocumentView(jumpToDoc)) {
-
- DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, undefined, undefined, NumCast((jumpToDoc === linkDoc.anchor2 ? linkDoc.anchor2Page : linkDoc.anchor1Page)));
- return;
- }
- }
- if (targetContext) {
- DocumentManager.Instance.jumpToDocument(targetContext, e.ctrlKey, false, document => addDocTab(document, undefined, location ? location : "inTab"));
- } else if (jumpToDoc) {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, e.ctrlKey, false, document => addDocTab(document, undefined, location ? location : "inTab"));
- } else {
- DocumentManager.Instance.jumpToDocument(linkDoc, e.ctrlKey, false, document => addDocTab(document, undefined, location ? location : "inTab"));
- }
- }
- });
+ DocServer.GetRefField(node.attrs.docid).then(async linkDoc =>
+ (linkDoc instanceof Doc) &&
+ DocumentManager.Instance.FollowLink(linkDoc, view.state.schema.Document,
+ document => addDocTab(document, undefined, node.attrs.location ? node.attrs.location : "inTab"), false));
}
};
this._handle.onpointerdown = function (e: any) {
e.preventDefault();
e.stopPropagation();
+ let wid = Number(getComputedStyle(self._img).width!.replace(/px/, ""));
+ let 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}`;
- //Array.from(FormattedTextBox.InputBoxOverlay!.CurrentDiv.getElementsByTagName("img")).map((img: any) => img.opacity = "0.1");
- FormattedTextBox.InputBoxOverlay!.CurrentDiv.style.opacity = "0";
+ self._outer.style.height = `${(startWidth + diffInPx) * hgt / wid}`;
};
const onpointerup = () => {
document.removeEventListener("pointermove", onpointermove);
document.removeEventListener("pointerup", onpointerup);
- view.dispatch(
- view.state.tr.setSelection(view.state.selection).setNodeMarkup(getPos(), null,
- { ...node.attrs, width: self._outer.style.width })
- );
- FormattedTextBox.InputBoxOverlay!.CurrentDiv.style.opacity = "1";
+ let 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._handle);
this._outer.appendChild(this._img);
+ this._outer.appendChild(this._handle);
(this as any).dom = this._outer;
}
@@ -688,6 +732,88 @@ export class ImageResizeView {
}
}
+export class DashDocView {
+ _dashSpan: HTMLDivElement;
+ _outer: HTMLElement;
+ _dashDoc: Doc | undefined;
+ _reactionDisposer: IReactionDisposer | undefined;
+ _textBox: FormattedTextBox;
+
+ getDocTransform = () => {
+ let { scale, translateX, translateY } = Utils.GetScreenTransform(this._outer);
+ return new Transform(-translateX, -translateY, 1).scale(1 / this.contentScaling() / scale);
+ }
+ contentScaling = () => NumCast(this._dashDoc!.nativeWidth) > 0 && !this._dashDoc!.ignoreAspect ? this._dashDoc![WidthSym]() / NumCast(this._dashDoc!.nativeWidth) : 1;
+ 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.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._dashSpan.style.width = node.attrs.width;
+ this._dashSpan.style.height = node.attrs.height;
+ this._dashSpan.style.position = "absolute";
+ this._dashSpan.style.display = "inline-block";
+ let removeDoc = () => {
+ let pos = getPos();
+ let ns = new NodeSelection(view.state.doc.resolve(pos));
+ view.dispatch(view.state.tr.setSelection(ns).deleteSelection());
+ return true;
+ };
+ DocServer.GetRefField(node.attrs.docid).then(async dashDoc => {
+ if (dashDoc instanceof Doc) {
+ self._dashDoc = dashDoc;
+ if (node.attrs.width !== dashDoc.width + "px" || node.attrs.height !== dashDoc.height + "px") {
+ view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: dashDoc.width + "px", height: dashDoc.height + "px" }));
+ }
+ this._reactionDisposer && this._reactionDisposer();
+ this._reactionDisposer = reaction(() => dashDoc[HeightSym]() + dashDoc[WidthSym](), () => {
+ this._dashSpan.style.height = this._outer.style.height = dashDoc[HeightSym]() + "px";
+ this._dashSpan.style.width = this._outer.style.width = dashDoc[WidthSym]() + "px";
+ });
+ ReactDOM.render(<DocumentView
+ fitToBox={BoolCast(dashDoc.fitToBox)}
+ Document={dashDoc}
+ addDocument={returnFalse}
+ removeDocument={removeDoc}
+ ruleProvider={undefined}
+ ScreenToLocalTransform={this.getDocTransform}
+ addDocTab={self._textBox.props.addDocTab}
+ pinToPres={returnFalse}
+ renderDepth={1}
+ PanelWidth={self._dashDoc[WidthSym]}
+ PanelHeight={self._dashDoc[HeightSym]}
+ focus={emptyFunction}
+ backgroundColor={returnEmptyString}
+ parentActive={returnFalse}
+ whenActiveChanged={returnFalse}
+ bringToFront={emptyFunction}
+ zoomToScale={emptyFunction}
+ getScale={returnOne}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
+ ContentScaling={this.contentScaling}
+ />, this._dashSpan);
+ }
+ });
+ let self = this;
+ this._dashSpan.onkeydown = function (e: any) { e.stopPropagation(); };
+ 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;
+ }
+ destroy() {
+ this._reactionDisposer && this._reactionDisposer();
+ }
+}
+
export class OrderedListView {
update(node: any) {
return false; // if attr's of an ordered_list (e.g., bulletStyle) change, return false forces the dom node to be recreated which is necessary for the bullet labels to update
@@ -727,7 +853,6 @@ export class FootnoteView {
if (this.innerView) this.close();
}
open() {
- if (!this.outerView.isOverlay) return;
// Append a tooltip to the outer node
let tooltip = this.dom.appendChild(document.createElement("div"));
tooltip.className = "footnote-tooltip";
@@ -741,7 +866,14 @@ export class FootnoteView {
"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) {
+ return FormattedTextBox.getToolTip(newView);
+ }
+ })
+ ],
+
}),
// This is the magic part
dispatchTransaction: this.dispatchInner.bind(this),
diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts
index d8b9dbec6..6706dcb89 100644
--- a/src/client/util/SearchUtil.ts
+++ b/src/client/util/SearchUtil.ts
@@ -3,7 +3,6 @@ import { DocServer } from '../DocServer';
import { Doc } from '../../new_fields/Doc';
import { Id } from '../../new_fields/FieldSymbols';
import { Utils } from '../../Utils';
-import { ResultParameters } from '../northstar/model/idea/idea';
import { DocumentType } from '../documents/DocumentTypes';
export namespace SearchUtil {
@@ -42,23 +41,45 @@ export namespace SearchUtil {
}
let { ids, numFound, highlighting } = result;
- let lines: string[][] = ids.map(i => []);
let txtresult = query !== "*" && JSON.parse(await rp.get(Utils.prepend("/textsearch"), {
qs: { ...options, q: query },
}));
+
let fileids = txtresult ? txtresult.ids : [];
+ let newIds: string[] = [];
+ let newLines: string[][] = [];
await Promise.all(fileids.map(async (tr: string, i: number) => {
let docQuery = "fileUpload_t:" + tr.substr(0, 7); //If we just have a filter query, search for * as the query
let docResult = JSON.parse(await rp.get(Utils.prepend("/search"), { qs: { ...options, q: docQuery } }));
- ids.push(...docResult.ids);
- lines.push(...docResult.ids.map((dr: any) => txtresult.lines[i]));
- numFound += docResult.numFound;
+ newIds.push(...docResult.ids);
+ newLines.push(...docResult.ids.map((dr: any) => txtresult.lines[i]));
}));
+
+ let theDocs: Doc[] = [];
+ let theLines: string[][] = [];
+ const textDocMap = await DocServer.GetRefFields(newIds);
+ const textDocs = newIds.map((id: string) => textDocMap[id]).map(doc => doc as Doc);
+ for (let i = 0; i < textDocs.length; i++) {
+ let testDoc = textDocs[i];
+ if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1) {
+ theDocs.push(Doc.GetProto(testDoc));
+ theLines.push(newLines[i].map(line => line.replace(query, query.toUpperCase())));
+ }
+ }
+
const docMap = await DocServer.GetRefFields(ids);
- const docs = ids.map((id: string) => docMap[id]).filter((doc: any) => doc instanceof Doc && doc.type !== DocumentType.KVP);
- return { docs, numFound, highlighting, lines };
+ const docs = ids.map((id: string) => docMap[id]).map(doc => doc as Doc);
+ for (let i = 0; i < ids.length; i++) {
+ let testDoc = docs[i];
+ if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1) {
+ theDocs.push(testDoc);
+ theLines.push([]);
+ }
+ }
+
+ return { docs: theDocs, numFound: theDocs.length, highlighting, lines: theLines };
}
export async function GetAliasesOfDocument(doc: Doc): Promise<Doc[]>;
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index a02a270ee..2d717ca57 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -27,7 +27,6 @@ export namespace SelectionManager {
} else if (!ctrlPressed && manager.SelectedDocuments.length > 1) {
manager.SelectedDocuments.map(dv => dv !== docView && dv.props.whenActiveChanged(false));
manager.SelectedDocuments = [docView];
- FormattedTextBox.InputBoxOverlay = undefined;
}
}
@action
@@ -42,22 +41,10 @@ export namespace SelectionManager {
DeselectAll(): void {
manager.SelectedDocuments.map(dv => dv.props.whenActiveChanged(false));
manager.SelectedDocuments = [];
- FormattedTextBox.InputBoxOverlay = undefined;
}
}
const manager = new Manager();
- reaction(() => manager.SelectedDocuments, sel => {
- let targetColor = "#FFFFFF";
- if (sel.length > 0) {
- let firstView = sel[0];
- let doc = firstView.props.Document;
- let targetDoc = doc.isTemplate ? doc : Doc.GetProto(doc);
- let stored = StrCast(targetDoc.backgroundColor);
- stored.length > 0 && (targetColor = stored);
- }
- InkingControl.Instance.updateSelectedColor(targetColor);
- }, { fireImmediately: true });
export function DeselectDoc(docView: DocumentView): void {
manager.DeselectDoc(docView);
diff --git a/src/client/util/SharingManager.scss b/src/client/util/SharingManager.scss
index 9a4c5db30..dec9f751a 100644
--- a/src/client/util/SharingManager.scss
+++ b/src/client/util/SharingManager.scss
@@ -2,6 +2,10 @@
display: flex;
flex-direction: column;
+ .focus-span {
+ text-decoration: underline;
+ }
+
p {
font-size: 20px;
text-align: left;
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index f427e40b1..2082d6324 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -1,12 +1,9 @@
-import { observable, runInAction, action, autorun } from "mobx";
+import { observable, runInAction, action } from "mobx";
import * as React from "react";
import MainViewModal from "../views/MainViewModal";
-import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
-import { Doc, Opt } from "../../new_fields/Doc";
+import { Doc, Opt, DocCastAsync } from "../../new_fields/Doc";
import { DocServer } from "../DocServer";
import { Cast, StrCast } from "../../new_fields/Types";
-import { listSpec } from "../../new_fields/Schema";
-import { List } from "../../new_fields/List";
import { RouteStore } from "../../server/RouteStore";
import * as RequestPromise from "request-promise";
import { Utils } from "../../Utils";
@@ -20,9 +17,8 @@ import * as fa from '@fortawesome/free-solid-svg-icons';
import { DocumentView } from "../views/nodes/DocumentView";
import { SelectionManager } from "./SelectionManager";
import { DocumentManager } from "./DocumentManager";
-import { CollectionVideoView } from "../views/collections/CollectionVideoView";
-import { CollectionPDFView } from "../views/collections/CollectionPDFView";
import { CollectionView } from "../views/collections/CollectionView";
+import { DictationOverlay } from "../views/DictationOverlay";
library.add(fa.faCopy);
@@ -49,11 +45,18 @@ const SharingKey = "sharingPermissions";
const PublicKey = "publicLinkPermissions";
const DefaultColor = "black";
+interface ValidatedUser {
+ user: User;
+ notificationDoc: Doc;
+}
+
+const storage = "data";
+
@observer
export default class SharingManager extends React.Component<{}> {
public static Instance: SharingManager;
@observable private isOpen = false;
- @observable private users: User[] = [];
+ @observable private users: ValidatedUser[] = [];
@observable private targetDoc: Doc | undefined;
@observable private targetDocView: DocumentView | undefined;
@observable private copied = false;
@@ -69,7 +72,7 @@ export default class SharingManager extends React.Component<{}> {
this.populateUsers().then(action(() => {
this.targetDocView = target;
this.targetDoc = target.props.Document;
- MainView.Instance.hasActiveModal = true;
+ DictationOverlay.Instance.hasActiveModal = true;
this.isOpen = true;
if (!this.sharingDoc) {
this.sharingDoc = new Doc;
@@ -79,9 +82,10 @@ export default class SharingManager extends React.Component<{}> {
public close = action(() => {
this.isOpen = false;
+ this.users = [];
setTimeout(action(() => {
this.copied = false;
- MainView.Instance.hasActiveModal = false;
+ DictationOverlay.Instance.hasActiveModal = false;
this.targetDoc = undefined;
}), 500);
});
@@ -101,53 +105,43 @@ export default class SharingManager extends React.Component<{}> {
populateUsers = async () => {
let userList = await RequestPromise.get(Utils.prepend(RouteStore.getUsers));
- runInAction(() => {
- this.users = (JSON.parse(userList) as User[]).filter(({ email }) => email !== Doc.CurrentUserEmail);
+ const raw = JSON.parse(userList) as User[];
+ const evaluating = raw.map(async user => {
+ let isCandidate = user.email !== Doc.CurrentUserEmail;
+ if (isCandidate) {
+ const userDocument = await DocServer.GetRefField(user.userDocumentId);
+ if (userDocument instanceof Doc) {
+ const notificationDoc = await Cast(userDocument.optionalRightCollection, Doc);
+ runInAction(() => {
+ if (notificationDoc instanceof Doc) {
+ this.users.push({ user, notificationDoc });
+ }
+ });
+ }
+ }
});
+ return Promise.all(evaluating);
}
- setInternalSharing = async (user: User, state: string) => {
- if (!this.sharingDoc) {
- console.log("SHARING ABORTED!");
- return;
- }
- let sharingDoc = await this.sharingDoc;
- sharingDoc[user.userDocumentId] = state;
- const userDocument = await DocServer.GetRefField(user.userDocumentId);
- if (!(userDocument instanceof Doc)) {
- console.log(`Couldn't get user document of user ${user.email}`);
- return;
- }
- let target = this.targetDoc;
- if (!target) {
- console.log("SharingManager trying to share an undefined document!!");
- return;
- }
- const notifDoc = await Cast(userDocument.optionalRightCollection, Doc);
- if (notifDoc instanceof Doc) {
- const data = await Cast(notifDoc.data, listSpec(Doc));
- if (!data) {
- console.log("UNABLE TO ACCESS NOTIFICATION DATA");
- return;
- }
- console.log(`Attempting to set permissions to ${state} for the document ${target[Id]}`);
- if (state !== SharingPermissions.None) {
- const sharedDoc = Doc.MakeAlias(target);
- if (data) {
- data.push(sharedDoc);
- } else {
- notifDoc.data = new List([sharedDoc]);
- }
- } else {
- let dataDocs = (await Promise.all(data.map(doc => doc))).map(doc => Doc.GetProto(doc));
- if (dataDocs.includes(target)) {
- console.log("Searching in ", dataDocs, "for", target);
- dataDocs.splice(dataDocs.indexOf(target), 1);
- console.log("SUCCESSFULLY UNSHARED DOC");
- } else {
- console.log("DIDN'T THINK WE HAD IT, SO NOT SUCCESSFULLY UNSHARED");
- }
+ setInternalSharing = async (recipient: ValidatedUser, state: string) => {
+ const { user, notificationDoc } = recipient;
+ const target = this.targetDoc!;
+ const manager = this.sharingDoc!;
+ const key = user.userDocumentId;
+ if (state === SharingPermissions.None) {
+ const metadata = (await DocCastAsync(manager[key]));
+ if (metadata) {
+ let sharedAlias = (await DocCastAsync(metadata.sharedAlias))!;
+ Doc.RemoveDocFromList(notificationDoc, storage, sharedAlias);
+ manager[key] = undefined;
}
+ } else {
+ const sharedAlias = Doc.MakeAlias(target);
+ Doc.AddDocToList(notificationDoc, storage, sharedAlias);
+ const metadata = new Doc;
+ metadata.permissions = state;
+ metadata.sharedAlias = sharedAlias;
+ manager[key] = metadata;
}
}
@@ -188,11 +182,12 @@ export default class SharingManager extends React.Component<{}> {
let title = this.targetDoc ? StrCast(this.targetDoc.title) : "";
return (
<span
+ className={"focus-span"}
title={title}
onClick={() => {
- let context: Opt<CollectionVideoView | CollectionPDFView | CollectionView>;
+ let context: Opt<CollectionView>;
if (this.targetDoc && this.targetDocView && (context = this.targetDocView.props.ContainingCollectionView)) {
- DocumentManager.Instance.jumpToDocument(this.targetDoc, true, undefined, undefined, undefined, context.props.Document);
+ DocumentManager.Instance.jumpToDocument(this.targetDoc, true, undefined, context.props.Document);
}
}}
onPointerEnter={action(() => {
@@ -213,7 +208,20 @@ export default class SharingManager extends React.Component<{}> {
);
}
+ private computePermissions = (userKey: string) => {
+ const sharingDoc = this.sharingDoc;
+ if (!sharingDoc) {
+ return SharingPermissions.None;
+ }
+ const metadata = sharingDoc[userKey] as Doc;
+ if (!metadata) {
+ return SharingPermissions.None;
+ }
+ return StrCast(metadata.permissions, SharingPermissions.None);
+ }
+
private get sharingInterface() {
+ const existOtherUsers = this.users.length > 0;
return (
<div className={"sharing-interface"}>
<p className={"share-link"}>Manage the public link to {this.focusOn("this document...")}</p>
@@ -247,25 +255,24 @@ export default class SharingManager extends React.Component<{}> {
</div>
<div className={"hr-substitute"} />
<p className={"share-individual"}>Privately share {this.focusOn("this document")} with an individual...</p>
- <div className={"users-list"} style={{ display: this.users.length ? "block" : "flex" }}>
- {!this.users.length ? "There are no other users in your database." :
- this.users.map(user => {
+ <div className={"users-list"} style={{ display: existOtherUsers ? "block" : "flex", minHeight: existOtherUsers ? undefined : 200 }}>
+ {!existOtherUsers ? "There are no other users in your database." :
+ this.users.map(({ user, notificationDoc }) => {
+ const userKey = user.userDocumentId;
+ const permissions = this.computePermissions(userKey);
+ const color = ColorMapping.get(permissions);
return (
<div
- key={user.email}
+ key={userKey}
className={"container"}
>
<select
className={"permissions-dropdown"}
- value={this.sharingDoc ? StrCast(this.sharingDoc[user.userDocumentId], SharingPermissions.None) : SharingPermissions.None}
- style={{
- color: this.sharingDoc ? ColorMapping.get(StrCast(this.sharingDoc[user.userDocumentId], SharingPermissions.None)) : DefaultColor,
- borderColor: this.sharingDoc ? ColorMapping.get(StrCast(this.sharingDoc[user.userDocumentId], SharingPermissions.None)) : DefaultColor
- }}
- onChange={e => this.setInternalSharing(user, e.currentTarget.value)}
+ value={permissions}
+ style={{ color, borderColor: color }}
+ onChange={e => this.setInternalSharing({ user, notificationDoc }, e.currentTarget.value)}
>
{this.sharingOptions}
-
</select>
<span className={"padding"}>{user.email}</span>
</div>
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index a83a3949d..31d98887f 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -10,7 +10,6 @@ import { Doc, Field, Opt } from "../../new_fields/Doc";
import { Id } from "../../new_fields/FieldSymbols";
import { Utils } from "../../Utils";
import { DocServer } from "../DocServer";
-import { CollectionDockingView } from "../views/collections/CollectionDockingView";
import { FieldViewProps } from "../views/nodes/FieldView";
import { FormattedTextBoxProps } from "../views/nodes/FormattedTextBox";
import { DocumentManager } from "./DocumentManager";
@@ -20,6 +19,7 @@ import { schema } from "./RichTextSchema";
import "./TooltipTextMenu.scss";
import { Cast, NumCast } from '../../new_fields/Types';
import { updateBullets } from './ProsemirrorExampleTransfer';
+import { DocumentDecorations } from '../views/DocumentDecorations';
const { toggleMark, setBlockType } = require("prosemirror-commands");
const { openPrompt, TextField } = require("./ProsemirrorCopy/prompt.js");
@@ -28,10 +28,10 @@ export class TooltipTextMenu {
public tooltip: HTMLElement;
private view: EditorView;
+ private editorProps: FieldViewProps & FormattedTextBoxProps | undefined;
private fontStyles: MarkType[];
private fontSizes: MarkType[];
private listTypes: (NodeType | any)[];
- private editorProps: FieldViewProps & FormattedTextBoxProps;
private fontSizeToNum: Map<MarkType, number>;
private fontStylesToName: Map<MarkType, string>;
private listTypeToIcon: Map<NodeType | any, string>;
@@ -59,9 +59,8 @@ export class TooltipTextMenu {
private _collapsed: boolean = false;
- constructor(view: EditorView, editorProps: FieldViewProps & FormattedTextBoxProps) {
+ constructor(view: EditorView) {
this.view = view;
- this.editorProps = editorProps;
this.wrapper = document.createElement("div");
this.tooltip = document.createElement("div");
@@ -120,10 +119,10 @@ export class TooltipTextMenu {
//pointer down handler to activate button effects
dom.addEventListener("pointerdown", e => {
e.preventDefault();
- view.focus();
+ this.view.focus();
if (dom.contains(e.target as Node)) {
e.stopPropagation();
- command(view.state, view.dispatch, view);
+ command(this.view.state, this.view.dispatch, this.view);
// if (this.view.state.selection.empty) {
// if (dom.style.color === "white") { dom.style.color = "greenyellow"; }
// else { dom.style.color = "white"; }
@@ -188,12 +187,10 @@ export class TooltipTextMenu {
this.updateListItemDropdown(":", this.listTypeBtnDom);
- this.update(view, undefined);
-
- // add tooltip to outerdiv to circumvent scaling problem
- const outer_div = this.editorProps.outer_div;
- outer_div && outer_div(this.wrapper);
+ this.updateFromDash(view, undefined, undefined);
+ TooltipTextMenu.Toolbar = this.wrapper;
}
+ public static Toolbar: HTMLDivElement | undefined;
//label of dropdown will change to given label
updateFontSizeDropdown(label: string) {
@@ -275,7 +272,7 @@ export class TooltipTextMenu {
if (DocumentManager.Instance.getDocumentView(f)) {
DocumentManager.Instance.getDocumentView(f)!.props.focus(f, false);
}
- else this.editorProps.addDocTab(f, undefined, "onRight");
+ else this.editorProps && this.editorProps.addDocTab(f, undefined, "onRight");
}
}));
}
@@ -293,6 +290,7 @@ export class TooltipTextMenu {
this.linkDrag.style.background = "black";
this.linkDrag.style.cssFloat = "left";
this.linkDrag.onpointerdown = (e: PointerEvent) => {
+ if (!this.editorProps) return;
let dragData = new DragManager.LinkDragData(this.editorProps.Document);
dragData.dontClearTextBox = true;
// hack to get source context -sy
@@ -442,7 +440,7 @@ export class TooltipTextMenu {
let tr = state.tr;
tr.addMark(state.selection.from, state.selection.to, mark);
let content = tr.selection.content();
- let newNode = schema.nodes.star.create({ visibility: false, text: content, textslice: content.toJSON() });
+ let newNode = state.schema.nodes.star.create({ visibility: false, text: content, textslice: content.toJSON() });
dispatch && dispatch(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark));
return true;
}
@@ -503,40 +501,35 @@ export class TooltipTextMenu {
if (markType.name[0] === 'p') {
let size = this.fontSizeToNum.get(markType);
if (size) { this.updateFontSizeDropdown(String(size) + " pt"); }
- let ruleProvider = this.editorProps.ruleProvider;
- let heading = NumCast(this.editorProps.Document.heading);
- if (ruleProvider && heading) {
- ruleProvider["ruleSize_" + heading] = size;
+ if (this.editorProps) {
+ let ruleProvider = this.editorProps.ruleProvider;
+ let heading = NumCast(this.editorProps.Document.heading);
+ if (ruleProvider && heading) {
+ ruleProvider["ruleSize_" + heading] = size;
+ }
}
}
else {
let fontName = this.fontStylesToName.get(markType);
if (fontName) { this.updateFontStyleDropdown(fontName); }
- let ruleProvider = this.editorProps.ruleProvider;
- let heading = NumCast(this.editorProps.Document.heading);
- if (ruleProvider && heading) {
- ruleProvider["ruleFont_" + heading] = fontName;
+ if (this.editorProps) {
+ let ruleProvider = this.editorProps.ruleProvider;
+ let heading = NumCast(this.editorProps.Document.heading);
+ if (ruleProvider && heading) {
+ ruleProvider["ruleFont_" + heading] = fontName;
+ }
}
}
//actually apply font
if ((view.state.selection as any).node && (view.state.selection as any).node.type === view.state.schema.nodes.ordered_list) {
- view.dispatch(updateBullets(view.state.tr.setNodeMarkup(view.state.selection.from, (view.state.selection as any).node.type,
- { ...(view.state.selection as NodeSelection).node.attrs, setFontSize: Number(markType.name.replace(/p/, "")) }), view.state.schema));
+ let status = updateBullets(view.state.tr.setNodeMarkup(view.state.selection.from, (view.state.selection as any).node.type,
+ { ...(view.state.selection as NodeSelection).node.attrs, setFontFamily: markType.name, setFontSize: Number(markType.name.replace(/p/, "")) }), view.state.schema);
+ view.dispatch(status.setSelection(new NodeSelection(status.doc.resolve(view.state.selection.from))));
}
else toggleMark(markType)(view.state, view.dispatch, view);
}
}
- updateBullets = (tx2: Transaction, style: string) => {
- tx2.doc.descendants((node: any, offset: any, index: any) => {
- if (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item) {
- let path = (tx2.doc.resolve(offset) as any).path;
- let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty("type") && c.type === schema.nodes.ordered_list ? 1 : 0), 0);
- if (node.type === schema.nodes.ordered_list) depth++;
- tx2.setNodeMarkup(offset, node.type, { mapStyle: style, bulletStyle: depth }, node.marks);
- }
- });
- }
//remove all node typeand apply the passed-in one to the selected text
changeToNodeType = (nodeType: NodeType | undefined, view: EditorView) => {
//remove oldif (nodeType) { //add new
@@ -545,18 +538,18 @@ export class TooltipTextMenu {
} else {
var marks = view.state.storedMarks || (view.state.selection.$to.parentOffset && view.state.selection.$from.marks());
if (!wrapInList(schema.nodes.ordered_list)(view.state, (tx2: any) => {
- this.updateBullets(tx2, (nodeType as any).attrs.mapStyle);
- marks && tx2.ensureMarks([...marks]);
- marks && tx2.setStoredMarks([...marks]);
+ let tx3 = updateBullets(tx2, schema, (nodeType as any).attrs.mapStyle);
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
view.dispatch(tx2);
})) {
let tx2 = view.state.tr;
- this.updateBullets(tx2, (nodeType as any).attrs.mapStyle);
- marks && tx2.ensureMarks([...marks]);
- marks && tx2.setStoredMarks([...marks]);
+ let tx3 = nodeType ? updateBullets(tx2, schema, (nodeType as any).attrs.mapStyle) : tx2;
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
- view.dispatch(tx2);
+ view.dispatch(tx3);
}
}
}
@@ -586,7 +579,7 @@ export class TooltipTextMenu {
class: "summarize",
execEvent: "",
run: (state, dispatch) => {
- TooltipTextMenu.insertStar(state, dispatch);
+ TooltipTextMenu.insertStar(this.view.state, this.view.dispatch);
}
});
@@ -848,9 +841,17 @@ export class TooltipTextMenu {
}
}
+ update(view: EditorView, lastState: EditorState | undefined) { this.updateFromDash(view, lastState, this.editorProps); }
//updates the tooltip menu when the selection changes
- update(view: EditorView, lastState: EditorState | undefined) {
+ public updateFromDash(view: EditorView, lastState: EditorState | undefined, props: any) {
+ if (!view) {
+ console.log("no editor? why?");
+ return;
+ }
+ this.view = view;
let state = view.state;
+ DocumentDecorations.Instance.showTextBar();
+ props && (this.editorProps = props);
// Don't do anything if the document/selection didn't change
if (lastState && lastState.doc.eq(state.doc) &&
lastState.selection.eq(state.selection)) return;
diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index 7abb9d1ee..472afac1d 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -73,8 +73,8 @@ export namespace UndoManager {
}
type UndoBatch = UndoEvent[];
- let undoStack: UndoBatch[] = observable([]);
- let redoStack: UndoBatch[] = observable([]);
+ export let undoStack: UndoBatch[] = observable([]);
+ export let redoStack: UndoBatch[] = observable([]);
let currentBatch: UndoBatch | undefined;
let batchCounter = 0;
let undoing = false;
diff --git a/src/client/views/CollectionLinearView.scss b/src/client/views/CollectionLinearView.scss
new file mode 100644
index 000000000..4423a7020
--- /dev/null
+++ b/src/client/views/CollectionLinearView.scss
@@ -0,0 +1,73 @@
+@import "globalCssVariables";
+@import "nodeModuleOverrides";
+
+.collectionLinearView-outer{
+ overflow: hidden;
+ height:100%;
+ .collectionLinearView {
+ display:flex;
+ height: 100%;
+ >label {
+ background: $dark-color;
+ color: $light-color;
+ display: inline-block;
+ border-radius: 18px;
+ font-size: 12.5px;
+ width: 18px;
+ height: 18px;
+ margin-top:auto;
+ margin-bottom:auto;
+ cursor: pointer;
+ transition: transform 0.2s;
+ }
+
+ label p {
+ padding-left:5px;
+ }
+
+ label:hover {
+ background: $main-accent;
+ transform: scale(1.15);
+ }
+
+ >input {
+ display: none;
+ }
+ >input:not(:checked)~.collectionLinearView-content {
+ display: none;
+ }
+
+ >input:checked~label {
+ transform: rotate(45deg);
+ transition: transform 0.5s;
+ cursor: pointer;
+ }
+
+ .collectionLinearView-content {
+ display: flex;
+ opacity: 1;
+ position: relative;
+ margin-top: auto;
+
+ .collectionLinearView-docBtn, .collectionLinearView-docBtn-scalable {
+ position:relative;
+ margin-top: auto;
+ margin-bottom: auto;
+ }
+ .collectionLinearView-docBtn-scalable:hover {
+ transform: scale(1.15);
+ }
+
+ .collectionLinearView-round-button {
+ width: 18px;
+ height: 18px;
+ border-radius: 18px;
+ font-size: 15px;
+ }
+
+ .collectionLinearView-round-button:hover {
+ transform: scale(1.15);
+ }
+ }
+ }
+}
diff --git a/src/client/views/CollectionLinearView.tsx b/src/client/views/CollectionLinearView.tsx
new file mode 100644
index 000000000..7c6d33d36
--- /dev/null
+++ b/src/client/views/CollectionLinearView.tsx
@@ -0,0 +1,110 @@
+import { action, IReactionDisposer, observable, reaction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { Doc, HeightSym, WidthSym } from '../../new_fields/Doc';
+import { makeInterface } from '../../new_fields/Schema';
+import { BoolCast, NumCast, StrCast } from '../../new_fields/Types';
+import { emptyFunction, returnEmptyString, returnOne, returnTrue, Utils } from '../../Utils';
+import { DragManager } from '../util/DragManager';
+import { Transform } from '../util/Transform';
+import "./CollectionLinearView.scss";
+import { CollectionViewType } from './collections/CollectionView';
+import { CollectionSubView } from './collections/CollectionSubView';
+import { DocumentView } from './nodes/DocumentView';
+import { documentSchema } from '../../new_fields/documentSchemas';
+import { Id } from '../../new_fields/FieldSymbols';
+
+
+type LinearDocument = makeInterface<[typeof documentSchema,]>;
+const LinearDocument = makeInterface(documentSchema);
+
+@observer
+export class CollectionLinearView extends CollectionSubView(LinearDocument) {
+ @observable public addMenuToggle = React.createRef<HTMLInputElement>();
+ private _dropDisposer?: DragManager.DragDropDisposer;
+ private _heightDisposer?: IReactionDisposer;
+ private _spacing = 20;
+
+ componentWillUnmount() {
+ this._dropDisposer && this._dropDisposer();
+ this._heightDisposer && this._heightDisposer();
+ }
+
+ componentDidMount() {
+ // is there any reason this needs to exist? -syip. yes, it handles autoHeight for stacking views (masonry isn't yet supported).
+ this._heightDisposer = reaction(() => NumCast(this.props.Document.height, 0) + this.childDocs.length + (this.props.Document.isExpanded ? 1 : 0),
+ () => this.props.Document.width = 18 + (this.props.Document.isExpanded ? this.childDocs.length * (this.props.Document[HeightSym]()) : 10),
+ { fireImmediately: true }
+ );
+ }
+ protected createDropTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view
+ this._dropDisposer && this._dropDisposer();
+ if (ele) {
+ this._dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
+ }
+ }
+
+ public isCurrent(doc: Doc) { return !doc.isMinimized && (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document.currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); }
+
+ dimension = () => NumCast(this.props.Document.height); // 2 * the padding
+ getTransform = (ele: React.RefObject<HTMLDivElement>) => () => {
+ if (!ele.current) return Transform.Identity();
+ let { scale, translateX, translateY } = Utils.GetScreenTransform(ele.current);
+ return new Transform(-translateX, -translateY, 1 / scale);
+ }
+ render() {
+ let guid = Utils.GenerateGuid();
+ return <div className="collectionLinearView-outer">
+ <div className="collectionLinearView" ref={this.createDropTarget} >
+ <input id={`${guid}`} type="checkbox" checked={BoolCast(this.props.Document.isExpanded)} ref={this.addMenuToggle}
+ onChange={action((e: any) => this.props.Document.isExpanded = this.addMenuToggle.current!.checked)} />
+ <label htmlFor={`${guid}`} style={{ marginTop: "auto", marginBottom: "auto", background: StrCast(this.props.Document.backgroundColor, "black") === StrCast(this.props.Document.color, "white") ? "black" : StrCast(this.props.Document.backgroundColor, "black") }} title="Close Menu"><p>+</p></label>
+
+ <div className="collectionLinearView-content">
+ {this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => {
+ let nested = pair.layout.viewType === CollectionViewType.Linear;
+ let dref = React.createRef<HTMLDivElement>();
+ let nativeWidth = NumCast(pair.layout.nativeWidth, this.dimension());
+ let scalingContent = nested ? 1 : this.dimension() / (this._spacing + nativeWidth);
+ let scalingBox = nested ? 1 : this.dimension() / nativeWidth;
+ let deltaSize = nativeWidth * scalingBox - nativeWidth * scalingContent;
+ return <div className={`collectionLinearView-docBtn` + (pair.layout.onClick || pair.layout.onDragStart ? "-scalable" : "")} key={pair.layout[Id]} ref={dref}
+ style={{
+ width: nested ? pair.layout[WidthSym]() : this.dimension(),
+ height: nested && pair.layout.isExpanded ? pair.layout[HeightSym]() : this.dimension(),
+ transform: nested ? undefined : `translate(${deltaSize / 2}px, ${deltaSize / 2}px)`
+ }} >
+ <DocumentView
+ Document={pair.layout}
+ DataDoc={pair.data}
+ addDocument={this.props.addDocument}
+ moveDocument={this.props.moveDocument}
+ addDocTab={this.props.addDocTab}
+ pinToPres={emptyFunction}
+ removeDocument={this.props.removeDocument}
+ ruleProvider={undefined}
+ onClick={undefined}
+ ScreenToLocalTransform={this.getTransform(dref)}
+ ContentScaling={() => scalingContent} // ugh - need to get rid of this inline function to avoid recomputing
+ PanelWidth={() => nested ? pair.layout[WidthSym]() : this.dimension()}
+ PanelHeight={() => nested ? pair.layout[HeightSym]() : this.dimension()}
+ renderDepth={this.props.renderDepth + 1}
+ focus={emptyFunction}
+ backgroundColor={returnEmptyString}
+ parentActive={returnTrue}
+ whenActiveChanged={emptyFunction}
+ bringToFront={emptyFunction}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
+ zoomToScale={emptyFunction}
+ getScale={returnOne}>
+ </DocumentView>
+ </div>;
+ })}
+ {/* <li key="undoTest"><button className="add-button round-button" title="Click if undo isn't working" onClick={() => UndoManager.TraceOpenBatches()}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li> */}
+
+ </div>
+ </div>
+ </div>;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/DictationOverlay.tsx b/src/client/views/DictationOverlay.tsx
new file mode 100644
index 000000000..2accf9bfd
--- /dev/null
+++ b/src/client/views/DictationOverlay.tsx
@@ -0,0 +1,71 @@
+import { computed, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import "normalize.css";
+import * as React from 'react';
+import { DictationManager } from '../util/DictationManager';
+import "./Main.scss";
+import MainViewModal from './MainViewModal';
+
+@observer
+export class DictationOverlay extends React.Component {
+ public static Instance: DictationOverlay;
+ @observable private _dictationState = DictationManager.placeholder;
+ @observable private _dictationSuccessState: boolean | undefined = undefined;
+ @observable private _dictationDisplayState = false;
+ @observable private _dictationListeningState: DictationManager.Controls.ListeningUIStatus = false;
+
+ public isPointerDown = false;
+ public overlayTimeout: NodeJS.Timeout | undefined;
+ public hasActiveModal = false;
+
+ constructor(props: any) {
+ super(props);
+ DictationOverlay.Instance = this;
+ }
+
+ public initiateDictationFade = () => {
+ let duration = DictationManager.Commands.dictationFadeDuration;
+ this.overlayTimeout = setTimeout(() => {
+ this.dictationOverlayVisible = false;
+ this.dictationSuccess = undefined;
+ DictationOverlay.Instance.hasActiveModal = false;
+ setTimeout(() => this.dictatedPhrase = DictationManager.placeholder, 500);
+ }, duration);
+ }
+ public cancelDictationFade = () => {
+ if (this.overlayTimeout) {
+ clearTimeout(this.overlayTimeout);
+ this.overlayTimeout = undefined;
+ }
+ }
+
+ @computed public get dictatedPhrase() { return this._dictationState; }
+ @computed public get dictationSuccess() { return this._dictationSuccessState; }
+ @computed public get dictationOverlayVisible() { return this._dictationDisplayState; }
+ @computed public get isListening() { return this._dictationListeningState; }
+
+ public set dictatedPhrase(value: string) { runInAction(() => this._dictationState = value); }
+ public set dictationSuccess(value: boolean | undefined) { runInAction(() => this._dictationSuccessState = value); }
+ public set dictationOverlayVisible(value: boolean) { runInAction(() => this._dictationDisplayState = value); }
+ public set isListening(value: DictationManager.Controls.ListeningUIStatus) { runInAction(() => this._dictationListeningState = value); }
+
+ render() {
+ let success = this.dictationSuccess;
+ let result = this.isListening && !this.isListening.interim ? DictationManager.placeholder : `"${this.dictatedPhrase}"`;
+ let dialogueBoxStyle = {
+ background: success === undefined ? "gainsboro" : success ? "lawngreen" : "red",
+ borderColor: this.isListening ? "red" : "black",
+ fontStyle: "italic"
+ };
+ let overlayStyle = {
+ backgroundColor: this.isListening ? "red" : "darkslategrey"
+ };
+ return (<MainViewModal
+ contents={result}
+ isDisplayed={this.dictationOverlayVisible}
+ interactive={false}
+ dialogueBoxStyle={dialogueBoxStyle}
+ overlayStyle={overlayStyle}
+ />);
+ }
+} \ No newline at end of file
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index d6562492f..ae4b7cf3a 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -1,14 +1,88 @@
import * as React from 'react';
import { Doc } from '../../new_fields/Doc';
-import { computed } from 'mobx';
+import { computed, action } from 'mobx';
+import { Cast } from '../../new_fields/Types';
+import { listSpec } from '../../new_fields/Schema';
+import { InkingControl } from './InkingControl';
+import { InkTool } from '../../new_fields/InkField';
+import { PositionDocument } from '../../new_fields/documentSchemas';
-export function DocComponent<P extends { Document: Doc }, T>(schemaCtor: (doc: Doc) => T) {
+
+/// DocComponent returns a generic React base class used by views that don't have any data extensions (e.g.,CollectionFreeFormDocumentView, DocumentView, ButtonBox)
+interface DocComponentProps {
+ Document: Doc;
+}
+export function DocComponent<P extends DocComponentProps, T>(schemaCtor: (doc: Doc) => T) {
+ class Component extends React.Component<P> {
+ //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
+ @computed get Document(): T { return schemaCtor(this.props.Document); }
+ @computed get layoutDoc() { return PositionDocument(Doc.Layout(this.props.Document)); }
+ }
+ return Component;
+}
+
+/// DocStaticProps return a base class for React document views that have data extensions but aren't annotatable (e.g. AudioBox, FormattedTextBox)
+interface DocExtendableProps {
+ Document: Doc;
+ DataDoc?: Doc;
+ fieldKey: string;
+ isSelected: () => boolean;
+ renderDepth: number;
+}
+export function DocExtendableComponent<P extends DocExtendableProps, T>(schemaCtor: (doc: Doc) => T) {
+ class Component extends React.Component<P> {
+ //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
+ @computed get Document(): T { return schemaCtor(this.props.Document); }
+ @computed get layoutDoc() { return Doc.Layout(this.props.Document); }
+ @computed get dataDoc() { return (this.props.DataDoc && this.props.Document.isTemplateField ? this.props.DataDoc : Doc.GetProto(this.props.Document)) as Doc; }
+ @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
+ active = () => !this.props.Document.isBackground && (this.props.Document.forceActive || this.props.isSelected() || this.props.renderDepth === 0);// && !InkingControl.Instance.selectedTool; // bcz: inking state shouldn't affect static tools
+ }
+ return Component;
+}
+
+
+/// DocAnnotatbleComponent return a base class for React views of document fields that are annotatable *and* interactive when selected (e.g., pdf, image)
+interface DocAnnotatableProps {
+ Document: Doc;
+ DataDoc?: Doc;
+ fieldKey: string;
+ whenActiveChanged: (isActive: boolean) => void;
+ isSelected: () => boolean;
+ renderDepth: number;
+}
+export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schemaCtor: (doc: Doc) => T) {
class Component extends React.Component<P> {
+ _isChildActive = false;
//TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
- @computed
- get Document(): T {
- return schemaCtor(this.props.Document);
+ @computed get Document(): T { return schemaCtor(this.props.Document); }
+ @computed get layoutDoc() { return Doc.Layout(this.props.Document); }
+ @computed get dataDoc() { return (this.props.DataDoc && this.props.Document.isTemplateField ? this.props.DataDoc : Doc.GetProto(this.props.Document)) as Doc; }
+ @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
+ @computed get annotationsKey() { return "annotations"; }
+
+ @action.bound
+ removeDocument(doc: Doc): boolean {
+ Doc.GetProto(doc).annotationOn = undefined;
+ let value = this.extensionDoc && Cast(this.extensionDoc[this.annotationsKey], listSpec(Doc), []);
+ let index = value ? Doc.IndexOf(doc, value.map(d => d as Doc), true) : -1;
+ return index !== -1 && value && value.splice(index, 1) ? true : false;
+ }
+ // if the moved document is already in this overlay collection nothing needs to be done.
+ // otherwise, if the document can be removed from where it was, it will then be added to this document's overlay collection.
+ @action.bound
+ moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean {
+ return Doc.AreProtosEqual(this.props.Document, targetCollection) ? true : this.removeDocument(doc) ? addDocument(doc) : false;
}
+ @action.bound
+ addDocument(doc: Doc): boolean {
+ Doc.GetProto(doc).annotationOn = this.props.Document;
+ return this.extensionDoc && Doc.AddDocToList(this.extensionDoc, this.annotationsKey, doc) ? true : false;
+ }
+
+ whenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive);
+ active = () => ((InkingControl.Instance.selectedTool === InkTool.None && !this.props.Document.isBackground) &&
+ (this.props.Document.forceActive || this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0) ? true : false)
}
return Component;
} \ No newline at end of file
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 338f6b83e..ba87ecfb4 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -6,7 +6,6 @@ import { observer } from "mobx-react";
import { Doc } from "../../new_fields/Doc";
import { RichTextField } from '../../new_fields/RichTextField';
import { NumCast } from "../../new_fields/Types";
-import { URLField } from '../../new_fields/URLField';
import { emptyFunction } from "../../Utils";
import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils';
import { DragLinksAsDocuments, DragManager } from "../util/DragManager";
@@ -45,10 +44,9 @@ const fetch: IconProp = "sync-alt";
export class DocumentButtonBar extends React.Component<{ views: DocumentView[], stack?: any }, {}> {
private _linkButton = React.createRef<HTMLDivElement>();
private _linkerButton = React.createRef<HTMLDivElement>();
- private _embedButton = React.createRef<HTMLDivElement>();
+ private _aliasButton = React.createRef<HTMLDivElement>();
private _tooltipoff = React.createRef<HTMLDivElement>();
private _textDoc?: Doc;
- private _linkDrag?: UndoManager.Batch;
public static Instance: DocumentButtonBar;
constructor(props: { views: DocumentView[] }) {
@@ -111,13 +109,13 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
document.addEventListener("pointerup", this.onLinkerButtonUp);
}
- onEmbedButtonDown = (e: React.PointerEvent): void => {
+ onAliasButtonDown = (e: React.PointerEvent): void => {
e.stopPropagation();
e.preventDefault();
- document.removeEventListener("pointermove", this.onEmbedButtonMoved);
- document.addEventListener("pointermove", this.onEmbedButtonMoved);
- document.removeEventListener("pointerup", this.onEmbedButtonUp);
- document.addEventListener("pointerup", this.onEmbedButtonUp);
+ document.removeEventListener("pointermove", this.onAliasButtonMoved);
+ document.addEventListener("pointermove", this.onAliasButtonMoved);
+ document.removeEventListener("pointerup", this.onAliasButtonUp);
+ document.addEventListener("pointerup", this.onAliasButtonUp);
}
onLinkerButtonUp = (e: PointerEvent): void => {
@@ -126,9 +124,9 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
e.stopPropagation();
}
- onEmbedButtonUp = (e: PointerEvent): void => {
- document.removeEventListener("pointermove", this.onEmbedButtonMoved);
- document.removeEventListener("pointerup", this.onEmbedButtonUp);
+ onAliasButtonUp = (e: PointerEvent): void => {
+ document.removeEventListener("pointermove", this.onAliasButtonMoved);
+ document.removeEventListener("pointerup", this.onAliasButtonUp);
e.stopPropagation();
}
@@ -140,16 +138,10 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
let selDoc = this.props.views[0];
let container = selDoc.props.ContainingCollectionDoc ? selDoc.props.ContainingCollectionDoc.proto : undefined;
let dragData = new DragManager.LinkDragData(selDoc.props.Document, container ? [container] : []);
- FormattedTextBox.InputBoxOverlay = undefined;
- this._linkDrag = UndoManager.StartBatch("Drag Link");
+ let _linkDrag = UndoManager.StartBatch("Drag Link");
DragManager.StartLinkDrag(this._linkerButton.current, dragData, e.pageX, e.pageY, {
handlers: {
- dragComplete: () => {
- if (this._linkDrag) {
- this._linkDrag.end();
- this._linkDrag = undefined;
- }
- },
+ dragComplete: () => _linkDrag && _linkDrag.end()
},
hideSource: false
});
@@ -158,15 +150,20 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
}
@action
- onEmbedButtonMoved = (e: PointerEvent): void => {
- if (this._embedButton.current !== null) {
- document.removeEventListener("pointermove", this.onEmbedButtonMoved);
- document.removeEventListener("pointerup", this.onEmbedButtonUp);
+ onAliasButtonMoved = (e: PointerEvent): void => {
+ if (this._aliasButton.current !== null) {
+ document.removeEventListener("pointermove", this.onAliasButtonMoved);
+ document.removeEventListener("pointerup", this.onAliasButtonUp);
let dragDocView = this.props.views[0];
- let dragData = new DragManager.EmbedDragData(dragDocView.props.Document);
-
- DragManager.StartEmbedDrag(dragDocView.ContentDiv!, dragData, e.x, e.y, {
+ let dragData = new DragManager.DocumentDragData([dragDocView.props.Document]);
+ const [left, top] = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).inverse().transformPoint(0, 0);
+ dragData.offset = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).transformDirection(e.clientX - left, e.clientY - top);
+ dragData.embedDoc = true;
+ dragData.dropAction = "alias";
+ DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, e.x, e.y, {
+ offsetX: dragData.offset[0],
+ offsetY: dragData.offset[1],
handlers: {
dragComplete: action(emptyFunction),
},
@@ -200,17 +197,12 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
e.stopPropagation();
}
- considerEmbed = () => {
- let thisDoc = this.props.views[0].props.Document;
- let canEmbed = thisDoc.data && thisDoc.data instanceof URLField;
- if (!canEmbed) return (null);
- return (
- <div className="linkButtonWrapper">
- <div title="Drag Embed" className="linkButton-linker" ref={this._embedButton} onPointerDown={this.onEmbedButtonDown}>
- <FontAwesomeIcon className="documentdecorations-icon" icon="image" size="sm" />
- </div>
+ aliasDragger = () => {
+ return (<div className="linkButtonWrapper">
+ <div title="Drag Alias" className="linkButton-linker" ref={this._aliasButton} onPointerDown={this.onAliasButtonDown}>
+ <FontAwesomeIcon className="documentdecorations-icon" icon="image" size="sm" />
</div>
- );
+ </div>);
}
private get targetDoc() {
@@ -354,7 +346,7 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
<TemplateMenu docs={this.props.views} templates={templates} />
</div>
{this.metadataMenu}
- {this.considerEmbed()}
+ {this.aliasDragger()}
{this.considerGoogleDocsPush()}
{this.considerGoogleDocsPull()}
<ParentDocSelector Document={this.props.views[0].props.Document} addDocTab={(doc, data, where) => {
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 9d42eb719..b46caf3ea 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -4,24 +4,24 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { action, computed, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
import { Doc, DocListCastAsync } from "../../new_fields/Doc";
+import { PositionDocument } from '../../new_fields/documentSchemas';
import { List } from "../../new_fields/List";
import { ObjectField } from '../../new_fields/ObjectField';
-import { BoolCast, Cast, NumCast, StrCast } from "../../new_fields/Types";
+import { Cast, NumCast, StrCast } from "../../new_fields/Types";
import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
-import { emptyFunction, Utils } from "../../Utils";
+import { Utils } from "../../Utils";
import { Docs, DocUtils } from "../documents/Documents";
import { DocumentManager } from "../util/DocumentManager";
import { DragManager } from "../util/DragManager";
import { SelectionManager } from "../util/SelectionManager";
+import { TooltipTextMenu } from '../util/TooltipTextMenu';
import { undoBatch, UndoManager } from "../util/UndoManager";
import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss";
import { CollectionView } from "./collections/CollectionView";
import { DocumentButtonBar } from './DocumentButtonBar';
import './DocumentDecorations.scss';
-import { PositionDocument } from './nodes/CollectionFreeFormDocumentView';
-import { DocumentView, swapViews } from "./nodes/DocumentView";
+import { DocumentView } from "./nodes/DocumentView";
import { FieldView } from "./nodes/FieldView";
-import { FormattedTextBox } from "./nodes/FormattedTextBox";
import { IconBox } from "./nodes/IconBox";
import React = require("react");
const higflyout = require("@hig/flyout");
@@ -48,7 +48,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
private _resizeBorderWidth = 16;
private _linkBoxHeight = 20 + 3; // link button height + margin
private _titleHeight = 20;
- private _embedButton = React.createRef<HTMLDivElement>();
private _downX = 0;
private _downY = 0;
private _iconDoc?: Doc = undefined;
@@ -107,11 +106,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
fieldTemplate.title = metaKey;
Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(docTemplate));
if (text.startsWith(">>")) {
- let layoutNative = Doc.MakeTitled("layoutNative");
- Doc.GetProto(docTemplate).layoutNative = layoutNative;
- swapViews(fieldTemplate, "", "layoutNative", layoutNative);
- layoutNative.layout = StrCast(fieldTemplateView.props.Document.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
- layoutNative.backgroundLayout = StrCast(fieldTemplateView.props.Document.backgroundLayout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
+ Doc.GetProto(docTemplate).layout = StrCast(fieldTemplateView.props.Document.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
}
}
}
@@ -202,6 +197,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
const [left, top] = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).inverse().transformPoint(0, 0);
dragData.offset = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).transformDirection(e.x - left, e.y - top);
dragData.moveDocument = SelectionManager.SelectedDocuments()[0].props.moveDocument;
+ dragData.isSelectionMove = true;
this.Interacting = true;
this._hidden = true;
document.removeEventListener("pointermove", this.onBackgroundMove);
@@ -284,7 +280,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
let selectedDocs = SelectionManager.SelectedDocuments().map(sd => sd);
if (selectedDocs.length > 1) {
- this._iconDoc = this._iconDoc ? this._iconDoc : this.createIcon(SelectionManager.SelectedDocuments(), CollectionView.LayoutString());
+ this._iconDoc = this._iconDoc ? this._iconDoc : this.createIcon(SelectionManager.SelectedDocuments(), CollectionView.LayoutString(""));
this.moveIconDoc(this._iconDoc);
} else {
this.getIconDoc(selectedDocs[0]).then(icon => icon && this.moveIconDoc(this._iconDoc = icon));
@@ -333,7 +329,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
iconDoc.y = NumCast(doc.y) - 24;
iconDoc.maximizedDocs = new List<Doc>(selected.map(s => s.props.Document));
selected.length === 1 && (doc.minimizedDoc = iconDoc);
- selected[0].props.addDocument && selected[0].props.addDocument(iconDoc, false);
+ selected[0].props.addDocument && selected[0].props.addDocument(iconDoc);
return iconDoc;
}
@action
@@ -342,7 +338,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
let iconDoc: Doc | undefined = await Cast(doc.minimizedDoc, Doc);
if (!iconDoc || !DocumentManager.Instance.getDocumentView(iconDoc)) {
- const layout = StrCast(doc.backgroundLayout, StrCast(doc.layout, FieldView.LayoutString(DocumentView)));
+ const layout = StrCast(doc.layout, FieldView.LayoutString(DocumentView, ""));
iconDoc = this.createIcon([docView], layout);
}
return iconDoc;
@@ -379,7 +375,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
ruleProvider && heading && (Doc.GetProto(ruleProvider)["ruleRounding_" + heading] = `${Math.min(100, dist)}%`);
usingRule = usingRule || (ruleProvider && heading ? true : false);
});
- !usingRule && SelectionManager.SelectedDocuments().map(dv => dv.props.Document.layout instanceof Doc ? dv.props.Document.layout : dv.props.Document.isTemplate ? dv.props.Document : Doc.GetProto(dv.props.Document)).
+ !usingRule && SelectionManager.SelectedDocuments().map(dv => dv.props.Document.layout instanceof Doc ? dv.props.Document.layout : dv.props.Document.isTemplateField ? dv.props.Document : Doc.GetProto(dv.props.Document)).
map(d => d.borderRounding = `${Math.min(100, dist)}%`);
e.stopPropagation();
e.preventDefault();
@@ -414,41 +410,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
}
- onEmbedButtonDown = (e: React.PointerEvent): void => {
- e.stopPropagation();
- document.removeEventListener("pointermove", this.onEmbedButtonMoved);
- document.addEventListener("pointermove", this.onEmbedButtonMoved);
- document.removeEventListener("pointerup", this.onEmbedButtonUp);
- document.addEventListener("pointerup", this.onEmbedButtonUp);
- }
-
-
-
- onEmbedButtonUp = (e: PointerEvent): void => {
- document.removeEventListener("pointermove", this.onEmbedButtonMoved);
- document.removeEventListener("pointerup", this.onEmbedButtonUp);
- e.stopPropagation();
- }
-
- @action
- onEmbedButtonMoved = (e: PointerEvent): void => {
- if (this._embedButton.current !== null) {
- document.removeEventListener("pointermove", this.onEmbedButtonMoved);
- document.removeEventListener("pointerup", this.onEmbedButtonUp);
-
- let dragDocView = SelectionManager.SelectedDocuments()[0];
- let dragData = new DragManager.EmbedDragData(dragDocView.props.Document);
-
- DragManager.StartEmbedDrag(dragDocView.ContentDiv!, dragData, e.x, e.y, {
- handlers: {
- dragComplete: action(emptyFunction),
- },
- hideSource: false
- });
- }
- e.stopPropagation();
- }
-
onPointerMove = (e: PointerEvent): void => {
e.stopPropagation();
e.preventDefault();
@@ -502,51 +463,50 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
break;
}
- if (!this._resizing) runInAction(() => FormattedTextBox.InputBoxOverlay = undefined);
SelectionManager.SelectedDocuments().forEach(element => {
if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) {
let doc = PositionDocument(element.props.Document);
- let nwidth = doc.nativeWidth || 0;
- let nheight = doc.nativeHeight || 0;
- let width = (doc.width || 0);
- let height = (doc.height || (nheight / nwidth * width));
+ let layoutDoc = PositionDocument(Doc.Layout(element.props.Document));
+ let nwidth = layoutDoc.nativeWidth || 0;
+ let nheight = layoutDoc.nativeHeight || 0;
+ let width = (layoutDoc.width || 0);
+ let height = (layoutDoc.height || (nheight / nwidth * width));
let scale = element.props.ScreenToLocalTransform().Scale * element.props.ContentScaling();
let actualdW = Math.max(width + (dW * scale), 20);
let actualdH = Math.max(height + (dH * scale), 20);
doc.x = (doc.x || 0) + dX * (actualdW - width);
doc.y = (doc.y || 0) + dY * (actualdH - height);
- let proto = doc.isTemplate ? doc : Doc.GetProto(element.props.Document); // bcz: 'doc' didn't work here...
- let fixedAspect = e.ctrlKey || (!doc.ignoreAspect && nwidth && nheight);
- if (fixedAspect && e.ctrlKey && doc.ignoreAspect) {
- doc.ignoreAspect = false;
- proto.nativeWidth = nwidth = doc.width || 0;
- proto.nativeHeight = nheight = doc.height || 0;
+ let fixedAspect = e.ctrlKey || (!layoutDoc.ignoreAspect && nwidth && nheight);
+ if (fixedAspect && e.ctrlKey && layoutDoc.ignoreAspect) {
+ layoutDoc.ignoreAspect = false;
+ layoutDoc.nativeWidth = nwidth = layoutDoc.width || 0;
+ layoutDoc.nativeHeight = nheight = layoutDoc.height || 0;
}
if (fixedAspect && (!nwidth || !nheight)) {
- proto.nativeWidth = nwidth = doc.width || 0;
- proto.nativeHeight = nheight = doc.height || 0;
+ layoutDoc.nativeWidth = nwidth = layoutDoc.width || 0;
+ layoutDoc.nativeHeight = nheight = layoutDoc.height || 0;
}
- if (nwidth > 0 && nheight > 0 && !BoolCast(doc.ignoreAspect)) {
+ if (nwidth > 0 && nheight > 0 && !layoutDoc.ignoreAspect) {
if (Math.abs(dW) > Math.abs(dH)) {
if (!fixedAspect) {
- Doc.SetInPlace(element.props.Document, "nativeWidth", actualdW / (doc.width || 1) * (doc.nativeWidth || 0), true);
+ layoutDoc.nativeWidth = actualdW / (layoutDoc.width || 1) * (layoutDoc.nativeWidth || 0);
}
- doc.width = actualdW;
- if (fixedAspect && !doc.fitWidth) doc.height = nheight / nwidth * doc.width;
- else doc.height = actualdH;
+ layoutDoc.width = actualdW;
+ if (fixedAspect && !layoutDoc.fitWidth) layoutDoc.height = nheight / nwidth * layoutDoc.width;
+ else layoutDoc.height = actualdH;
}
else {
if (!fixedAspect) {
- Doc.SetInPlace(element.props.Document, "nativeHeight", actualdH / (doc.height || 1) * (doc.nativeHeight || 0), true);
+ layoutDoc.nativeHeight = actualdH / (layoutDoc.height || 1) * (doc.nativeHeight || 0);
}
- doc.height = actualdH;
- if (fixedAspect && !doc.fitWidth) doc.width = nwidth / nheight * doc.height;
- else doc.width = actualdW;
+ layoutDoc.height = actualdH;
+ if (fixedAspect && !layoutDoc.fitWidth) layoutDoc.width = nwidth / nheight * layoutDoc.height;
+ else layoutDoc.width = actualdW;
}
} else {
- dW && (doc.width = actualdW);
- dH && (doc.height = actualdH);
- dH && element.props.Document.autoHeight && Doc.SetInPlace(element.props.Document, "autoHeight", false, true);
+ dW && (layoutDoc.width = actualdW);
+ dH && (layoutDoc.height = actualdH);
+ dH && layoutDoc.autoHeight && (layoutDoc.autoHeight = false);
}
}
});
@@ -582,6 +542,17 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
return "-unset-";
}
+ TextBar: HTMLDivElement | undefined;
+ private setTextBar = (ele: HTMLDivElement) => {
+ if (ele) {
+ this.TextBar = ele;
+ }
+ }
+ public showTextBar = () => {
+ if (this.TextBar) {
+ TooltipTextMenu.Toolbar && Array.from(this.TextBar.childNodes).indexOf(TooltipTextMenu.Toolbar) === -1 && this.TextBar.appendChild(TooltipTextMenu.Toolbar);
+ }
+ }
render() {
var bounds = this.Bounds;
let seldoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined;
@@ -614,7 +585,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
zIndex: SelectionManager.SelectedDocuments().length > 1 ? 900 : 0,
}} onPointerDown={this.onBackgroundDown} onContextMenu={(e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); }} >
</div>
- <div className="documentDecorations-container" style={{
+ <div className="documentDecorations-container" ref={this.setTextBar} style={{
width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px",
height: (bounds.b - bounds.y + this._resizeBorderWidth + this._linkBoxHeight + this._titleHeight + 3) + "px",
left: bounds.x - this._resizeBorderWidth / 2,
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index e9db4b048..8e86f58ee 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -4,6 +4,7 @@ import { observable, action, trace } from 'mobx';
import "./EditableView.scss";
import * as Autosuggest from 'react-autosuggest';
import { undoBatch } from '../util/UndoManager';
+import { SchemaHeaderField } from '../../new_fields/SchemaHeaderField';
export interface EditableProps {
/**
@@ -42,6 +43,10 @@ export interface EditableProps {
editing?: boolean;
onClick?: (e: React.MouseEvent) => boolean;
isEditingCallback?: (isEditing: boolean) => void;
+ HeadingObject?: SchemaHeaderField | undefined;
+ HeadingsHack?: number;
+ toggle?: () => void;
+ color?: string | undefined;
}
/**
@@ -52,6 +57,7 @@ export interface EditableProps {
@observer
export class EditableView extends React.Component<EditableProps> {
@observable _editing: boolean = false;
+ @observable _headingsHack: number = 1;
constructor(props: EditableProps) {
super(props);
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index c519991a5..9ca9fc163 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -7,6 +7,11 @@ import { action, runInAction } from "mobx";
import { Doc } from "../../new_fields/Doc";
import { DictationManager } from "../util/DictationManager";
import SharingManager from "../util/SharingManager";
+import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
+import { Cast, PromiseValue } from "../../new_fields/Types";
+import { ScriptField } from "../../new_fields/ScriptField";
+import { InkingControl } from "./InkingControl";
+import { InkTool } from "../../new_fields/InkField";
const modifiers = ["control", "meta", "shift", "alt"];
type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise<KeyControlInfo>;
@@ -61,6 +66,7 @@ export default class KeyManager {
switch (keyname) {
case "escape":
let main = MainView.Instance;
+ InkingControl.Instance.switchTool(InkTool.None);
if (main.isPointerDown) {
DragManager.AbortDrag();
} else {
@@ -70,7 +76,6 @@ export default class KeyManager {
SelectionManager.DeselectAll();
}
}
- main.toggleColorPicker(true);
SelectionManager.DeselectAll();
DictationManager.Controls.stop();
SharingManager.Instance.close();
@@ -120,10 +125,10 @@ export default class KeyManager {
let preventDefault = true;
switch (keyname) {
- case "n":
- let toggle = MainView.Instance.addMenuToggle.current!;
- toggle.checked = !toggle.checked;
- break;
+ // case "n":
+ // let toggle = MainView.Instance.addMenuToggle.current!;
+ // toggle.checked = !toggle.checked;
+ // break;
}
return {
@@ -160,8 +165,29 @@ export default class KeyManager {
}
}
break;
+ case "c":
+ PromiseValue(Cast(CurrentUserUtils.UserDocument.Create, Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv }));
+ if (MainView.Instance.flyoutWidth === 240) {
+ MainView.Instance.flyoutWidth = 0;
+ } else {
+ MainView.Instance.flyoutWidth = 240;
+ }
+ break;
+ case "l":
+ PromiseValue(Cast(CurrentUserUtils.UserDocument.Library, Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv }));
+ if (MainView.Instance.flyoutWidth === 250) {
+ MainView.Instance.flyoutWidth = 0;
+ } else {
+ MainView.Instance.flyoutWidth = 250;
+ }
+ break;
case "f":
- MainView.Instance.isSearchVisible = !MainView.Instance.isSearchVisible;
+ PromiseValue(Cast(CurrentUserUtils.UserDocument.Search, Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv }));
+ if (MainView.Instance.flyoutWidth === 400) {
+ MainView.Instance.flyoutWidth = 0;
+ } else {
+ MainView.Instance.flyoutWidth = 400;
+ }
break;
case "o":
let target = SelectionManager.SelectedDocuments()[0];
diff --git a/src/client/views/InkingCanvas.scss b/src/client/views/InkingCanvas.scss
index 9cc220a1d..8f32652ed 100644
--- a/src/client/views/InkingCanvas.scss
+++ b/src/client/views/InkingCanvas.scss
@@ -8,7 +8,8 @@
position: absolute;
width: 100%;
height: 100%;
- z-index: -1; // allows annotations to appear on videos when screen is full-size & ...
+ background: inherit;
+ //z-index: -1; // allows annotations to appear on videos when screen is full-size & ...
}
}
diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx
index 1cfa8d644..0037b95d0 100644
--- a/src/client/views/InkingCanvas.tsx
+++ b/src/client/views/InkingCanvas.tsx
@@ -78,7 +78,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
this.previousState = new Map(this.inkData);
- if (InkingControl.Instance.selectedTool !== InkTool.Eraser) {
+ if (InkingControl.Instance.selectedTool !== InkTool.Eraser && InkingControl.Instance.selectedTool !== InkTool.Scrubber) {
// start the new line, saves a uuid to represent the field of the stroke
this._currentStrokeId = Utils.GenerateGuid();
const data = this.inkData;
@@ -87,7 +87,8 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
color: InkingControl.Instance.selectedColor,
width: InkingControl.Instance.selectedWidth,
tool: InkingControl.Instance.selectedTool,
- page: NumCast(this.props.Document.curPage, -1)
+ displayTimecode: NumCast(this.props.Document.currentTimecode, -1),
+ creationTime: new Date().getTime()
});
this.inkData = data;
}
@@ -120,7 +121,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
onPointerMove = (e: PointerEvent): void => {
e.stopPropagation();
e.preventDefault();
- if (InkingControl.Instance.selectedTool !== InkTool.Eraser) {
+ if (InkingControl.Instance.selectedTool !== InkTool.Eraser && InkingControl.Instance.selectedTool !== InkTool.Scrubber) {
let data = this.inkData; // add points to new line as it is being drawn
let strokeData = data.get(this._currentStrokeId);
if (strokeData) {
@@ -150,9 +151,9 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
@computed
get drawnPaths() {
- let curPage = NumCast(this.props.Document.curPage, -1);
+ let curTimecode = NumCast(this.props.Document.currentTimecode, -1);
let paths = Array.from(this.inkData).reduce((paths, [id, strokeData]) => {
- if (strokeData.page === -1 || (Math.abs(Math.round(strokeData.page) - Math.round(curPage)) < 3)) {
+ if (strokeData.displayTimecode === -1 || (Math.abs(Math.round(strokeData.displayTimecode) - Math.round(curTimecode)) < 3)) {
paths.push(<InkingStroke key={id} id={id}
line={strokeData.pathData}
count={strokeData.pathData.length}
@@ -161,6 +162,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
color={strokeData.color}
width={strokeData.width}
tool={strokeData.tool}
+ creationTime={strokeData.creationTime}
deleteCallback={this.removeLine} />);
}
return paths;
@@ -181,9 +183,11 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
render() {
let svgCanvasStyle = InkingControl.Instance.selectedTool !== InkTool.None && !this.props.Document.isBackground ? "canSelect" : "noSelect";
+ let cursor = svgCanvasStyle === "canSelect" ? (InkingControl.Instance.selectedTool === InkTool.Eraser ||
+ InkingControl.Instance.selectedTool === InkTool.Scrubber ? "pointer" : "default") : undefined;
return (
<div className="inkingCanvas">
- <div className={`inkingCanvas-${svgCanvasStyle}`} onPointerDown={this.onPointerDown} />
+ <div className={`inkingCanvas-${svgCanvasStyle}`} onPointerDown={this.onPointerDown} style={{ cursor: cursor }} />
{this.props.children()}
{this.drawnPaths}
</div >
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index a10df0e75..75faa9641 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -1,39 +1,31 @@
-import { observable, action, computed, runInAction } from "mobx";
+import { action, computed, observable } from "mobx";
import { ColorResult } from 'react-color';
-import React = require("react");
-import { observer } from "mobx-react";
-import "./InkingControl.scss";
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faPen, faHighlighter, faEraser, faBan } from '@fortawesome/free-solid-svg-icons';
-import { SelectionManager } from "../util/SelectionManager";
-import { InkTool } from "../../new_fields/InkField";
import { Doc } from "../../new_fields/Doc";
-import { undoBatch, UndoManager } from "../util/UndoManager";
-import { StrCast, NumCast, Cast } from "../../new_fields/Types";
-import { MainOverlayTextBox } from "./MainOverlayTextBox";
-import { listSpec } from "../../new_fields/Schema";
+import { InkTool } from "../../new_fields/InkField";
import { List } from "../../new_fields/List";
+import { listSpec } from "../../new_fields/Schema";
+import { Cast, NumCast, StrCast } from "../../new_fields/Types";
import { Utils } from "../../Utils";
+import { Scripting } from "../util/Scripting";
+import { SelectionManager } from "../util/SelectionManager";
+import { undoBatch, UndoManager } from "../util/UndoManager";
+import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
-library.add(faPen, faHighlighter, faEraser, faBan);
-@observer
-export class InkingControl extends React.Component {
- static Instance: InkingControl = new InkingControl({});
+export class InkingControl {
+ @observable static Instance: InkingControl;
@observable private _selectedTool: InkTool = InkTool.None;
@observable private _selectedColor: string = "rgb(244, 67, 54)";
@observable private _selectedWidth: string = "5";
@observable public _open: boolean = false;
- constructor(props: Readonly<{}>) {
- super(props);
+ constructor() {
InkingControl.Instance = this;
}
- @action
- switchTool = (tool: InkTool): void => {
+ switchTool = action((tool: InkTool): void => {
this._selectedTool = tool;
- }
+ });
decimalToHexString(number: number) {
if (number < 0) {
number = 0xFFFFFFFF + number + 1;
@@ -45,11 +37,21 @@ export class InkingControl extends React.Component {
@undoBatch
switchColor = action((color: ColorResult): void => {
this._selectedColor = color.hex + (color.rgb.a !== undefined ? this.decimalToHexString(Math.round(color.rgb.a * 255)) : "ff");
+
if (InkingControl.Instance.selectedTool === InkTool.None) {
- if (MainOverlayTextBox.Instance.SetColor(color.hex)) return;
let selected = SelectionManager.SelectedDocuments();
let oldColors = selected.map(view => {
- let targetDoc = view.props.Document.layout instanceof Doc ? view.props.Document.layout : view.props.Document.isTemplate ? view.props.Document : Doc.GetProto(view.props.Document);
+ let targetDoc = view.props.Document.dragFactory instanceof Doc ? view.props.Document.dragFactory :
+ view.props.Document.layout instanceof Doc ? view.props.Document.layout :
+ view.props.Document.isTemplateField ? view.props.Document : Doc.GetProto(view.props.Document);
+ let sel = window.getSelection();
+ if (StrCast(targetDoc.layout).indexOf("FormattedTextBox") !== -1 && (!sel || sel.toString() !== "")) {
+ targetDoc.color = this._selectedColor;
+ return {
+ target: targetDoc,
+ previous: StrCast(targetDoc.color)
+ };
+ }
let oldColor = StrCast(targetDoc.backgroundColor);
let matchedColor = this._selectedColor;
const cvd = view.props.ContainingCollectionDoc;
@@ -79,18 +81,20 @@ export class InkingControl extends React.Component {
ruleProvider = (view.props.Document.heading && ruleProvider) ? ruleProvider : undefined;
ruleProvider && ((Doc.GetProto(ruleProvider)["ruleColor_" + NumCast(view.props.Document.heading)] = Utils.toRGBAstr(color.rgb)));
}
- !ruleProvider && (targetDoc.backgroundColor = matchedColor);
+ (!ruleProvider && targetDoc) && (Doc.Layout(view.props.Document).backgroundColor = matchedColor);
return {
target: targetDoc,
previous: oldColor
};
});
- let captured = this._selectedColor;
- UndoManager.AddEvent({
- undo: () => oldColors.forEach(pair => pair.target.backgroundColor = pair.previous),
- redo: () => oldColors.forEach(pair => pair.target.backgroundColor = captured)
- });
+ //let captured = this._selectedColor;
+ // UndoManager.AddEvent({
+ // undo: () => oldColors.forEach(pair => pair.target.backgroundColor = pair.previous),
+ // redo: () => oldColors.forEach(pair => pair.target.backgroundColor = captured)
+ // });
+ } else {
+ CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.backgroundColor = this._selectedColor);
}
});
@action
@@ -118,22 +122,11 @@ export class InkingControl extends React.Component {
return this._selectedWidth;
}
- @action
- toggleDisplay = () => {
- this._open = !this._open;
- this.switchTool(this._open ? InkTool.Pen : InkTool.None);
- }
- render() {
- return (
- <ul className="inking-control" style={this._open ? { display: "flex" } : { display: "none" }}>
- <li className="ink-size ink-panel">
- <label htmlFor="stroke-width">SIZE: </label>
- <input type="text" min="1" max="100" value={this._selectedWidth} name="stroke-width"
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.switchWidth(e.target.value)} />
- <input type="range" min="1" max="100" value={this._selectedWidth} name="stroke-width"
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.switchWidth(e.target.value)} />
- </li>
- </ul >
- );
- }
-} \ No newline at end of file
+}
+Scripting.addGlobal(function activatePen(pen: any, width: any, color: any) { InkingControl.Instance.switchTool(pen ? InkTool.Pen : InkTool.None); InkingControl.Instance.switchWidth(width); InkingControl.Instance.updateSelectedColor(color); });
+Scripting.addGlobal(function activateBrush(pen: any, width: any, color: any) { InkingControl.Instance.switchTool(pen ? InkTool.Highlighter : InkTool.None); InkingControl.Instance.switchWidth(width); InkingControl.Instance.updateSelectedColor(color); });
+Scripting.addGlobal(function activateEraser(pen: any) { return InkingControl.Instance.switchTool(pen ? InkTool.Eraser : InkTool.None); });
+Scripting.addGlobal(function activateScrubber(pen: any) { return InkingControl.Instance.switchTool(pen ? InkTool.Scrubber : InkTool.None); });
+Scripting.addGlobal(function deactivateInk() { return InkingControl.Instance.switchTool(InkTool.None); });
+Scripting.addGlobal(function setInkWidth(width: any) { return InkingControl.Instance.switchWidth(width); });
+Scripting.addGlobal(function setInkColor(color: any) { return InkingControl.Instance.updateSelectedColor(color); }); \ No newline at end of file
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index b8d428d31..332c22512 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -1,9 +1,10 @@
import { observer } from "mobx-react";
-import { observable, trace } from "mobx";
+import { observable, trace, runInAction } from "mobx";
import { InkingControl } from "./InkingControl";
import React = require("react");
import { InkTool } from "../../new_fields/InkField";
import "./InkingStroke.scss";
+import { AudioBox } from "./nodes/AudioBox";
interface StrokeProps {
@@ -15,6 +16,7 @@ interface StrokeProps {
color: string;
width: string;
tool: InkTool;
+ creationTime: number;
deleteCallback: (index: string) => void;
}
@@ -31,6 +33,11 @@ export class InkingStroke extends React.Component<StrokeProps> {
e.stopPropagation();
e.preventDefault();
}
+ if (InkingControl.Instance.selectedTool === InkTool.Scrubber && e.buttons === 1) {
+ AudioBox.SetScrubTime(this.props.creationTime);
+ e.stopPropagation();
+ e.preventDefault();
+ }
}
parseData = (line: Array<{ x: number, y: number }>): string => {
@@ -55,10 +62,9 @@ export class InkingStroke extends React.Component<StrokeProps> {
let pathlength = this.props.count; // bcz: this is needed to force reactions to the line's data changes
let marker = this.props.tool === InkTool.Highlighter ? "-marker" : "";
- let pointerEvents: any = InkingControl.Instance.selectedTool === InkTool.Eraser ? "all" : "none";
- return (
- <path className={`inkingStroke${marker}`} d={pathData} style={{ ...pathStyle, pointerEvents: pointerEvents }} strokeLinejoin="round" strokeLinecap="round"
- onPointerOver={this.deleteStroke} onPointerDown={this.deleteStroke} />
- );
+ let pointerEvents: any = InkingControl.Instance.selectedTool === InkTool.Eraser ||
+ InkingControl.Instance.selectedTool === InkTool.Scrubber ? "all" : "none";
+ return (<path className={`inkingStroke${marker}`} d={pathData} style={{ ...pathStyle, pointerEvents: pointerEvents }}
+ strokeLinejoin="round" strokeLinecap="round" onPointerOver={this.deleteStroke} onPointerDown={this.deleteStroke} />);
}
} \ No newline at end of file
diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss
index 04249506a..134a4ac85 100644
--- a/src/client/views/Main.scss
+++ b/src/client/views/Main.scss
@@ -21,12 +21,13 @@ div {
}
-
.jsx-parser {
width: 100%;
height: 100%;
pointer-events: none;
border-radius: inherit;
+ position: inherit;
+ background: inherit;
}
p {
@@ -64,208 +65,10 @@ button:hover {
cursor: pointer;
}
-.clear-db-button {
- position: absolute;
- right: 45%;
- bottom: 3%;
- font-size: 50%;
-}
-
-.round-button {
- width: 36px;
- height: 36px;
- border-radius: 18px;
- font-size: 15px;
-}
-
-.round-button:hover {
- transform: scale(1.15);
-}
-
-.add-button {
- position: relative;
- margin-right: 10px;
-}
-
-.main-undoButtons {
- position: absolute;
- width: 150px;
- right: 0px;
-}
-
-.main-notifs-badge {
- position: absolute;
- top: -10px;
- right: -10px;
- color: white;
- background: #f44b42;
- font-weight: 300;
- border-radius: 100%;
- width: 25px;
- height: 25px;
- text-align: center;
- padding-top: 4px;
- font-size: 12px;
-}
-
-//toolbar stuff
-#toolbar {
- position: absolute;
- right: 8px;
- top: 5px;
-
- .toolbar-button {
- display: block;
- margin-bottom: 10px;
- }
-}
-
-.toolbar-color-picker {
- background-color: $light-color;
- border-radius: 5px;
- padding: 12px;
- position: absolute;
- bottom: 36px;
- left: -3px;
- box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw;
-}
-
-.toolbar-color-button {
- border-radius: 11px;
- width: 22px;
- height: 22px;
- cursor: pointer;
- text-align: center; // span {
- // color: $light-color;
- // font-size: 8px;
- // user-select: none;
- // }
- margin-top: -2.55px;
- margin-left: -2.55px;
-}
-
-// add nodes menu. Note that the + button is actually an input label, not an actual button.
-#add-nodes-menu {
- position: absolute;
- bottom: 22px;
- left: 250px;
-
- >label {
- background: $dark-color;
- color: $light-color;
- display: inline-block;
- border-radius: 18px;
- font-size: 25px;
- width: 36px;
- height: 36px;
- margin-right: 10px;
- cursor: pointer;
- transition: transform 0.2s;
- }
-
- label p {
- padding-left: 10.5px;
- }
-
- label:hover {
- background: $main-accent;
- transform: scale(1.15);
- }
-
- >input {
- display: none;
- }
-
- >input:not(:checked)~#add-options-content {
- display: none;
- }
-
- >input:checked~label {
- transform: rotate(45deg);
- transition: transform 0.5s;
- cursor: pointer;
- }
-}
-
#root {
overflow: visible;
}
-#main-div {
- width: 100%;
- height: 100%;
- position: absolute;
- top: 0;
- left: 0;
- overflow: auto;
- z-index: 1;
-}
-
-#mainContent-div {
- width: 100%;
- height: 100%;
- position: absolute;
- top: 0;
- left: 0;
- overflow: hidden;
-}
-
-#add-options-content {
- display: table;
- opacity: 1;
- margin: 0;
- padding: 0;
- position: relative;
- float: right;
- bottom: 0.3em;
- margin-bottom: -1.68em;
-}
-
-ul#add-options-list {
- list-style: none;
- padding: 5 0 0 0;
-
- >li {
- display: inline-block;
- padding: 0;
- }
-}
-
-.mainView-libraryFlyout {
- height: 100%;
- position: absolute;
- display: flex;
- flex-direction: column;
-}
-
-.mainView-libraryHandle {
- width: 20px;
- height: 40px;
- top: 50%;
- border: 1px solid black;
- border-radius: 5px;
- position: absolute;
- z-index: 1;
-}
-
.svg-inline--fa {
vertical-align: unset;
-}
-
-.mainView-workspace {
- height: 200px;
- position: relative;
- display: flex;
-}
-
-.mainView-library {
- height: 75%;
- position: relative;
- display: flex;
-}
-
-.mainView-recentlyClosed {
- height: 25%;
- position: relative;
- display: flex;
} \ No newline at end of file
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 3bd898ac0..a91a2b69e 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -38,11 +38,6 @@ let swapDocs = async () => {
if (info.id !== "__guest__") {
// a guest will not have an id registered
await CurrentUserUtils.loadUserDocument(info);
- // updates old user documents to prevent chrome on tree view.
- (await Cast(CurrentUserUtils.UserDocument.workspaces, Doc))!.chromeStatus = "disabled";
- (await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc))!.chromeStatus = "disabled";
- (await Cast(CurrentUserUtils.UserDocument.sidebar, Doc))!.chromeStatus = "disabled";
- CurrentUserUtils.UserDocument.chromeStatus = "disabled";
await swapDocs();
}
document.getElementById('root')!.addEventListener('wheel', event => {
diff --git a/src/client/views/MainOverlayTextBox.scss b/src/client/views/MainOverlayTextBox.scss
deleted file mode 100644
index c9d44e194..000000000
--- a/src/client/views/MainOverlayTextBox.scss
+++ /dev/null
@@ -1,29 +0,0 @@
-@import "globalCssVariables";
-
-.mainOverlayTextBox-textInput {
- background-color: rgba(248, 6, 6, 0.001);
- width: 400px;
- height: 200px;
- position: absolute;
- overflow: visible;
- top: 0;
- left: 0;
- pointer-events: none;
- z-index: $mainTextInput-zindex;
-
- .formattedTextBox-cont {
- background-color: rgba(248, 6, 6, 0.001);
- width: 100%;
- height: 100%;
- position: absolute;
- top: 0;
- left: 0;
- }
-}
-
-.mainOverlayTextBox-unscaled_div {
- // width: 0px;
- z-index: 10000;
- position: absolute;
- pointer-events: none;
-} \ No newline at end of file
diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx
deleted file mode 100644
index 335cc609f..000000000
--- a/src/client/views/MainOverlayTextBox.tsx
+++ /dev/null
@@ -1,155 +0,0 @@
-import { action, observable, reaction, trace } from 'mobx';
-import { observer } from 'mobx-react';
-import "normalize.css";
-import * as React from 'react';
-import { Doc, DocListCast, Opt } from '../../new_fields/Doc';
-import { BoolCast } from '../../new_fields/Types';
-import { emptyFunction, returnTrue, returnZero, Utils, returnOne } from '../../Utils';
-import { DragManager } from '../util/DragManager';
-import { Transform } from '../util/Transform';
-import { CollectionDockingView } from './collections/CollectionDockingView';
-import "./MainOverlayTextBox.scss";
-import { FormattedTextBox } from './nodes/FormattedTextBox';
-
-interface MainOverlayTextBoxProps {
- firstinstance?: boolean;
-}
-
-@observer
-export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> {
- public static Instance: MainOverlayTextBox;
- @observable _textXf: () => Transform = () => Transform.Identity();
- public TextFieldKey: string = "data";
- private _textColor: string | null = null;
- private _textHideOnLeave?: boolean;
- private _textTargetDiv: HTMLDivElement | undefined;
- private _textProxyDiv: React.RefObject<HTMLDivElement>;
- private _textBottom: boolean | undefined;
- private _setouterdiv = (outerdiv: HTMLElement | null) => { this._outerdiv = outerdiv; this.updateTooltip(); };
- private _outerdiv: HTMLElement | null = null;
- private _textBox: FormattedTextBox | undefined;
- private _tooltip?: HTMLElement;
- ChromeHeight?: () => number;
- @observable public TextDoc?: Doc;
- @observable public TextDataDoc?: Doc;
-
- updateTooltip = () => {
- this._outerdiv && this._tooltip && !this._outerdiv.contains(this._tooltip) && this._outerdiv.appendChild(this._tooltip);
- }
-
- public SetColor(color: string) {
- return this._textBox && this._textBox.setFontColor(color);
- }
-
- constructor(props: MainOverlayTextBoxProps) {
- super(props);
- this._textProxyDiv = React.createRef();
- MainOverlayTextBox.Instance = this;
- reaction(() => FormattedTextBox.InputBoxOverlay,
- (box?: FormattedTextBox) => {
- this._textBox = box;
- if (box) {
- this.ChromeHeight = box.props.ChromeHeight;
- this.TextDoc = box.props.Document;
- this.TextDataDoc = box.props.DataDoc;
- let xf = () => {
- box.props.ScreenToLocalTransform();
- let sxf = Utils.GetScreenTransform(box ? box.CurrentDiv : undefined);
- return new Transform(-sxf.translateX, -sxf.translateY, 1 / sxf.scale);
- };
- this.setTextDoc(box.props.fieldKey, box.CurrentDiv, xf, BoolCast(box.props.Document.autoHeight) || box.props.height === "min-content");
- }
- else {
- this.TextDoc = undefined;
- this.TextDataDoc = undefined;
- this.setTextDoc();
- }
- });
- }
-
- @action
- private setTextDoc(textFieldKey?: string, div?: HTMLDivElement, tx?: () => Transform, autoHeight?: boolean) {
- if (this._textTargetDiv) {
- this._textTargetDiv.style.color = this._textColor;
- }
- this.TextFieldKey = textFieldKey!;
- let txf = tx ? tx : () => Transform.Identity();
- this._textXf = txf;
- this._textTargetDiv = div;
- this._textHideOnLeave = FormattedTextBox.InputBoxOverlay && FormattedTextBox.InputBoxOverlay.props.hideOnLeave;
- if (div) {
- this._textBottom = div.parentElement && getComputedStyle(div.parentElement).top !== "0px" ? true : false;
- this._textColor = (getComputedStyle(div) as any).color;
- div.style.color = "transparent";
- }
- }
-
- @action
- textScroll = (e: React.UIEvent) => {
- if (this._textProxyDiv.current && this._textTargetDiv) {
- this._textTargetDiv.scrollTop = (e as any)._targetInst.stateNode.scrollTop;
- }
- }
-
- textBoxDown = (e: React.PointerEvent) => {
- if (e.button !== 0 || e.metaKey || e.altKey) {
- document.addEventListener("pointermove", this.textBoxMove);
- document.addEventListener('pointerup', this.textBoxUp);
- }
- }
- @action
- textBoxMove = (e: PointerEvent) => {
- if ((e.movementX > 1 || e.movementY > 1) && FormattedTextBox.InputBoxOverlay) {
- document.removeEventListener("pointermove", this.textBoxMove);
- document.removeEventListener('pointerup', this.textBoxUp);
- let dragData = new DragManager.DocumentDragData([FormattedTextBox.InputBoxOverlay.props.Document]);
- const [left, top] = this._textXf().inverse().transformPoint(0, 0);
- dragData.offset = [e.clientX - left, e.clientY - top];
- DragManager.StartDocumentDrag([this._textTargetDiv!], dragData, e.clientX, e.clientY, {
- handlers: {
- dragComplete: action(emptyFunction),
- },
- hideSource: false
- });
- }
- }
- textBoxUp = (e: PointerEvent) => {
- document.removeEventListener("pointermove", this.textBoxMove);
- document.removeEventListener('pointerup', this.textBoxUp);
- }
-
- addDocTab = (doc: Doc, dataDoc: Opt<Doc>, location: string) => {
- return this._textBox && this._textBox.props.addDocTab(doc, dataDoc, location) ? true : false;
- }
- render() {
- this.TextDoc; this.TextDataDoc;
- if (FormattedTextBox.InputBoxOverlay && this._textTargetDiv) {
- let wid = FormattedTextBox.InputBoxOverlay.props.Document.width; // need to force overlay to render when underlying text box is resized (eg, w/ DocDecorations)
- let hgtx = FormattedTextBox.InputBoxOverlay.props.Document.height; // need to force overlay to render when underlying text box is resized (eg, w/ DocDecorations)
- let textRect = this._textTargetDiv.getBoundingClientRect();
- let s = this._textXf().Scale;
- let location = this._textBottom ? textRect.bottom : textRect.top;
- let hgt = (this._textBox && this._textBox.props.Document.autoHeight) || this._textBottom ? "auto" : this._textTargetDiv.clientHeight;
- return <div ref={this._setouterdiv} className="mainOverlayTextBox-unscaled_div" style={{ transform: `translate(${textRect.left}px, ${location}px)` }} >
- <div className="mainOverlayTextBox-textInput" style={{ transform: `scale(${1 / s})`, width: "auto", height: "0px" }} >
- <div className="mainOverlayTextBox-textInput" onPointerDown={this.textBoxDown} ref={this._textProxyDiv} onScroll={this.textScroll}
- style={{ width: `${textRect.width * s}px`, height: "0px" }}>
- <div style={{ height: hgt, width: "100%", position: "absolute", bottom: this._textBottom ? "0px" : undefined }}>
- <FormattedTextBox color={`${this._textColor}`} fieldKey={this.TextFieldKey} fieldExt="" hideOnLeave={this._textHideOnLeave} isOverlay={true}
- Document={FormattedTextBox.InputBoxOverlay.props.Document}
- DataDoc={FormattedTextBox.InputBoxOverlay.props.DataDoc}
- onClick={undefined}
- ruleProvider={this._textBox ? this._textBox.props.ruleProvider : undefined}
- ChromeHeight={this.ChromeHeight} isSelected={returnTrue} select={emptyFunction} renderDepth={0}
- ContainingCollectionDoc={undefined} ContainingCollectionView={undefined}
- whenActiveChanged={emptyFunction} active={returnTrue} ContentScaling={returnOne}
- ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyFunction}
- pinToPres={returnZero} addDocTab={this.addDocTab} outer_div={(tooltip: HTMLElement) => { this._tooltip = tooltip; this.updateTooltip(); }} />
- </div>
- </div>
- </div>
- </ div>;
- }
- else return (null);
- }
-} \ No newline at end of file
diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss
new file mode 100644
index 000000000..21b135c49
--- /dev/null
+++ b/src/client/views/MainView.scss
@@ -0,0 +1,96 @@
+@import "globalCssVariables";
+@import "nodeModuleOverrides";
+
+
+.mainView-tabButtons {
+ position: relative;
+ width:100%;
+}
+// add nodes menu. Note that the + button is actually an input label, not an actual button.
+.mainView-docButtons {
+ position: absolute;
+ bottom: 20px;
+ left: 250px;
+}
+
+.mainView-container {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ left: 0;
+ overflow: auto;
+ z-index: 1;
+}
+.mainView-mainContent {
+ width:100%;
+ height:100%;
+ position:absolute;
+}
+.mainView-flyoutContainer{
+ display:flex;
+ flex-direction: column;
+ position: absolute;
+ width:100%;
+ height:100%;
+ .documentView-node-topmost {
+ background: lightgrey;
+ }
+}
+.mainView-mainDiv {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ left: 0;
+ overflow: hidden;
+}
+
+.mainView-logout {
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ font-size: 8px;
+}
+
+.mainView-libraryFlyout {
+ height: 100%;
+ position: absolute;
+ display: flex;
+ flex-direction: column;
+}
+
+.mainView-expandFlyoutButton {
+ position: absolute;
+ top: 5px;
+ right: 5px;
+ cursor: pointer;
+}
+
+.mainView-libraryHandle {
+ width: 20px;
+ height: 40px;
+ top: 50%;
+ border: 1px solid black;
+ border-radius: 5px;
+ position: absolute;
+ z-index: 1;
+}
+
+.mainView-workspace {
+ height: 200px;
+ position: relative;
+ display: flex;
+}
+
+.mainView-library {
+ height: 75%;
+ position: relative;
+ display: flex;
+}
+
+.mainView-recentlyClosed {
+ height: 25%;
+ position: relative;
+ display: flex;
+} \ No newline at end of file
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 7e2b4fa3a..180e02e7e 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,134 +1,62 @@
-import { IconName, library } from '@fortawesome/fontawesome-svg-core';
-import { faLink, faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faClone, faCloudUploadAlt, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, faMusic, faObjectGroup, faPause, faPenNib, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faUndoAlt, faTv, faVideo } from '@fortawesome/free-solid-svg-icons';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import {
+ faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faChevronRight, faClone, faCloudUploadAlt, faCommentAlt, faCut, faEllipsisV, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight,
+ faMusic, faObjectGroup, faPause, faMousePointer, faPenNib, faFileAudio, faPen, faEraser, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt, faHighlighter, faMicrophone, 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';
import "normalize.css";
import * as React from 'react';
-import { SketchPicker } from 'react-color';
import Measure from 'react-measure';
-import { Doc, DocListCast, Field, FieldResult, HeightSym, Opt } from '../../new_fields/Doc';
+import { Doc, DocListCast, Field, FieldResult, Opt } from '../../new_fields/Doc';
import { Id } from '../../new_fields/FieldSymbols';
-import { InkTool } from '../../new_fields/InkField';
import { List } from '../../new_fields/List';
import { listSpec } from '../../new_fields/Schema';
-import { BoolCast, Cast, FieldValue, StrCast } from '../../new_fields/Types';
+import { Cast, FieldValue, StrCast } from '../../new_fields/Types';
import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
import { RouteStore } from '../../server/RouteStore';
-import { emptyFunction, returnEmptyString, returnOne, returnTrue, Utils } from '../../Utils';
+import { emptyFunction, returnEmptyString, returnFalse, returnOne, returnTrue, Utils } from '../../Utils';
+import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager';
import { DocServer } from '../DocServer';
import { Docs, DocumentOptions } from '../documents/Documents';
-import { ClientUtils } from '../util/ClientUtils';
-import { DictationManager } from '../util/DictationManager';
-import { SetupDrag } from '../util/DragManager';
import { HistoryUtil } from '../util/History';
import SharingManager from '../util/SharingManager';
import { Transform } from '../util/Transform';
-import { UndoManager } from '../util/UndoManager';
-import { CollectionBaseView, CollectionViewType } from './collections/CollectionBaseView';
+import { CollectionLinearView } from './CollectionLinearView';
+import { CollectionViewType, CollectionView } from './collections/CollectionView';
import { CollectionDockingView } from './collections/CollectionDockingView';
-import { CollectionTreeView } from './collections/CollectionTreeView';
import { ContextMenu } from './ContextMenu';
+import { DictationOverlay } from './DictationOverlay';
import { DocumentDecorations } from './DocumentDecorations';
import KeyManager from './GlobalKeyHandler';
-import { InkingControl } from './InkingControl';
-import "./Main.scss";
-import { MainOverlayTextBox } from './MainOverlayTextBox';
-import MainViewModal from './MainViewModal';
+import "./MainView.scss";
+import { MainViewNotifs } from './MainViewNotifs';
import { DocumentView } from './nodes/DocumentView';
-import { PresBox } from './nodes/PresBox';
import { OverlayView } from './OverlayView';
import PDFMenu from './pdf/PDFMenu';
import { PreviewCursor } from './PreviewCursor';
-import { FilterBox } from './search/FilterBox';
+import { Scripting } from '../util/Scripting';
+import { LinkManager } from '../util/LinkManager';
@observer
export class MainView extends React.Component {
public static Instance: MainView;
- @observable addMenuToggle = React.createRef<HTMLInputElement>();
- @observable public pwidth: number = 0;
- @observable public pheight: number = 0;
+ private _buttonBarHeight = 75;
+ private _flyoutSizeOnDown = 0;
+ private _urlState: HistoryUtil.DocUrl;
+ private _docBtnRef = React.createRef<HTMLDivElement>();
- @observable private dictationState = DictationManager.placeholder;
- @observable private dictationSuccessState: boolean | undefined = undefined;
- @observable private dictationDisplayState = false;
- @observable private dictationListeningState: DictationManager.Controls.ListeningUIStatus = false;
+ @observable private _panelWidth: number = 0;
+ @observable private _panelHeight: number = 0;
+ @observable private _flyoutTranslate: boolean = true;
+ @observable public flyoutWidth: number = 250;
- public hasActiveModal = false;
+ @computed private get userDoc() { return CurrentUserUtils.UserDocument; }
+ @computed private get mainContainer() { return this.userDoc ? FieldValue(Cast(this.userDoc.activeWorkspace, Doc)) : CurrentUserUtils.GuestWorkspace; }
+ @computed public get mainFreeform(): Opt<Doc> { return (docs => (docs && docs.length > 1) ? docs[1] : undefined)(DocListCast(this.mainContainer!.data)); }
- public overlayTimeout: NodeJS.Timeout | undefined;
-
- public initiateDictationFade = () => {
- let duration = DictationManager.Commands.dictationFadeDuration;
- this.overlayTimeout = setTimeout(() => {
- this.dictationOverlayVisible = false;
- this.dictationSuccess = undefined;
- this.hasActiveModal = false;
- setTimeout(() => this.dictatedPhrase = DictationManager.placeholder, 500);
- }, duration);
- }
-
- private urlState: HistoryUtil.DocUrl;
-
- @computed private get userDoc() {
- return CurrentUserUtils.UserDocument;
- }
-
- public cancelDictationFade = () => {
- if (this.overlayTimeout) {
- clearTimeout(this.overlayTimeout);
- this.overlayTimeout = undefined;
- }
- }
-
- @computed private get mainContainer(): Opt<Doc> {
- return this.userDoc ? FieldValue(Cast(this.userDoc.activeWorkspace, Doc)) : CurrentUserUtils.GuestWorkspace;
- }
- @computed get mainFreeform(): Opt<Doc> {
- let docs = DocListCast(this.mainContainer!.data);
- return (docs && docs.length > 1) ? docs[1] : undefined;
- }
public isPointerDown = false;
- private set mainContainer(doc: Opt<Doc>) {
- if (doc) {
- if (!("presentationView" in doc)) {
- doc.presentationView = new List<Doc>([Docs.Create.TreeDocument([], { title: "Presentation" })]);
- }
- this.userDoc ? (this.userDoc.activeWorkspace = doc) : (CurrentUserUtils.GuestWorkspace = doc);
- }
- }
-
- @computed public get dictatedPhrase() {
- return this.dictationState;
- }
-
- public set dictatedPhrase(value: string) {
- runInAction(() => this.dictationState = value);
- }
-
- @computed public get dictationSuccess() {
- return this.dictationSuccessState;
- }
-
- public set dictationSuccess(value: boolean | undefined) {
- runInAction(() => this.dictationSuccessState = value);
- }
-
- @computed public get dictationOverlayVisible() {
- return this.dictationDisplayState;
- }
-
- public set dictationOverlayVisible(value: boolean) {
- runInAction(() => this.dictationDisplayState = value);
- }
-
- @computed public get isListening() {
- return this.dictationListeningState;
- }
-
- public set isListening(value: DictationManager.Controls.ListeningUIStatus) {
- runInAction(() => this.dictationListeningState = value);
- }
componentWillMount() {
var tag = document.createElement('script');
@@ -138,29 +66,10 @@ export class MainView extends React.Component {
firstScriptTag.parentNode!.insertBefore(tag, firstScriptTag);
window.removeEventListener("keydown", KeyManager.Instance.handle);
window.addEventListener("keydown", KeyManager.Instance.handle);
-
- if (this.userDoc) {
- reaction(() => {
- let workspaces = this.userDoc.workspaces;
- let recent = this.userDoc.recentlyClosed;
- if (!(recent instanceof Doc)) return 0;
- if (!(workspaces instanceof Doc)) return 0;
- let workspacesDoc = workspaces;
- let recentDoc = recent;
- let libraryHeight = this.getPHeight() - workspacesDoc[HeightSym]() - recentDoc[HeightSym]() - 20 + this.userDoc[HeightSym]() * 0.00001;
- return libraryHeight;
- }, (libraryHeight: number) => {
- if (libraryHeight && Math.abs(this.userDoc[HeightSym]() - libraryHeight) > 5) {
- this.userDoc.height = libraryHeight;
- }
- (Cast(this.userDoc.recentlyClosed, Doc) as Doc).allowClear = true;
- }, { fireImmediately: true });
- }
}
componentWillUnMount() {
window.removeEventListener("keydown", KeyManager.Instance.handle);
- //close presentation
window.removeEventListener("pointerdown", this.globalPointerDown);
window.removeEventListener("pointerup", this.globalPointerUp);
}
@@ -168,7 +77,7 @@ export class MainView extends React.Component {
constructor(props: Readonly<{}>) {
super(props);
MainView.Instance = this;
- this.urlState = HistoryUtil.parseUrl(window.location) || {} as any;
+ this._urlState = HistoryUtil.parseUrl(window.location) || {} as any;
// causes errors to be generated when modifying an observable outside of an action
configure({ enforceActions: "observed" });
if (window.location.pathname !== RouteStore.home) {
@@ -196,7 +105,13 @@ export class MainView extends React.Component {
library.add(faGlobeAsia);
library.add(faUndoAlt);
library.add(faRedoAlt);
+ library.add(faMousePointer);
+ library.add(faPen);
+ library.add(faHighlighter);
+ library.add(faEraser);
+ library.add(faFileAudio);
library.add(faPenNib);
+ library.add(faMicrophone);
library.add(faFilm);
library.add(faMusic);
library.add(faTree);
@@ -214,6 +129,8 @@ export class MainView extends React.Component {
library.add(faCloudUploadAlt);
library.add(faBolt);
library.add(faVideo);
+ library.add(faChevronRight);
+ library.add(faEllipsisV);
this.initEventListeners();
this.initAuthenticationRouters();
}
@@ -229,7 +146,6 @@ export class MainView extends React.Component {
globalPointerUp = () => this.isPointerDown = false;
initEventListeners = () => {
- // window.addEventListener("pointermove", (e) => this.reportLocation(e))
window.addEventListener("drop", (e) => e.preventDefault(), false); // drop event handler
window.addEventListener("dragover", (e) => e.preventDefault(), false); // drag event handler
// click interactions for the context menu
@@ -247,25 +163,17 @@ export class MainView extends React.Component {
{ fireImmediately: true }
);
} else {
- if (received && this.urlState.sharing) {
- reaction(
- () => {
- let docking = CollectionDockingView.Instance;
- return docking && docking.initialized;
- },
- initialized => {
- if (initialized && received) {
- DocServer.GetRefField(received).then(field => {
- if (field instanceof Doc && field.viewType !== CollectionViewType.Docking) {
- CollectionDockingView.AddRightSplit(field, undefined);
- }
- });
+ if (received && this._urlState.sharing) {
+ reaction(() => CollectionDockingView.Instance && CollectionDockingView.Instance.initialized,
+ initialized => initialized && received && DocServer.GetRefField(received).then(field => {
+ if (field instanceof Doc && field.viewType !== CollectionViewType.Docking) {
+ CollectionDockingView.AddRightSplit(field, undefined);
}
- },
+ }),
);
}
- let doc: Opt<Doc>;
- if (this.userDoc && (doc = await Cast(this.userDoc.activeWorkspace, Doc))) {
+ let doc = this.userDoc && await Cast(this.userDoc.activeWorkspace, Doc);
+ if (doc) {
this.openWorkspace(doc);
} else {
this.createNewWorkspace();
@@ -278,39 +186,37 @@ export class MainView extends React.Component {
let freeformOptions: DocumentOptions = {
x: 0,
y: 400,
- width: this.pwidth * .7,
- height: this.pheight,
- title: "My Blank Collection"
+ width: this._panelWidth * .7,
+ height: this._panelHeight,
+ title: "My Blank Collection",
+ backgroundColor: "white"
};
let workspaces: FieldResult<Doc>;
let freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions);
var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(freeformDoc, freeformDoc, 600)] }] };
- let mainDoc = Docs.Create.DockDocument([this.userDoc, freeformDoc], JSON.stringify(dockingLayout), {}, id);
+ let mainDoc = Docs.Create.DockDocument([freeformDoc], JSON.stringify(dockingLayout), {}, id);
if (this.userDoc && ((workspaces = Cast(this.userDoc.workspaces, Doc)) instanceof Doc)) {
- const list = Cast((workspaces).data, listSpec(Doc));
- if (list) {
- if (!this.userDoc.linkManagerDoc) {
- let linkManagerDoc = new Doc();
- linkManagerDoc.allLinks = new List<Doc>([]);
- this.userDoc.linkManagerDoc = linkManagerDoc;
- }
- list.push(mainDoc);
- mainDoc.title = `Workspace ${list.length}`;
+ if (!this.userDoc.linkManagerDoc) {
+ let linkManagerDoc = new Doc();
+ linkManagerDoc.allLinks = new List<Doc>([]);
+ this.userDoc.linkManagerDoc = linkManagerDoc;
}
+ Doc.AddDocToList(workspaces, "data", mainDoc);
+ mainDoc.title = `Workspace ${DocListCast(workspaces.data).length}`;
}
// bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container)
- setTimeout(() => {
- this.openWorkspace(mainDoc);
- // let pendingDocument = Docs.StackingDocument([], { title: "New Mobile Uploads" });
- // mainDoc.optionalRightCollection = pendingDocument;
- }, 0);
+ setTimeout(() => this.openWorkspace(mainDoc), 0);
}
@action
openWorkspace = async (doc: Doc, fromHistory = false) => {
CurrentUserUtils.MainDocId = doc[Id];
- this.mainContainer = doc;
- let state = this.urlState;
+
+ if (doc) { // this has the side-effect of setting the main container since we're assigning the active/guest workspace
+ !("presentationView" in doc) && (doc.presentationView = new List<Doc>([Docs.Create.TreeDocument([], { title: "Presentation" })]));
+ this.userDoc ? (this.userDoc.activeWorkspace = doc) : (CurrentUserUtils.GuestWorkspace = doc);
+ }
+ let state = this._urlState;
if (state.sharing === true && !this.userDoc) {
DocServer.Control.makeReadOnly();
} else {
@@ -327,23 +233,18 @@ export class MainView extends React.Component {
if (!state.nro) {
DocServer.Control.makeReadOnly();
}
- CollectionBaseView.SetSafeMode(true);
+ CollectionView.SetSafeMode(true);
} else if (state.nro || state.nro === null || state.readonly === false) {
- } else if (BoolCast(doc.readOnly)) {
+ } else if (doc.readOnly) {
DocServer.Control.makeReadOnly();
} else {
DocServer.Control.makeEditable();
}
}
- let col: Opt<Doc>;
// if there is a pending doc, and it has new data, show it (syip: we use a timeout to prevent collection docking view from being uninitialized)
setTimeout(async () => {
- if (this.userDoc && (col = await Cast(this.userDoc.optionalRightCollection, Doc))) {
- const l = Cast(col.data, listSpec(Doc));
- if (l) {
- runInAction(() => CollectionTreeView.NotifsCol = col);
- }
- }
+ const col = this.userDoc && await Cast(this.userDoc.optionalRightCollection, Doc);
+ col && Cast(col.data, listSpec(Doc)) && runInAction(() => MainViewNotifs.NotifsCol = col);
}, 100);
return true;
}
@@ -356,25 +257,22 @@ export class MainView extends React.Component {
@action
onResize = (r: any) => {
- this.pwidth = r.offset.width;
- this.pheight = r.offset.height;
- }
- getPWidth = () => {
- return this.pwidth;
- }
- getPHeight = () => {
- return this.pheight;
+ this._panelWidth = r.offset.width;
+ this._panelHeight = r.offset.height;
}
+ getPWidth = () => this._panelWidth;
+ getPHeight = () => this._panelHeight;
+ getContentsHeight = () => this._panelHeight - this._buttonBarHeight;
- @observable flyoutWidth: number = 250;
@computed get dockingContent() {
- let flyoutWidth = this.flyoutWidth;
- let mainCont = this.mainContainer;
+ const mainContainer = this.mainContainer;
+ let flyoutWidth = this.flyoutWidth; // bcz: need to be here because Measure messes with observables.
+ let flyoutTranslate = this._flyoutTranslate;
return <Measure offset onResize={this.onResize}>
{({ measureRef }) =>
- <div ref={measureRef} id="mainContent-div" style={{ width: `calc(100% - ${flyoutWidth}px`, transform: `translate(${flyoutWidth}px, 0px)` }} onDrop={this.onDrop}>
- {!mainCont ? (null) :
- <DocumentView Document={mainCont}
+ <div ref={measureRef} id="mainContent-div" style={{ width: `calc(100% - ${flyoutTranslate ? flyoutWidth : 0}px`, transform: `translate(${flyoutTranslate ? flyoutWidth : 0}px, 0px)` }} onDrop={this.onDrop}>
+ {!mainContainer ? (null) :
+ <DocumentView Document={mainContainer}
DataDoc={undefined}
addDocument={undefined}
addDocTab={this.addDocTabFunc}
@@ -402,9 +300,8 @@ export class MainView extends React.Component {
</Measure>;
}
- _downsize = 0;
onPointerDown = (e: React.PointerEvent) => {
- this._downsize = e.clientX;
+ this._flyoutSizeOnDown = e.clientX;
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
document.addEventListener("pointermove", this.onPointerMove);
@@ -412,15 +309,31 @@ export class MainView extends React.Component {
e.stopPropagation();
e.preventDefault();
}
+
+ @action
+ pointerOverDragger = () => {
+ if (this.flyoutWidth === 0) {
+ this.flyoutWidth = 250;
+ this._flyoutTranslate = false;
+ }
+ }
+
+ @action
+ pointerLeaveDragger = () => {
+ if (!this._flyoutTranslate) {
+ this.flyoutWidth = 0;
+ this._flyoutTranslate = true;
+ }
+ }
+
@action
onPointerMove = (e: PointerEvent) => {
this.flyoutWidth = Math.max(e.clientX, 0);
}
@action
onPointerUp = (e: PointerEvent) => {
- if (Math.abs(e.clientX - this._downsize) < 4) {
- if (this.flyoutWidth < 5) this.flyoutWidth = 250;
- else this.flyoutWidth = 0;
+ if (Math.abs(e.clientX - this._flyoutSizeOnDown) < 4) {
+ this.flyoutWidth = this.flyoutWidth < 5 ? 250 : 0;
}
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
@@ -437,214 +350,171 @@ export class MainView extends React.Component {
return CollectionDockingView.AddRightSplit(doc, undefined);
}
}
- @computed
- get flyout() {
- let sidebar: FieldResult<Field>;
- if (!this.userDoc || !((sidebar = this.userDoc.sidebar) instanceof Doc)) {
- return (null);
- }
- return <DocumentView
- Document={sidebar}
- DataDoc={undefined}
- addDocument={undefined}
- addDocTab={this.addDocTabFunc}
- pinToPres={emptyFunction}
- removeDocument={undefined}
- ruleProvider={undefined}
- onClick={undefined}
- ScreenToLocalTransform={Transform.Identity}
- ContentScaling={returnOne}
- PanelWidth={this.flyoutWidthFunc}
- PanelHeight={this.getPHeight}
- renderDepth={0}
- focus={emptyFunction}
- backgroundColor={returnEmptyString}
- parentActive={returnTrue}
- whenActiveChanged={emptyFunction}
- bringToFront={emptyFunction}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
- zoomToScale={emptyFunction}
- getScale={returnOne}>
- </DocumentView>;
- }
- @computed
- get mainContent() {
- if (!this.userDoc) {
- return <div>{this.dockingContent}</div>;
- }
- let sidebar = this.userDoc.sidebar;
- if (!(sidebar instanceof Doc)) {
+ mainContainerXf = () => new Transform(0, -this._buttonBarHeight, 1);
+
+ @computed get flyout() {
+ let sidebarContent = this.userDoc && this.userDoc.sidebarContainer;
+ if (!(sidebarContent instanceof Doc)) {
return (null);
}
- return <div>
- <div className="mainView-libraryHandle"
- style={{ cursor: "ew-resize", left: `${this.flyoutWidth - 10}px`, backgroundColor: `${StrCast(sidebar.backgroundColor, "lightGray")}` }}
- onPointerDown={this.onPointerDown}>
- <span title="library View Dragger" style={{ width: "100%", height: "100%", position: "absolute" }} />
- </div>
- <div className="mainView-libraryFlyout" style={{ width: `${this.flyoutWidth}px`, zIndex: 1 }}>
- {this.flyout}
+ let sidebarButtonsDoc = Cast(CurrentUserUtils.UserDocument.sidebarButtons, Doc) as Doc;
+ sidebarButtonsDoc.columnWidth = this.flyoutWidth / 3 - 30;
+ return <div className="mainView-flyoutContainer" >
+ <div className="mainView-tabButtons" style={{ height: `${this._buttonBarHeight}px` }}>
+ <DocumentView
+ Document={sidebarButtonsDoc}
+ DataDoc={undefined}
+ addDocument={undefined}
+ addDocTab={this.addDocTabFunc}
+ pinToPres={emptyFunction}
+ removeDocument={undefined}
+ ruleProvider={undefined}
+ onClick={undefined}
+ ScreenToLocalTransform={Transform.Identity}
+ ContentScaling={returnOne}
+ PanelWidth={this.flyoutWidthFunc}
+ PanelHeight={this.getPHeight}
+ renderDepth={0}
+ focus={emptyFunction}
+ backgroundColor={returnEmptyString}
+ parentActive={returnTrue}
+ whenActiveChanged={emptyFunction}
+ bringToFront={emptyFunction}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
+ zoomToScale={emptyFunction}
+ getScale={returnOne}>
+ </DocumentView>
</div>
- {this.dockingContent}
- </div>;
+ <div style={{ position: "relative", height: `calc(100% - ${this._buttonBarHeight}px)`, width: "100%", overflow: "auto" }}>
+ <DocumentView
+ Document={sidebarContent}
+ DataDoc={undefined}
+ addDocument={undefined}
+ addDocTab={this.addDocTabFunc}
+ pinToPres={emptyFunction}
+ removeDocument={returnFalse}
+ ruleProvider={undefined}
+ onClick={undefined}
+ ScreenToLocalTransform={this.mainContainerXf}
+ ContentScaling={returnOne}
+ PanelWidth={this.flyoutWidthFunc}
+ PanelHeight={this.getContentsHeight}
+ renderDepth={0}
+ focus={emptyFunction}
+ backgroundColor={returnEmptyString}
+ parentActive={returnTrue}
+ whenActiveChanged={emptyFunction}
+ bringToFront={emptyFunction}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
+ zoomToScale={emptyFunction}
+ getScale={returnOne}>
+ </DocumentView>
+ <button className="mainView-logout" key="logout" onClick={() => window.location.assign(Utils.prepend(RouteStore.logout))}>
+ {CurrentUserUtils.GuestWorkspace ? "Exit" : "Log Out"}
+ </button>
+ </div></div>;
+ }
+
+ @computed get mainContent() {
+ const sidebar = this.userDoc && this.userDoc.sidebarContainer;
+ return !this.userDoc || !(sidebar instanceof Doc) ? (null) : (
+ <div className="mainView-mainContent" >
+ <div className="mainView-flyoutContainer" onPointerLeave={this.pointerLeaveDragger}>
+ <div className="mainView-libraryHandle"
+ style={{ cursor: "ew-resize", left: `${(this.flyoutWidth * (this._flyoutTranslate ? 1 : 0)) - 10}px`, backgroundColor: `${StrCast(sidebar.backgroundColor, "lightGray")}` }}
+ onPointerDown={this.onPointerDown} onPointerOver={this.pointerOverDragger}>
+ <span title="library View Dragger" style={{
+ width: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "100%" : "5vw",
+ height: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "100%" : "30vh",
+ position: "absolute",
+ top: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "" : "-10vh"
+ }} />
+ </div>
+ <div className="mainView-libraryFlyout" style={{
+ width: `${this.flyoutWidth}px`,
+ zIndex: 1,
+ transformOrigin: this._flyoutTranslate ? "" : "left center",
+ transition: this._flyoutTranslate ? "" : "width .5s",
+ transform: `scale(${this._flyoutTranslate ? 1 : 0.8})`,
+ boxShadow: this._flyoutTranslate ? "" : "rgb(156, 147, 150) 0.2vw 0.2vw 0.8vw"
+ }}>
+ {this.flyout}
+ {this.expandButton}
+ </div>
+ </div>
+ {this.dockingContent}
+ </div>);
}
- selected = (tool: InkTool) => {
- if (!InkingControl.Instance || InkingControl.Instance.selectedTool === InkTool.None) return { display: "none" };
- if (InkingControl.Instance.selectedTool === tool) {
- return { color: "#61aaa3", fontSize: "50%" };
- }
- return { fontSize: "50%" };
- }
+ public static expandFlyout = action(() => {
+ MainView.Instance._flyoutTranslate = true;
+ MainView.Instance.flyoutWidth = 250;
+ });
- onColorClick = (e: React.MouseEvent) => {
- let target = (e.nativeEvent as any).path[0];
- let parent = (e.nativeEvent as any).path[1];
- if (target.localName === "input" || parent.localName === "span") {
- e.stopPropagation();
+ @computed get expandButton() {
+ return !this._flyoutTranslate ? (<div className="mainView-expandFlyoutButton" title="Re-attach sidebar" onPointerDown={MainView.expandFlyout}><FontAwesomeIcon icon="chevron-right" color="grey" size="lg" /></div>) : (null);
+ }
+
+ addButtonDoc = (doc: Doc) => Doc.AddDocToList(CurrentUserUtils.UserDocument.expandingButtons as Doc, "data", doc);
+ remButtonDoc = (doc: Doc) => Doc.RemoveDocFromList(CurrentUserUtils.UserDocument.expandingButtons as Doc, "data", doc);
+ moveButtonDoc = (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => this.remButtonDoc(doc) && addDocument(doc);
+
+ buttonBarXf = () => {
+ if (!this._docBtnRef.current) return Transform.Identity();
+ let { scale, translateX, translateY } = Utils.GetScreenTransform(this._docBtnRef.current);
+ return new Transform(-translateX, -translateY, 1 / scale);
+ }
+ @computed get docButtons() {
+ if (CurrentUserUtils.UserDocument.expandingButtons instanceof Doc) {
+ return <div className="mainView-docButtons" ref={this._docBtnRef}
+ style={{ left: (this._flyoutTranslate ? this.flyoutWidth : 0) + 20, height: !CurrentUserUtils.UserDocument.expandingButtons.isExpanded ? "42px" : undefined }} >
+ <MainViewNotifs />
+ <CollectionLinearView
+ Document={CurrentUserUtils.UserDocument.expandingButtons}
+ DataDoc={undefined}
+ fieldKey={"data"}
+ annotationsKey={""}
+ select={emptyFunction}
+ chromeCollapsed={true}
+ active={returnFalse}
+ isSelected={returnFalse}
+ moveDocument={this.moveButtonDoc}
+ CollectionView={undefined}
+ addDocument={this.addButtonDoc}
+ addDocTab={this.addDocTabFunc}
+ pinToPres={emptyFunction}
+ removeDocument={this.remButtonDoc}
+ ruleProvider={undefined}
+ onClick={undefined}
+ ScreenToLocalTransform={this.buttonBarXf}
+ ContentScaling={returnOne}
+ PanelWidth={this.flyoutWidthFunc}
+ PanelHeight={this.getContentsHeight}
+ renderDepth={0}
+ focus={emptyFunction}
+ whenActiveChanged={emptyFunction}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined} />
+ </div>;
}
- }
-
- setWriteMode = (mode: DocServer.WriteMode) => {
- console.log(DocServer.WriteMode[mode]);
- const mode1 = mode;
- const mode2 = mode === DocServer.WriteMode.Default ? mode : DocServer.WriteMode.Playground;
- DocServer.setFieldWriteMode("x", mode1);
- DocServer.setFieldWriteMode("y", mode1);
- DocServer.setFieldWriteMode("width", mode1);
- DocServer.setFieldWriteMode("height", mode1);
-
- DocServer.setFieldWriteMode("panX", mode2);
- DocServer.setFieldWriteMode("panY", mode2);
- DocServer.setFieldWriteMode("scale", mode2);
- DocServer.setFieldWriteMode("viewType", mode2);
- }
-
-
- @observable private _colorPickerDisplay = false;
- /* for the expandable add nodes menu. Not included with the miscbuttons because once it expands it expands the whole div with it, making canvas interactions limited. */
- nodesMenu() {
- let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg";
-
- let addColNode = action(() => Docs.Create.FreeformDocument([], { width: this.pwidth * .7, height: this.pheight, title: "a freeform collection" }));
- let addPresNode = action(() => Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List<Doc>(), { width: 200, height: 500, title: "a presentation trail" }));
- let addWebNode = action(() => Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" }));
- let addDragboxNode = action(() => Docs.Create.DragboxDocument({ width: 40, height: 40, title: "drag collection" }));
- let addImageNode = action(() => Docs.Create.ImageDocument(imgurl, { width: 200, title: "an image of a cat" }));
- let addButtonDocument = action(() => Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" }));
- let addImportCollectionNode = action(() => Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 }));
- // let youtubeurl = "https://www.youtube.com/embed/TqcApsGRzWw";
- // let addYoutubeSearcher = action(() => Docs.Create.YoutubeDocument(youtubeurl, { width: 600, height: 600, title: "youtube search" }));
- let addWebCam = action(() => Docs.Create.WebCamDocument("", {}));
-
- // let googlePhotosSearch = () => GooglePhotosClientUtils.CollectionFromSearch(Docs.Create.MasonryDocument, { included: [GooglePhotosClientUtils.ContentCategories.LANDSCAPES] });
-
- let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc | Promise<Doc>][] = [
- [React.createRef<HTMLDivElement>(), "object-group", "Add Collection", addColNode],
- [React.createRef<HTMLDivElement>(), "tv", "Add Presentation Trail", addPresNode],
- [React.createRef<HTMLDivElement>(), "globe-asia", "Add Website", addWebNode],
- [React.createRef<HTMLDivElement>(), "bolt", "Add Button", addButtonDocument],
- [React.createRef<HTMLDivElement>(), "file", "Add Document Dragger", addDragboxNode],
- // [React.createRef<HTMLDivElement>(), "object-group", "Test Google Photos Search", googlePhotosSearch],
- [React.createRef<HTMLDivElement>(), "cloud-upload-alt", "Import Directory", addImportCollectionNode], //remove at some point in favor of addImportCollectionNode
- [React.createRef<HTMLDivElement>(), "video", "Add WebCam", addWebCam]
- //[React.createRef<HTMLDivElement>(), "play", "Add Youtube Searcher", addYoutubeSearcher],
- ];
- if (!ClientUtils.RELEASE) btns.unshift([React.createRef<HTMLDivElement>(), "cat", "Add Cat Image", addImageNode]);
-
- return < div id="add-nodes-menu" style={{ left: this.flyoutWidth + 20, bottom: 20 }} >
-
- <input type="checkbox" id="add-menu-toggle" ref={this.addMenuToggle} />
- <label htmlFor="add-menu-toggle" style={{ marginTop: 2 }} title="Close Menu"><p>+</p></label>
-
- <div id="add-options-content">
- <ul id="add-options-list">
- <li key="search"><button className="add-button round-button" title="Search" onClick={this.toggleSearch}><FontAwesomeIcon icon="search" size="sm" /></button></li>
- <li key="undo"><button className="add-button round-button" title="Undo" style={{ opacity: UndoManager.CanUndo() ? 1 : 0.5, transition: "0.4s ease all" }} onClick={() => UndoManager.Undo()}><FontAwesomeIcon icon="undo-alt" size="sm" /></button></li>
- <li key="redo"><button className="add-button round-button" title="Redo" style={{ opacity: UndoManager.CanRedo() ? 1 : 0.5, transition: "0.4s ease all" }} onClick={() => UndoManager.Redo()}><FontAwesomeIcon icon="redo-alt" size="sm" /></button></li>
- {btns.map(btn =>
- <li key={btn[1]} ><div ref={btn[0]}>
- <button className="round-button add-button" title={btn[2]} onPointerDown={SetupDrag(btn[0], btn[3])}>
- <FontAwesomeIcon icon={btn[1]} size="sm" />
- </button>
- </div></li>)}
- <li key="undoTest"><button className="add-button round-button" title="Click if undo isn't working" onClick={() => UndoManager.TraceOpenBatches()}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li>
- <li key="color"><button className="add-button round-button" title="Select Color" style={{ zIndex: 1000 }} onClick={() => this.toggleColorPicker()}><div className="toolbar-color-button" style={{ backgroundColor: InkingControl.Instance.selectedColor }} >
- <div className="toolbar-color-picker" onClick={this.onColorClick} style={this._colorPickerDisplay ? { color: "black", display: "block" } : { color: "black", display: "none" }}>
- <SketchPicker color={InkingControl.Instance.selectedColor} onChange={InkingControl.Instance.switchColor} />
- </div>
- </div></button></li>
- <li key="ink" style={{ paddingRight: "6px" }}><button className="toolbar-button round-button" title="Ink" onClick={() => InkingControl.Instance.toggleDisplay()}><FontAwesomeIcon icon="pen-nib" size="sm" /> </button></li>
- <li key="pen"><button onClick={() => InkingControl.Instance.switchTool(InkTool.Pen)} title="Pen" style={this.selected(InkTool.Pen)}><FontAwesomeIcon icon="pen" size="lg" /></button></li>
- <li key="marker"><button onClick={() => InkingControl.Instance.switchTool(InkTool.Highlighter)} title="Highlighter" style={this.selected(InkTool.Highlighter)}><FontAwesomeIcon icon="highlighter" size="lg" /></button></li>
- <li key="eraser"><button onClick={() => InkingControl.Instance.switchTool(InkTool.Eraser)} title="Eraser" style={this.selected(InkTool.Eraser)}><FontAwesomeIcon icon="eraser" size="lg" /></button></li>
- <li key="inkControls"><InkingControl /></li>
- <li key="logout"><button onClick={() => window.location.assign(Utils.prepend(RouteStore.logout))}>{CurrentUserUtils.GuestWorkspace ? "Exit" : "Log Out"}</button></li>
- </ul>
- </div>
- </div >;
- }
-
-
-
- @action
- toggleColorPicker = (close = false) => {
- this._colorPickerDisplay = close ? false : !this._colorPickerDisplay;
- }
-
- /* @TODO this should really be moved into a moveable toolbar component, but for now let's put it here to meet the deadline */
- @computed
- get miscButtons() {
- return [
- this.isSearchVisible ? <div className="main-searchDiv" key="search" style={{ top: '34px', right: '1px', position: 'absolute' }} > <FilterBox /> </div> : null,
- ];
-
- }
-
- @observable isSearchVisible = false;
- @action.bound
- toggleSearch = () => {
- this.isSearchVisible = !this.isSearchVisible;
- }
-
- @computed private get dictationOverlay() {
- let success = this.dictationSuccess;
- let result = this.isListening && !this.isListening.interim ? DictationManager.placeholder : `"${this.dictatedPhrase}"`;
- let dialogueBoxStyle = {
- background: success === undefined ? "gainsboro" : success ? "lawngreen" : "red",
- borderColor: this.isListening ? "red" : "black",
- fontStyle: "italic"
- };
- let overlayStyle = {
- backgroundColor: this.isListening ? "red" : "darkslategrey"
- };
- return (
- <MainViewModal
- contents={result}
- isDisplayed={this.dictationOverlayVisible}
- interactive={false}
- dialogueBoxStyle={dialogueBoxStyle}
- overlayStyle={overlayStyle}
- />
- );
+ return (null);
}
render() {
- return (
- <div id="main-div">
- {this.dictationOverlay}
- <SharingManager />
- <DocumentDecorations />
- {this.mainContent}
- <PreviewCursor />
- <ContextMenu />
- {this.nodesMenu()}
- {this.miscButtons}
- <PDFMenu />
- <MainOverlayTextBox firstinstance={true} />
- <OverlayView />
- </div >
- );
+ return (<div className="mainView-container">
+ <DictationOverlay />
+ <SharingManager />
+ <GoogleAuthenticationManager />
+ <DocumentDecorations />
+ {this.mainContent}
+ <PreviewCursor />
+ <ContextMenu />
+ {this.docButtons}
+ <PDFMenu />
+ <OverlayView />
+ </div >);
}
}
+Scripting.addGlobal(function freezeSidebar() { MainView.expandFlyout(); }); \ No newline at end of file
diff --git a/src/client/views/MainViewModal.tsx b/src/client/views/MainViewModal.tsx
index b7fdd6168..221a0260a 100644
--- a/src/client/views/MainViewModal.tsx
+++ b/src/client/views/MainViewModal.tsx
@@ -17,7 +17,7 @@ export default class MainViewModal extends React.Component<MainViewOverlayProps>
let p = this.props;
let dialogueOpacity = p.dialogueBoxDisplayedOpacity || 1;
let overlayOpacity = p.overlayDisplayedOpacity || 0.4;
- return (
+ return !p.isDisplayed ? (null) : (
<div style={{ pointerEvents: p.isDisplayed ? p.interactive ? "all" : "none" : "none" }}>
<div
className={"dialogue-box"}
diff --git a/src/client/views/MainViewNotifs.scss b/src/client/views/MainViewNotifs.scss
new file mode 100644
index 000000000..25ec95643
--- /dev/null
+++ b/src/client/views/MainViewNotifs.scss
@@ -0,0 +1,18 @@
+.mainNotifs-container {
+ position:absolute;
+
+ .mainNotifs-badge {
+ position: absolute;
+ top: -10px;
+ right: -10px;
+ color: white;
+ background: #f44b42;
+ font-weight: 300;
+ border-radius: 100%;
+ width: 25px;
+ height: 25px;
+ text-align: center;
+ padding-top: 4px;
+ font-size: 12px;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/MainViewNotifs.tsx b/src/client/views/MainViewNotifs.tsx
new file mode 100644
index 000000000..09fa1cb0c
--- /dev/null
+++ b/src/client/views/MainViewNotifs.tsx
@@ -0,0 +1,32 @@
+import { action, computed, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import "normalize.css";
+import * as React from 'react';
+import { Doc, DocListCast, Opt } from '../../new_fields/Doc';
+import { emptyFunction } from '../../Utils';
+import { SetupDrag } from '../util/DragManager';
+import "./MainViewNotifs.scss";
+import { CollectionDockingView } from './collections/CollectionDockingView';
+
+
+@observer
+export class MainViewNotifs extends React.Component {
+
+ @observable static NotifsCol: Opt<Doc>;
+ openNotifsCol = () => {
+ if (MainViewNotifs.NotifsCol) {
+ CollectionDockingView.AddRightSplit(MainViewNotifs.NotifsCol, undefined);
+ }
+ }
+ render() {
+ const length = MainViewNotifs.NotifsCol ? DocListCast(MainViewNotifs.NotifsCol.data).length : 0;
+ const notifsRef = React.createRef<HTMLDivElement>();
+ const dragNotifs = action(() => MainViewNotifs.NotifsCol!);
+ return <div className="mainNotifs-container" ref={notifsRef}>
+ <button className="mainNotifs-badge" style={length > 0 ? { "display": "initial" } : { "display": "none" }}
+ onClick={this.openNotifsCol} onPointerDown={MainViewNotifs.NotifsCol ? SetupDrag(notifsRef, dragNotifs) : emptyFunction}>
+ {length}
+ </button>
+ </div>;
+ }
+}
diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx
index f1b101b8e..41453f8b2 100644
--- a/src/client/views/MetadataEntryMenu.tsx
+++ b/src/client/views/MetadataEntryMenu.tsx
@@ -3,7 +3,7 @@ import "./MetadataEntryMenu.scss";
import { observer } from 'mobx-react';
import { observable, action, runInAction, trace, computed, IReactionDisposer, reaction } from 'mobx';
import { KeyValueBox } from './nodes/KeyValueBox';
-import { Doc, Field, DocListCast, DocListCastAsync } from '../../new_fields/Doc';
+import { Doc, Field, DocListCastAsync } from '../../new_fields/Doc';
import * as Autosuggest from 'react-autosuggest';
import { undoBatch } from '../util/UndoManager';
import { emptyFunction } from '../../Utils';
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index bb6dd5076..9869e24d1 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -12,6 +12,7 @@ import { Transform } from "../util/Transform";
import { CollectionFreeFormDocumentView } from "./nodes/CollectionFreeFormDocumentView";
import { DocumentContentsView } from "./nodes/DocumentContentsView";
import { NumCast } from "../../new_fields/Types";
+import { CollectionFreeFormLinksView } from "./collections/collectionFreeForm/CollectionFreeFormLinksView";
export type OverlayDisposer = () => void;
@@ -145,6 +146,7 @@ export class OverlayView extends React.Component {
return (null);
}
return CurrentUserUtils.UserDocument.overlays instanceof Doc && DocListCast(CurrentUserUtils.UserDocument.overlays.data).map(d => {
+ d.inOverlay = true;
let offsetx = 0, offsety = 0;
let onPointerMove = action((e: PointerEvent) => {
if (e.buttons === 1) {
@@ -170,14 +172,14 @@ export class OverlayView extends React.Component {
document.addEventListener("pointerup", onPointerUp);
};
return <div className="overlayView-doc" key={d[Id]} onPointerDown={onPointerDown} style={{ transform: `translate(${d.x}px, ${d.y}px)`, display: d.isMinimized ? "none" : "" }}>
- <DocumentContentsView
+ <DocumentView
Document={d}
ChromeHeight={returnZero}
- isSelected={returnFalse}
- select={emptyFunction}
- ruleProvider={undefined}
- layoutKey={"layout"}
+ // isSelected={returnFalse}
+ // select={emptyFunction}
+ // layoutKey={"layout"}
bringToFront={emptyFunction}
+ ruleProvider={undefined}
addDocument={undefined}
removeDocument={undefined}
ContentScaling={returnOne}
@@ -205,6 +207,7 @@ export class OverlayView extends React.Component {
<div>
{this._elements}
</div>
+ <CollectionFreeFormLinksView key="freeformLinks" />
{this.overlayDocs}
</div>
);
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index 1aed51e64..eed2cc5da 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -14,7 +14,7 @@ export class PreviewCursor extends React.Component<{}> {
static _onKeyPress?: (e: KeyboardEvent) => void;
static _getTransform: () => Transform;
static _addLiveTextDoc: (doc: Doc) => void;
- static _addDocument: (doc: Doc, allowDuplicates: false) => boolean;
+ static _addDocument: (doc: Doc) => boolean;
@observable static _clickPoint = [0, 0];
@observable public static Visible = false;
//when focus is lost, this will remove the preview cursor
@@ -44,7 +44,7 @@ export class PreviewCursor extends React.Component<{}> {
title: url, width: 400, height: 315,
nativeWidth: 600, nativeHeight: 472.5,
x: newPoint[0], y: newPoint[1]
- }), false);
+ }));
return;
}
@@ -56,7 +56,7 @@ export class PreviewCursor extends React.Component<{}> {
title: url, width: 300, height: 300,
// nativeWidth: 300, nativeHeight: 472.5,
x: newPoint[0], y: newPoint[1]
- }), false);
+ }));
return;
}
@@ -79,11 +79,11 @@ export class PreviewCursor extends React.Component<{}> {
let img: Doc = Docs.Create.ImageDocument(
arr[1], {
- width: 300, title: arr[1],
- x: newPoint[0],
- y: newPoint[1],
- });
- PreviewCursor._addDocument(img, false);
+ width: 300, title: arr[1],
+ x: newPoint[0],
+ y: newPoint[1],
+ });
+ PreviewCursor._addDocument(img);
return;
}
@@ -113,7 +113,7 @@ export class PreviewCursor extends React.Component<{}> {
onKeyPress: (e: KeyboardEvent) => void,
addLiveText: (doc: Doc) => void,
getTransform: () => Transform,
- addDocument: (doc: Doc, allowDuplicates: false) => boolean) {
+ addDocument: (doc: Doc) => boolean) {
this._clickPoint = [x, y];
this._onKeyPress = onKeyPress;
this._addLiveTextDoc = addLiveText;
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index 9e5e62e03..96265385e 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -9,6 +9,7 @@ import { DocumentView } from "./nodes/DocumentView";
import { Template, Templates } from "./Templates";
import React = require("react");
import { Doc } from "../../new_fields/Doc";
+import { StrCast } from "../../new_fields/Types";
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -100,7 +101,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
clearTemplates = (event: React.MouseEvent) => {
Templates.TemplateList.forEach(template => this.props.docs.forEach(d => d.Document["show" + template.Name] = undefined));
["backgroundColor", "borderRounding", "width", "height"].forEach(field => this.props.docs.forEach(d => {
- if (d.Document.isTemplate && d.props.DataDoc) {
+ if (d.Document.isTemplateDoc && d.props.DataDoc) {
d.Document[field] = undefined;
} else if (d.Document["default" + field[0].toUpperCase() + field.slice(1)] !== undefined) {
d.Document[field] = Doc.GetProto(d.Document)[field] = undefined;
@@ -117,18 +118,18 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
@action
toggleChrome = (): void => {
this.props.docs.map(dv => {
- let layout = dv.Document.layout instanceof Doc ? dv.Document.layout : dv.Document;
+ let layout = Doc.Layout(dv.Document);
layout.chromeStatus = (layout.chromeStatus !== "disabled" ? "disabled" : "enabled");
});
}
render() {
- let layout = this.props.docs[0].Document.layout instanceof Doc ? this.props.docs[0].Document.layout : this.props.docs[0].Document;
+ let layout = Doc.Layout(this.props.docs[0].Document);
let templateMenu: Array<JSX.Element> = [];
this.props.templates.forEach((checked, template) =>
templateMenu.push(<TemplateToggle key={template.Name} template={template} checked={checked} toggle={this.toggleTemplate} />));
templateMenu.push(<OtherToggle key={"float"} name={"Float"} checked={this.props.docs[0].Document.z ? true : false} toggle={this.toggleFloat} />);
- templateMenu.push(<OtherToggle key={"custom"} name={"Custom"} checked={typeof this.props.docs[0].Document.layout === "string" ? false : true} toggle={this.toggleCustom} />);
+ templateMenu.push(<OtherToggle key={"custom"} name={"Custom"} checked={StrCast(this.props.docs[0].Document.layoutKey, "layout") !== "layout"} toggle={this.toggleCustom} />);
templateMenu.push(<OtherToggle key={"chrome"} name={"Chrome"} checked={layout.chromeStatus !== "disabled"} toggle={this.toggleChrome} />);
return (
<div className="templating-menu" >
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
deleted file mode 100644
index e928887e2..000000000
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ /dev/null
@@ -1,196 +0,0 @@
-import { action, computed, observable } from 'mobx';
-import { observer } from 'mobx-react';
-import * as React from 'react';
-import { Doc, DocListCast } from '../../../new_fields/Doc';
-import { Id } from '../../../new_fields/FieldSymbols';
-import { List } from '../../../new_fields/List';
-import { listSpec } from '../../../new_fields/Schema';
-import { BoolCast, Cast, NumCast, PromiseValue, StrCast } from '../../../new_fields/Types';
-import { DocumentManager } from '../../util/DocumentManager';
-import { SelectionManager } from '../../util/SelectionManager';
-import { ContextMenu } from '../ContextMenu';
-import { FieldViewProps } from '../nodes/FieldView';
-import './CollectionBaseView.scss';
-import { DateField } from '../../../new_fields/DateField';
-import { ImageField } from '../../../new_fields/URLField';
-
-export enum CollectionViewType {
- Invalid,
- Freeform,
- Schema,
- Docking,
- Tree,
- Stacking,
- Masonry,
- Pivot,
-}
-
-export namespace CollectionViewType {
-
- const stringMapping = new Map<string, CollectionViewType>([
- ["invalid", CollectionViewType.Invalid],
- ["freeform", CollectionViewType.Freeform],
- ["schema", CollectionViewType.Schema],
- ["docking", CollectionViewType.Docking],
- ["tree", CollectionViewType.Tree],
- ["stacking", CollectionViewType.Stacking],
- ["masonry", CollectionViewType.Masonry],
- ["pivot", CollectionViewType.Pivot]
- ]);
-
- export const valueOf = (value: string) => {
- return stringMapping.get(value.toLowerCase());
- };
-
-}
-
-export interface CollectionRenderProps {
- addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
- removeDocument: (document: Doc) => boolean;
- moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
- active: () => boolean;
- whenActiveChanged: (isActive: boolean) => void;
-}
-
-export interface CollectionViewProps extends FieldViewProps {
- onContextMenu?: (e: React.MouseEvent) => void;
- children: (type: CollectionViewType, props: CollectionRenderProps) => JSX.Element | JSX.Element[] | null | (JSX.Element | null)[];
- className?: string;
- contentRef?: React.Ref<HTMLDivElement>;
-}
-
-@observer
-export class CollectionBaseView extends React.Component<CollectionViewProps> {
- @observable private static _safeMode = false;
- static InSafeMode() { return this._safeMode; }
- static SetSafeMode(safeMode: boolean) { this._safeMode = safeMode; }
- get collectionViewType(): CollectionViewType | undefined {
- let Document = this.props.Document;
- let viewField = Cast(Document.viewType, "number");
- if (CollectionBaseView._safeMode) {
- if (viewField === CollectionViewType.Freeform) {
- return CollectionViewType.Tree;
- }
- if (viewField === CollectionViewType.Invalid) {
- return CollectionViewType.Freeform;
- }
- }
- if (viewField !== undefined) {
- return viewField;
- } else {
- return CollectionViewType.Invalid;
- }
- }
-
- @computed get dataDoc() { return Doc.fieldExtensionDoc(this.props.Document.isTemplate && this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt); }
- @computed get dataField() { return this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey; }
-
- active = (): boolean => {
- var isSelected = this.props.isSelected();
- return isSelected || BoolCast(this.props.Document.forceActive) || this._isChildActive || this.props.renderDepth === 0;
- }
-
- //TODO should this be observable?
- private _isChildActive = false;
- whenActiveChanged = (isActive: boolean) => {
- this._isChildActive = isActive;
- this.props.whenActiveChanged(isActive);
- }
-
- @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt); }
-
- @action.bound
- addDocument(doc: Doc, allowDuplicates: boolean = false): boolean {
- var curPage = NumCast(this.props.Document.curPage, -1);
- Doc.GetProto(doc).page = curPage;
- if (this.props.fieldExt) { // bcz: fieldExt !== undefined means this is an overlay layer
- Doc.GetProto(doc).annotationOn = this.props.Document;
- }
- let targetDataDoc = this.props.fieldExt || this.props.Document.isTemplate ? this.extensionDoc : this.props.Document;
- let targetField = (this.props.fieldExt || this.props.Document.isTemplate) && this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey;
- const value = Cast(targetDataDoc[targetField], listSpec(Doc));
- if (value !== undefined) {
- if (allowDuplicates || !value.some(v => v instanceof Doc && v[Id] === doc[Id])) {
- value.push(doc);
- }
- } else {
- Doc.GetProto(targetDataDoc)[targetField] = new List([doc]);
- }
- Doc.GetProto(doc).lastOpened = new DateField;
- return true;
- }
-
- @action.bound
- removeDocument(doc: Doc): boolean {
- let docView = DocumentManager.Instance.getDocumentView(doc, this.props.ContainingCollectionView);
- docView && SelectionManager.DeselectDoc(docView);
- //TODO This won't create the field if it doesn't already exist
- let targetDataDoc = this.props.fieldExt || this.props.Document.isTemplate ? this.extensionDoc : this.props.Document;
- let targetField = (this.props.fieldExt || this.props.Document.isTemplate) && this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey;
- let value = Cast(targetDataDoc[targetField], listSpec(Doc), []);
- let index = value.reduce((p, v, i) => (v instanceof Doc && v === doc) ? i : p, -1);
- index = index !== -1 ? index : value.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, doc)) ? i : p, -1);
- PromiseValue(Cast(doc.annotationOn, Doc)).then(annotationOn =>
- annotationOn === this.dataDoc.Document && (doc.annotationOn = undefined));
-
- if (index !== -1) {
- value.splice(index, 1);
-
- // SelectionManager.DeselectAll()
- ContextMenu.Instance.clearItems();
- return true;
- }
- return false;
- }
-
- // this is called with the document that was dragged and the collection to move it into.
- // if the target collection is the same as this collection, then the move will be allowed.
- // otherwise, the document being moved must be able to be removed from its container before
- // moving it into the target.
- @action.bound
- moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean {
- if (Doc.AreProtosEqual(this.props.Document, targetCollection)) {
- return true;
- }
- return this.removeDocument(doc) ? addDocument(doc) : false;
- }
-
- showIsTagged = () => {
- const children = DocListCast(this.props.Document.data);
- const imageProtos = children.filter(doc => Cast(doc.data, ImageField)).map(Doc.GetProto);
- const allTagged = imageProtos.length > 0 && imageProtos.every(image => image.googlePhotosTags);
- if (allTagged) {
- return (
- <img
- id={"google-tags"}
- src={"/assets/google_tags.png"}
- />
- );
- }
- return (null);
- }
-
- render() {
- const props: CollectionRenderProps = {
- addDocument: this.addDocument,
- removeDocument: this.removeDocument,
- moveDocument: this.moveDocument,
- active: this.active,
- whenActiveChanged: this.whenActiveChanged,
- };
- const viewtype = this.collectionViewType;
- return (
- <div id="collectionBaseView"
- style={{
- pointerEvents: this.props.Document.isBackground ? "none" : "all",
- boxShadow: this.props.Document.isBackground ? undefined : `#9c9396 ${StrCast(this.props.Document.boxShadow, "0.2vw 0.2vw 0.8vw")}`
- }}
- className={this.props.className || "collectionView-cont"}
- onContextMenu={this.props.onContextMenu} ref={this.props.contentRef}>
- {this.showIsTagged()}
- {viewtype !== undefined ? this.props.children(viewtype, props) : (null)}
- </div>
- );
- }
-
-}
diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss
index 6f5abd05b..12f54d69d 100644
--- a/src/client/views/collections/CollectionDockingView.scss
+++ b/src/client/views/collections/CollectionDockingView.scss
@@ -25,6 +25,9 @@
top: 0;
left: 0;
overflow: hidden;
+ .lm_content {
+ background: white;
+ }
.lm_controls>li {
opacity: 0.6;
transform: scale(1.2);
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 98aff41d3..42d372f4a 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -31,14 +31,14 @@ import { SubCollectionViewProps } from "./CollectionSubView";
import React = require("react");
import { ButtonSelector } from './ParentDocumentSelector';
import { DocumentType } from '../../documents/DocumentTypes';
-import { compileFunction } from 'vm';
import { ComputedField } from '../../../new_fields/ScriptField';
library.add(faFile);
const _global = (window /* browser */ || global /* node */) as any;
@observer
export class CollectionDockingView extends React.Component<SubCollectionViewProps> {
- @observable public static Instance: CollectionDockingView;
+ @observable public static Instances: CollectionDockingView[] = [];
+ @computed public static get Instance() { return CollectionDockingView.Instances[0]; }
public static makeDocumentConfig(document: Doc, dataDoc: Doc | undefined, width?: number) {
return {
type: 'react-component',
@@ -66,7 +66,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
constructor(props: SubCollectionViewProps) {
super(props);
- !CollectionDockingView.Instance && runInAction(() => CollectionDockingView.Instance = this);
+ runInAction(() => !CollectionDockingView.Instances ? CollectionDockingView.Instances = [this] : CollectionDockingView.Instances.push(this));
//Why is this here?
(window as any).React = React;
(window as any).ReactDOM = ReactDOM;
@@ -318,13 +318,14 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
} catch (e) {
}
- if (this._goldenLayout) this._goldenLayout.destroy();
- runInAction(() => this._goldenLayout = null);
+ this._goldenLayout && this._goldenLayout.destroy();
+ runInAction(() => {
+ CollectionDockingView.Instances.splice(CollectionDockingView.Instances.indexOf(this), 1);
+ this._goldenLayout = null;
+ });
window.removeEventListener('resize', this.onResize);
- if (this.reactionDisposer) {
- this.reactionDisposer();
- }
+ this.reactionDisposer && this.reactionDisposer();
}
@action
onResize = (event: any) => {
@@ -574,8 +575,8 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
let curPres = Cast(CurrentUserUtils.UserDocument.curPresentation, Doc) as Doc;
if (curPres) {
let pinDoc = Docs.Create.PresElementBoxDocument({ backgroundColor: "transparent" });
- Doc.GetProto(pinDoc).target = doc;
- Doc.GetProto(pinDoc).title = ComputedField.MakeFunction('(this.target instanceof Doc) && this.target.title.toString()');
+ Doc.GetProto(pinDoc).presentationTargetDoc = doc;
+ Doc.GetProto(pinDoc).title = ComputedField.MakeFunction('(this.presentationTargetDoc instanceof Doc) && this.presentationTargetDoc.title.toString()');
const data = Cast(curPres.data, listSpec(Doc));
if (data) {
data.push(pinDoc);
@@ -614,19 +615,20 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
}
}
- panelWidth = () => this._document!.ignoreAspect || this._document!.fitWidth ? this._panelWidth : Math.min(this._panelWidth, Math.max(NumCast(this._document!.width), this.nativeWidth()));
- panelHeight = () => this._document!.ignoreAspect || this._document!.fitWidth ? this._panelHeight : Math.min(this._panelHeight, Math.max(NumCast(this._document!.height), this.nativeHeight()));
+ get layoutDoc() { return this._document && Doc.Layout(this._document);}
+ panelWidth = () => this.layoutDoc && this.layoutDoc.maxWidth ? Math.min(Math.max(NumCast(this.layoutDoc.width), NumCast(this.layoutDoc.nativeWidth)), this._panelWidth) : this._panelWidth;
+ panelHeight = () => this._panelHeight;
- nativeWidth = () => !this._document!.ignoreAspect && !this._document!.fitWidth ? NumCast(this._document!.nativeWidth) || this._panelWidth : 0;
- nativeHeight = () => !this._document!.ignoreAspect && !this._document!.fitWidth ? NumCast(this._document!.nativeHeight) || this._panelHeight : 0;
+ nativeWidth = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!.fitWidth ? NumCast(this.layoutDoc!.nativeWidth) || this._panelWidth : 0;
+ nativeHeight = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!.fitWidth ? NumCast(this.layoutDoc!.nativeHeight) || this._panelHeight : 0;
contentScaling = () => {
- if (this._document!.type === DocumentType.PDF) {
- if ((this._document && this._document.fitWidth) ||
- this._panelHeight / NumCast(this._document!.nativeHeight) > this._panelWidth / NumCast(this._document!.nativeWidth)) {
- return this._panelWidth / NumCast(this._document!.nativeWidth);
+ if (this.layoutDoc!.type === DocumentType.PDF) {
+ if ((this.layoutDoc && this.layoutDoc.fitWidth) ||
+ this._panelHeight / NumCast(this.layoutDoc!.nativeHeight) > this._panelWidth / NumCast(this.layoutDoc!.nativeWidth)) {
+ return this._panelWidth / NumCast(this.layoutDoc!.nativeWidth);
} else {
- return this._panelHeight / NumCast(this._document!.nativeHeight);
+ return this._panelHeight / NumCast(this.layoutDoc!.nativeHeight);
}
}
const nativeH = this.nativeHeight();
@@ -644,7 +646,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
}
return Transform.Identity();
}
- get previewPanelCenteringOffset() { return this.nativeWidth() && !BoolCast(this._document!.ignoreAspect) ? (this._panelWidth - this.nativeWidth() / this.ScreenToLocalTransform().Scale) / 2 : 0; }
+ get previewPanelCenteringOffset() { return this.nativeWidth() && !this.layoutDoc!.ignoreAspect ? (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2 : 0; }
addDocTab = (doc: Doc, dataDoc: Opt<Doc>, location: string) => {
SelectionManager.DeselectAll();
@@ -689,11 +691,11 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
}
render() {
- return (!this._isActive || !this._document) ? (null) :
+ return (!this._isActive || !this.layoutDoc) ? (null) :
(<div className="collectionDockingView-content" ref={ref => this._mainCont = ref}
style={{
transform: `translate(${this.previewPanelCenteringOffset}px, 0px)`,
- height: this._document && this._document.fitWidth ? undefined : "100%"
+ height: this.layoutDoc && this.layoutDoc.fitWidth ? undefined : "100%"
}}>
{this.docView}
</div >);
diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
new file mode 100644
index 000000000..52ebfafd3
--- /dev/null
+++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
@@ -0,0 +1,367 @@
+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, observable } from "mobx";
+import { observer } from "mobx-react";
+import Measure from "react-measure";
+import { Doc } from "../../../new_fields/Doc";
+import { PastelSchemaPalette, SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
+import { ScriptField } from "../../../new_fields/ScriptField";
+import { StrCast } from "../../../new_fields/Types";
+import { numberRange } from "../../../Utils";
+import { Docs } from "../../documents/Documents";
+import { DragManager } from "../../util/DragManager";
+import { CompileScript } from "../../util/Scripting";
+import { SelectionManager } from "../../util/SelectionManager";
+import { Transform } from "../../util/Transform";
+import { undoBatch } from "../../util/UndoManager";
+import { anchorPoints, Flyout } from "../DocumentDecorations";
+import { EditableView } from "../EditableView";
+import { CollectionStackingView } from "./CollectionStackingView";
+import "./CollectionStackingView.scss";
+import { undo } from "prosemirror-history";
+
+library.add(faPalette);
+
+interface CMVFieldRowProps {
+ rows: () => number;
+ headings: () => object[];
+ heading: string;
+ headingObject: SchemaHeaderField | undefined;
+ docList: Doc[];
+ parent: CollectionStackingView;
+ type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined;
+ createDropTarget: (ele: HTMLDivElement) => void;
+ screenToLocalTransform: () => Transform;
+ setDocHeight: (key: string, thisHeight: number) => void;
+}
+
+@observer
+export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowProps> {
+ @observable private _background = "inherit";
+ @observable private _createAliasSelected: boolean = false;
+ @observable private _collapsed: boolean = false;
+ @observable private _headingsHack: number = 1;
+ @observable private _heading = this.props.headingObject ? this.props.headingObject.heading : this.props.heading;
+ @observable private _color = this.props.headingObject ? this.props.headingObject.color : "#f1efeb";
+
+ 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 _counter: number = 0;
+
+
+ createRowDropRef = (ele: HTMLDivElement | null) => {
+ this._dropDisposer && this._dropDisposer();
+ if (ele) {
+ this._dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.rowDrop.bind(this) } });
+ }
+ }
+
+ getTrueHeight = () => {
+ if (this._collapsed) {
+ this.props.setDocHeight(this._heading, 20);
+ } else {
+ let rawHeight = this._contRef.current!.getBoundingClientRect().height + 15; //+ 15 accounts for the group header
+ let transformScale = this.props.screenToLocalTransform().Scale;
+ let trueHeight = rawHeight * transformScale;
+ this.props.setDocHeight(this._heading, trueHeight);
+ }
+ }
+
+ @undoBatch
+ rowDrop = action((e: Event, de: DragManager.DropEvent) => {
+ this._createAliasSelected = false;
+ if (de.data instanceof DragManager.DocumentDragData) {
+ (this.props.parent.Document.dropConverter instanceof ScriptField) &&
+ this.props.parent.Document.dropConverter.script.run({ dragData: de.data });
+ let key = StrCast(this.props.parent.props.Document.sectionFilter);
+ let castedValue = this.getValue(this._heading);
+ de.data.droppedDocuments.forEach(d => d[key] = castedValue);
+ this.props.parent.drop(e, de);
+ e.stopPropagation();
+ }
+ });
+
+ getValue = (value: string): any => {
+ let parsed = parseInt(value);
+ if (!isNaN(parsed)) return parsed;
+ if (value.toLowerCase().indexOf("true") > -1) return true;
+ if (value.toLowerCase().indexOf("false") > -1) return false;
+ return value;
+ }
+
+ @action
+ headingChanged = (value: string, shiftDown?: boolean) => {
+ this._createAliasSelected = false;
+ let key = StrCast(this.props.parent.props.Document.sectionFilter);
+ let castedValue = this.getValue(value);
+ if (castedValue) {
+ if (this.props.parent.sectionHeaders) {
+ if (this.props.parent.sectionHeaders.map(i => i.heading).indexOf(castedValue.toString()) > -1) {
+ return false;
+ }
+ }
+ this.props.docList.forEach(d => d[key] = castedValue);
+ if (this.props.headingObject) {
+ this.props.headingObject.setHeading(castedValue.toString());
+ this._heading = this.props.headingObject.heading;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @action
+ changeColumnColor = (color: string) => {
+ this._createAliasSelected = false;
+ if (this.props.headingObject) {
+ this.props.headingObject.setColor(color);
+ this._color = color;
+ }
+ }
+
+ pointerEnteredRow = action(() => SelectionManager.GetIsDragging() && (this._background = "#b4b4b4"));
+
+ @action
+ pointerLeaveRow = () => {
+ this._createAliasSelected = false;
+ this._background = "inherit";
+ document.removeEventListener("pointermove", this.startDrag);
+ }
+
+ @action
+ addDocument = (value: string, shiftDown?: boolean) => {
+ this._createAliasSelected = false;
+ let key = StrCast(this.props.parent.props.Document.sectionFilter);
+ let newDoc = Docs.Create.TextDocument({ height: 18, width: 200, title: value });
+ newDoc[key] = this.getValue(this.props.heading);
+ return this.props.parent.props.addDocument(newDoc);
+ }
+
+ deleteRow = undoBatch(action(() => {
+ this._createAliasSelected = false;
+ let key = StrCast(this.props.parent.props.Document.sectionFilter);
+ this.props.docList.forEach(d => d[key] = undefined);
+ if (this.props.parent.sectionHeaders && this.props.headingObject) {
+ let index = this.props.parent.sectionHeaders.indexOf(this.props.headingObject);
+ this.props.parent.sectionHeaders.splice(index, 1);
+ }
+ }));
+
+ @action
+ collapseSection = () => {
+ this._createAliasSelected = false;
+ if (this.props.headingObject) {
+ this._headingsHack++;
+ this.props.headingObject.setCollapsed(!this.props.headingObject.collapsed);
+ this.toggleVisibility();
+ }
+ }
+
+ startDrag = (e: PointerEvent) => {
+ let [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) {
+ let alias = Doc.MakeAlias(this.props.parent.props.Document);
+ let key = StrCast(this.props.parent.props.Document.sectionFilter);
+ let value = this.getValue(this._heading);
+ value = typeof value === "string" ? `"${value}"` : value;
+ let script = `return doc.${key} === ${value}`;
+ let compiled = CompileScript(script, { params: { doc: Doc.name } });
+ if (compiled.compiled) {
+ let scriptField = new ScriptField(compiled);
+ alias.viewSpecScript = scriptField;
+ let dragData = new DragManager.DocumentDragData([alias]);
+ DragManager.StartDocumentDrag([this._headerRef.current!], dragData, e.clientX, e.clientY);
+ }
+
+ e.stopPropagation();
+ document.removeEventListener("pointermove", this.startDrag);
+ document.removeEventListener("pointerup", this.pointerUp);
+ }
+ }
+
+ pointerUp = (e: PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+
+ document.removeEventListener("pointermove", this.startDrag);
+ document.removeEventListener("pointerup", this.pointerUp);
+ }
+
+ @action
+ headerDown = (e: React.PointerEvent<HTMLDivElement>) => {
+ e.stopPropagation();
+ e.preventDefault();
+
+ let [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX, e.clientY);
+ this._startDragPosition = { x: dx, y: dy };
+
+ if (this._createAliasSelected) {
+ document.removeEventListener("pointermove", this.startDrag);
+ document.addEventListener("pointermove", this.startDrag);
+ document.removeEventListener("pointerup", this.pointerUp);
+ document.addEventListener("pointerup", this.pointerUp);
+ }
+ this._createAliasSelected = false;
+ }
+
+ renderColorPicker = () => {
+ let selected = this.props.headingObject ? this.props.headingObject.color : "#f1efeb";
+
+ let pink = PastelSchemaPalette.get("pink2");
+ let purple = PastelSchemaPalette.get("purple4");
+ let blue = PastelSchemaPalette.get("bluegreen1");
+ let yellow = PastelSchemaPalette.get("yellow4");
+ let red = PastelSchemaPalette.get("red2");
+ let green = PastelSchemaPalette.get("bluegreen7");
+ let cyan = PastelSchemaPalette.get("bluegreen5");
+ let orange = PastelSchemaPalette.get("orange1");
+ let gray = "#f1efeb";
+
+ return (
+ <div className="collectionStackingView-colorPicker">
+ <div className="colorOptions">
+ <div className={"colorPicker" + (selected === pink ? " active" : "")} style={{ backgroundColor: pink }} onClick={() => this.changeColumnColor(pink!)}></div>
+ <div className={"colorPicker" + (selected === purple ? " active" : "")} style={{ backgroundColor: purple }} onClick={() => this.changeColumnColor(purple!)}></div>
+ <div className={"colorPicker" + (selected === blue ? " active" : "")} style={{ backgroundColor: blue }} onClick={() => this.changeColumnColor(blue!)}></div>
+ <div className={"colorPicker" + (selected === yellow ? " active" : "")} style={{ backgroundColor: yellow }} onClick={() => this.changeColumnColor(yellow!)}></div>
+ <div className={"colorPicker" + (selected === red ? " active" : "")} style={{ backgroundColor: red }} onClick={() => this.changeColumnColor(red!)}></div>
+ <div className={"colorPicker" + (selected === gray ? " active" : "")} style={{ backgroundColor: gray }} onClick={() => this.changeColumnColor(gray)}></div>
+ <div className={"colorPicker" + (selected === green ? " active" : "")} style={{ backgroundColor: green }} onClick={() => this.changeColumnColor(green!)}></div>
+ <div className={"colorPicker" + (selected === cyan ? " active" : "")} style={{ backgroundColor: cyan }} onClick={() => this.changeColumnColor(cyan!)}></div>
+ <div className={"colorPicker" + (selected === orange ? " active" : "")} style={{ backgroundColor: orange }} onClick={() => this.changeColumnColor(orange!)}></div>
+ </div>
+ </div>
+ );
+ }
+
+ toggleAlias = action(() => this._createAliasSelected = true);
+ toggleVisibility = action(() => this._collapsed = !this._collapsed);
+
+ renderMenu = () => {
+ let selected = this._createAliasSelected;
+ return (<div className="collectionStackingView-optionPicker">
+ <div className="optionOptions">
+ <div className={"optionPicker" + (selected === true ? " active" : "")} onClick={this.toggleAlias}>Create Alias</div>
+ <div className={"optionPicker" + (selected === true ? " active" : "")} onClick={this.deleteRow}>Delete</div>
+ </div>
+ </div>);
+ }
+
+ handleResize = (size: any) => {
+ if (++this._counter !== 1) {
+ this.getTrueHeight();
+ }
+ }
+
+
+ render() {
+ let 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))));
+ let key = StrCast(this.props.parent.props.Document.sectionFilter);
+ let heading = this._heading;
+ let style = this.props.parent;
+ let evContents = heading ? heading : this.props.type && this.props.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`;
+ let headerEditableViewProps = {
+ GetValue: () => evContents,
+ SetValue: this.headingChanged,
+ contents: evContents,
+ oneLine: true,
+ HeadingObject: this.props.headingObject,
+ HeadingsHack: this._headingsHack,
+ toggle: this.toggleVisibility,
+ color: this._color
+ };
+ let newEditableViewProps = {
+ GetValue: () => "",
+ SetValue: this.addDocument,
+ contents: "+ NEW",
+ HeadingObject: this.props.headingObject,
+ HeadingsHack: this._headingsHack,
+ toggle: this.toggleVisibility,
+ color: this._color
+ };
+ let headingView = this.props.parent.props.Document.miniHeaders ?
+ <div className="collectionStackingView-miniHeader" style={{ width: "100%" }}>
+ {<EditableView {...headerEditableViewProps} />}
+ </div> :
+ this.props.headingObject ?
+ <div className="collectionStackingView-sectionHeader" ref={this._headerRef} >
+ <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 : "lightgrey",
+ color: "grey"
+ }}>
+ {<EditableView {...headerEditableViewProps} />}
+ {evContents === `NO ${key.toUpperCase()} VALUE` ? (null) :
+ <div className="collectionStackingView-sectionColor">
+ <Flyout anchorPoint={anchorPoints.CENTER_RIGHT} content={this.renderColorPicker()}>
+ <button className="collectionStackingView-sectionColorButton">
+ <FontAwesomeIcon icon="palette" size="lg" />
+ </button>
+ </ Flyout >
+ </div>
+ }
+ <button className="collectionStackingView-sectionDelete" onClick={this.collapseSection}>
+ <FontAwesomeIcon icon={this._collapsed ? "chevron-down" : "chevron-up"} size="lg" />
+ </button>
+ {evContents === `NO ${key.toUpperCase()} VALUE` ? (null) :
+ <div className="collectionStackingView-sectionOptions">
+ <Flyout anchorPoint={anchorPoints.TOP_RIGHT} content={this.renderMenu()}>
+ <button className="collectionStackingView-sectionOptionButton">
+ <FontAwesomeIcon icon="ellipsis-v" size="lg" />
+ </button>
+ </Flyout>
+ </div>
+ }
+ </div>
+ </div > : (null);
+ const background = this._background; //to account for observables in Measure
+ const collapsed = this._collapsed;
+ let chromeStatus = this.props.parent.props.Document.chromeStatus;
+ return (
+ <Measure offset onResize={this.handleResize}>
+ {({ measureRef }) => {
+ return <div ref={measureRef}>
+ <div className="collectionStackingView-masonrySection"
+ key={heading = "empty"}
+ style={{ width: this.props.parent.NodeWidth, background }}
+ ref={this.createRowDropRef}
+ onPointerEnter={this.pointerEnteredRow}
+ onPointerLeave={this.pointerLeaveRow}
+ >
+ {headingView}
+ {collapsed ? (null) :
+ < div style={{ position: "relative" }}>
+ <div key={`${heading}-stack`} className={`collectionStackingView-masonryGrid`}
+ ref={this._contRef}
+ style={{
+ padding: `${this.props.parent.yMargin}px ${this.props.parent.xMargin}px`,
+ width: this.props.parent.NodeWidth,
+ gridGap: this.props.parent.gridGap,
+ gridTemplateColumns: numberRange(rows).reduce((list: string, i: any) => list + ` ${this.props.parent.columnWidth}px`, ""),
+ }}>
+ {this.props.parent.children(this.props.docList)}
+ {this.props.parent.columnDragger}
+ </div>
+ {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ?
+ <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton"
+ style={{ width: style.columnWidth / style.numGroupColumns }}>
+ <EditableView {...newEditableViewProps} />
+ </div> : null
+ }
+ </div>
+ }
+ </div >
+ </div>;
+ }}
+ </Measure>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionPDFView.scss b/src/client/views/collections/CollectionPDFView.scss
deleted file mode 100644
index 62ec8a5be..000000000
--- a/src/client/views/collections/CollectionPDFView.scss
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-.collectionPdfView-cont {
- width: 100%;
- height: 100%;
- position: absolute;
- top: 0;
- left: 0;
- z-index: -1;
- overflow: hidden !important;
-}
diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx
deleted file mode 100644
index cc8142ec0..000000000
--- a/src/client/views/collections/CollectionPDFView.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { trace } from "mobx";
-import { observer } from "mobx-react";
-import { Id } from "../../../new_fields/FieldSymbols";
-import { emptyFunction } from "../../../Utils";
-import { ContextMenu } from "../ContextMenu";
-import { FieldView, FieldViewProps } from "../nodes/FieldView";
-import { CollectionBaseView, CollectionRenderProps, CollectionViewType } from "./CollectionBaseView";
-import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";
-import "./CollectionPDFView.scss";
-import React = require("react");
-
-
-@observer
-export class CollectionPDFView extends React.Component<FieldViewProps> {
- public static LayoutString(fieldKey: string = "data", fieldExt: string = "annotations") {
- return FieldView.LayoutString(CollectionPDFView, fieldKey, fieldExt);
- }
-
- onContextMenu = (e: React.MouseEvent): void => {
- if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
- ContextMenu.Instance.addItem({ description: "PDFOptions", event: emptyFunction, icon: "file-pdf" });
- }
- }
-
- subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => {
- return (<CollectionFreeFormView {...this.props} {...renderProps} CollectionView={this} chromeCollapsed={true} />);
- }
-
- render() {
- trace();
- return (
- <CollectionBaseView {...this.props} className={"collectionPdfView-cont"} onContextMenu={this.onContextMenu}>
- {this.subView}
- </CollectionBaseView>
- );
- }
-} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx
index 179e44266..54a36f691 100644
--- a/src/client/views/collections/CollectionSchemaCells.tsx
+++ b/src/client/views/collections/CollectionSchemaCells.tsx
@@ -13,17 +13,13 @@ import { COLLECTION_BORDER_WIDTH, MAX_ROW_HEIGHT } from '../globalCssVariables.s
import '../DocumentDecorations.scss';
import { EditableView } from "../EditableView";
import { FieldView, FieldViewProps } from "../nodes/FieldView";
-import { CollectionPDFView } from "./CollectionPDFView";
import "./CollectionSchemaView.scss";
-import { CollectionVideoView } from "./CollectionVideoView";
import { CollectionView } from "./CollectionView";
import { NumCast, StrCast, BoolCast, FieldValue, Cast } from "../../../new_fields/Types";
import { Docs } from "../../documents/Documents";
-import { DocumentContentsView } from "../nodes/DocumentContentsView";
import { SelectionManager } from "../../util/SelectionManager";
import { library } from '@fortawesome/fontawesome-svg-core';
import { faExpand } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
import { KeyCodes } from "../../northstar/utils/KeyCodes";
import { undoBatch } from "../../util/UndoManager";
@@ -34,8 +30,8 @@ export interface CellProps {
row: number;
col: number;
rowProps: CellInfo;
- CollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
- ContainingCollection: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
+ CollectionView: Opt<CollectionView>;
+ ContainingCollection: Opt<CollectionView>;
Document: Doc;
fieldKey: string;
renderDepth: number;
@@ -148,7 +144,6 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
Document: this.props.rowProps.original,
DataDoc: this.props.rowProps.original,
fieldKey: this.props.rowProps.column.id as string,
- fieldExt: "",
ruleProvider: undefined,
ContainingCollectionView: this.props.CollectionView,
ContainingCollectionDoc: this.props.CollectionView && this.props.CollectionView.props.Document,
diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss
index e0cedc210..6a9392253 100644
--- a/src/client/views/collections/CollectionSchemaView.scss
+++ b/src/client/views/collections/CollectionSchemaView.scss
@@ -40,7 +40,6 @@
.collectionSchemaView-previewRegion {
position: relative;
- background: $light-color;
height: auto !important;
.collectionSchemaView-previewDoc {
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 52a41fc84..34f642f80 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -21,10 +21,8 @@ import { COLLECTION_BORDER_WIDTH } from '../../views/globalCssVariables.scss';
import { ContextMenu } from "../ContextMenu";
import '../DocumentDecorations.scss';
import { DocumentView } from "../nodes/DocumentView";
-import { CollectionPDFView } from "./CollectionPDFView";
import "./CollectionSchemaView.scss";
import { CollectionSubView } from "./CollectionSubView";
-import { CollectionVideoView } from "./CollectionVideoView";
import { CollectionView } from "./CollectionView";
import { undoBatch } from "../../util/UndoManager";
import { CollectionSchemaHeader, CollectionSchemaAddColumnHeader } from "./CollectionSchemaHeaders";
@@ -51,7 +49,7 @@ const columnTypes: Map<string, ColumnType> = new Map([
["title", ColumnType.String],
["x", ColumnType.Number], ["y", ColumnType.Number], ["width", ColumnType.Number], ["height", ColumnType.Number],
["nativeWidth", ColumnType.Number], ["nativeHeight", ColumnType.Number], ["isPrototype", ColumnType.Boolean],
- ["page", ColumnType.Number], ["curPage", ColumnType.Number], ["zIndex", ColumnType.Number]
+ ["page", ColumnType.Number], ["curPage", ColumnType.Number], ["currentTimecode", ColumnType.Number], ["zIndex", ColumnType.Number]
]);
@observer
@@ -160,6 +158,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
<CollectionSchemaPreview
Document={layoutDoc}
DataDocument={this.previewDocument !== this.props.DataDoc ? this.props.DataDoc : undefined}
+ fieldKey={this.props.fieldKey}
childDocs={this.childDocs}
renderDepth={this.props.renderDepth}
ruleProvider={this.props.Document.isRuleProvider && layoutDoc && layoutDoc.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider}
@@ -228,7 +227,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
}
render() {
- Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey);
return (
<div className="collectionSchemaView-container" style={{ height: "100%", marginTop: "0", }}>
<div className="collectionSchemaView-tableContainer" onPointerDown={this.onPointerDown} onWheel={this.onWheel} onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createTarget}>
@@ -247,8 +245,8 @@ export interface SchemaTableProps {
PanelHeight: () => number;
PanelWidth: () => number;
childDocs?: Doc[];
- CollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
- ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
+ CollectionView: Opt<CollectionView>;
+ ContainingCollectionView: Opt<CollectionView>;
ContainingCollectionDoc: Opt<Doc>;
fieldKey: string;
renderDepth: number;
@@ -900,16 +898,17 @@ interface CollectionSchemaPreviewProps {
childDocs?: Doc[];
renderDepth: number;
fitToBox?: boolean;
+ fieldKey: string;
PanelWidth: () => number;
PanelHeight: () => number;
ruleProvider: Doc | undefined;
focus?: (doc: Doc) => void;
showOverlays?: (doc: Doc) => { title?: string, caption?: string };
- CollectionView?: CollectionView | CollectionPDFView | CollectionVideoView;
+ CollectionView?: CollectionView;
CollectionDoc?: Doc;
onClick?: ScriptField;
getTransform: () => Transform;
- addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
+ addDocument: (document: Doc) => boolean;
moveDocument: (document: Doc, target: Doc, addDoc: ((doc: Doc) => boolean)) => boolean;
removeDocument: (document: Doc) => boolean;
active: () => boolean;
@@ -924,8 +923,9 @@ interface CollectionSchemaPreviewProps {
export class CollectionSchemaPreview extends React.Component<CollectionSchemaPreviewProps>{
private dropDisposer?: DragManager.DragDropDisposer;
_mainCont?: HTMLDivElement;
- private get nativeWidth() { return NumCast(this.props.Document!.nativeWidth, this.props.PanelWidth()); }
- private get nativeHeight() { return NumCast(this.props.Document!.nativeHeight, this.props.PanelHeight()); }
+ private get layoutDoc() { return this.props.Document && Doc.Layout(this.props.Document); }
+ private get nativeWidth() { return NumCast(this.layoutDoc!.nativeWidth, this.props.PanelWidth()); }
+ private get nativeHeight() { return NumCast(this.layoutDoc!.nativeHeight, this.props.PanelHeight()); }
private contentScaling = () => {
let wscale = this.props.PanelWidth() / (this.nativeWidth ? this.nativeWidth : this.props.PanelWidth());
if (wscale * this.nativeHeight > this.props.PanelHeight()) {
@@ -949,10 +949,8 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre
if (de.data instanceof DragManager.DocumentDragData) {
this.props.childDocs && this.props.childDocs.map(otherdoc => {
let target = Doc.GetProto(otherdoc);
- let layoutNative = Doc.MakeTitled("layoutNative");
- layoutNative.layout = ComputedField.MakeFunction("this.image_data[0]");
- target.layoutNative = layoutNative;
- target.layoutCUstom = target.layout = Doc.MakeDelegate(de.data.draggedDocuments[0]);
+ target.layout = ComputedField.MakeFunction("this.image_data[0]");
+ target.layoutCustom = Doc.MakeDelegate(de.data.draggedDocuments[0]);
});
e.stopPropagation();
}
@@ -970,7 +968,7 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre
let br = StrCast(this.props.Document!.borderRounding);
if (br.endsWith("%")) {
let percent = Number(br.substr(0, br.length - 1)) / 100;
- let nativeDim = Math.min(NumCast(this.props.Document!.nativeWidth), NumCast(this.props.Document!.nativeHeight));
+ let nativeDim = Math.min(NumCast(this.layoutDoc!.nativeWidth), NumCast(this.layoutDoc!.nativeHeight));
let minDim = percent * (nativeDim ? nativeDim : Math.min(this.PanelWidth(), this.PanelHeight()));
return minDim;
}
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index 74da4ef85..29178b909 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -1,19 +1,31 @@
@import "../globalCssVariables";
.collectionMasonryView {
- display:inline;
+ display: inline;
}
-.collectionStackingView{
+
+.collectionStackingView {
display: flex;
}
-.collectionStackingView, .collectionMasonryView{
+.collectionStackingMasonry-cont {
+ position:relative;
+ height:100%;
+ width:100%;
+}
+.collectionStackingView,
+.collectionMasonryView {
height: 100%;
width: 100%;
- position: relative;
+ position: absolute;
top: 0;
overflow-y: auto;
flex-wrap: wrap;
transition: top .5s;
+ >div {
+ position: relative;
+ display: block;
+ }
+
.collectionSchemaView-previewDoc {
height: 100%;
position: absolute;
@@ -40,10 +52,12 @@
top: 0;
left: 0;
}
+
.collectionStackingView-masonrySingle {
height: 100%;
position: absolute;
}
+
.collectionStackingView-masonryGrid {
margin: auto;
height: max-content;
@@ -90,18 +104,64 @@
grid-column-end: span 1;
height: 100%;
margin: auto;
+ display: inline-grid;
}
-
+
.collectionStackingView-masonrySection {
margin: auto;
}
+ .collectionStackingView-collapseBar {
+ margin-left: 2px;
+ margin-right: 2px;
+ margin-top: 2px;
+ background: $main-accent;
+ height: 5px;
+
+ &.active {
+ margin-left: 0;
+ margin-right: 0;
+ background: red;
+ }
+ }
+ .collectionStackingView-miniHeader {
+ width: 100%;
+ .editableView-container-editing-oneLine {
+ min-height: 20px;
+ display: flex;
+ align-items: center;
+ flex-direction: row;
+ }
+ span::before , span::after{
+ content: "";
+ width: 50%;
+ border-top: dashed gray 1px;
+ position: relative;
+ display: inline-block;
+ }
+ span::before {
+ margin-right: 10px;
+ }
+ span::after{
+ margin-left: 10px;
+ }
+ span {
+ position: relative;
+ text-align: center;
+ white-space: nowrap;
+ overflow: visible;
+ width: 100%;
+ display: flex;
+ color:gray;
+ align-items: center;
+ }
+ }
.collectionStackingView-sectionHeader {
text-align: center;
margin-left: 2px;
margin-right: 2px;
margin-top: 10px;
- background: gray;
+ background: $main-accent;
// overflow: hidden; overflow is visible so the color menu isn't hidden -ftong
.editableView-input {
@@ -132,7 +192,7 @@
.editableView-input:hover,
.editableView-container-editing:hover,
.editableView-container-editing-oneLine:hover {
- cursor: text
+ cursor: text;
}
.editableView-input {
@@ -182,11 +242,45 @@
}
}
- .collectionStackingView-sectionDelete {
+ .collectionStackingView-sectionOptions {
position: absolute;
right: 0;
top: 0;
height: 100%;
+
+ [class*="css"] {
+ max-width: 102px;
+ }
+
+ .collectionStackingView-sectionOptionButton {
+ height: 35px;
+ }
+
+ .collectionStackingView-optionPicker {
+
+ .optionOptions {
+ display: inline;
+ }
+
+ .optionPicker {
+ cursor: pointer;
+ height: 20px;
+ border-radius: 10px;
+ margin: 3px;
+ width:max-content;
+
+ &.active {
+ color: red;
+ }
+ }
+ }
+ }
+
+ .collectionStackingView-sectionDelete {
+ position: absolute;
+ right: 25px;
+ top: 0;
+ height: 100%;
}
}
@@ -288,6 +382,4 @@
.rc-switch-checked .rc-switch-inner {
left: 8px;
}
-
-
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 4b81db3a6..7f0165ad4 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -23,6 +23,7 @@ import { CollectionSubView } from "./CollectionSubView";
import { ContextMenu } from "../ContextMenu";
import { ContextMenuProps } from "../ContextMenuItem";
import { ScriptBox } from "../ScriptBox";
+import { CollectionMasonryViewFieldRow } from "./CollectionMasonryViewFieldRow";
@observer
export class CollectionStackingView extends CollectionSubView(doc => doc) {
@@ -32,7 +33,8 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
_sectionFilterDisposer?: IReactionDisposer;
_docXfs: any[] = [];
_columnStart: number = 0;
- @observable private cursor: CursorProperty = "grab";
+ @observable _heightMap = new Map<string, number>();
+ @observable _cursor: CursorProperty = "grab";
@observable _scroll = 0; // used to force the document decoration to update when scrolling
@computed get sectionHeaders() { return Cast(this.props.Document.sectionHeaders, listSpec(SchemaHeaderField)); }
@computed get sectionFilter() { return StrCast(this.props.Document.sectionFilter); }
@@ -42,18 +44,35 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
@computed get gridGap() { return NumCast(this.props.Document.gridGap, 10); }
@computed get isStackingView() { return BoolCast(this.props.Document.singleColumn, true); }
@computed get numGroupColumns() { return this.isStackingView ? Math.max(1, this.Sections.size + (this.showAddAGroup ? 1 : 0)) : 1; }
- @computed get showAddAGroup() { return (this.sectionFilter && this.props.ContainingCollectionDoc && (this.props.ContainingCollectionDoc.chromeStatus !== 'view-mode' && this.props.ContainingCollectionDoc.chromeStatus !== 'disabled')); }
+ @computed get showAddAGroup() { return (this.sectionFilter && (this.props.Document.chromeStatus !== 'view-mode' && this.props.Document.chromeStatus !== 'disabled')); }
@computed get columnWidth() {
return Math.min(this.props.PanelWidth() / (this.props as any).ContentScaling() - 2 * this.xMargin,
this.isStackingView ? Number.MAX_VALUE : NumCast(this.props.Document.columnWidth, 250));
}
+ @computed get NodeWidth() { return this.props.PanelWidth() - this.gridGap; }
childDocHeight(child: Doc) { return this.getDocHeight(Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, child).layout); }
- get layoutDoc() {
- // if this document's layout field contains a document (ie, a rendering template), then we will use that
- // to determine the render JSX string, otherwise the layout field should directly contain a JSX layout string.
- return this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document;
+ children(docs: Doc[]) {
+ this._docXfs.length = 0;
+ return docs.map((d, i) => {
+ let pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, d);
+ let layoutDoc = pair.layout ? Doc.Layout(pair.layout) : d;
+ let width = () => Math.min(layoutDoc.nativeWidth && !layoutDoc.ignoreAspect && !this.props.Document.fillColumn ? layoutDoc[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns);
+ let height = () => this.getDocHeight(layoutDoc);
+ let dref = React.createRef<HTMLDivElement>();
+ let dxf = () => this.getDocTransform(layoutDoc, dref.current!);
+ this._docXfs.push({ dxf: dxf, width: width, height: height });
+ let rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap);
+ let style = this.isStackingView ? { width: width(), margin: "auto", marginTop: i === 0 ? 0 : this.gridGap, height: height() } : { gridRowEnd: `span ${rowSpan}` };
+ return <div className={`collectionStackingView-${this.isStackingView ? "columnDoc" : "masonryDoc"}`} key={d[Id]} ref={dref} style={style} >
+ {this.getDisplayDoc(pair.layout || d, pair.data, dxf, width)}
+ </div>;
+ });
+ }
+ @action
+ setDocHeight = (key: string, sectionHeight: number) => {
+ this._heightMap.set(key, sectionHeight);
}
get Sections() {
@@ -86,19 +105,30 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
componentDidMount() {
- // is there any reason this needs to exist? -syip. yes, it handles autoHeight for stacking views (masonry isn't yet supported).
+ super.componentDidMount();
this._heightDisposer = reaction(() => {
- if (this.isStackingView && BoolCast(this.props.Document.autoHeight)) {
+ if (this.props.Document.autoHeight) {
let sectionsList = Array.from(this.Sections.size ? this.Sections.values() : [this.filteredChildren]);
- return this.props.ContentScaling() * sectionsList.reduce((maxHght, s) => Math.max(maxHght,
- (this.Sections.size ? 50 : 0) + s.reduce((height, d, i) => height + this.childDocHeight(d) + (i === s.length - 1 ? this.yMargin : this.gridGap), this.yMargin)
- ), 0);
+ if (this.isStackingView) {
+ let res = this.props.ContentScaling() * sectionsList.reduce((maxHght, s) => {
+ let r1 = Math.max(maxHght,
+ (this.Sections.size ? 50 : 0) + s.reduce((height, d, i) => {
+ let val = height + this.childDocHeight(d) + (i === s.length - 1 ? this.yMargin : this.gridGap);
+ return val;
+ }, this.yMargin));
+ return r1;
+ }, 0);
+ return res;
+ } else {
+ let sum = Array.from(this._heightMap.values()).reduce((acc: number, curr: number) => acc += curr, 0);
+ return this.props.ContentScaling() * (sum + (this.Sections.size ? (this.props.Document.miniHeaders ? 20 : 85) : -15));
+ }
}
return -1;
},
(hgt: number) => {
let doc = hgt === -1 ? undefined : this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc;
- doc && (doc.height = hgt);
+ doc && hgt > 0 && (Doc.Layout(doc).height = hgt);
},
{ fireImmediately: true }
);
@@ -110,6 +140,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
);
}
componentWillUnmount() {
+ super.componentWillUnmount();
this._heightDisposer && this._heightDisposer();
this._sectionFilterDisposer && this._sectionFilterDisposer();
}
@@ -120,7 +151,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
createRef = (ele: HTMLDivElement | null) => {
this._masonryGridRef = ele;
- this.createDropTarget(ele!);
+ this.createDropTarget(ele!); //so the whole grid is the drop target?
}
overlays = (doc: Doc) => {
@@ -130,17 +161,19 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
@computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); }
@computed get onClickHandler() { return ScriptCast(this.Document.onChildClick); }
- getDisplayDoc(layoutDoc: Doc, dataDoc: Doc | undefined, dxf: () => Transform, width: () => number) {
- let height = () => this.getDocHeight(layoutDoc);
+ getDisplayDoc(doc: Doc, dataDoc: Doc | undefined, dxf: () => Transform, width: () => number) {
+ let layoutDoc = Doc.Layout(doc);
+ let height = () => this.getDocHeight(doc);
let finalDxf = () => dxf().scale(this.columnWidth / layoutDoc[WidthSym]());
return <CollectionSchemaPreview
- Document={layoutDoc}
+ Document={doc}
DataDocument={dataDoc}
+ fieldKey={this.props.fieldKey}
showOverlays={this.overlays}
renderDepth={this.props.renderDepth}
ruleProvider={this.props.Document.isRuleProvider && layoutDoc.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider}
fitToBox={this.props.fitToBox}
- onClick={layoutDoc.isTemplate ? this.onClickHandler : this.onChildClickHandler}
+ onClick={layoutDoc.isTemplateDoc ? this.onClickHandler : this.onChildClickHandler}
PanelWidth={width}
PanelHeight={height}
getTransform={finalDxf}
@@ -160,21 +193,23 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
getDocHeight(d?: Doc) {
if (!d) return 0;
- let nw = NumCast(d.nativeWidth);
- let nh = NumCast(d.nativeHeight);
- if (!d.ignoreAspect && !d.fitWidth && nw && nh) {
+ let layoutDoc = Doc.Layout(d);
+ let nw = NumCast(layoutDoc.nativeWidth);
+ let nh = NumCast(layoutDoc.nativeHeight);
+ let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1);
+ if (!layoutDoc.ignoreAspect && !layoutDoc.fitWidth && nw && nh) {
let aspect = nw && nh ? nh / nw : 1;
- let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1);
- if (!(d.nativeWidth && !d.ignoreAspect && this.props.Document.fillColumn)) wid = Math.min(d[WidthSym](), wid);
+ if (!(d.nativeWidth && !layoutDoc.ignoreAspect && this.props.Document.fillColumn)) wid = Math.min(layoutDoc[WidthSym](), wid);
return wid * aspect;
}
- return d.fitWidth ? this.props.PanelHeight() - 2 * this.yMargin : d[HeightSym]();
+ return layoutDoc.fitWidth ? !layoutDoc.nativeHeight ? this.props.PanelHeight() - 2 * this.yMargin :
+ Math.min(wid * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc.nativeHeight)) / NumCast(layoutDoc.nativeWidth, 1), this.props.PanelHeight() - 2 * this.yMargin) : layoutDoc[HeightSym]();
}
columnDividerDown = (e: React.PointerEvent) => {
e.stopPropagation();
e.preventDefault();
- runInAction(() => this.cursor = "grabbing");
+ runInAction(() => this._cursor = "grabbing");
document.addEventListener("pointermove", this.onDividerMove);
document.addEventListener('pointerup', this.onDividerUp);
this._columnStart = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0];
@@ -184,18 +219,18 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
let dragPos = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0];
let delta = dragPos - this._columnStart;
this._columnStart = dragPos;
- this.layoutDoc.columnWidth = this.columnWidth + delta;
+ this.layoutDoc.columnWidth = Math.max(10, this.columnWidth + delta);
}
@action
onDividerUp = (e: PointerEvent): void => {
- runInAction(() => this.cursor = "grab");
+ runInAction(() => this._cursor = "grab");
document.removeEventListener("pointermove", this.onDividerMove);
document.removeEventListener('pointerup', this.onDividerUp);
}
@computed get columnDragger() {
- return <div className="collectionStackingView-columnDragger" onPointerDown={this.columnDividerDown} ref={this._draggerRef} style={{ cursor: this.cursor, left: `${this.columnWidth + this.xMargin}px` }} >
+ return <div className="collectionStackingView-columnDragger" onPointerDown={this.columnDividerDown} ref={this._draggerRef} style={{ cursor: this._cursor, left: `${this.columnWidth + this.xMargin}px` }} >
<FontAwesomeIcon icon={"arrows-alt-h"} />
</div>;
}
@@ -286,52 +321,29 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
translate(offset[0], offset[1] + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0)).
scale(NumCast(doc.width, 1) / this.columnWidth);
}
- masonryChildren(docs: Doc[]) {
- this._docXfs.length = 0;
- return docs.map((d, i) => {
- const pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, d);
- if (!pair.layout || pair.data instanceof Promise) {
- return (null);
- }
- let dref = React.createRef<HTMLDivElement>();
- let width = () => (d.nativeWidth && !d.ignoreAspect && !this.props.Document.fillColumn ? Math.min(d[WidthSym](), this.columnWidth) : this.columnWidth);/// (uniqueHeadings.length + 1);
- let height = () => this.getDocHeight(pair.layout);
- let dxf = () => this.getDocTransform(pair.layout!, dref.current!);
- let rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap);
- this._docXfs.push({ dxf: dxf, width: width, height: height });
- return !pair.layout ? (null) : <div className="collectionStackingView-masonryDoc" key={d[Id]} ref={dref} style={{ gridRowEnd: `span ${rowSpan}` }} >
- {this.getDisplayDoc(pair.layout, pair.data, dxf, width)}
- </div>;
- });
- }
- @observable _headingsHack: number = 1;
- sectionMasonry(heading: SchemaHeaderField | undefined, docList: Doc[]) {
- let cols = Math.max(1, Math.min(docList.length,
- Math.floor((this.props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap))));
- if (isNaN(cols)) {
- console.log("naN");
- cols = 1;
+ sectionMasonry = (heading: SchemaHeaderField | undefined, docList: Doc[]) => {
+ let key = this.sectionFilter;
+ let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined;
+ let types = docList.length ? docList.map(d => typeof d[key]) : this.childDocs.map(d => typeof d[key]);
+ if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) {
+ type = types[0];
}
- return <div key={heading ? heading.heading : "empty"} className="collectionStackingView-masonrySection">
- {!heading ? (null) :
- <div key={`${heading.heading}`} className="collectionStackingView-sectionHeader" style={{ background: heading.color }}
- onClick={action(() => this._headingsHack++ && heading.setCollapsed(!heading.collapsed))} >
- {heading.heading}
- </div>}
- {this._headingsHack && heading && heading.collapsed ? (null) :
- <div key={`${heading}-stack`} className={`collectionStackingView-masonryGrid`}
- style={{
- padding: `${this.yMargin}px ${this.xMargin}px`,
- width: `${cols * (this.columnWidth + this.gridGap) + 2 * this.xMargin - this.gridGap}px`,
- gridGap: this.gridGap,
- gridTemplateColumns: numberRange(cols).reduce((list, i) => list + ` ${this.columnWidth}px`, ""),
- }}>
- {this.masonryChildren(docList)}
- {this.columnDragger}
- </div>
- }
- </div>;
+ let rows = () => !this.isStackingView ? 1 : Math.max(1, Math.min(docList.length,
+ Math.floor((this.props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap))));
+ return <CollectionMasonryViewFieldRow
+ key={heading ? heading.heading : ""}
+ rows={rows}
+ headings={this.headings}
+ heading={heading ? heading.heading : ""}
+ headingObject={heading}
+ docList={docList}
+ parent={this}
+ type={type}
+ createDropTarget={this.createDropTarget}
+ screenToLocalTransform={this.props.ScreenToLocalTransform}
+ setDocHeight={this.setDocHeight}
+ />;
}
@action
@@ -351,7 +363,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
onToggle = (checked: Boolean) => {
- this.props.ContainingCollectionDoc && (this.props.ContainingCollectionDoc.chromeStatus = checked ? "collapsed" : "view-mode");
+ this.props.Document.chromeStatus = checked ? "collapsed" : "view-mode";
}
onContextMenu = (e: React.MouseEvent): void => {
@@ -376,33 +388,34 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
SetValue: this.addGroup,
contents: "+ ADD A GROUP"
};
- Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey);
let sections = [[undefined, this.filteredChildren] as [SchemaHeaderField | undefined, Doc[]]];
if (this.sectionFilter) {
let entries = Array.from(this.Sections.entries());
sections = entries.sort(this.sortFunc);
}
+ console.log("NUM = " + this.numGroupColumns);
return (
- <div className={this.isStackingView ? "collectionStackingView" : "collectionMasonryView"}
- ref={this.createRef}
- onScroll={action((e: React.UIEvent<HTMLDivElement>) => this._scroll = e.currentTarget.scrollTop)}
- onDrop={this.onDrop.bind(this)}
- onContextMenu={this.onContextMenu}
- onWheel={(e: React.WheelEvent) => e.stopPropagation()} >
- {sections.map(section => this.isStackingView ? this.sectionStacking(section[0], section[1]) : this.sectionMasonry(section[0], section[1]))}
- {!this.showAddAGroup ? (null) :
- <div key={`${this.props.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton"
- style={{ width: this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}>
- <EditableView {...editableViewProps} />
- </div>}
- {this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.chromeStatus !== 'disabled' ? <Switch
- onChange={this.onToggle}
- onClick={this.onToggle}
- defaultChecked={this.props.ContainingCollectionDoc.chromeStatus !== 'view-mode'}
- checkedChildren="edit"
- unCheckedChildren="view"
- /> : null}
- </div>
+ <div className="collectionStackingMasonry-cont" >
+ <div className={this.isStackingView ? "collectionStackingView" : "collectionMasonryView"}
+ ref={this.createRef}
+ onScroll={action((e: React.UIEvent<HTMLDivElement>) => this._scroll = e.currentTarget.scrollTop)}
+ onDrop={this.onDrop.bind(this)}
+ onContextMenu={this.onContextMenu}
+ onWheel={(e: React.WheelEvent) => e.stopPropagation()} >
+ {sections.map(section => this.isStackingView ? this.sectionStacking(section[0], section[1]) : this.sectionMasonry(section[0], section[1]))}
+ {!this.showAddAGroup ? (null) :
+ <div key={`${this.props.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton"
+ style={{ width: !this.isStackingView ? "100%" : this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}>
+ <EditableView {...editableViewProps} />
+ </div>}
+ {this.props.Document.chromeStatus !== 'disabled' ? <Switch
+ onChange={this.onToggle}
+ onClick={this.onToggle}
+ defaultChecked={this.props.Document.chromeStatus !== 'view-mode'}
+ checkedChildren="edit"
+ unCheckedChildren="view"
+ /> : null}
+ </div> </div>
);
}
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 240adf428..b9d334b10 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -2,7 +2,7 @@ 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, observable, trace } from "mobx";
+import { action, observable, trace, runInAction } from "mobx";
import { observer } from "mobx-react";
import { Doc, WidthSym } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
@@ -23,7 +23,6 @@ import "./CollectionStackingView.scss";
library.add(faPalette);
-
interface CSVFieldColumnProps {
cols: () => number;
headings: () => object[];
@@ -39,6 +38,7 @@ interface CSVFieldColumnProps {
@observer
export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldColumnProps> {
@observable private _background = "inherit";
+ @observable private _createAliasSelected: boolean = false;
private _dropRef: HTMLDivElement | null = null;
private dropDisposer?: DragManager.DragDropDisposer;
@@ -58,8 +58,8 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
}
@undoBatch
- @action
- columnDrop = (e: Event, de: DragManager.DropEvent) => {
+ columnDrop = action((e: Event, de: DragManager.DropEvent) => {
+ this._createAliasSelected = false;
if (de.data instanceof DragManager.DocumentDragData) {
let key = StrCast(this.props.parent.props.Document.sectionFilter);
let castedValue = this.getValue(this._heading);
@@ -72,29 +72,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
this.props.parent.drop(e, de);
e.stopPropagation();
}
- }
-
- children(docs: Doc[]) {
- let parent = this.props.parent;
- parent._docXfs.length = 0;
- return docs.map((d, i) => {
- const pair = Doc.GetLayoutDataDocPair(parent.props.Document, parent.props.DataDoc, parent.props.fieldKey, d);
- if (!pair.layout || pair.data instanceof Promise) {
- return (null);
- }
- let width = () => Math.min(d.nativeWidth && !d.ignoreAspect && !parent.props.Document.fillColumn ? d[WidthSym]() : Number.MAX_VALUE, parent.columnWidth / parent.numGroupColumns);
- let height = () => parent.getDocHeight(pair.layout);
- let dref = React.createRef<HTMLDivElement>();
- let dxf = () => parent.getDocTransform(pair.layout!, dref.current!);
- parent._docXfs.push({ dxf: dxf, width: width, height: height });
- let rowSpan = Math.ceil((height() + parent.gridGap) / parent.gridGap);
- let style = parent.isStackingView ? { width: width(), margin: "auto", marginTop: i === 0 ? 0 : parent.gridGap, height: height() } : { gridRowEnd: `span ${rowSpan}` };
- return <div className={`collectionStackingView-${parent.isStackingView ? "columnDoc" : "masonryDoc"}`} key={d[Id]} ref={dref} style={style} >
- {parent.getDisplayDoc(pair.layout, pair.data, dxf, width)}
- </div>;
- });
- }
-
+ });
getValue = (value: string): any => {
let parsed = parseInt(value);
if (!isNaN(parsed)) {
@@ -111,6 +89,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
@action
headingChanged = (value: string, shiftDown?: boolean) => {
+ this._createAliasSelected = false;
let key = StrCast(this.props.parent.props.Document.sectionFilter);
let castedValue = this.getValue(value);
if (castedValue) {
@@ -131,6 +110,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
@action
changeColumnColor = (color: string) => {
+ this._createAliasSelected = false;
if (this.props.headingObject) {
this.props.headingObject.setColor(color);
this._color = color;
@@ -140,18 +120,21 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
@action
pointerEntered = () => {
if (SelectionManager.GetIsDragging()) {
+ this._createAliasSelected = false;
this._background = "#b4b4b4";
}
}
@action
pointerLeave = () => {
+ this._createAliasSelected = false;
this._background = "inherit";
document.removeEventListener("pointermove", this.startDrag);
}
@action
addDocument = (value: string, shiftDown?: boolean) => {
+ this._createAliasSelected = false;
let key = StrCast(this.props.parent.props.Document.sectionFilter);
let newDoc = Docs.Create.TextDocument({ height: 18, width: 200, documentText: "@@@" + value, title: value, autoHeight: true });
newDoc[key] = this.getValue(this.props.heading);
@@ -163,6 +146,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
@action
deleteColumn = () => {
+ this._createAliasSelected = false;
let key = StrCast(this.props.parent.props.Document.sectionFilter);
this.props.docList.forEach(d => d[key] = undefined);
if (this.props.parent.sectionHeaders && this.props.headingObject) {
@@ -171,6 +155,16 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
}
}
+ @action
+ collapseSection = () => {
+ this._createAliasSelected = false;
+ if (this.props.headingObject) {
+ this._headingsHack++;
+ this.props.headingObject.setCollapsed(!this.props.headingObject.collapsed);
+ this.toggleVisibility();
+ }
+ }
+
startDrag = (e: PointerEvent) => {
let [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) {
@@ -204,10 +198,13 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
let [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX, e.clientY);
this._startDragPosition = { x: dx, y: dy };
- document.removeEventListener("pointermove", this.startDrag);
- document.addEventListener("pointermove", this.startDrag);
- document.removeEventListener("pointerup", this.pointerUp);
- document.addEventListener("pointerup", this.pointerUp);
+ if (this._createAliasSelected) {
+ document.removeEventListener("pointermove", this.startDrag);
+ document.addEventListener("pointermove", this.startDrag);
+ document.removeEventListener("pointerup", this.pointerUp);
+ document.addEventListener("pointerup", this.pointerUp);
+ }
+ runInAction(() => this._createAliasSelected = false);
}
renderColorPicker = () => {
@@ -240,6 +237,30 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
);
}
+ @action
+ toggleAlias = () => {
+ this._createAliasSelected = true;
+ }
+
+ renderMenu = () => {
+ let selected = this._createAliasSelected;
+ return (
+ <div className="collectionStackingView-optionPicker">
+ <div className="optionOptions">
+ <div className={"optionPicker" + (selected === true ? " active" : "")} onClick={this.toggleAlias}>Create Alias</div>
+ </div>
+ </div >
+ );
+ }
+
+ @observable private collapsed: boolean = false;
+
+ private toggleVisibility = action(() => {
+ this.collapsed = !this.collapsed;
+ });
+
+ @observable _headingsHack: number = 1;
+
render() {
let cols = this.props.cols();
let key = StrCast(this.props.parent.props.Document.sectionFilter);
@@ -254,20 +275,29 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
GetValue: () => evContents,
SetValue: this.headingChanged,
contents: evContents,
- oneLine: true
+ oneLine: true,
+ HeadingObject: this.props.headingObject,
+ HeadingsHack: this._headingsHack,
+ toggle: this.toggleVisibility,
+ color: this._color
};
let newEditableViewProps = {
GetValue: () => "",
SetValue: this.addDocument,
- contents: "+ NEW"
+ contents: "+ NEW",
+ HeadingObject: this.props.headingObject,
+ HeadingsHack: this._headingsHack,
+ toggle: this.toggleVisibility,
+ color: this._color
};
let headingView = this.props.headingObject ?
<div key={heading} className="collectionStackingView-sectionHeader" ref={this._headerRef}
style={{
width: (style.columnWidth) /
((uniqueHeadings.length +
- ((this.props.parent.props.ContainingCollectionDoc && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'view-mode' && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'disabled') ? 1 : 0)) || 1)
+ ((this.props.parent.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.Document.chromeStatus !== 'disabled') ? 1 : 0)) || 1)
}}>
+ <div className={"collectionStackingView-collapseBar" + (this.props.headingObject.collapsed === true ? " active" : "")} onClick={this.collapseSection}></div>
{/* the default bucket (no key value) has a tooltip that describes what it is.
Further, it does not have a color and cannot be deleted. */}
<div className="collectionStackingView-sectionHeader-subCont" onPointerDown={this.headerDown}
@@ -281,9 +311,9 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
<EditableView {...headerEditableViewProps} />
{evContents === `NO ${key.toUpperCase()} VALUE` ? (null) :
<div className="collectionStackingView-sectionColor">
- <Flyout anchorPoint={anchorPoints.TOP_CENTER} content={this.renderColorPicker()}>
+ <Flyout anchorPoint={anchorPoints.CENTER_RIGHT} content={this.renderColorPicker()}>
<button className="collectionStackingView-sectionColorButton">
- <FontAwesomeIcon icon="palette" size="sm" />
+ <FontAwesomeIcon icon="palette" size="lg" />
</button>
</ Flyout >
</div>
@@ -291,36 +321,50 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
{evContents === `NO ${key.toUpperCase()} VALUE` ?
(null) :
<button className="collectionStackingView-sectionDelete" onClick={this.deleteColumn}>
- <FontAwesomeIcon icon="trash" />
+ <FontAwesomeIcon icon="trash" size="lg" />
</button>}
+ {evContents === `NO ${key.toUpperCase()} VALUE` ? (null) :
+ <div className="collectionStackingView-sectionOptions">
+ <Flyout anchorPoint={anchorPoints.TOP_RIGHT} content={this.renderMenu()}>
+ <button className="collectionStackingView-sectionOptionButton">
+ <FontAwesomeIcon icon="ellipsis-v" size="lg"></FontAwesomeIcon>
+ </button>
+ </Flyout>
+ </div>
+ }
</div>
</div> : (null);
for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth / style.numGroupColumns}px `;
+ let chromeStatus = this.props.parent.props.Document.chromeStatus;
return (
- <div className="collectionStackingViewFieldColumn" key={heading} style={{ width: `${100 / ((uniqueHeadings.length + ((this.props.parent.props.ContainingCollectionDoc && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'view-mode' && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'disabled') ? 1 : 0)) || 1)}%`, background: this._background }}
+ <div className="collectionStackingViewFieldColumn" key={heading} style={{ width: `${100 / ((uniqueHeadings.length + ((chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? 1 : 0)) || 1)}%`, background: this._background }}
ref={this.createColumnDropRef} onPointerEnter={this.pointerEntered} onPointerLeave={this.pointerLeave}>
- {headingView}
- <div key={`${heading}-stack`} className={`collectionStackingView-masonry${singleColumn ? "Single" : "Grid"}`}
- style={{
- padding: singleColumn ? `${style.yMargin}px ${0}px ${style.yMargin}px ${0}px` : `${style.yMargin}px ${0}px`,
- margin: "auto",
- width: "max-content", //singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`,
- height: 'max-content',
- position: "relative",
- gridGap: style.gridGap,
- gridTemplateColumns: singleColumn ? undefined : templatecols,
- gridAutoRows: singleColumn ? undefined : "0px"
- }}
- >
- {this.children(this.props.docList)}
- {singleColumn ? (null) : this.props.parent.columnDragger}
- </div>
- {(this.props.parent.props.ContainingCollectionDoc && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'view-mode' && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'disabled') ?
- <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton"
- style={{ width: style.columnWidth / style.numGroupColumns }}>
- <EditableView {...newEditableViewProps} />
- </div> : null}
- </div>
+ {this.props.parent.Document.hideHeadings ? (null) : headingView}
+ {
+ this.collapsed ? (null) :
+ <div>
+ <div key={`${heading}-stack`} className={`collectionStackingView-masonry${singleColumn ? "Single" : "Grid"}`}
+ style={{
+ padding: singleColumn ? `${style.yMargin}px ${0}px ${style.yMargin}px ${0}px` : `${style.yMargin}px ${0}px`,
+ margin: "auto",
+ width: "max-content", //singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`,
+ height: 'max-content',
+ position: "relative",
+ gridGap: style.gridGap,
+ gridTemplateColumns: singleColumn ? undefined : templatecols,
+ gridAutoRows: singleColumn ? undefined : "0px"
+ }}>
+ {this.props.parent.children(this.props.docList)}
+ {singleColumn ? (null) : this.props.parent.columnDragger}
+ </div>
+ {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ?
+ <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton"
+ style={{ width: style.columnWidth / style.numGroupColumns }}>
+ <EditableView {...newEditableViewProps} />
+ </div> : null}
+ </div>
+ }
+ </div >
);
}
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 155f2718b..d7e9494a3 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -6,7 +6,7 @@ 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 } from "../../../new_fields/Types";
+import { Cast, StrCast } from "../../../new_fields/Types";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
import { RouteStore } from "../../../server/RouteStore";
import { Utils } from "../../../Utils";
@@ -18,62 +18,73 @@ import { undoBatch, UndoManager } from "../../util/UndoManager";
import { DocComponent } from "../DocComponent";
import { FieldViewProps } from "../nodes/FieldView";
import { FormattedTextBox, GoogleRef } from "../nodes/FormattedTextBox";
-import { CollectionPDFView } from "./CollectionPDFView";
-import { CollectionVideoView } from "./CollectionVideoView";
import { CollectionView } from "./CollectionView";
import React = require("react");
var path = require('path');
import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
+import { ImageUtils } from "../../util/Import & Export/ImageUtils";
export interface CollectionViewProps extends FieldViewProps {
- addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
+ addDocument: (document: Doc) => boolean;
removeDocument: (document: Doc) => boolean;
moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
PanelWidth: () => number;
PanelHeight: () => number;
+ VisibleHeight?: () => number;
chromeCollapsed: boolean;
setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void;
+ fieldKey: string;
}
export interface SubCollectionViewProps extends CollectionViewProps {
- CollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
+ CollectionView: Opt<CollectionView>;
ruleProvider: Doc | undefined;
+ children?: never | (() => JSX.Element[]) | React.ReactNode;
+ isAnnotationOverlay?: boolean;
+ annotationsKey: string;
}
export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
class CollectionSubView extends DocComponent<SubCollectionViewProps, T>(schemaCtor) {
private dropDisposer?: DragManager.DragDropDisposer;
private _childLayoutDisposer?: IReactionDisposer;
-
- protected createDropTarget = (ele: HTMLDivElement) => {
+ protected createDropTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view
this.dropDisposer && this.dropDisposer();
if (ele) {
this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
}
}
- protected CreateDropTarget(ele: HTMLDivElement) {
+ protected CreateDropTarget(ele: HTMLDivElement) { //used in schema view
this.createDropTarget(ele);
}
componentDidMount() {
this._childLayoutDisposer = reaction(() => [this.childDocs, Cast(this.props.Document.childLayout, Doc)],
- async (args) => args[1] instanceof Doc &&
- this.childDocs.map(async doc => !Doc.AreProtosEqual(args[1] as Doc, (await doc).layout as Doc) && Doc.ApplyTemplateTo(args[1] as Doc, (await doc))));
+ async (args) => {
+ if (args[1] instanceof Doc) {
+ this.childDocs.map(async doc => !Doc.AreProtosEqual(args[1] as Doc, (await doc).layout as Doc) && Doc.ApplyTemplateTo(args[1] as Doc, (await doc), "layoutFromParent"));
+ }
+ else if (!(args[1] instanceof Promise)) {
+ this.childDocs.filter(d => !d.isTemplateField).map(async doc => doc.layoutKey === "layoutFromParent" && (doc.layoutKey = "layout"));
+ }
+ });
}
componentWillUnmount() {
this._childLayoutDisposer && this._childLayoutDisposer();
}
- // The data field for rendeing this collection will be on the this.props.Document unless we're rendering a template in which case we try to use props.DataDoc.
+ @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateField ? Doc.GetProto(this.props.DataDoc) : Doc.GetProto(this.props.Document); }
+ @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
+
+ // The data field for rendering this collection will be on the this.props.Document unless we're rendering a template in which case we try to use props.DataDoc.
// When a document has a DataDoc but it's not a template, then it contains its own rendering data, but needs to pass the DataDoc through
// to its children which may be templates.
- // The name of the data field comes from fieldExt if it's an extension, or fieldKey otherwise.
+ // If 'annotationField' is specified, then all children exist on that field of the extension document, otherwise, they exist directly on the data document under 'fieldKey'
@computed get dataField() {
- return Doc.fieldExtensionDoc(this.props.Document.isTemplate && this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt)[this.props.fieldExt || this.props.fieldKey];
+ return this.props.annotationsKey ? (this.extensionDoc ? this.extensionDoc[this.props.annotationsKey] : undefined) : this.dataDoc[this.props.fieldKey];
}
-
get childLayoutPairs() {
return this.childDocs.map(cd => Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, cd)).filter(pair => pair.layout).map(pair => ({ layout: pair.layout!, data: pair.data! }));
}
@@ -121,11 +132,12 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
@undoBatch
@action
protected drop(e: Event, de: DragManager.DropEvent): boolean {
+ (this.props.Document.dropConverter instanceof ScriptField) &&
+ this.props.Document.dropConverter.script.run({ dragData: de.data });
if (de.data instanceof DragManager.DocumentDragData && !de.data.applyAsTemplate) {
if (de.mods === "AltKey" && de.data.draggedDocuments.length) {
this.childDocs.map(doc =>
- Doc.ApplyTemplateTo(de.data.draggedDocuments[0], doc)
- );
+ Doc.ApplyTemplateTo(de.data.draggedDocuments[0], doc, "layoutFromParent"));
e.stopPropagation();
return true;
}
@@ -133,11 +145,10 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
if (de.data.dropAction || de.data.userDropAction) {
added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false);
} else if (de.data.moveDocument) {
- let movedDocs = de.data.draggedDocuments;// de.data.options === this.props.Document[Id] ? de.data.draggedDocuments : de.data.droppedDocuments;
- // note that it's possible the drag function might create a drop document that's not the same as the
- // original dragged document. So we explicitly call addDocument() with a droppedDocument and
+ let movedDocs = de.data.draggedDocuments;
added = movedDocs.reduce((added: boolean, d, i) =>
- de.data.moveDocument(d, this.props.Document, (doc: Doc) => this.props.addDocument(de.data.droppedDocuments[i])) || added, false);
+ de.data.droppedDocuments[i] !== d ? this.props.addDocument(de.data.droppedDocuments[i]) :
+ de.data.moveDocument(d, this.props.Document, this.props.addDocument) || added, false);
} else {
added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false);
}
@@ -175,14 +186,14 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
DocServer.GetRefField(docid).then(f => {
if (f instanceof Doc) {
if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView
- (f instanceof Doc) && this.props.addDocument(f, false);
+ (f instanceof Doc) && this.props.addDocument(f);
}
});
} else {
this.props.addDocument && this.props.addDocument(Docs.Create.WebDocument(href, options));
}
} else if (text) {
- this.props.addDocument && this.props.addDocument(Docs.Create.TextDocument({ ...options, width: 100, height: 25, documentText: "@@@" + text }), false);
+ this.props.addDocument && this.props.addDocument(Docs.Create.TextDocument({ ...options, width: 100, height: 25, documentText: "@@@" + text }));
}
return;
}
@@ -193,7 +204,8 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
if (img) {
let split = img.split("src=\"")[1].split("\"")[0];
let doc = Docs.Create.ImageDocument(split, { ...options, width: 300 });
- this.props.addDocument(doc, false);
+ ImageUtils.ExtractExif(doc);
+ this.props.addDocument(doc);
return;
} else {
let path = window.location.origin + "/doc/";
@@ -202,12 +214,12 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
DocServer.GetRefField(docid).then(f => {
if (f instanceof Doc) {
if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView
- (f instanceof Doc) && this.props.addDocument(f, false);
+ (f instanceof Doc) && this.props.addDocument(f);
}
});
} else {
let htmlDoc = Docs.Create.HtmlDocument(html, { ...options, width: 300, height: 300, documentText: text });
- this.props.addDocument(htmlDoc, false);
+ this.props.addDocument(htmlDoc);
}
return;
}
@@ -251,7 +263,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
let type = result["content-type"];
if (type) {
Docs.Get.DocumentFromType(type, str, { ...options, width: 300, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300 })
- .then(doc => doc && this.props.addDocument(doc, false));
+ .then(doc => doc && this.props.addDocument(doc));
}
});
promises.push(prom);
@@ -274,7 +286,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
let full = { ...options, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300, width: 300, title: dropFileName };
let pathname = Utils.prepend(file.path);
Docs.Get.DocumentFromType(type, pathname, full).then(doc => {
- doc && (doc.fileUpload = path.basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, ""));
+ doc && (Doc.GetProto(doc).fileUpload = path.basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, ""));
doc && this.props.addDocument(doc);
});
}));
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index ab01a1086..7d0c900a6 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -10,12 +10,12 @@
width:100%;
position: relative;
top:0;
- padding-top: 20px;
padding-left: 10px;
padding-right: 10px;
background: $light-color-secondary;
font-size: 13px;
overflow: auto;
+ cursor: default;
ul {
list-style: none;
@@ -51,7 +51,7 @@
.editableView-container-editing {
display: block;
text-overflow: ellipsis;
- font-size: 24px;
+ font-size: 1vw;
white-space: nowrap;
}
}
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index ffd1f9170..47c355fc8 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -9,7 +9,7 @@ import { List } from '../../../new_fields/List';
import { Document, listSpec } from '../../../new_fields/Schema';
import { ComputedField, ScriptField } from '../../../new_fields/ScriptField';
import { BoolCast, Cast, NumCast, StrCast } from '../../../new_fields/Types';
-import { emptyFunction, Utils } from '../../../Utils';
+import { emptyFunction, Utils, returnFalse } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentType } from "../../documents/DocumentTypes";
import { DocumentManager } from '../../util/DocumentManager';
@@ -23,11 +23,12 @@ import { EditableView } from "../EditableView";
import { MainView } from '../MainView';
import { KeyValueBox } from '../nodes/KeyValueBox';
import { Templates } from '../Templates';
-import { CollectionViewType } from './CollectionBaseView';
+import { CollectionViewType } from './CollectionView';
import { CollectionSchemaPreview } from './CollectionSchemaView';
import { CollectionSubView } from "./CollectionSubView";
import "./CollectionTreeView.scss";
import React = require("react");
+import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
export interface TreeViewProps {
@@ -81,56 +82,46 @@ class TreeView extends React.Component<TreeViewProps> {
private _header?: React.RefObject<HTMLDivElement> = React.createRef();
private _treedropDisposer?: DragManager.DragDropDisposer;
private _dref = React.createRef<HTMLDivElement>();
- get defaultExpandedView() { return this.childDocs ? this.fieldKey : this.props.document.defaultExpandedView ? StrCast(this.props.document.defaultExpandedView) : ""; }
+ get defaultExpandedView() { return this.childDocs ? this.fieldKey : StrCast(this.props.document.defaultExpandedView, "fields"); }
@observable _overrideTreeViewOpen = false; // override of the treeViewOpen field allowing the display state to be independent of the document's state
- @computed get treeViewOpen() { return (BoolCast(this.props.document.treeViewOpen) && !this.props.preventTreeViewOpen) || this._overrideTreeViewOpen; }
set treeViewOpen(c: boolean) { if (this.props.preventTreeViewOpen) this._overrideTreeViewOpen = c; else this.props.document.treeViewOpen = c; }
+ @computed get treeViewOpen() { return (BoolCast(this.props.document.treeViewOpen) && !this.props.preventTreeViewOpen) || this._overrideTreeViewOpen; }
@computed get treeViewExpandedView() { return StrCast(this.props.document.treeViewExpandedView, this.defaultExpandedView); }
@computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.document.maxEmbedHeight, 300); }
- @computed get dataDoc() { return this.resolvedDataDoc ? this.resolvedDataDoc : this.props.document; }
+ @computed get dataDoc() { return this.templateDataDoc ? this.templateDataDoc : this.props.document; }
@computed get fieldKey() {
- let splits = StrCast(this.props.document.layout).split("fieldKey={\"");
+ let splits = StrCast(Doc.LayoutField(this.props.document)).split("fieldKey={\"");
return splits.length > 1 ? splits[1].split("\"")[0] : "data";
}
- @computed get childDocs() {
- let layout = this.props.document.layout instanceof Doc ? this.props.document.layout : undefined;
- return (this.props.dataDoc ? Cast(this.props.dataDoc[this.fieldKey], listSpec(Doc)) : undefined) ||
- (layout ? Cast(layout[this.fieldKey], listSpec(Doc)) : undefined) ||
- Cast(this.props.document[this.fieldKey], listSpec(Doc));
+ childDocList(field: string) {
+ let layout = Doc.LayoutField(this.props.document) instanceof Doc ? Doc.LayoutField(this.props.document) as Doc : undefined;
+ return ((this.props.dataDoc ? Cast(this.props.dataDoc[field], listSpec(Doc)) : undefined) ||
+ (layout ? Cast(layout[field], listSpec(Doc)) : undefined) ||
+ Cast(this.props.document[field], listSpec(Doc))) as Doc[];
}
- @computed get childLinks() {
- let layout = this.props.document.layout instanceof Doc ? this.props.document.layout : undefined;
- return (this.props.dataDoc ? Cast(this.props.dataDoc.links, listSpec(Doc)) : undefined) ||
- (layout instanceof Doc ? Cast(layout.links, listSpec(Doc)) : undefined) ||
- Cast(this.props.document.links, listSpec(Doc));
- }
- @computed get resolvedDataDoc() {
- if (this.props.dataDoc === undefined && this.props.document.layout instanceof Doc) {
- // if there is no dataDoc (ie, we're not rendering a template layout), but this document
- // has a template layout document, then we will render the template layout but use
- // this document as the data document for the layout.
+ @computed get childDocs() { return this.childDocList(this.fieldKey); }
+ @computed get childLinks() { return this.childDocList("links"); }
+ @computed get templateDataDoc() {
+ if (this.props.dataDoc === undefined && Doc.LayoutField(this.props.document) !== "string") {
+ // if there is no dataDoc (ie, we're not rendering a template layout), but this document has a layout document (not a layout string),
+ // then we render the layout document as a template and use this document as the data context for the template layout.
return this.props.document;
}
return this.props.dataDoc;
}
@computed get boundsOfCollectionDocument() {
return StrCast(this.props.document.type).indexOf(DocumentType.COL) === -1 ? undefined :
- Doc.ComputeContentBounds(DocListCast(this.props.document.data));
+ Doc.ComputeContentBounds(DocListCast(this.props.document[this.fieldKey]));
}
- @undoBatch delete = () => this.props.deleteDoc(this.dataDoc);
- @undoBatch openRight = () => this.props.addDocTab(this.props.document, undefined, "onRight");
+ @undoBatch delete = () => this.props.deleteDoc(this.props.document);
+ @undoBatch openRight = () => this.props.addDocTab(this.props.document, this.templateDataDoc, "onRight");
@undoBatch indent = () => this.props.addDocument(this.props.document) && this.delete();
@undoBatch move = (doc: Doc, target: Doc, addDoc: (doc: Doc) => boolean) => {
return this.props.document !== target && this.props.deleteDoc(doc) && addDoc(doc);
}
- @undoBatch @action remove = (document: Document, key: string): boolean => {
- let children = Cast(this.dataDoc[key], listSpec(Doc), []);
- if (children.indexOf(document) !== -1) {
- children.splice(children.indexOf(document), 1);
- return true;
- }
- return false;
+ @undoBatch @action remove = (document: Document, key: string) => {
+ return Doc.RemoveDocFromList(this.dataDoc, key, document);
}
protected createTreeDropTarget = (ele: HTMLDivElement) => {
@@ -174,9 +165,9 @@ class TreeView extends React.Component<TreeViewProps> {
fontStyle={style}
fontSize={12}
GetValue={() => StrCast(this.props.document[key])}
- SetValue={undoBatch((value: string) => (Doc.GetProto(this.dataDoc)[key] = value) ? true : true)}
+ SetValue={undoBatch((value: string) => Doc.SetInPlace(this.props.document, key, value, false) || true)}
OnFillDown={undoBatch((value: string) => {
- Doc.GetProto(this.dataDoc)[key] = value;
+ Doc.SetInPlace(this.props.document, key, value, false);
let doc = this.props.document.layoutCustom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.document.layoutCustom)) : undefined;
if (!doc) doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
TreeView.loadId = doc[Id];
@@ -187,17 +178,18 @@ class TreeView extends React.Component<TreeViewProps> {
onWorkspaceContextMenu = (e: React.MouseEvent): void => {
if (!e.isPropagationStopped()) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
- if (NumCast(this.props.document.viewType) !== CollectionViewType.Docking) {
+ if (NumCast(this.props.document.viewType) !== CollectionViewType.Docking && this.props.document !== CurrentUserUtils.UserDocument.workspaces) {
ContextMenu.Instance.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.document), icon: "tv" });
- ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, this.resolvedDataDoc, "inTab"), icon: "folder" });
- ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, this.resolvedDataDoc, "onRight"), icon: "caret-square-right" });
+ ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, this.templateDataDoc, "inTab"), icon: "folder" });
+ ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, this.templateDataDoc, "onRight"), icon: "caret-square-right" });
if (DocumentManager.Instance.getDocumentViews(this.dataDoc).length) {
- ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.dataDoc).map(view => view.props.focus(this.props.document, true)), icon: "camera" });
+ ContextMenu.Instance.addItem({ description: "Focus", event: () => (view => view && view.props.focus(this.props.document, true))(DocumentManager.Instance.getFirstDocumentView(this.dataDoc)), icon: "camera" });
}
ContextMenu.Instance.addItem({ description: "Delete Item", event: () => this.props.deleteDoc(this.props.document), icon: "trash-alt" });
} else {
ContextMenu.Instance.addItem({ description: "Open as Workspace", event: () => MainView.Instance.openWorkspace(this.dataDoc), icon: "caret-square-right" });
ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.props.deleteDoc(this.props.document), icon: "trash-alt" });
+ ContextMenu.Instance.addItem({ description: "Create New Workspace", event: () => MainView.Instance.createNewWorkspace(), icon: "plus" });
}
ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { let kvp = Docs.Create.KVPDocument(this.props.document, { width: 300, height: 300 }); this.props.addDocTab(kvp, this.props.dataDoc ? this.props.dataDoc : kvp, "onRight"); }, icon: "layer-group" });
ContextMenu.Instance.addItem({ description: "Publish", event: () => DocUtils.Publish(this.props.document, StrCast(this.props.document.title), () => { }, () => { }), icon: "file" });
@@ -217,25 +209,22 @@ class TreeView extends React.Component<TreeViewProps> {
if (de.data instanceof DragManager.LinkDragData) {
let sourceDoc = de.data.linkSourceDocument;
let destDoc = this.props.document;
- DocUtils.MakeLink(sourceDoc, destDoc);
+ DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc });
e.stopPropagation();
}
if (de.data instanceof DragManager.DocumentDragData) {
e.stopPropagation();
if (de.data.draggedDocuments[0] === this.props.document) return true;
- let addDoc = (doc: Doc) => this.props.addDocument(doc, this.resolvedDataDoc, before);
+ let addDoc = (doc: Doc) => this.props.addDocument(doc, undefined, before);
if (inside) {
- let docList = Cast(this.dataDoc.data, listSpec(Doc));
- if (docList !== undefined) {
- addDoc = (doc: Doc) => { docList && docList.push(doc); return true; };
- }
+ addDoc = (doc: Doc) => Doc.AddDocToList(this.dataDoc, this.fieldKey, doc) || addDoc(doc);
}
let movedDocs = (de.data.options === this.props.treeViewId ? de.data.draggedDocuments : de.data.droppedDocuments);
return (de.data.dropAction || de.data.userDropAction) ?
- de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.resolvedDataDoc, before) || added, false)
- : (de.data.moveDocument) ?
- movedDocs.reduce((added: boolean, d) => de.data.moveDocument(d, this.resolvedDataDoc, addDoc) || added, false)
- : de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.resolvedDataDoc, before), false);
+ de.data.droppedDocuments.reduce((added, d) => this.props.addDocument(d, undefined, before) || added, false)
+ : de.data.moveDocument ?
+ movedDocs.reduce((added, d) => de.data.moveDocument(d, undefined, addDoc) || added, false)
+ : de.data.droppedDocuments.reduce((added, d) => this.props.addDocument(d, undefined, before), false);
}
return false;
}
@@ -248,17 +237,22 @@ class TreeView extends React.Component<TreeViewProps> {
return finalXf;
}
docWidth = () => {
- let aspect = NumCast(this.props.document.nativeHeight) / NumCast(this.props.document.nativeWidth);
- if (aspect) return Math.min(this.props.document[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.panelWidth() - 20));
- return NumCast(this.props.document.nativeWidth) ? Math.min(this.props.document[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20;
+ let layoutDoc = Doc.Layout(this.props.document);
+ let aspect = NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth);
+ if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.panelWidth() - 20));
+ return NumCast(layoutDoc.nativeWidth) ? Math.min(layoutDoc[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20;
}
docHeight = () => {
+ let layoutDoc = Doc.Layout(this.props.document);
let bounds = this.boundsOfCollectionDocument;
return Math.min(this.MAX_EMBED_HEIGHT, (() => {
- let aspect = NumCast(this.props.document.nativeHeight) / NumCast(this.props.document.nativeWidth);
+ let aspect = NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth, 1);
if (aspect) return this.docWidth() * aspect;
if (bounds) return this.docWidth() * (bounds.b - bounds.y) / (bounds.r - bounds.x);
- return NumCast(this.props.document.height) ? NumCast(this.props.document.height) : 50;
+ return layoutDoc.fitWidth ? (!this.props.document.nativeHeight ? NumCast(this.props.containingCollection.height) :
+ Math.min(this.docWidth() * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc.nativeHeight)) / NumCast(layoutDoc.nativeWidth,
+ NumCast(this.props.containingCollection.height)))) :
+ NumCast(layoutDoc.height) ? NumCast(layoutDoc.height) : 50;
})());
}
@@ -307,22 +301,23 @@ class TreeView extends React.Component<TreeViewProps> {
let docs = expandKey === "links" ? this.childLinks : this.childDocs;
return <ul key={expandKey + "more"}>
{!docs ? (null) :
- TreeView.GetChildElements(docs as Doc[], this.props.treeViewId, this.props.document.layout as Doc,
- this.resolvedDataDoc, expandKey, addDoc, remDoc, this.move,
+ TreeView.GetChildElements(docs, this.props.treeViewId, Doc.Layout(this.props.document),
+ this.templateDataDoc, expandKey, addDoc, remDoc, this.move,
this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform,
this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth, this.props.showHeaderFields, this.props.preventTreeViewOpen,
[...this.props.renderedIds, this.props.document[Id]])}
</ul >;
} else if (this.treeViewExpandedView === "fields") {
return <ul><div ref={this._dref} style={{ display: "inline-block" }} key={this.props.document[Id] + this.props.document.title}>
- {this.dataDoc ? this.expandedField(this.dataDoc) : (null)}
+ {this.expandedField(this.props.document)}
</div></ul>;
} else {
- let layoutDoc = this.props.document;
+ let layoutDoc = Doc.Layout(this.props.document);
return <div ref={this._dref} style={{ display: "inline-block", height: this.docHeight() }} key={this.props.document[Id] + this.props.document.title}>
<CollectionSchemaPreview
Document={layoutDoc}
- DataDocument={this.resolvedDataDoc}
+ DataDocument={this.templateDataDoc}
+ fieldKey={this.fieldKey}
renderDepth={this.props.renderDepth}
showOverlays={this.noOverlays}
ruleProvider={this.props.document.isRuleProvider && layoutDoc.type !== DocumentType.TEXT ? this.props.document : this.props.ruleProvider}
@@ -332,15 +327,14 @@ class TreeView extends React.Component<TreeViewProps> {
getTransform={this.docTransform}
CollectionDoc={this.props.containingCollection}
CollectionView={undefined}
- addDocument={emptyFunction as any}
+ addDocument={returnFalse}
moveDocument={this.props.moveDocument}
- removeDocument={emptyFunction as any}
+ removeDocument={returnFalse}
active={this.props.active}
- whenActiveChanged={emptyFunction as any}
+ whenActiveChanged={emptyFunction}
addDocTab={this.props.addDocTab}
pinToPres={this.props.pinToPres}
- setPreviewScript={emptyFunction}>
- </CollectionSchemaPreview>
+ setPreviewScript={emptyFunction} />
</div>;
}
}
@@ -364,7 +358,7 @@ class TreeView extends React.Component<TreeViewProps> {
onPointerDown={action(() => {
if (this.treeViewOpen) {
this.props.document.treeViewExpandedView = this.treeViewExpandedView === this.fieldKey ? "fields" :
- this.treeViewExpandedView === "fields" && this.props.document.layout ? "layout" :
+ this.treeViewExpandedView === "fields" && Doc.Layout(this.props.document) ? "layout" :
this.treeViewExpandedView === "layout" && this.props.document.links ? "links" :
this.childDocs ? this.fieldKey : "fields";
}
@@ -456,7 +450,7 @@ class TreeView extends React.Component<TreeViewProps> {
let rowWidth = () => panelWidth() - 20;
return docs.map((child, i) => {
- let pair = Doc.GetLayoutDataDocPair(containingCollection, dataDoc, key, child);
+ const pair = Doc.GetLayoutDataDocPair(containingCollection, dataDoc, key, child);
if (!pair.layout || pair.data instanceof Promise) {
return (null);
}
@@ -475,9 +469,10 @@ class TreeView extends React.Component<TreeViewProps> {
let addDocument = (doc: Doc, relativeTo?: Doc, before?: boolean) => {
return add(doc, relativeTo ? relativeTo : docs[i], before !== undefined ? before : false);
};
+ const childLayout = Doc.Layout(pair.layout);
let rowHeight = () => {
- let aspect = NumCast(child.nativeWidth, 0) / NumCast(child.nativeHeight, 0);
- return aspect ? Math.min(child[WidthSym](), rowWidth()) / aspect : child[HeightSym]();
+ let aspect = NumCast(childLayout.nativeWidth, 0) / NumCast(childLayout.nativeHeight, 0);
+ return aspect ? Math.min(childLayout[WidthSym](), rowWidth()) / aspect : childLayout[HeightSym]();
};
return <TreeView
document={pair.layout}
@@ -512,9 +507,7 @@ export class CollectionTreeView extends CollectionSubView(Document) {
private treedropDisposer?: DragManager.DragDropDisposer;
private _mainEle?: HTMLDivElement;
- @observable static NotifsCol: Opt<Doc>;
-
- @computed get resolvedDataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; }
+ @computed get dataDoc() { return this.props.DataDoc || this.props.Document; }
protected createTreeDropTarget = (ele: HTMLDivElement) => {
this.treedropDisposer && this.treedropDisposer();
@@ -524,6 +517,7 @@ export class CollectionTreeView extends CollectionSubView(Document) {
}
componentWillUnmount() {
+ super.componentWillUnmount();
this.treedropDisposer && this.treedropDisposer();
}
@@ -538,7 +532,7 @@ export class CollectionTreeView extends CollectionSubView(Document) {
}
onContextMenu = (e: React.MouseEvent): void => {
// need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout
- if (!e.isPropagationStopped() && this.props.Document.workspaceLibrary) {
+ if (!e.isPropagationStopped() && this.props.Document === CurrentUserUtils.UserDocument.workspaces) {
ContextMenu.Instance.addItem({ description: "Create Workspace", event: () => MainView.Instance.createNewWorkspace(), icon: "plus" });
ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.remove(this.props.Document), icon: "minus" });
e.stopPropagation();
@@ -552,31 +546,10 @@ export class CollectionTreeView extends CollectionSubView(Document) {
}
outerXf = () => Utils.GetScreenTransform(this._mainEle!);
onTreeDrop = (e: React.DragEvent) => this.onDrop(e, {});
- openNotifsCol = () => {
- if (CollectionTreeView.NotifsCol) {
- this.props.addDocTab(CollectionTreeView.NotifsCol, undefined, "onRight");
- }
- }
- @computed get renderNotifsButton() {
- const length = CollectionTreeView.NotifsCol ? DocListCast(CollectionTreeView.NotifsCol.data).length : 0;
- const notifsRef = React.createRef<HTMLDivElement>();
- const dragNotifs = action(() => CollectionTreeView.NotifsCol!);
- return <div id="toolbar" key="toolbar">
- <div ref={notifsRef}>
- <button className="toolbar-button round-button" title="Notifs"
- onClick={this.openNotifsCol} onPointerDown={CollectionTreeView.NotifsCol ? SetupDrag(notifsRef, dragNotifs) : emptyFunction}>
- <FontAwesomeIcon icon={faBell} size="sm" />
- </button>
- <div className="main-notifs-badge" style={length > 0 ? { "display": "initial" } : { "display": "none" }}>
- {length}
- </div>
- </div>
- </div >;
- }
@computed get renderClearButton() {
return <div id="toolbar" key="toolbar">
- <button className="toolbar-button round-button" title="Notifs"
+ <button className="toolbar-button round-button" title="Empty"
onClick={undoBatch(action(() => Doc.GetProto(this.props.Document)[this.props.fieldKey] = undefined))}>
<FontAwesomeIcon icon={faTrash} size="sm" />
</button>
@@ -584,38 +557,36 @@ export class CollectionTreeView extends CollectionSubView(Document) {
}
render() {
- Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey);
let dropAction = StrCast(this.props.Document.dropAction) as dropActionType;
let addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before, false, false, false);
let moveDoc = (d: Doc, target: Doc, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc);
return !this.childDocs ? (null) : (
<div id="body" className="collectionTreeView-dropTarget"
- style={{ overflow: "auto", background: StrCast(this.props.Document.backgroundColor, "lightgray") }}
+ style={{ overflow: "auto", background: StrCast(this.props.Document.backgroundColor, "lightgray"), paddingTop: `${NumCast(this.props.Document.yMargin, 20)}px` }}
onContextMenu={this.onContextMenu}
onWheel={(e: React.WheelEvent) => this._mainEle && this._mainEle.scrollHeight > this._mainEle.clientHeight && e.stopPropagation()}
onDrop={this.onTreeDrop}
ref={this.createTreeDropTarget}>
<EditableView
- contents={this.resolvedDataDoc.title}
+ contents={this.dataDoc.title}
display={"block"}
maxHeight={72}
height={"auto"}
- GetValue={() => StrCast(this.resolvedDataDoc.title)}
- SetValue={undoBatch((value: string) => (Doc.GetProto(this.resolvedDataDoc).title = value) ? true : true)}
+ GetValue={() => StrCast(this.dataDoc.title)}
+ SetValue={undoBatch((value: string) => Doc.SetInPlace(this.dataDoc, "title", value, false) || true)}
OnFillDown={undoBatch((value: string) => {
- Doc.GetProto(this.props.Document).title = value;
+ Doc.SetInPlace(this.dataDoc, "title", value, false);
let doc = this.props.Document.layoutCustom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.Document.layoutCustom)) : undefined;
if (!doc) doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
TreeView.loadId = doc[Id];
Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, this.childDocs.length ? this.childDocs[0] : undefined, true, false, false, false);
})} />
- {this.props.Document.workspaceLibrary ? this.renderNotifsButton : (null)}
{this.props.Document.allowClear ? this.renderClearButton : (null)}
<ul className="no-indent" style={{ width: "max-content" }} >
{
TreeView.GetChildElements(this.childDocs, this.props.Document[Id], this.props.Document, this.props.DataDoc, this.props.fieldKey, addDoc, this.remove,
moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform,
- this.outerXf, this.props.active, this.props.PanelWidth, this.props.renderDepth, () => this.props.Document.chromeStatus !== "disabled",
+ this.outerXf, this.props.active, this.props.PanelWidth, this.props.renderDepth, () => !this.props.Document.hideHeaderFields,
BoolCast(this.props.Document.preventTreeViewOpen), [])
}
</ul>
diff --git a/src/client/views/collections/CollectionVideoView.scss b/src/client/views/collections/CollectionVideoView.scss
deleted file mode 100644
index 509851ebb..000000000
--- a/src/client/views/collections/CollectionVideoView.scss
+++ /dev/null
@@ -1,51 +0,0 @@
-
-.collectionVideoView-cont{
- width: 100%;
- height: 100%;
- position: inherit;
- top: 0;
- left:0;
- z-index: -1;
- display:inline-table;
-}
-.collectionVideoView-time{
- color : white;
- top :25px;
- left : 25px;
- position: absolute;
- background-color: rgba(50, 50, 50, 0.2);
- transform-origin: left top;
-}
-.collectionVideoView-snapshot{
- color : white;
- top :25px;
- right : 25px;
- position: absolute;
- background-color: rgba(50, 50, 50, 0.2);
- transform-origin: left top;
-}
-.collectionVideoView-play {
- width: 25px;
- height: 20px;
- bottom: 25px;
- left : 25px;
- position: absolute;
- color : white;
- background-color: rgba(50, 50, 50, 0.2);
- border-radius: 4px;
- text-align: center;
- transform-origin: left bottom;
-}
-.collectionVideoView-full {
- width: 25px;
- height: 20px;
- bottom: 25px;
- right : 25px;
- position: absolute;
- color : white;
- background-color: rgba(50, 50, 50, 0.2);
- border-radius: 4px;
- text-align: center;
- transform-origin: right bottom;
-
-} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx
deleted file mode 100644
index 5185d9d0e..000000000
--- a/src/client/views/collections/CollectionVideoView.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-import { action } from "mobx";
-import { observer } from "mobx-react";
-import { NumCast } from "../../../new_fields/Types";
-import { FieldView, FieldViewProps } from "../nodes/FieldView";
-import { VideoBox } from "../nodes/VideoBox";
-import { CollectionBaseView, CollectionRenderProps, CollectionViewType } from "./CollectionBaseView";
-import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";
-import "./CollectionVideoView.scss";
-import React = require("react");
-import { InkingControl } from "../InkingControl";
-import { InkTool } from "../../../new_fields/InkField";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-
-
-@observer
-export class CollectionVideoView extends React.Component<FieldViewProps> {
- private _videoBox?: VideoBox;
-
- public static LayoutString(fieldKey: string = "data", fieldExt: string = "annotations") {
- return FieldView.LayoutString(CollectionVideoView, fieldKey, fieldExt);
- }
- private get uIButtons() {
- let scaling = Math.min(1.8, this.props.ScreenToLocalTransform().Scale);
- let curTime = NumCast(this.props.Document.curPage);
- return ([<div className="collectionVideoView-time" key="time" onPointerDown={this.onResetDown} style={{ transform: `scale(${scaling})` }}>
- <span>{"" + Math.round(curTime)}</span>
- <span style={{ fontSize: 8 }}>{" " + Math.round((curTime - Math.trunc(curTime)) * 100)}</span>
- </div>,
- <div className="collectionVideoView-snapshot" key="time" onPointerDown={this.onSnapshot} style={{ transform: `scale(${scaling})` }}>
- <FontAwesomeIcon icon="camera" size="lg" />
- </div>,
- VideoBox._showControls ? (null) : [
- <div className="collectionVideoView-play" key="play" onPointerDown={this.onPlayDown} style={{ transform: `scale(${scaling})` }}>
- <FontAwesomeIcon icon={this._videoBox && this._videoBox.Playing ? "pause" : "play"} size="lg" />
- </div>,
- <div className="collectionVideoView-full" key="full" onPointerDown={this.onFullDown} style={{ transform: `scale(${scaling})` }}>
- F
- </div>
- ]]);
- }
-
- @action
- onPlayDown = () => {
- if (this._videoBox) {
- if (this._videoBox.Playing) {
- this._videoBox.Pause();
- } else {
- this._videoBox.Play();
- }
- }
- }
-
- @action
- onFullDown = (e: React.PointerEvent) => {
- if (this._videoBox) {
- this._videoBox.FullScreen();
- e.stopPropagation();
- e.preventDefault();
- }
- }
-
- @action
- onSnapshot = (e: React.PointerEvent) => {
- if (this._videoBox) {
- this._videoBox.Snapshot();
- e.stopPropagation();
- e.preventDefault();
- }
- }
-
- _isclick = 0;
- @action
- onResetDown = (e: React.PointerEvent) => {
- if (this._videoBox) {
- this._videoBox.Pause();
- e.stopPropagation();
- this._isclick = 0;
- document.addEventListener("pointermove", this.onPointerMove, true);
- document.addEventListener("pointerup", this.onPointerUp, true);
- InkingControl.Instance.switchTool(InkTool.Eraser);
- }
- }
-
- @action
- onPointerMove = (e: PointerEvent) => {
- this._isclick += Math.abs(e.movementX) + Math.abs(e.movementY);
- if (this._videoBox) {
- this._videoBox.Seek(Math.max(0, NumCast(this.props.Document.curPage, 0) + Math.sign(e.movementX) * 0.0333));
- }
- e.stopImmediatePropagation();
- }
- @action
- onPointerUp = (e: PointerEvent) => {
- document.removeEventListener("pointermove", this.onPointerMove, true);
- document.removeEventListener("pointerup", this.onPointerUp, true);
- InkingControl.Instance.switchTool(InkTool.None);
- this._isclick < 10 && (this.props.Document.curPage = 0);
- }
- setVideoBox = (videoBox: VideoBox) => { this._videoBox = videoBox; };
-
- private subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => {
- let props = { ...this.props, ...renderProps };
- return (<>
- <CollectionFreeFormView {...props} setVideoBox={this.setVideoBox} CollectionView={this} chromeCollapsed={true} />
- {this.props.isSelected() ? this.uIButtons : (null)}
- </>);
- }
-
- render() {
- return (
- <CollectionBaseView {...this.props} className="collectionVideoView-cont" >
- {this.subView}
- </CollectionBaseView>);
- }
-} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionBaseView.scss b/src/client/views/collections/CollectionView.scss
index aff965469..e4187e4d6 100644
--- a/src/client/views/collections/CollectionBaseView.scss
+++ b/src/client/views/collections/CollectionView.scss
@@ -1,6 +1,6 @@
@import "../globalCssVariables";
-#collectionBaseView {
+.collectionView {
border-width: 0;
border-color: $light-color-secondary;
border-style: solid;
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 534246326..8d5694bf0 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -5,12 +5,9 @@ import { action, IReactionDisposer, observable, reaction, runInAction } from 'mo
import { observer } from "mobx-react";
import * as React from 'react';
import { Id } from '../../../new_fields/FieldSymbols';
-import { StrCast } from '../../../new_fields/Types';
+import { StrCast, BoolCast, Cast } from '../../../new_fields/Types';
import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
import { ContextMenu } from "../ContextMenu";
-import { ContextMenuProps } from '../ContextMenuItem';
-import { FieldView, FieldViewProps } from '../nodes/FieldView';
-import { CollectionBaseView, CollectionRenderProps, CollectionViewType } from './CollectionBaseView';
import { CollectionDockingView } from "./CollectionDockingView";
import { AddCustomFreeFormLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines';
import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
@@ -18,20 +15,82 @@ import { CollectionSchemaView } from "./CollectionSchemaView";
import { CollectionStackingView } from './CollectionStackingView';
import { CollectionTreeView } from "./CollectionTreeView";
import { CollectionViewBaseChrome } from './CollectionViewChromes';
+import { ImageUtils } from '../../util/Import & Export/ImageUtils';
+import { CollectionLinearView } from '../CollectionLinearView';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { ImageField } from '../../../new_fields/URLField';
+import { DocListCast } from '../../../new_fields/Doc';
+import Lightbox from 'react-image-lightbox-with-rotate';
+import 'react-image-lightbox-with-rotate/style.css'; // This only needs to be imported once in your app
export const COLLECTION_BORDER_WIDTH = 2;
-
+import { DateField } from '../../../new_fields/DateField';
+import { Doc, } from '../../../new_fields/Doc';
+import { listSpec } from '../../../new_fields/Schema';
+import { DocumentManager } from '../../util/DocumentManager';
+import { SelectionManager } from '../../util/SelectionManager';
+import './CollectionView.scss';
+import { FieldViewProps, FieldView } from '../nodes/FieldView';
library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faEllipsisV, faImage, faEye as any, faCopy);
+export enum CollectionViewType {
+ Invalid,
+ Freeform,
+ Schema,
+ Docking,
+ Tree,
+ Stacking,
+ Masonry,
+ Pivot,
+ Linear,
+}
+
+export namespace CollectionViewType {
+ const stringMapping = new Map<string, CollectionViewType>([
+ ["invalid", CollectionViewType.Invalid],
+ ["freeform", CollectionViewType.Freeform],
+ ["schema", CollectionViewType.Schema],
+ ["docking", CollectionViewType.Docking],
+ ["tree", CollectionViewType.Tree],
+ ["stacking", CollectionViewType.Stacking],
+ ["masonry", CollectionViewType.Masonry],
+ ["pivot", CollectionViewType.Pivot],
+ ["linear", CollectionViewType.Linear]
+ ]);
+
+ export const valueOf = (value: string) => stringMapping.get(value.toLowerCase());
+}
+
+export interface CollectionRenderProps {
+ addDocument: (document: Doc) => boolean;
+ removeDocument: (document: Doc) => boolean;
+ moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
+ active: () => boolean;
+ whenActiveChanged: (isActive: boolean) => void;
+}
+
@observer
export class CollectionView extends React.Component<FieldViewProps> {
- @observable private _collapsed = true;
+ public static LayoutString(fieldStr: string) { return FieldView.LayoutString(CollectionView, fieldStr); }
private _reactionDisposer: IReactionDisposer | undefined;
+ private _isChildActive = false; //TODO should this be observable?
+ @observable private _isLightboxOpen = false;
+ @observable private _curLightboxImg = 0;
+ @observable private _collapsed = true;
+ @observable private static _safeMode = false;
+ public static SetSafeMode(safeMode: boolean) { this._safeMode = safeMode; }
- public static LayoutString(fieldStr: string = "data", fieldExt: string = "") { return FieldView.LayoutString(CollectionView, fieldStr, fieldExt); }
-
- constructor(props: any) {
- super(props);
+ get collectionViewType(): CollectionViewType | undefined {
+ let viewField = Cast(this.props.Document.viewType, "number");
+ if (CollectionView._safeMode) {
+ if (viewField === CollectionViewType.Freeform) {
+ return CollectionViewType.Tree;
+ }
+ if (viewField === CollectionViewType.Invalid) {
+ return CollectionViewType.Freeform;
+ }
+ }
+ return viewField === undefined ? CollectionViewType.Invalid : viewField;
}
componentDidMount = () => {
@@ -46,31 +105,73 @@ export class CollectionView extends React.Component<FieldViewProps> {
});
}
- componentWillUnmount = () => {
- this._reactionDisposer && this._reactionDisposer();
+ componentWillUnmount = () => this._reactionDisposer && this._reactionDisposer();
+
+ // bcz: Argh? What's the height of the collection chromes??
+ chromeHeight = () => (this.props.ChromeHeight ? this.props.ChromeHeight() : 0) + (this.props.Document.chromeStatus === "enabled" ? -60 : 0);
+
+ active = () => this.props.isSelected() || BoolCast(this.props.Document.forceActive) || this._isChildActive || this.props.renderDepth === 0;
+
+ whenActiveChanged = (isActive: boolean) => { this.props.whenActiveChanged(this._isChildActive = isActive); };
+
+ @action.bound
+ addDocument(doc: Doc): boolean {
+ let targetDataDoc = Doc.GetProto(this.props.Document);
+ Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc);
+ let extension = Doc.fieldExtensionDoc(targetDataDoc, this.props.fieldKey); // set metadata about the field being rendered (ie, the set of documents) on an extension field for that field
+ extension && (extension.lastModified = new DateField(new Date(Date.now())));
+ Doc.GetProto(doc).lastOpened = new DateField;
+ return true;
+ }
+
+ @action.bound
+ removeDocument(doc: Doc): boolean {
+ let docView = DocumentManager.Instance.getDocumentView(doc, this.props.ContainingCollectionView);
+ docView && SelectionManager.DeselectDoc(docView);
+ let value = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []);
+ let index = value.reduce((p, v, i) => (v instanceof Doc && v === doc) ? i : p, -1);
+ index = index !== -1 ? index : value.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, doc)) ? i : p, -1);
+
+ ContextMenu.Instance.clearItems();
+ if (index !== -1) {
+ value.splice(index, 1);
+ return true;
+ }
+ return false;
+ }
+
+ // this is called with the document that was dragged and the collection to move it into.
+ // if the target collection is the same as this collection, then the move will be allowed.
+ // otherwise, the document being moved must be able to be removed from its container before
+ // moving it into the target.
+ @action.bound
+ moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean {
+ if (Doc.AreProtosEqual(this.props.Document, targetCollection)) {
+ return true;
+ }
+ return this.removeDocument(doc) ? addDocument(doc) : false;
}
- // bcz: Argh? What's the height of the collection chomes??
- chromeHeight = () => {
- return (this.props.ChromeHeight ? this.props.ChromeHeight() : 0) + (this.props.Document.chromeStatus === "enabled" ? -60 : 0);
+ showIsTagged = () => {
+ const children = DocListCast(this.props.Document[this.props.fieldKey]);
+ const imageProtos = children.filter(doc => Cast(doc.data, ImageField)).map(Doc.GetProto);
+ const allTagged = imageProtos.length > 0 && imageProtos.every(image => image.googlePhotosTags);
+ return !allTagged ? (null) : <img id={"google-tags"} src={"/assets/google_tags.png"} />;
}
private SubViewHelper = (type: CollectionViewType, renderProps: CollectionRenderProps) => {
- let props = { ...this.props, ...renderProps };
- switch (this.isAnnotationOverlay ? CollectionViewType.Freeform : type) {
- case CollectionViewType.Schema: return (<CollectionSchemaView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />);
- // 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
- case CollectionViewType.Docking: return (<CollectionDockingView chromeCollapsed={true} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />);
- case CollectionViewType.Tree: return (<CollectionTreeView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />);
- case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); }
- case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); }
- case CollectionViewType.Pivot: { this.props.Document.freeformLayoutEngine = "pivot"; return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); }
+ let props = { ...this.props, ...renderProps, chromeCollapsed: this._collapsed, ChromeHeight: this.chromeHeight, CollectionView: this, annotationsKey: "" };
+ switch (type) {
+ case CollectionViewType.Schema: return (<CollectionSchemaView key="collview" {...props} />);
+ case CollectionViewType.Docking: return (<CollectionDockingView key="collview" {...props} />);
+ case CollectionViewType.Tree: return (<CollectionTreeView key="collview" {...props} />);
+ case CollectionViewType.Linear: { return (<CollectionLinearView key="collview" {...props} />); }
+ case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView key="collview" {...props} />); }
+ case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView key="collview" {...props} />); }
+ case CollectionViewType.Pivot: { this.props.Document.freeformLayoutEngine = "pivot"; return (<CollectionFreeFormView key="collview" {...props} />); }
case CollectionViewType.Freeform:
- default:
- this.props.Document.freeformLayoutEngine = undefined;
- return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />);
+ default: { this.props.Document.freeformLayoutEngine = undefined; return (<CollectionFreeFormView key="collview" {...props} />); }
}
- return (null);
}
@action
@@ -81,23 +182,18 @@ export class CollectionView extends React.Component<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
- if (this.isAnnotationOverlay || this.props.Document.chromeStatus === "disabled" || type === CollectionViewType.Docking) {
- return [(null), this.SubViewHelper(type, renderProps)];
- }
- return [
- <CollectionViewBaseChrome CollectionView={this} key="chrome" type={type} collapse={this.collapse} />,
- this.SubViewHelper(type, renderProps)
- ];
+ let chrome = this.props.Document.chromeStatus === "disabled" || type === CollectionViewType.Docking ? (null) :
+ <CollectionViewBaseChrome CollectionView={this} key="chrome" type={type} collapse={this.collapse} />;
+ return [chrome, this.SubViewHelper(type, renderProps)];
}
- get isAnnotationOverlay() { return this.props.fieldExt ? true : false; }
onContextMenu = (e: React.MouseEvent): void => {
- if (!this.isAnnotationOverlay && !e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
+ if (!e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
let existingVm = ContextMenu.Instance.findByDescription("View Modes...");
- let subItems: ContextMenuProps[] = existingVm && "subitems" in existingVm ? existingVm.subitems : [];
+ let subItems = existingVm && "subitems" in existingVm ? existingVm.subitems : [];
subItems.push({ description: "Freeform", event: () => { this.props.Document.viewType = CollectionViewType.Freeform; }, icon: "signature" });
- if (CollectionBaseView.InSafeMode()) {
+ if (CollectionView._safeMode) {
ContextMenu.Instance.addItem({ description: "Test Freeform", event: () => this.props.Document.viewType = CollectionViewType.Invalid, icon: "project-diagram" });
}
subItems.push({ description: "Schema", event: () => this.props.Document.viewType = CollectionViewType.Schema, icon: "th-list" });
@@ -117,20 +213,43 @@ export class CollectionView extends React.Component<FieldViewProps> {
break;
}
}
+ subItems.push({ description: "lightbox", event: action(() => this._isLightboxOpen = true), icon: "eye" });
!existingVm && ContextMenu.Instance.addItem({ description: "View Modes...", subitems: subItems, icon: "eye" });
let existing = ContextMenu.Instance.findByDescription("Layout...");
- let layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : [];
+ let layoutItems = existing && "subitems" in existing ? existing.subitems : [];
layoutItems.push({ description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" });
!existing && ContextMenu.Instance.addItem({ description: "Layout...", subitems: layoutItems, icon: "hand-point-right" });
+ ContextMenu.Instance.addItem({ description: "Export Image Hierarchy", icon: "columns", event: () => ImageUtils.ExportHierarchyToFileSystem(this.props.Document) });
}
}
+ lightbox = (images: string[]) => {
+ return !this._isLightboxOpen ? (null) : (<Lightbox key="lightbox"
+ mainSrc={images[this._curLightboxImg]}
+ nextSrc={images[(this._curLightboxImg + 1) % images.length]}
+ prevSrc={images[(this._curLightboxImg + images.length - 1) % images.length]}
+ onCloseRequest={action(() => this._isLightboxOpen = false)}
+ onMovePrevRequest={action(() => this._curLightboxImg = (this._curLightboxImg + images.length - 1) % images.length)}
+ onMoveNextRequest={action(() => this._curLightboxImg = (this._curLightboxImg + 1) % images.length)} />);
+ }
render() {
- return (
- <CollectionBaseView {...this.props} onContextMenu={this.onContextMenu}>
- {this.SubView}
- </CollectionBaseView>
- );
+ const props: CollectionRenderProps = {
+ addDocument: this.addDocument,
+ removeDocument: this.removeDocument,
+ moveDocument: this.moveDocument,
+ active: this.active,
+ whenActiveChanged: this.whenActiveChanged,
+ };
+ return (<div className={"collectionView"}
+ style={{
+ pointerEvents: this.props.Document.isBackground ? "none" : "all",
+ boxShadow: this.props.Document.isBackground || this.collectionViewType === CollectionViewType.Linear ? undefined : `#9c9396 ${StrCast(this.props.Document.boxShadow, "0.2vw 0.2vw 0.8vw")}`
+ }}
+ onContextMenu={this.onContextMenu}>
+ {this.showIsTagged()}
+ {this.collectionViewType !== undefined ? this.SubView(this.collectionViewType, props) : (null)}
+ {this.lightbox(DocListCast(this.props.Document[this.props.fieldKey]).filter(d => d.type === DocumentType.IMG).map(d => Cast(d.data, ImageField) ? Cast(d.data, ImageField)!.url.href : ""))}
+ </div>);
}
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
index 23001f0f5..cfc6c2a3f 100644
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -14,7 +14,7 @@ import { undoBatch } from "../../util/UndoManager";
import { EditableView } from "../EditableView";
import { COLLECTION_BORDER_WIDTH } from "../globalCssVariables.scss";
import { DocLike } from "../MetadataEntryMenu";
-import { CollectionViewType } from "./CollectionBaseView";
+import { CollectionViewType } from "./CollectionView";
import { CollectionView } from "./CollectionView";
import "./CollectionViewChromes.scss";
import * as Autosuggest from 'react-autosuggest';
@@ -40,9 +40,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
//(!)?\(\(\(doc.(\w+) && \(doc.\w+ as \w+\).includes\(\"(\w+)\"\)
_templateCommand = {
- title: "set template", script: "this.target.childLayout = this.source ? this.source[0] : undefined", params: ["target", "source"],
+ title: "set template", script: "setChildLayout(this.target, this.source && this.source.length ? this.source[0]:undefined)", params: ["target", "source"],
initialize: emptyFunction,
- immediate: (draggedDocs: Doc[]) => this.props.CollectionView.props.Document.childLayout = draggedDocs.length ? draggedDocs[0] : undefined
+ immediate: (draggedDocs: Doc[]) => Doc.setChildLayout(this.props.CollectionView.props.Document, draggedDocs.length ? draggedDocs[0] : undefined)
};
_contentCommand = {
// title: "set content", script: "getProto(this.target).data = aliasDocs(this.source.map(async p => await p));", params: ["target", "source"], // bcz: doesn't look like we can do async stuff in scripting...
@@ -243,6 +243,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
case CollectionViewType.Stacking: return (<CollectionStackingViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />);
case CollectionViewType.Schema: return (<CollectionSchemaViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />);
case CollectionViewType.Tree: return (<CollectionTreeViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />);
+ case CollectionViewType.Masonry: return (<CollectionStackingViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />);
default: return null;
}
}
@@ -398,6 +399,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
<option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="5">Stacking View</option>
<option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="6">Masonry View</option>
<option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="7">Pivot View</option>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="8">Linear View</option>
</select>
<div className="collectionViewBaseChrome-viewSpecs" style={{ display: collapsed ? "none" : "grid" }}>
<input className="collectionViewBaseChrome-viewSpecsInput"
@@ -478,12 +480,7 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView
getKeySuggestions = async (value: string): Promise<string[]> => {
value = value.toLowerCase();
- let docs: Doc | Doc[] | Promise<Doc> | Promise<Doc[]> | (() => DocLike)
- = () => DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldExt ? this.props.CollectionView.props.fieldExt : this.props.CollectionView.props.fieldKey]);
- if (typeof docs === "function") {
- docs = docs();
- }
- docs = await docs;
+ let docs = DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey]);
if (docs instanceof Doc) {
return Object.keys(docs).filter(key => key.toLowerCase().startsWith(value));
} else {
@@ -589,19 +586,9 @@ export class CollectionSchemaViewChrome extends React.Component<CollectionViewCh
if (textwrappedRows.length) {
this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>([]);
} else {
- let docs: Doc | Doc[] | Promise<Doc> | Promise<Doc[]> | (() => DocLike)
- = () => DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldExt ? this.props.CollectionView.props.fieldExt : this.props.CollectionView.props.fieldKey]);
- if (typeof docs === "function") {
- docs = docs();
- }
- docs = await docs;
- if (docs instanceof Doc) {
- let allRows = [docs[Id]];
- this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>(allRows);
- } else {
- let allRows = docs.map(doc => doc[Id]);
- this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>(allRows);
- }
+ let docs = DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey]);
+ let allRows = docs instanceof Doc ? [docs[Id]] : docs.map(doc => doc[Id]);
+ this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>(allRows);
}
}
@@ -636,63 +623,14 @@ export class CollectionSchemaViewChrome extends React.Component<CollectionViewCh
@observer
export class CollectionTreeViewChrome extends React.Component<CollectionViewChromeProps> {
- @observable private _currentKey: string = "";
- @observable private suggestions: string[] = [];
@computed private get descending() { return Cast(this.props.CollectionView.props.Document.sortAscending, "boolean", null); }
- @computed get sectionFilter() { return StrCast(this.props.CollectionView.props.Document.sectionFilter); }
-
- getKeySuggestions = async (value: string): Promise<string[]> => {
- value = value.toLowerCase();
- let docs: Doc | Doc[] | Promise<Doc> | Promise<Doc[]> | (() => DocLike)
- = () => DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldExt ? this.props.CollectionView.props.fieldExt : this.props.CollectionView.props.fieldKey]);
- if (typeof docs === "function") {
- docs = docs();
- }
- docs = await docs;
- if (docs instanceof Doc) {
- return Object.keys(docs).filter(key => key.toLowerCase().startsWith(value));
- } else {
- const keys = new Set<string>();
- docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key)));
- return Array.from(keys).filter(key => key.toLowerCase().startsWith(value));
- }
- }
-
- @action
- onKeyChange = (e: React.ChangeEvent, { newValue }: { newValue: string }) => {
- this._currentKey = newValue;
- }
-
- getSuggestionValue = (suggestion: string) => suggestion;
-
- renderSuggestion = (suggestion: string) => {
- return <p>{suggestion}</p>;
- }
-
- onSuggestionFetch = async ({ value }: { value: string }) => {
- const sugg = await this.getKeySuggestions(value);
- runInAction(() => {
- this.suggestions = sugg;
- });
- }
-
- @action
- onSuggestionClear = () => {
- this.suggestions = [];
- }
-
- setValue = (value: string) => {
- this.props.CollectionView.props.Document.sectionFilter = value;
- return true;
- }
@action toggleSort = () => {
if (this.props.CollectionView.props.Document.sortAscending) this.props.CollectionView.props.Document.sortAscending = undefined;
else if (this.props.CollectionView.props.Document.sortAscending === undefined) this.props.CollectionView.props.Document.sortAscending = false;
else this.props.CollectionView.props.Document.sortAscending = true;
}
- @action resetValue = () => { this._currentKey = this.sectionFilter; };
render() {
return (
diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx
index 7f2913214..8b6fa330c 100644
--- a/src/client/views/collections/ParentDocumentSelector.tsx
+++ b/src/client/views/collections/ParentDocumentSelector.tsx
@@ -7,7 +7,7 @@ import { Id } from "../../../new_fields/FieldSymbols";
import { SearchUtil } from "../../util/SearchUtil";
import { CollectionDockingView } from "./CollectionDockingView";
import { NumCast } from "../../../new_fields/Types";
-import { CollectionViewType } from "./CollectionBaseView";
+import { CollectionViewType } from "./CollectionView";
import { DocumentButtonBar } from "../DocumentButtonBar";
import { DocumentManager } from "../../util/DocumentManager";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 886692172..48d330674 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -63,11 +63,12 @@ export function computePivotLayout(pivotDoc: Doc, childDocs: Doc[], childPairs:
fontSize: NumCast(pivotDoc.pivotFontSize, 10)
});
for (const doc of val) {
+ let layoutDoc = Doc.Layout(doc);
docMap.set(doc, {
x: x + xCount * pivotAxisWidth * 1.25,
y: -y,
width: pivotAxisWidth,
- height: doc.nativeWidth ? (NumCast(doc.nativeHeight) / NumCast(doc.nativeWidth)) * pivotAxisWidth : pivotAxisWidth
+ height: layoutDoc.nativeWidth ? (NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth)) * pivotAxisWidth : pivotAxisWidth
});
xCount++;
if (xCount >= numCols) {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
index cfd18ad35..75af11537 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
@@ -1,20 +1,18 @@
.collectionfreeformlinkview-linkLine {
stroke: black;
- transform: translate(10000px,10000px);
opacity: 0.8;
pointer-events: all;
stroke-width: 3px;
+ transition: opacity 0.5s ease-in;
}
.collectionfreeformlinkview-linkCircle {
stroke: rgb(0,0,0);
opacity: 0.5;
- transform: translate(10000px,10000px);
pointer-events: all;
cursor: pointer;
}
.collectionfreeformlinkview-linkText {
stroke: rgb(0,0,0);
opacity: 0.5;
- transform: translate(10000px,10000px);
pointer-events: all;
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index df089eb00..837413842 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -1,63 +1,51 @@
import { observer } from "mobx-react";
-import { Doc, HeightSym, WidthSym } from "../../../../new_fields/Doc";
-import { BoolCast, NumCast, StrCast } from "../../../../new_fields/Types";
-import { InkingControl } from "../../InkingControl";
+import { Doc } from "../../../../new_fields/Doc";
+import { Utils } from '../../../../Utils';
+import { DocumentView } from "../../nodes/DocumentView";
import "./CollectionFreeFormLinkView.scss";
import React = require("react");
import v5 = require("uuid/v5");
+import { DocumentType } from "../../../documents/DocumentTypes";
+import { observable, action } from "mobx";
export interface CollectionFreeFormLinkViewProps {
- A: Doc;
- B: Doc;
+ A: DocumentView;
+ B: DocumentView;
LinkDocs: Doc[];
- addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
- removeDocument: (document: Doc) => boolean;
}
@observer
export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFormLinkViewProps> {
-
- onPointerDown = (e: React.PointerEvent) => {
- if (e.button === 0 && !InkingControl.Instance.selectedTool) {
- let a = this.props.A;
- let b = this.props.B;
- let x1 = NumCast(a.x) + (BoolCast(a.isMinimized) ? 5 : a[WidthSym]() / 2);
- let y1 = NumCast(a.y) + (BoolCast(a.isMinimized) ? 5 : a[HeightSym]() / 2);
- let x2 = NumCast(b.x) + (BoolCast(b.isMinimized) ? 5 : b[WidthSym]() / 2);
- let y2 = NumCast(b.y) + (BoolCast(b.isMinimized) ? 5 : b[HeightSym]() / 2);
- // this.props.LinkDocs.map(l => {
- // let width = l[WidthSym]();
- // l.x = (x1 + x2) / 2 - width / 2;
- // l.y = (y1 + y2) / 2 + 10;
- // if (!this.props.removeDocument(l)) this.props.addDocument(l, false);
- // });
- e.stopPropagation();
- e.preventDefault();
- }
+ @observable _alive: number = 0;
+ @observable _opacity: number = 1;
+ @action
+ componentDidMount() {
+ this._alive = 1;
+ setTimeout(this.rerender, 50);
+ setTimeout(action(() => this._opacity = 0.05), 50);
+ }
+ @action
+ componentWillUnmount() {
+ this._alive = 0;
}
+ rerender = action(() => {
+ if (this._alive) {
+ setTimeout(this.rerender, 50);
+ this._alive++;
+ }
+ });
+
render() {
- // let l = this.props.LinkDocs;
- let a = this.props.A;
- let b = this.props.B;
- let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.width) / 2);
- let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.height) / 2);
- let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.width) / 2);
- let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.height) / 2);
- let text = "";
- // let first = this.props.LinkDocs[0];
- // if (this.props.LinkDocs.length === 1) text += first.title + (first.linkDescription ? "(" + StrCast(first.linkDescription) + ")" : "");
- // else text = "-multiple-";
- return (
- <>
- <line key="linkLine" className="collectionfreeformlinkview-linkLine"
- x1={`${x1}`} y1={`${y1}`}
- x2={`${x2}`} y2={`${y2}`} />
- {/* <circle key="linkCircle" className="collectionfreeformlinkview-linkCircle"
- cx={(x1 + x2) / 2} cy={(y1 + y2) / 2} r={8} onPointerDown={this.onPointerDown} /> */}
- <text key="linkText" textAnchor="middle" className="collectionfreeformlinkview-linkText" x={`${(x1 + x2) / 2}`} y={`${(y1 + y2) / 2}`}>
- {text}
- </text>
- </>
- );
+ let y = this._alive;
+ let acont = this.props.A.props.Document.type === DocumentType.LINK ? this.props.A.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : [];
+ let bcont = this.props.B.props.Document.type === DocumentType.LINK ? this.props.B.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : [];
+ let a = (acont.length ? acont[0] : this.props.A.ContentDiv!).getBoundingClientRect();
+ let b = (bcont.length ? bcont[0] : this.props.B.ContentDiv!).getBoundingClientRect();
+ let pt1 = Utils.getNearestPointInPerimeter(a.left, a.top, a.width, a.height, b.left + b.width / 2, b.top + b.height / 2);
+ let pt2 = Utils.getNearestPointInPerimeter(b.left, b.top, b.width, b.height, a.left + a.width / 2, a.top + a.height / 2);
+ return (<line key="linkLine" className="collectionfreeformlinkview-linkLine"
+ style={{ opacity: this._opacity }}
+ x1={`${pt1[0]}`} y1={`${pt1[1]}`}
+ x2={`${pt2[0]}`} y2={`${pt2[1]}`} />);
}
} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss
index 30e158603..cb5cef29c 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss
@@ -1,10 +1,9 @@
.collectionfreeformlinksview-svgCanvas{
- transform: translate(-10000px,-10000px);
position: absolute;
top: 0;
left: 0;
- width: 20000px;
- height: 20000px;
+ width: 100%;
+ height: 100%;
pointer-events: none;
}
.collectionfreeformlinksview-container {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index a81f5315a..e9191c176 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -1,19 +1,18 @@
-import { computed, IReactionDisposer, reaction } from "mobx";
+import { computed, IReactionDisposer } from "mobx";
import { observer } from "mobx-react";
-import { Doc, DocListCast } from "../../../../new_fields/Doc";
+import { Doc } from "../../../../new_fields/Doc";
import { Id } from "../../../../new_fields/FieldSymbols";
-import { List } from "../../../../new_fields/List";
-import { listSpec } from "../../../../new_fields/Schema";
-import { Cast, FieldValue, NumCast, StrCast } from "../../../../new_fields/Types";
import { DocumentManager } from "../../../util/DocumentManager";
import { DocumentView } from "../../nodes/DocumentView";
-import { CollectionViewProps } from "../CollectionSubView";
import "./CollectionFreeFormLinksView.scss";
import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView";
import React = require("react");
+import { Utils } from "../../../../Utils";
+import { SelectionManager } from "../../../util/SelectionManager";
+import { DocumentType } from "../../../documents/DocumentTypes";
@observer
-export class CollectionFreeFormLinksView extends React.Component<CollectionViewProps> {
+export class CollectionFreeFormLinksView extends React.Component {
_brushReactionDisposer?: IReactionDisposer;
componentDidMount() {
@@ -69,59 +68,33 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
// });
}
componentWillUnmount() {
- if (this._brushReactionDisposer) {
- this._brushReactionDisposer();
- }
+ this._brushReactionDisposer && this._brushReactionDisposer();
}
- documentAnchors(view: DocumentView) {
- let equalViews = [view];
- let containerDoc = FieldValue(Cast(view.props.Document.annotationOn, Doc));
- if (containerDoc) {
- equalViews = DocumentManager.Instance.getDocumentViews(containerDoc.proto!);
- }
- if (view.props.ContainingCollectionDoc) {
- let collid = view.props.ContainingCollectionDoc[Id];
- DocListCast(this.props.Document[this.props.fieldKey]).
- filter(child =>
- child[Id] === collid).map(view =>
- DocumentManager.Instance.getDocumentViews(view).map(view =>
- equalViews.push(view)));
- }
- return equalViews.filter(sv => sv.props.ContainingCollectionDoc === this.props.Document);
- }
-
@computed
get uniqueConnections() {
let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => {
- let srcViews = this.documentAnchors(connection.a);
- let targetViews = this.documentAnchors(connection.b);
-
- let possiblePairs: { a: Doc, b: Doc, }[] = [];
- srcViews.map(sv => targetViews.map(tv => possiblePairs.push({ a: sv.props.Document, b: tv.props.Document })));
- possiblePairs.map(possiblePair => {
- if (!drawnPairs.reduce((found, drawnPair) => {
- let match1 = (Doc.AreProtosEqual(possiblePair.a, drawnPair.a) && Doc.AreProtosEqual(possiblePair.b, drawnPair.b));
- let match2 = (Doc.AreProtosEqual(possiblePair.a, drawnPair.b) && Doc.AreProtosEqual(possiblePair.b, drawnPair.a));
- let match = match1 || match2;
- if (match && !drawnPair.l.reduce((found, link) => found || link[Id] === connection.l[Id], false)) {
- drawnPair.l.push(connection.l);
- }
- return match || found;
- }, false)) {
- drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] });
+ if (!drawnPairs.reduce((found, drawnPair) => {
+ let match1 = (connection.a === drawnPair.a && connection.b === drawnPair.b);
+ let match2 = (connection.a === drawnPair.b && connection.b === drawnPair.a);
+ let match = match1 || match2;
+ if (match && !drawnPair.l.reduce((found, link) => found || link[Id] === connection.l[Id], false)) {
+ drawnPair.l.push(connection.l);
}
- });
+ return match || found;
+ }, false)) {
+ drawnPairs.push({ a: connection.a, b: connection.b, l: [connection.l] });
+ }
return drawnPairs;
- }, [] as { a: Doc, b: Doc, l: Doc[] }[]);
- return connections.map(c => <CollectionFreeFormLinkView key={c.l.reduce((p, l) => p + l[Id], "")} A={c.a} B={c.b} LinkDocs={c.l}
- removeDocument={this.props.removeDocument} addDocument={this.props.addDocument} />);
+ }, [] as { a: DocumentView, b: DocumentView, l: Doc[] }[]);
+ return connections.filter(c => c.a.props.Document.type === DocumentType.LINK) // get rid of the filter to show links to documents in addition to document anchors
+ .map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l} />);
}
render() {
return (
<div className="collectionfreeformlinksview-container">
<svg className="collectionfreeformlinksview-svgCanvas">
- {this.uniqueConnections}
+ {SelectionManager.GetIsDragging() ? (null) : this.uniqueConnections}
</svg>
{this.props.children}
</div>
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index c4311fa52..bb1a12f88 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -46,12 +46,6 @@
border-radius: inherit;
box-sizing: border-box;
position: absolute;
- overflow: hidden;
-
- .marqueeView {
- overflow: hidden;
- }
-
top: 0;
left: 0;
width: 100%;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index eb40e0bcb..6e0f75bc1 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,6 +1,6 @@
import { library } from "@fortawesome/fontawesome-svg-core";
import { faEye } from "@fortawesome/free-regular-svg-icons";
-import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faPaintBrush, faTable, faUpload, faFileUpload } from "@fortawesome/free-solid-svg-icons";
+import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faFileUpload, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons";
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../../new_fields/Doc";
@@ -10,7 +10,7 @@ import { createSchema, makeInterface } from "../../../../new_fields/Schema";
import { ScriptField } from "../../../../new_fields/ScriptField";
import { BoolCast, Cast, DateCast, NumCast, StrCast } from "../../../../new_fields/Types";
import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
-import { aggregateBounds, emptyFunction, intersectRect, returnEmptyString, returnOne, Utils } from "../../../../Utils";
+import { aggregateBounds, emptyFunction, intersectRect, returnOne, Utils } from "../../../../Utils";
import { CognitiveServices } from "../../../cognitive_services/CognitiveServices";
import { DocServer } from "../../../DocServer";
import { Docs } from "../../../documents/Documents";
@@ -25,19 +25,17 @@ import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss"
import { ContextMenu } from "../../ContextMenu";
import { ContextMenuProps } from "../../ContextMenuItem";
import { InkingCanvas } from "../../InkingCanvas";
-import { CollectionFreeFormDocumentView, positionSchema } from "../../nodes/CollectionFreeFormDocumentView";
-import { DocumentContentsView } from "../../nodes/DocumentContentsView";
-import { documentSchema, DocumentViewProps } from "../../nodes/DocumentView";
+import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
+import { DocumentViewProps } from "../../nodes/DocumentView";
import { FormattedTextBox } from "../../nodes/FormattedTextBox";
import { pageSchema } from "../../nodes/ImageBox";
-import PDFMenu from "../../pdf/PDFMenu";
import { CollectionSubView } from "../CollectionSubView";
import { computePivotLayout, ViewDefResult } from "./CollectionFreeFormLayoutEngines";
-import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView";
import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors";
import "./CollectionFreeFormView.scss";
import { MarqueeView } from "./MarqueeView";
import React = require("react");
+import { documentSchema, positionSchema } from "../../../../new_fields/documentSchemas";
library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
@@ -51,6 +49,11 @@ export const panZoomSchema = createSchema({
isRuleProvider: "boolean",
fitToBox: "boolean",
panTransformType: "string",
+ scrollHeight: "number",
+ fitX: "number",
+ fitY: "number",
+ fitW: "number",
+ fitH: "number"
});
type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof documentSchema, typeof positionSchema, typeof pageSchema]>;
@@ -67,9 +70,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@computed get fitToContent() { return (this.props.fitToBox || this.Document.fitToBox) && !this.isAnnotationOverlay; }
@computed get parentScaling() { return this.props.ContentScaling && this.fitToContent && !this.isAnnotationOverlay ? this.props.ContentScaling() : 1; }
@computed get contentBounds() { return aggregateBounds(this.elements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!)); }
- @computed get nativeWidth() { return this.fitToContent ? 0 : this.Document.nativeWidth || 0; }
+ @computed get nativeWidth() { return this.Document.fitToContent ? 0 : this.Document.nativeWidth || 0; }
@computed get nativeHeight() { return this.fitToContent ? 0 : this.Document.nativeHeight || 0; }
- private get isAnnotationOverlay() { return this.props.fieldExt ? true : false; } // fieldExt will be "" or "annotation". should maybe generalize this, or make it more specific (ie, 'annotation' instead of 'fieldExt')
+ private get isAnnotationOverlay() { return this.props.isAnnotationOverlay; }
private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; }
private easing = () => this.props.Document.panTransformType === "Ease";
private panX = () => this.fitToContent ? (this.contentBounds.x + this.contentBounds.r) / 2 : this.Document.panX || 0;
@@ -93,10 +96,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
heading = !sorted.length ? Math.max(1, maxHeading) : NumCast(sorted[sorted.length - 1].heading) === 1 ? 2 : NumCast(sorted[sorted.length - 1].heading);
}
!this.Document.isRuleProvider && (newBox.heading = heading);
- this.addDocument(newBox, false);
+ this.addDocument(newBox);
}
- private addDocument = (newBox: Doc, allowDuplicates: boolean) => {
- let added = this.props.addDocument(newBox, false);
+ private addDocument = (newBox: Doc) => {
+ let added = this.props.addDocument(newBox);
added && this.bringToFront(newBox);
added && this.updateCluster(newBox);
return added;
@@ -105,16 +108,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
SelectionManager.DeselectAll();
docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).map(dv => dv && SelectionManager.SelectDoc(dv, true));
}
- public isCurrent(doc: Doc) { return !doc.isMinimized && (Math.abs(NumCast(doc.page, -1) - NumCast(this.Document.curPage, -1)) < 1.5 || NumCast(doc.page, -1) === -1); }
+ public isCurrent(doc: Doc) { return !doc.isMinimized && (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document.currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); }
public getActiveDocuments = () => {
return this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout);
}
- @computed get fieldExtensionDoc() {
- return Doc.fieldExtensionDoc(this.props.DataDoc || this.props.Document, this.props.fieldKey);
- }
-
@action
onDrop = (e: React.DragEvent): Promise<void> => {
var pt = this.getTransform().transformPoint(e.pageX, e.pageY);
@@ -131,21 +130,23 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
if (super.drop(e, de)) {
if (de.data instanceof DragManager.DocumentDragData) {
if (de.data.droppedDocuments.length) {
- let z = NumCast(de.data.droppedDocuments[0].z);
+ let firstDoc = de.data.droppedDocuments[0];
+ let z = NumCast(firstDoc.z);
let x = (z ? xpo : xp) - de.data.offset[0];
let y = (z ? ypo : yp) - de.data.offset[1];
- let dropX = NumCast(de.data.droppedDocuments[0].x);
- let dropY = NumCast(de.data.droppedDocuments[0].y);
+ let dropX = NumCast(firstDoc.x);
+ let dropY = NumCast(firstDoc.y);
de.data.droppedDocuments.forEach(action((d: Doc) => {
+ let layoutDoc = Doc.Layout(d);
d.x = x + NumCast(d.x) - dropX;
d.y = y + NumCast(d.y) - dropY;
- if (!NumCast(d.width)) {
- d.width = 300;
+ if (!NumCast(layoutDoc.width)) {
+ layoutDoc.width = 300;
}
- if (!NumCast(d.height)) {
- let nw = NumCast(d.nativeWidth);
- let nh = NumCast(d.nativeHeight);
- d.height = nw && nh ? nh / nw * NumCast(d.width) : 300;
+ if (!NumCast(layoutDoc.height)) {
+ let nw = NumCast(layoutDoc.nativeWidth);
+ let nh = NumCast(layoutDoc.nativeHeight);
+ layoutDoc.height = nw && nh ? nh / nw * NumCast(layoutDoc.width) : 300;
}
this.bringToFront(d);
}));
@@ -158,12 +159,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
let dragDoc = de.data.dropDocument;
let x = xp - de.data.offset[0];
let y = yp - de.data.offset[1];
- let dropX = NumCast(de.data.dropDocument.x);
- let dropY = NumCast(de.data.dropDocument.y);
+ let dropX = NumCast(dragDoc.x);
+ let dropY = NumCast(dragDoc.y);
dragDoc.x = x + NumCast(dragDoc.x) - dropX;
dragDoc.y = y + NumCast(dragDoc.y) - dropY;
- de.data.targetContext = this.props.Document;
- dragDoc.targetContext = this.props.Document;
+ de.data.targetContext = this.props.Document; // dropped a PDF annotation, so we need to set the targetContext on the dragData which the PDF view uses at the end of the drop operation
this.bringToFront(dragDoc);
}
}
@@ -173,11 +173,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
pickCluster(probe: number[]) {
return this.childLayoutPairs.map(pair => pair.layout).reduce((cluster, cd) => {
+ let layoutDoc = Doc.Layout(cd);
let cx = NumCast(cd.x) - this._clusterDistance;
let cy = NumCast(cd.y) - this._clusterDistance;
- let cw = NumCast(cd.width) + 2 * this._clusterDistance;
- let ch = NumCast(cd.height) + 2 * this._clusterDistance;
- return !cd.z && intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 }) ?
+ let cw = NumCast(layoutDoc.width) + 2 * this._clusterDistance;
+ let ch = NumCast(layoutDoc.height) + 2 * this._clusterDistance;
+ return !layoutDoc.z && intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 }) ?
NumCast(cd.cluster) : cluster;
}, -1);
}
@@ -185,14 +186,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
let cluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY));
if (cluster !== -1) {
let eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => NumCast(cd.cluster) === cluster);
-
- // hacky way to get a list of DocumentViews in the current view given a list of Documents in the current view
- let prevSelected = SelectionManager.SelectedDocuments();
- this.selectDocuments(eles);
- let clusterDocs = SelectionManager.SelectedDocuments();
- SelectionManager.DeselectAll();
- prevSelected.map(dv => SelectionManager.SelectDoc(dv, true));
-
+ let clusterDocs = eles.map(ele => DocumentManager.Instance.getDocumentView(ele, this.props.CollectionView)!);
let de = new DragManager.DocumentDragData(eles);
de.moveDocument = this.props.moveDocument;
const [left, top] = clusterDocs[0].props.ScreenToLocalTransform().scale(clusterDocs[0].props.ContentScaling()).inverse().transformPoint(0, 0);
@@ -267,8 +261,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
onPointerDown = (e: React.PointerEvent): void => {
+ if (e.nativeEvent.cancelBubble) return;
this._hitCluster = this.props.Document.useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false;
- if (e.button === 0 && !e.shiftKey && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1) && this.props.active()) {
+ if (e.button === 0 && !e.shiftKey && !e.altKey && !e.ctrlKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1) && this.props.active()) {
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
document.addEventListener("pointermove", this.onPointerMove);
@@ -285,7 +280,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
onPointerMove = (e: PointerEvent): void => {
- if (!e.cancelBubble && !this.isAnnotationOverlay) {
+ if (!e.cancelBubble) {
if (this._hitCluster && this.tryDragCluster(e)) {
e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
e.preventDefault();
@@ -298,20 +293,20 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
let docs = this.childLayoutPairs.map(pair => pair.layout);
let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
if (!this.isAnnotationOverlay) {
- PDFMenu.Instance.fadeOut(true);
let minx = docs.length ? NumCast(docs[0].x) : 0;
- let maxx = docs.length ? NumCast(docs[0].width) + minx : minx;
+ let maxx = docs.length ? NumCast(Doc.Layout(docs[0]).width) + minx : minx;
let miny = docs.length ? NumCast(docs[0].y) : 0;
- let maxy = docs.length ? NumCast(docs[0].height) + miny : miny;
+ let maxy = docs.length ? NumCast(Doc.Layout(docs[0]).height) + miny : miny;
let ranges = docs.filter(doc => doc).reduce((range, doc) => {
+ let layoutDoc = Doc.Layout(doc);
let x = NumCast(doc.x);
- let xe = x + NumCast(doc.width);
+ let xe = x + NumCast(layoutDoc.width);
let y = NumCast(doc.y);
- let ye = y + NumCast(doc.height);
+ let ye = y + NumCast(layoutDoc.height);
return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]],
[range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]];
}, [[minx, maxx], [miny, maxy]]);
- let ink = Cast(this.fieldExtensionDoc.ink, InkField);
+ let ink = this.extensionDoc && Cast(this.extensionDoc.ink, InkField);
if (ink && ink.inkData) {
ink.inkData.forEach((value: StrokeData, key: string) => {
let bounds = InkingCanvas.StrokeRect(value);
@@ -338,7 +333,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
onPointerWheel = (e: React.WheelEvent): void => {
- if (this.props.Document.lockedPosition || this.isAnnotationOverlay) return;
+ if (this.props.Document.lockedPosition || this.props.Document.inOverlay) return;
if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) { // things that can scroll vertically should do that instead of zooming
e.stopPropagation();
}
@@ -359,14 +354,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
@action
- setPan(panX: number, panY: number) {
- if (!this.props.Document.lockedPosition) {
- this.props.Document.panTransformType = "None";
+ setPan(panX: number, panY: number, panType: string = "None") {
+ if (!this.Document.lockedPosition || this.Document.inOverlay) {
+ this.Document.panTransformType = panType;
var scale = this.getLocalTransform().inverse().Scale;
const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX));
- const newPanY = Math.min((this.props.Document.scrollHeight !== undefined ? NumCast(this.props.Document.scrollHeight) : (1 - 1 / scale) * this.nativeHeight), Math.max(0, panY));
- this.props.Document.panX = this.isAnnotationOverlay ? newPanX : panX;
- this.props.Document.panY = this.isAnnotationOverlay ? newPanY : panY;
+ const newPanY = Math.min((this.props.Document.scrollHeight !== undefined ? NumCast(this.Document.scrollHeight) : (1 - 1 / scale) * this.nativeHeight), Math.max(0, panY));
+ this.Document.panX = this.isAnnotationOverlay ? newPanX : panX;
+ this.Document.panY = this.isAnnotationOverlay ? newPanY : panY;
}
}
@@ -403,21 +398,27 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
SelectionManager.DeselectAll();
if (this.props.Document.scrollHeight) {
let annotOn = Cast(doc.annotationOn, Doc) as Doc;
- let offset = annotOn && (NumCast(annotOn.height) / 2 * 72 / 96);
- this.props.Document.scrollY = NumCast(doc.y) - offset;
+ if (!annotOn) {
+ this.props.focus(doc);
+ } else {
+ let contextHgt = Doc.AreProtosEqual(annotOn, this.props.Document) && this.props.VisibleHeight ? this.props.VisibleHeight() : NumCast(annotOn.height);
+ let offset = annotOn && (contextHgt / 2 * 96 / 72);
+ this.props.Document.scrollY = NumCast(doc.y) - offset;
+ }
} else {
- const newPanX = NumCast(doc.x) + NumCast(doc.width) / 2;
- const newPanY = NumCast(doc.y) + NumCast(doc.height) / 2;
+ let layoutdoc = Doc.Layout(doc);
+ const newPanX = NumCast(doc.x) + NumCast(layoutdoc.width) / 2;
+ const newPanY = NumCast(doc.y) + NumCast(layoutdoc.height) / 2;
const newState = HistoryUtil.getState();
newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY };
HistoryUtil.pushState(newState);
let savedState = { px: this.Document.panX, py: this.Document.panY, s: this.Document.scale, pt: this.Document.panTransformType };
- this.setPan(newPanX, newPanY);
- this.Document.panTransformType = "Ease";
+ this.setPan(newPanX, newPanY, "Ease");
+ Doc.BrushDoc(this.props.Document);
this.props.focus(this.props.Document);
- willZoom && this.setScaleToZoom(doc, scale);
+ willZoom && this.setScaleToZoom(layoutdoc, scale);
afterFocus && setTimeout(() => {
if (afterFocus && afterFocus()) {
@@ -446,6 +447,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
...this.props,
DataDoc: childData,
Document: childLayout,
+ layoutKey: undefined,
ruleProvider: this.Document.isRuleProvider && childLayout.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider, //bcz: hack! - currently ruleProviders apply to documents in nested colleciton, not direct children of themselves
onClick: undefined, // this.props.onClick, // bcz: check this out -- I don't think we want to inherit click handlers, or we at least need a way to ignore them
ScreenToLocalTransform: childLayout.z ? this.getTransformOverlay : this.getTransform,
@@ -463,30 +465,15 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
getScale: this.getScale
};
}
- getDocumentViewProps(layoutDoc: Doc): DocumentViewProps {
- return {
- ...this.props,
- ScreenToLocalTransform: this.getTransform,
- PanelWidth: layoutDoc[WidthSym],
- PanelHeight: layoutDoc[HeightSym],
- ContentScaling: returnOne,
- ContainingCollectionView: this.props.CollectionView,
- focus: this.focusDocument,
- backgroundColor: returnEmptyString,
- parentActive: this.props.active,
- bringToFront: this.bringToFront,
- zoomToScale: this.zoomToScale,
- getScale: this.getScale
- };
- }
getCalculatedPositions(params: { doc: Doc, index: number, collection: Doc, docs: Doc[], state: any }): { x?: number, y?: number, z?: number, width?: number, height?: number, transition?: string, state?: any } {
const script = this.Document.arrangeScript;
const result = script && script.script.run(params, console.log);
+ const layoutDoc = Doc.Layout(params.doc);
if (result && result.success) {
return { ...result, transition: "transform 1s" };
}
- return { x: Cast(params.doc.x, "number"), y: Cast(params.doc.y, "number"), z: Cast(params.doc.z, "number"), width: Cast(params.doc.width, "number"), height: Cast(params.doc.height, "number") };
+ return { x: Cast(params.doc.x, "number"), y: Cast(params.doc.y, "number"), z: Cast(params.doc.z, "number"), width: Cast(layoutDoc.width, "number"), height: Cast(layoutDoc.height, "number") };
}
viewDefsToJSX = (views: any[]) => {
@@ -603,7 +590,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
// find rule colorations when rule providing is turned on by looking at each document to see if it has a coloring -- if so, use it's color as the rule for its associated heading.
this.Document.isRuleProvider && this.childLayoutPairs.map(pair =>
// iterate over the children of a displayed document (or if the displayed document is a template, iterate over the children of that template)
- DocListCast(pair.layout.layout instanceof Doc ? pair.layout.layout.data : pair.layout.data).map(heading => {
+ DocListCast(Doc.Layout(pair.layout).data).map(heading => {
let headingPair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, heading);
let headingLayout = headingPair.layout && (pair.layout.data_ext instanceof Doc) && (pair.layout.data_ext[`Layout[${headingPair.layout[Id]}]`] as Doc) || headingPair.layout;
if (headingLayout && NumCast(headingLayout.heading) > 0 && headingLayout.backgroundColor !== headingLayout.defaultBackgroundColor) {
@@ -614,16 +601,17 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
analyzeStrokes = async () => {
- let data = Cast(this.fieldExtensionDoc.ink, InkField);
- if (data) {
- CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.fieldExtensionDoc, ["inkAnalysis", "handwriting"], data.inkData);
+ const extensionDoc = this.extensionDoc;
+ let data = extensionDoc && Cast(extensionDoc.ink, InkField);
+ if (data && extensionDoc) {
+ CognitiveServices.Inking.Appliers.ConcatenateHandwriting(extensionDoc, ["inkAnalysis", "handwriting"], data.inkData);
}
}
onContextMenu = (e: React.MouseEvent) => {
let layoutItems: ContextMenuProps[] = [];
- if (this.childDocs.some(d => BoolCast(d.isTemplate))) {
+ if (this.childDocs.some(d => BoolCast(d.isTemplateDoc))) {
layoutItems.push({ description: "Template Layout Instance", event: () => this.props.addDocTab(Doc.ApplyTemplate(this.props.Document)!, undefined, "onRight"), icon: "project-diagram" });
}
layoutItems.push({ description: "reset view", event: () => { this.props.Document.panX = this.props.Document.panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" });
@@ -652,7 +640,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
if (doc instanceof Doc) {
const [xx, yy] = this.props.ScreenToLocalTransform().transformPoint(x, y);
doc.x = xx, doc.y = yy;
- this.props.addDocument && this.props.addDocument(doc, false);
+ this.props.addDocument && this.props.addDocument(doc);
}
}
}
@@ -674,57 +662,38 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
- private childViews = () => [
- <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />,
- ...this.views
- ]
+ private childViews = () => {
+ let children = typeof this.props.children === "function" ? (this.props.children as any)() as JSX.Element[] : [];
+ return [
+ ...children,
+ ...this.views,
+ ];
+ }
render() {
// update the actual dimensions of the collection so that they can inquired (e.g., by a minimap)
- this.props.Document.fitX = this.contentBounds && this.contentBounds.x;
- this.props.Document.fitY = this.contentBounds && this.contentBounds.y;
- this.props.Document.fitW = this.contentBounds && (this.contentBounds.r - this.contentBounds.x);
- this.props.Document.fitH = this.contentBounds && (this.contentBounds.b - this.contentBounds.y);
- // if fieldExt is set, then children will be stored in the extension document for the fieldKey.
+ this.Document.fitX = this.contentBounds && this.contentBounds.x;
+ this.Document.fitY = this.contentBounds && this.contentBounds.y;
+ this.Document.fitW = this.contentBounds && (this.contentBounds.r - this.contentBounds.x);
+ this.Document.fitH = this.contentBounds && (this.contentBounds.b - this.contentBounds.y);
+ // if isAnnotationOverlay is set, then children will be stored in the extension document for the fieldKey.
// otherwise, they are stored in fieldKey. All annotations to this document are stored in the extension document
- Doc.UpdateDocumentExtensionForField(this.props.DataDoc || this.props.Document, this.props.fieldKey);
- return (
+ return !this.extensionDoc ? (null) :
<div className={"collectionfreeformview-container"} ref={this.createDropTarget} onWheel={this.onPointerWheel}
- style={{ pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, height: this.isAnnotationOverlay ? (NumCast(this.props.Document.scrollHeight) ? NumCast(this.props.Document.scrollHeight) : "100%") : this.props.PanelHeight() }}
+ style={{ pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, height: this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() }}
onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu}>
- <MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} isSelected={this.props.isSelected}
- addDocument={this.addDocument} removeDocument={this.props.removeDocument} addLiveTextDocument={this.addLiveTextBox} setPreviewCursor={this.props.setPreviewCursor}
- getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}>
+ <MarqueeView {...this.props} extensionDoc={this.extensionDoc} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} addDocument={this.addDocument}
+ addLiveTextDocument={this.addLiveTextBox} getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}>
<CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY}
easing={this.easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
- <CollectionFreeFormLinksView {...this.props} key="freeformLinks">
- <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} AnnotationDocument={this.fieldExtensionDoc} inkFieldKey={"ink"} >
+ {!this.extensionDoc ? (null) :
+ <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} AnnotationDocument={this.extensionDoc} inkFieldKey={"ink"} >
{this.childViews}
- </InkingCanvas>
- </CollectionFreeFormLinksView>
+ </InkingCanvas>}
<CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />
</CollectionFreeFormViewPannableContents>
</MarqueeView>
{this.overlayViews}
- <CollectionFreeFormOverlayView {...this.props} {...this.getDocumentViewProps(this.props.Document)} />
- </div>
- );
- }
-}
-
-@observer
-class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> {
- render() {
- return <DocumentContentsView {...this.props} layoutKey={"overlayLayout"}
- renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />;
- }
-}
-
-@observer
-class CollectionFreeFormBackgroundView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> {
- render() {
- return !this.props.Document.backgroundLayout ? (null) :
- (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"}
- renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />);
+ </div>;
}
}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
index 9fc2e44fb..04f6ec2ad 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
@@ -6,6 +6,13 @@
width:100%;
height:100%;
}
+.marqueeView {
+ overflow: hidden;
+}
+
+.marqueeView:focus-within {
+ overflow: hidden;
+}
.marquee {
border-style: dashed;
box-sizing: border-box;
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 82193aefa..44b6fe030 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -15,27 +15,28 @@ import { Transform } from "../../../util/Transform";
import { undoBatch } from "../../../util/UndoManager";
import { InkingCanvas } from "../../InkingCanvas";
import { PreviewCursor } from "../../PreviewCursor";
-import { CollectionViewType } from "../CollectionBaseView";
+import { CollectionViewType } from "../CollectionView";
import { CollectionFreeFormView } from "./CollectionFreeFormView";
import "./MarqueeView.scss";
import React = require("react");
+import { SubCollectionViewProps } from "../CollectionSubView";
interface MarqueeViewProps {
getContainerTransform: () => Transform;
getTransform: () => Transform;
- container: CollectionFreeFormView;
- addDocument: (doc: Doc, allowDuplicates: false) => boolean;
+ addDocument: (doc: Doc) => boolean;
activeDocuments: () => Doc[];
selectDocuments: (docs: Doc[]) => void;
removeDocument: (doc: Doc) => boolean;
addLiveTextDocument: (doc: Doc) => void;
isSelected: () => boolean;
- isAnnotationOverlay: boolean;
+ extensionDoc: Doc;
+ isAnnotationOverlay?: boolean;
setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void;
}
@observer
-export class MarqueeView extends React.Component<MarqueeViewProps>
+export class MarqueeView extends React.Component<SubCollectionViewProps & MarqueeViewProps>
{
private _mainCont = React.createRef<HTMLDivElement>();
@observable _lastX: number = 0;
@@ -83,7 +84,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
ns.map(line => {
let indent = line.search(/\S|$/);
let newBox = Docs.Create.TextDocument({ width: 200, height: 35, x: x + indent / 3 * 10, y: y, documentText: "@@@" + line, title: line });
- this.props.addDocument(newBox, false);
+ this.props.addDocument(newBox);
y += 40 * this.props.getTransform().Scale;
});
})();
@@ -92,7 +93,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
navigator.clipboard.readText().then(text => {
let ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== "");
if (ns.length === 1 && text.startsWith("http")) {
- this.props.addDocument(Docs.Create.ImageDocument(text, { nativeWidth: 300, width: 300, x: x, y: y }), false);// paste an image from its URL in the paste buffer
+ this.props.addDocument(Docs.Create.ImageDocument(text, { nativeWidth: 300, width: 300, x: x, y: y }));// paste an image from its URL in the paste buffer
} else {
this.pasteTable(ns, x, y);
}
@@ -146,7 +147,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
}
let newCol = Docs.Create.SchemaDocument([...(groupAttr ? [new SchemaHeaderField("_group", "#f1efeb")] : []), ...columns.filter(c => c).map(c => new SchemaHeaderField(c, "#f1efeb"))], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 });
- this.props.addDocument(newCol, false);
+ this.props.addDocument(newCol);
}
}
@action
@@ -187,13 +188,13 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
@action
onPointerUp = (e: PointerEvent): void => {
- if (!this.props.container.props.active()) this.props.selectDocuments([this.props.container.props.Document]);
+ if (!this.props.active()) this.props.selectDocuments([this.props.Document]);
if (this._visible) {
let mselect = this.marqueeSelect();
if (!e.shiftKey) {
- SelectionManager.DeselectAll(mselect.length ? undefined : this.props.container.props.Document);
+ SelectionManager.DeselectAll(mselect.length ? undefined : this.props.Document);
}
- this.props.selectDocuments(mselect.length ? mselect : [this.props.container.props.Document]);
+ this.props.selectDocuments(mselect.length ? mselect : [this.props.Document]);
}
this.cleanupInteractions(true);
@@ -202,7 +203,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
}
}
- setPreviewCursor = (x: number, y: number, drag: boolean) => {
+ setPreviewCursor = action((x: number, y: number, drag: boolean) => {
if (drag) {
this._downX = this._lastX = x;
this._downY = this._lastY = y;
@@ -217,7 +218,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
this._downY = y;
PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument);
}
- }
+ });
@action
onClick = (e: React.MouseEvent): void => {
@@ -246,13 +247,11 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
}
get ink() { // ink will be stored on the extension doc for the field (fieldKey) where the container's data is stored.
- let cprops = this.props.container.props;
- return Cast(Doc.fieldExtensionDoc(cprops.Document, cprops.fieldKey).ink, InkField);
+ return this.props.extensionDoc && Cast(this.props.extensionDoc.ink, InkField);
}
set ink(value: InkField | undefined) {
- let cprops = this.props.container.props;
- Doc.fieldExtensionDoc(cprops.Document, cprops.fieldKey).ink = value;
+ this.props.extensionDoc && (this.props.extensionDoc.ink = value);
}
@undoBatch
@@ -285,18 +284,18 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
this.props.removeDocument(d);
d.x = NumCast(d.x) - bounds.left - bounds.width / 2;
d.y = NumCast(d.y) - bounds.top - bounds.height / 2;
- d.page = -1;
+ d.displayTimecode = undefined;
return d;
});
}
let defaultPalette = ["rgb(114,229,239)", "rgb(255,246,209)", "rgb(255,188,156)", "rgb(247,220,96)", "rgb(122,176,238)",
"rgb(209,150,226)", "rgb(127,235,144)", "rgb(252,188,189)", "rgb(247,175,81)",];
- let colorPalette = Cast(this.props.container.props.Document.colorPalette, listSpec("string"));
- if (!colorPalette) this.props.container.props.Document.colorPalette = new List<string>(defaultPalette);
- let palette = Array.from(Cast(this.props.container.props.Document.colorPalette, listSpec("string")) as string[]);
+ let colorPalette = Cast(this.props.Document.colorPalette, listSpec("string"));
+ if (!colorPalette) this.props.Document.colorPalette = new List<string>(defaultPalette);
+ let palette = Array.from(Cast(this.props.Document.colorPalette, listSpec("string")) as string[]);
let usedPaletted = new Map<string, number>();
- [...this.props.activeDocuments(), this.props.container.props.Document].map(child => {
- let bg = StrCast(child.layout instanceof Doc ? child.layout.backgroundColor : child.backgroundColor);
+ [...this.props.activeDocuments(), this.props.Document].map(child => {
+ let bg = StrCast(Doc.Layout(child).backgroundColor);
if (palette.indexOf(bg) !== -1) {
palette.splice(palette.indexOf(bg), 1);
if (usedPaletted.get(bg)) usedPaletted.set(bg, usedPaletted.get(bg)! + 1);
@@ -350,7 +349,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
}
}
else {
- this.props.addDocument(newCollection, false);
+ this.props.addDocument(newCollection);
this.props.selectDocuments([newCollection]);
}
this.cleanupInteractions(false);
@@ -394,20 +393,22 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
let selRect = this.Bounds;
let selection: Doc[] = [];
this.props.activeDocuments().filter(doc => !doc.isBackground && doc.z === undefined).map(doc => {
+ let layoutDoc = Doc.Layout(doc);
var x = NumCast(doc.x);
var y = NumCast(doc.y);
- var w = NumCast(doc.width);
- var h = NumCast(doc.height);
+ var w = NumCast(layoutDoc.width);
+ var h = NumCast(layoutDoc.height);
if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) {
selection.push(doc);
}
});
if (!selection.length && selectBackgrounds) {
this.props.activeDocuments().filter(doc => doc.z === undefined).map(doc => {
+ let layoutDoc = Doc.Layout(doc);
var x = NumCast(doc.x);
var y = NumCast(doc.y);
- var w = NumCast(doc.width);
- var h = NumCast(doc.height);
+ var w = NumCast(layoutDoc.width);
+ var h = NumCast(layoutDoc.height);
if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) {
selection.push(doc);
}
@@ -420,10 +421,11 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
let size = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
let otherBounds = { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) };
this.props.activeDocuments().filter(doc => doc.z !== undefined).map(doc => {
+ let layoutDoc = Doc.Layout(doc);
var x = NumCast(doc.x);
var y = NumCast(doc.y);
- var w = NumCast(doc.width);
- var h = NumCast(doc.height);
+ var w = NumCast(layoutDoc.width);
+ var h = NumCast(layoutDoc.height);
if (this.intersectRect({ left: x, top: y, width: w, height: h }, otherBounds)) {
selection.push(doc);
}
@@ -434,21 +436,17 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
@computed
get marqueeDiv() {
+ let p: [number, number] = this._visible ? this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY) : [0, 0];
let v = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
- return <div className="marquee" style={{ width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}`, zIndex: 2000 }} >
+ return <div className="marquee" style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}`, zIndex: 2000 }} >
<span className="marquee-legend" />
</div>;
}
render() {
- let p: [number, number] = this._visible ? this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY) : [0, 0];
- return <div className="marqueeView" style={{ borderRadius: "inherit" }} onClick={this.onClick} onPointerDown={this.onPointerDown}>
- <div style={{ position: "relative", transform: `translate(${p[0]}px, ${p[1]}px)` }} >
- {this._visible ? this.marqueeDiv : null}
- <div ref={this._mainCont} style={{ transform: `translate(${-p[0]}px, ${-p[1]}px)` }} >
- {this.props.children}
- </div>
- </div>
+ return <div className="marqueeView" onScroll={(e) => e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0} style={{ borderRadius: "inherit" }} onClick={this.onClick} onPointerDown={this.onPointerDown}>
+ {this._visible ? this.marqueeDiv : null}
+ {this.props.children}
</div>;
}
} \ No newline at end of file
diff --git a/src/client/views/linking/LinkFollowBox.tsx b/src/client/views/linking/LinkFollowBox.tsx
index 72fff8e53..efe2c7f2a 100644
--- a/src/client/views/linking/LinkFollowBox.tsx
+++ b/src/client/views/linking/LinkFollowBox.tsx
@@ -5,7 +5,7 @@ import { FieldViewProps, FieldView } from "../nodes/FieldView";
import { Doc, DocListCastAsync, Opt } from "../../../new_fields/Doc";
import { undoBatch } from "../../util/UndoManager";
import { NumCast, FieldValue, Cast, StrCast } from "../../../new_fields/Types";
-import { CollectionViewType } from "../collections/CollectionBaseView";
+import { CollectionViewType } from "../collections/CollectionView";
import { CollectionDockingView } from "../collections/CollectionDockingView";
import { SelectionManager } from "../../util/SelectionManager";
import { DocumentManager } from "../../util/DocumentManager";
@@ -37,7 +37,7 @@ enum FollowOptions {
@observer
export class LinkFollowBox extends React.Component<FieldViewProps> {
- public static LayoutString() { return FieldView.LayoutString(LinkFollowBox); }
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(LinkFollowBox, fieldKey); }
public static Instance: LinkFollowBox | undefined;
@observable static linkDoc: Doc | undefined = undefined;
@observable static destinationDoc: Doc | undefined = undefined;
@@ -128,7 +128,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
runInAction(async () => {
this._docs = docs.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance.props.Document)).map(doc => ({ col: doc, target: dest }));
this._otherDocs = Array.from(map.entries()).filter(entry => !Doc.AreProtosEqual(entry[0], CollectionDockingView.Instance.props.Document)).map(([col, target]) => ({ col, target }));
- let tcontext = LinkFollowBox.linkDoc && (await Cast(LinkFollowBox.linkDoc.targetContext, Doc)) as Doc;
+ let tcontext = LinkFollowBox.linkDoc && (await Cast(LinkFollowBox.linkDoc.anchor2Context, Doc)) as Doc;
runInAction(() => tcontext && this._docs.splice(0, 0, { col: tcontext, target: dest }));
});
}
@@ -152,26 +152,12 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
this.resetPan();
}
- unhighlight = () => {
- Doc.UnhighlightAll();
- document.removeEventListener("pointerdown", this.unhighlight);
- }
-
- @action
- highlightDoc = () => {
- if (LinkFollowBox.destinationDoc) {
- document.removeEventListener("pointerdown", this.unhighlight);
- Doc.HighlightDoc(LinkFollowBox.destinationDoc);
- window.setTimeout(() => {
- document.addEventListener("pointerdown", this.unhighlight);
- }, 10000);
- }
- }
+ highlightDoc = () => LinkFollowBox.destinationDoc && Doc.linkFollowHighlight(LinkFollowBox.destinationDoc);
@undoBatch
openFullScreen = () => {
if (LinkFollowBox.destinationDoc) {
- let view: DocumentView | null = DocumentManager.Instance.getDocumentView(LinkFollowBox.destinationDoc);
+ let view = DocumentManager.Instance.getDocumentView(LinkFollowBox.destinationDoc);
view && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(view);
}
}
@@ -235,45 +221,11 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
@undoBatch
jumpToLink = async (options: { shouldZoom: boolean }) => {
- if (LinkFollowBox.destinationDoc && LinkFollowBox.linkDoc) {
- let jumpToDoc: Doc = LinkFollowBox.destinationDoc;
- let pdfDoc = FieldValue(Cast(LinkFollowBox.destinationDoc, Doc));
- if (pdfDoc) {
- jumpToDoc = pdfDoc;
- }
- let proto = Doc.GetProto(LinkFollowBox.linkDoc);
- let targetContext = await Cast(proto.targetContext, Doc);
- let sourceContext = await Cast(proto.sourceContext, Doc);
- let guid = StrCast(LinkFollowBox.linkDoc[Id]);
- const shouldZoom = options ? options.shouldZoom : false;
-
- let dockingFunc = (document: Doc) => { (LinkFollowBox._addDocTab || this.props.addDocTab)(document, undefined, "inTab"); SelectionManager.DeselectAll(); };
-
- if (LinkFollowBox.destinationDoc === LinkFollowBox.linkDoc.anchor2 && targetContext) {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, false, async document => dockingFunc(document), undefined, targetContext);
- }
- else if (LinkFollowBox.destinationDoc === LinkFollowBox.linkDoc.anchor1 && sourceContext) {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, false, document => dockingFunc(sourceContext!));
- if (LinkFollowBox.sourceDoc && LinkFollowBox.destinationDoc) {
- if (guid) {
- let views = DocumentManager.Instance.getDocumentViews(jumpToDoc);
- views.length && (views[0].props.Document.scrollToLinkID = guid);
- } else {
- jumpToDoc.linkHref = Utils.prepend("/doc/" + StrCast(LinkFollowBox.linkDoc[Id]));
- }
- }
- }
- else if (DocumentManager.Instance.getDocumentView(jumpToDoc)) {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, undefined, undefined,
- NumCast((LinkFollowBox.destinationDoc === LinkFollowBox.linkDoc.anchor2 ? LinkFollowBox.linkDoc.anchor2Page : LinkFollowBox.linkDoc.anchor1Page)));
-
- }
- else {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, false, dockingFunc);
- }
+ if (LinkFollowBox.sourceDoc && LinkFollowBox.linkDoc) {
+ let focus = (document: Doc) => { (LinkFollowBox._addDocTab || this.props.addDocTab)(document, undefined, "inTab"); SelectionManager.DeselectAll(); };
+ //let focus = (doc: Doc, maxLocation: string) => this.props.focus(docthis.props.focus(LinkFollowBox.destinationDoc, true, 1, () => this.props.addDocTab(doc, undefined, maxLocation));
- this.highlightDoc();
- SelectionManager.DeselectAll();
+ DocumentManager.Instance.FollowLink(LinkFollowBox.linkDoc, LinkFollowBox.sourceDoc, focus, options && options.shouldZoom, false, undefined);
}
}
@@ -311,20 +263,23 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
openLinkInPlace = (options: { shouldZoom: boolean }) => {
if (LinkFollowBox.destinationDoc && LinkFollowBox.sourceDoc) {
- let alias = Doc.MakeAlias(LinkFollowBox.destinationDoc);
- let y = NumCast(LinkFollowBox.sourceDoc.y);
- let x = NumCast(LinkFollowBox.sourceDoc.x);
+ if (this.sourceView && this.sourceView.props.addDocument) {
+ let destViews = DocumentManager.Instance.getDocumentViews(LinkFollowBox.destinationDoc);
+ if (!destViews.find(dv => dv.props.ContainingCollectionView === this.sourceView!.props.ContainingCollectionView)) {
+ let alias = Doc.MakeAlias(LinkFollowBox.destinationDoc);
+ let y = NumCast(LinkFollowBox.sourceDoc.y);
+ let x = NumCast(LinkFollowBox.sourceDoc.x);
- let width = NumCast(LinkFollowBox.sourceDoc.width);
- let height = NumCast(LinkFollowBox.sourceDoc.height);
+ let width = NumCast(LinkFollowBox.sourceDoc.width);
+ let height = NumCast(LinkFollowBox.sourceDoc.height);
- alias.x = x + width + 30;
- alias.y = y;
- alias.width = width;
- alias.height = height;
+ alias.x = x + width + 30;
+ alias.y = y;
+ alias.width = width;
+ alias.height = height;
- if (this.sourceView && this.sourceView.props.addDocument) {
- this.sourceView.props.addDocument(alias, false);
+ this.sourceView.props.addDocument(alias);
+ }
}
this.jumpToLink({ shouldZoom: options.shouldZoom });
diff --git a/src/client/views/nodes/Annotation.tsx b/src/client/views/nodes/Annotation.tsx
deleted file mode 100644
index 3e4ed6bf1..000000000
--- a/src/client/views/nodes/Annotation.tsx
+++ /dev/null
@@ -1,117 +0,0 @@
-import "./ImageBox.scss";
-import React = require("react");
-import { observer } from "mobx-react";
-import { observable, action } from 'mobx';
-import 'react-pdf/dist/Page/AnnotationLayer.css';
-
-interface IProps {
- Span: HTMLSpanElement;
- X: number;
- Y: number;
- Highlights: any[];
- Annotations: any[];
- CurrAnno: any[];
-
-}
-
-/**
- * Annotation class is used to take notes on a particular highlight. You can also change highlighted span's color
- * Improvements to be made: Removing the annotation when onRemove is called. (Removing this, not just the highlighted span).
- * Also need to support multiline highlighting
- *
- * Written by: Andrew Kim
- */
-@observer
-export class Annotation extends React.Component<IProps> {
-
- /**
- * changes color of the span (highlighted section)
- */
- onColorChange = (e: React.PointerEvent) => {
- if (e.currentTarget.innerHTML === "r") {
- this.props.Span.style.backgroundColor = "rgba(255,0,0, 0.3)";
- } else if (e.currentTarget.innerHTML === "b") {
- this.props.Span.style.backgroundColor = "rgba(0,255, 255, 0.3)";
- } else if (e.currentTarget.innerHTML === "y") {
- this.props.Span.style.backgroundColor = "rgba(255,255,0, 0.3)";
- } else if (e.currentTarget.innerHTML === "g") {
- this.props.Span.style.backgroundColor = "rgba(76, 175, 80, 0.3)";
- }
-
- }
-
- /**
- * removes the highlighted span. Supposed to remove Annotation too, but I don't know how to unmount this
- */
- @action
- onRemove = (e: any) => {
- let index: number = -1;
- //finding the highlight in the highlight array
- this.props.Highlights.forEach((e) => {
- for (const span of e.spans) {
- if (span === this.props.Span) {
- index = this.props.Highlights.indexOf(e);
- this.props.Highlights.splice(index, 1);
- }
- }
- });
-
- //removing from CurrAnno and Annotation array
- this.props.Annotations.splice(index, 1);
- this.props.CurrAnno.pop();
-
- //removing span from div
- if (this.props.Span.parentElement) {
- let nodesArray = this.props.Span.parentElement.childNodes;
- nodesArray.forEach((e) => {
- if (e === this.props.Span) {
- if (this.props.Span.parentElement) {
- this.props.Highlights.forEach((item) => {
- if (item === e) {
- item.remove();
- }
- });
- e.remove();
- }
- }
- });
- }
-
-
- }
-
- render() {
- return (
- <div
- style={{
- position: "absolute",
- top: "20px",
- left: "0px",
- zIndex: 1,
- transform: `translate(${this.props.X}px, ${this.props.Y}px)`,
-
- }}>
- <div style={{ width: "200px", height: "50px", backgroundColor: "orange" }}>
- <button
- style={{ borderRadius: "25px", width: "25%", height: "100%" }}
- onClick={this.onRemove}
- >x</button>
- <div style={{ width: "75%", height: "100%", display: "inline-block" }}>
- <button onPointerDown={this.onColorChange} style={{ backgroundColor: "red", borderRadius: "50%", color: "transparent" }}>r</button>
- <button onPointerDown={this.onColorChange} style={{ backgroundColor: "blue", borderRadius: "50%", color: "transparent" }}>b</button>
- <button onPointerDown={this.onColorChange} style={{ backgroundColor: "yellow", borderRadius: "50%", color: "transparent" }}>y</button>
- <button onPointerDown={this.onColorChange} style={{ backgroundColor: "green", borderRadius: "50%", color: "transparent" }}>g</button>
- </div>
-
- </div>
- <div style={{ width: "200px", height: "200" }}>
- <textarea style={{ width: "100%", height: "100%" }}
- defaultValue="Enter Text Here..."
-
- ></textarea>
- </div>
- </div>
-
- );
- }
-} \ No newline at end of file
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss
index 972966204..3b19a6dba 100644
--- a/src/client/views/nodes/AudioBox.scss
+++ b/src/client/views/nodes/AudioBox.scss
@@ -1,6 +1,134 @@
-.audiobox-cont{
- top:0;
- max-height: 32px;
- position: absolute;
+.audiobox-container, .audiobox-container-interactive {
width: 100%;
+ height: 100%;
+ position: inherit;
+ display:flex;
+ pointer-events: all;
+ cursor:default;
+ .audiobox-handle {
+ width:20px;
+ height:100%;
+ display:inline-block;
+ background: gray;
+ }
+ .audiobox-control, .audiobox-control-interactive {
+ top:0;
+ max-height: 32px;
+ width: 100%;
+ display:inline-block;
+ pointer-events: none;
+ }
+ .audiobox-control-interactive {
+ pointer-events: all;
+ }
+ .audiobox-record {
+ pointer-events: all;
+ width:100%;
+ height:100%;
+ position: absolute;
+ pointer-events: none;
+ }
+ .audiobox-record-interactive {
+ pointer-events: all;
+ }
+ .audiobox-controls {
+ width:100%;
+ height:100%;
+ position: relative;
+ display: flex;
+ padding-left: 2px;
+ border: gray solid 3px;
+ .audiobox-player {
+ margin-top:auto;
+ margin-bottom:auto;
+ width:100%;
+ height: 80%;
+ position: relative;
+ padding-right: 5px;
+ display: flex;
+ .audiobox-playhead {
+ position: relative;
+ margin-top: auto;
+ margin-bottom: auto;
+ width: 25px;
+ padding: 2px;
+ }
+ .audiobox-timeline {
+ position:relative;
+ height:100%;
+ width:100%;
+ background: white;
+ border: gray solid 1px;
+ border-radius: 3px;
+ .audiobox-current {
+ width: 1px;
+ height:100%;
+ background-color: red;
+ position: absolute;
+ }
+ .audiobox-linker, .audiobox-linker-mini {
+ position:absolute;
+ width:15px;
+ min-height:10px;
+ height:15px;
+ margin-left:-2.55px;
+ background:gray;
+ border-radius: 100%;
+ background-color: transparent;
+ box-shadow: black 2px 2px 1px;
+ .docuLinkBox-cont {
+ position: relative !important;
+ height: 100% !important;
+ width: 100% !important;
+ left:unset !important;
+ top:unset !important;
+ }
+ }
+ .audiobox-linker-mini {
+ width:8px;
+ min-height:8px;
+ height:8px;
+ box-shadow: black 1px 1px 1px;
+ margin-left: -1;
+ margin-top: -2;
+ .docuLinkBox-cont {
+ position: relative !important;
+ height: 100% !important;
+ width: 100% !important;
+ left:unset !important;
+ top:unset !important;
+ }
+ }
+ .audiobox-linker:hover, .audiobox-linker-mini:hover {
+ transform:scale(1.5);
+ }
+ .audiobox-marker-container, .audiobox-marker-minicontainer {
+ position:absolute;
+ width:10px;
+ height:90%;
+ top:2.5%;
+ background:gray;
+ border-radius: 5px;
+ box-shadow: black 2px 2px 1px;
+ .audiobox-marker {
+ position:relative;
+ height: calc(100% - 15px);
+ margin-top: 15px;
+ }
+ .audio-marker:hover {
+ border: orange 2px solid;
+ }
+ }
+ .audiobox-marker-minicontainer {
+ width:5px;
+ border-radius: 1px;
+ .audiobox-marker {
+ position:relative;
+ height: calc(100% - 8px);
+ margin-top: 8px;
+ }
+ }
+ }
+ }
+ }
} \ No newline at end of file
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index be6ae630f..268255b46 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -2,24 +2,264 @@ import React = require("react");
import { FieldViewProps, FieldView } from './FieldView';
import { observer } from "mobx-react";
import "./AudioBox.scss";
-import { Cast } from "../../../new_fields/Types";
-import { AudioField } from "../../../new_fields/URLField";
+import { Cast, DateCast, NumCast } from "../../../new_fields/Types";
+import { AudioField, nullAudio } from "../../../new_fields/URLField";
+import { DocExtendableComponent } from "../DocComponent";
+import { makeInterface, createSchema } from "../../../new_fields/Schema";
+import { documentSchema } from "../../../new_fields/documentSchemas";
+import { Utils, returnTrue, emptyFunction, returnOne, returnTransparent } from "../../../Utils";
+import { RouteStore } from "../../../server/RouteStore";
+import { runInAction, observable, reaction, IReactionDisposer, computed, action } from "mobx";
+import { DateField } from "../../../new_fields/DateField";
+import { SelectionManager } from "../../util/SelectionManager";
+import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { ContextMenuProps } from "../ContextMenuItem";
+import { ContextMenu } from "../ContextMenu";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { DocumentView } from "./DocumentView";
+
+interface Window {
+ MediaRecorder: MediaRecorder;
+}
+
+declare class MediaRecorder {
+ // whatever MediaRecorder has
+ constructor(e: any);
+}
+export const audioSchema = createSchema({
+ playOnSelect: "boolean"
+});
+
+type AudioDocument = makeInterface<[typeof documentSchema, typeof audioSchema]>;
+const AudioDocument = makeInterface(documentSchema, audioSchema);
-const defaultField: AudioField = new AudioField(new URL("http://techslides.com/demos/samples/sample.mp3"));
@observer
-export class AudioBox extends React.Component<FieldViewProps> {
+export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocument>(AudioDocument) {
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(AudioBox, fieldKey); }
+
+ _linkPlayDisposer: IReactionDisposer | undefined;
+ _reactionDisposer: IReactionDisposer | undefined;
+ _scrubbingDisposer: IReactionDisposer | undefined;
+ _ele: HTMLAudioElement | null = null;
+ _recorder: any;
+ _recordStart = 0;
+
+ @observable private static _scrubTime = 0;
+ @observable private _audioState: "unrecorded" | "recording" | "recorded" = "unrecorded";
+ @observable private _playing = false;
+ public static SetScrubTime = action((timeInMillisFrom1970: number) => AudioBox._scrubTime = timeInMillisFrom1970);
+ public static ActiveRecordings: Doc[] = [];
+
+ componentDidMount() {
+ runInAction(() => this._audioState = this.path ? "recorded" : "unrecorded");
+ this._linkPlayDisposer = reaction(() => this.layoutDoc.scrollToLinkID,
+ scrollLinkId => {
+ scrollLinkId && DocListCast(this.dataDoc.links).filter(l => l[Id] === scrollLinkId).map(l => {
+ let la1 = l.anchor1 as Doc;
+ let linkTime = Doc.AreProtosEqual(la1, this.dataDoc) ? NumCast(l.anchor1Timecode) : NumCast(l.anchor2Timecode);
+ setTimeout(() => { this.playFrom(linkTime); Doc.linkFollowHighlight(l); }, 250);
+ });
+ scrollLinkId && Doc.SetInPlace(this.layoutDoc, "scrollToLinkID", undefined, false);
+ }, { fireImmediately: true });
+ this._reactionDisposer = reaction(() => SelectionManager.SelectedDocuments(),
+ selected => {
+ let sel = selected.length ? selected[0].props.Document : undefined;
+ this.Document.playOnSelect && sel && !Doc.AreProtosEqual(sel, this.props.Document) && this.playFrom(DateCast(sel.creationTime).date.getTime());
+ });
+ this._scrubbingDisposer = reaction(() => AudioBox._scrubTime, timeInMillisecondsFrom1970 => {
+ let start = this.extensionDoc && DateCast(this.extensionDoc.recordingStart);
+ start && this.playFrom((timeInMillisecondsFrom1970 - start.date.getTime()) / 1000);
+ });
+ }
+
+ timecodeChanged = () => {
+ const htmlEle = this._ele;
+ if (this._audioState === "recorded" && htmlEle) {
+ htmlEle.duration && htmlEle.duration !== Infinity && runInAction(() => this.dataDoc.duration = htmlEle.duration);
+ DocListCast(this.dataDoc.links).map(l => {
+ let la1 = l.anchor1 as Doc;
+ let linkTime = NumCast(l.anchor2Timecode);
+ if (Doc.AreProtosEqual(la1, this.dataDoc)) {
+ la1 = l.anchor2 as Doc;
+ linkTime = NumCast(l.anchor1Timecode);
+ }
+ if (linkTime > NumCast(this.Document.currentTimecode) && linkTime < htmlEle.currentTime) {
+ Doc.linkFollowHighlight(la1);
+ }
+ });
+ this.Document.currentTimecode = htmlEle.currentTime;
+ }
+ }
+
+ pause = action(() => {
+ this._ele!.pause();
+ this._playing = false;
+ });
+
+ playFrom = (seekTimeInSeconds: number) => {
+ if (this._ele) {
+ if (seekTimeInSeconds < 0) {
+ this.pause();
+ } else if (seekTimeInSeconds <= this._ele.duration) {
+ this._ele.currentTime = seekTimeInSeconds;
+ this._ele.play();
+ runInAction(() => this._playing = true);
+ } else {
+ this.pause();
+ }
+ }
+ }
+
+ componentWillUnmount() {
+ this._reactionDisposer && this._reactionDisposer();
+ this._linkPlayDisposer && this._linkPlayDisposer();
+ this._scrubbingDisposer && this._scrubbingDisposer();
+ }
+
+
+ updateRecordTime = () => {
+ if (this._audioState === "recording") {
+ setTimeout(this.updateRecordTime, 30);
+ this.Document.currentTimecode = (new Date().getTime() - this._recordStart) / 1000;
+ }
+ }
- public static LayoutString() { return FieldView.LayoutString(AudioBox); }
+ recordAudioAnnotation = () => {
+ let gumStream: any;
+ let self = this;
+ const extensionDoc = this.extensionDoc;
+ extensionDoc && navigator.mediaDevices.getUserMedia({
+ audio: true
+ }).then(function (stream) {
+ gumStream = stream;
+ self._recorder = new MediaRecorder(stream);
+ extensionDoc.recordingStart = new DateField(new Date());
+ AudioBox.ActiveRecordings.push(self.props.Document);
+ self._recorder.ondataavailable = async function (e: any) {
+ const formData = new FormData();
+ formData.append("file", e.data);
+ const res = await fetch(Utils.prepend(RouteStore.upload), {
+ method: 'POST',
+ body: formData
+ });
+ const files = await res.json();
+ const url = Utils.prepend(files[0].path);
+ // upload to server with known URL
+ self.props.Document[self.props.fieldKey] = new AudioField(url);
+ };
+ runInAction(() => self._audioState = "recording");
+ self._recordStart = new Date().getTime();
+ setTimeout(self.updateRecordTime, 0);
+ self._recorder.start();
+ setTimeout(() => {
+ self.stopRecording();
+ gumStream.getAudioTracks()[0].stop();
+ }, 60 * 60 * 1000); // stop after an hour?
+ });
+ }
+
+ specificContextMenu = (e: React.MouseEvent): void => {
+ let funcs: ContextMenuProps[] = [];
+ funcs.push({ description: (this.Document.playOnSelect ? "Don't play" : "Play") + " when document selected", event: () => this.Document.playOnSelect = !this.Document.playOnSelect, icon: "expand-arrows-alt" });
+
+ ContextMenu.Instance.addItem({ description: "Audio Funcs...", subitems: funcs, icon: "asterisk" });
+ }
+
+ stopRecording = action(() => {
+ this._recorder.stop();
+ this.dataDoc.duration = (new Date().getTime() - this._recordStart) / 1000;
+ this._audioState = "recorded";
+ let ind = AudioBox.ActiveRecordings.indexOf(this.props.Document);
+ ind !== -1 && (AudioBox.ActiveRecordings.splice(ind, 1));
+ });
+
+ recordClick = (e: React.MouseEvent) => {
+ if (e.button === 0 && !e.ctrlKey) {
+ this._recorder ? this.stopRecording() : this.recordAudioAnnotation();
+ e.stopPropagation();
+ }
+ }
+
+ onPlay = (e: any) => {
+ this.playFrom(this._ele!.paused ? this._ele!.currentTime : -1);
+ e.stopPropagation();
+ }
+ onStop = (e: any) => {
+ this.pause();
+ this._ele!.currentTime = 0;
+ e.stopPropagation();
+ }
+
+ setRef = (e: HTMLAudioElement | null) => {
+ e && e.addEventListener("timeupdate", this.timecodeChanged);
+ this._ele = e;
+ }
+
+ @computed get path() {
+ let field = Cast(this.props.Document[this.props.fieldKey], AudioField);
+ let path = (field instanceof AudioField) ? field.url.href : "";
+ return path === nullAudio ? "" : path;
+ }
+
+ @computed get audio() {
+ let interactive = this.active() ? "-interactive" : "";
+ return <audio ref={this.setRef} className={`audiobox-control${interactive}`}>
+ <source src={this.path} type="audio/mpeg" />
+ Not supported.
+ </audio>;
+ }
render() {
- let field = Cast(this.props.Document[this.props.fieldKey], AudioField, defaultField);
- let path = field.url.href;
-
- return (
- <audio controls className="audiobox-cont" style={{ pointerEvents: "all" }}>
- <source src={path} type="audio/mpeg" />
- Not supported.
- </audio>
+ let interactive = this.active() ? "-interactive" : "";
+ return (!this.extensionDoc ? (null) :
+ <div className={`audiobox-container`} onContextMenu={this.specificContextMenu}
+ onClick={!this.path ? this.recordClick : undefined}>
+ <div className="audiobox-handle"></div>
+ {!this.path ?
+ <button className={`audiobox-record${interactive}`} style={{ backgroundColor: this._audioState === "recording" ? "red" : "black" }}>
+ {this._audioState === "recording" ? "STOP" : "RECORD"}
+ </button> :
+ <div className="audiobox-controls">
+ <div className="audiobox-player" onClick={this.onPlay}>
+ <div className="audiobox-playhead"> <FontAwesomeIcon style={{ width: "100%" }} icon={this._playing ? "pause" : "play"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
+ <div className="audiobox-playhead" onClick={this.onStop}><FontAwesomeIcon style={{ width: "100%" }} icon="stop" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
+ <div className="audiobox-timeline" onClick={e => e.stopPropagation()}
+ onPointerDown={e => {
+ if (e.button === 0 && !e.ctrlKey) {
+ let rect = (e.target as any).getBoundingClientRect();
+ this._ele!.currentTime = this.Document.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration);
+ this.pause();
+ e.stopPropagation();
+ }
+ }} >
+ {DocListCast(this.dataDoc.links).map((l, i) => {
+ let la1 = l.anchor1 as Doc;
+ let la2 = l.anchor2 as Doc;
+ let linkTime = NumCast(l.anchor2Timecode);
+ if (Doc.AreProtosEqual(la1, this.dataDoc)) {
+ la1 = l.anchor2 as Doc;
+ la2 = l.anchor1 as Doc;
+ linkTime = NumCast(l.anchor1Timecode);
+ }
+ return !linkTime ? (null) : <div className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container"} style={{ left: `${linkTime / NumCast(this.dataDoc.duration, 1) * 100}%` }}>
+ <div className={this.props.PanelHeight() < 32 ? "audioBox-linker-mini" : "audioBox-linker"} key={"linker" + i}>
+ <DocumentView {...this.props} Document={l} layoutKey={Doc.LinkEndpoint(l, la2)}
+ parentActive={returnTrue} bringToFront={emptyFunction} zoomToScale={emptyFunction} getScale={returnOne}
+ backgroundColor={returnTransparent} />
+ </div>
+ <div key={i} className="audiobox-marker" onPointerEnter={() => Doc.linkFollowHighlight(la1)}
+ onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { this.playFrom(linkTime); e.stopPropagation(); } }}
+ onClick={e => { if (e.button === 0 && !e.ctrlKey) { this.pause(); e.stopPropagation(); } }} />
+ </div>;
+ })}
+ <div className="audiobox-current" style={{ left: `${NumCast(this.Document.currentTimecode) / NumCast(this.dataDoc.duration, 1) * 100}%` }} />
+ {this.audio}
+ </div>
+ </div>
+ </div>
+ }
+ </div>
);
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/ButtonBox.scss b/src/client/views/nodes/ButtonBox.scss
index 75a790667..e8a3d1479 100644
--- a/src/client/views/nodes/ButtonBox.scss
+++ b/src/client/views/nodes/ButtonBox.scss
@@ -13,6 +13,8 @@
border-radius: inherit;
text-align: center;
display: table;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
.buttonBox-mainButtonCenter {
height: 100%;
diff --git a/src/client/views/nodes/ButtonBox.tsx b/src/client/views/nodes/ButtonBox.tsx
index f08ea4891..beb2b30fd 100644
--- a/src/client/views/nodes/ButtonBox.tsx
+++ b/src/client/views/nodes/ButtonBox.tsx
@@ -3,11 +3,11 @@ import { faEdit } from '@fortawesome/free-regular-svg-icons';
import { action, computed } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { Doc, DocListCastAsync, DocListCast } from '../../../new_fields/Doc';
+import { Doc, DocListCast } from '../../../new_fields/Doc';
import { List } from '../../../new_fields/List';
import { createSchema, makeInterface, listSpec } from '../../../new_fields/Schema';
import { ScriptField } from '../../../new_fields/ScriptField';
-import { BoolCast, StrCast, Cast } from '../../../new_fields/Types';
+import { BoolCast, StrCast, Cast, FieldValue } from '../../../new_fields/Types';
import { DragManager } from '../../util/DragManager';
import { undoBatch } from '../../util/UndoManager';
import { DocComponent } from '../DocComponent';
@@ -15,24 +15,30 @@ import './ButtonBox.scss';
import { FieldView, FieldViewProps } from './FieldView';
import { ContextMenuProps } from '../ContextMenuItem';
import { ContextMenu } from '../ContextMenu';
+import { documentSchema } from '../../../new_fields/documentSchemas';
library.add(faEdit as any);
const ButtonSchema = createSchema({
onClick: ScriptField,
+ buttonParams: listSpec("string"),
text: "string"
});
-type ButtonDocument = makeInterface<[typeof ButtonSchema]>;
-const ButtonDocument = makeInterface(ButtonSchema);
+type ButtonDocument = makeInterface<[typeof ButtonSchema, typeof documentSchema]>;
+const ButtonDocument = makeInterface(ButtonSchema, documentSchema);
@observer
export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(ButtonDocument) {
- public static LayoutString() { return FieldView.LayoutString(ButtonBox); }
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ButtonBox, fieldKey); }
private dropDisposer?: DragManager.DragDropDisposer;
- @computed get dataDoc() { return this.props.DataDoc && (BoolCast(this.props.Document.isTemplate) || BoolCast(this.props.DataDoc.isTemplate) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
+ @computed get dataDoc() {
+ return this.props.DataDoc &&
+ (this.Document.isTemplateField || BoolCast(this.props.DataDoc.isTemplateField) ||
+ this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document);
+ }
protected createDropTarget = (ele: HTMLDivElement) => {
@@ -48,7 +54,7 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt
let funcs: ContextMenuProps[] = [];
funcs.push({
description: "Clear Script Params", event: () => {
- let params = Cast(this.props.Document.buttonParams, listSpec("string"));
+ let params = FieldValue(this.Document.buttonParams);
params && params.map(p => this.props.Document[p] = undefined);
}, icon: "trash"
});
@@ -60,18 +66,20 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt
@action
drop = (e: Event, de: DragManager.DropEvent) => {
if (de.data instanceof DragManager.DocumentDragData && e.target) {
- this.props.Document[(e.target as any).textContent] = new List<Doc>(de.data.droppedDocuments);
+ this.props.Document[(e.target as any).textContent] = new List<Doc>(de.data.droppedDocuments.map((d, i) =>
+ d.onDragStart ? de.data.draggedDocuments[i] : d));
e.stopPropagation();
}
}
// (!missingParams || !missingParams.length ? "" : "(" + missingParams.map(m => m + ":").join(" ") + ")")
render() {
- let params = Cast(this.props.Document.buttonParams, listSpec("string"));
+ let params = this.Document.buttonParams;
let missingParams = params && params.filter(p => this.props.Document[p] === undefined);
params && params.map(p => DocListCast(this.props.Document[p])); // bcz: really hacky form of prefetching ...
return (
- <div className="buttonBox-outerDiv" ref={this.createDropTarget} onContextMenu={this.specificContextMenu}>
- <div className="buttonBox-mainButton" style={{ background: StrCast(this.props.Document.backgroundColor), color: StrCast(this.props.Document.color, "black") }} >
+ <div className="buttonBox-outerDiv" ref={this.createDropTarget} onContextMenu={this.specificContextMenu}
+ style={{ boxShadow: this.Document.opacity === 0 ? undefined : StrCast(this.Document.boxShadow, "") }}>
+ <div className="buttonBox-mainButton" style={{ background: this.Document.backgroundColor || "", color: this.Document.color || "black" }} >
<div className="buttonBox-mainButtonCenter">
{(this.Document.text || this.Document.title)}
</div>
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.scss b/src/client/views/nodes/CollectionFreeFormDocumentView.scss
index c0d9e1267..af9232c2f 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.scss
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.scss
@@ -2,4 +2,6 @@
transform-origin: left top;
position: absolute;
background-color: transparent;
+ top:0;
+ left:0;
} \ No newline at end of file
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index c4fed200f..58cb831f8 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,15 +1,16 @@
-import { computed, action, observable, reaction, IReactionDisposer, trace } from "mobx";
+import { random } from "animejs";
+import { computed, IReactionDisposer, observable, reaction } from "mobx";
import { observer } from "mobx-react";
-import { createSchema, makeInterface, listSpec } from "../../../new_fields/Schema";
-import { FieldValue, NumCast, StrCast, Cast } from "../../../new_fields/Types";
+import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc";
+import { listSpec } from "../../../new_fields/Schema";
+import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { percent2frac } from "../../../Utils";
import { Transform } from "../../util/Transform";
import { DocComponent } from "../DocComponent";
-import { percent2frac } from "../../../Utils";
-import { DocumentView, DocumentViewProps, documentSchema } from "./DocumentView";
import "./CollectionFreeFormDocumentView.scss";
+import { DocumentView, DocumentViewProps } from "./DocumentView";
import React = require("react");
-import { Doc, WidthSym, HeightSym } from "../../../new_fields/Doc";
-import { random } from "animejs";
+import { PositionDocument } from "../../../new_fields/documentSchemas";
export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
dataProvider?: (doc: Doc, dataDoc?: Doc) => { x: number, y: number, width: number, height: number, z: number, transition?: string } | undefined;
@@ -20,27 +21,18 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
jitterRotation: number;
transition?: string;
}
-export const positionSchema = createSchema({
- zIndex: "number",
- x: "number",
- y: "number",
- z: "number",
-});
-
-export type PositionDocument = makeInterface<[typeof documentSchema, typeof positionSchema]>;
-export const PositionDocument = makeInterface(documentSchema, positionSchema);
@observer
export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, PositionDocument>(PositionDocument) {
_disposer: IReactionDisposer | undefined = undefined;
get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) rotate(${random(-1, 1) * this.props.jitterRotation}deg)`; }
- get X() { return this._animPos !== undefined ? this._animPos[0] : this.renderScriptDim ? this.renderScriptDim.x : this.props.x !== undefined ? this.props.x : this.dataProvider ? this.dataProvider.x : this.Document.x || 0; }
- get Y() { return this._animPos !== undefined ? this._animPos[1] : this.renderScriptDim ? this.renderScriptDim.y : this.props.y !== undefined ? this.props.y : this.dataProvider ? this.dataProvider.y : this.Document.y || 0; }
- get width() { return this.renderScriptDim ? this.renderScriptDim.width : this.props.width !== undefined ? this.props.width : this.props.dataProvider && this.dataProvider ? this.dataProvider.width : this.props.Document[WidthSym](); }
- get height() { return this.renderScriptDim ? this.renderScriptDim.height : this.props.height !== undefined ? this.props.height : this.props.dataProvider && this.dataProvider ? this.dataProvider.height : this.props.Document[HeightSym](); }
+ get X() { return this._animPos !== undefined ? this._animPos[0] : this.renderScriptDim ? this.renderScriptDim.x : this.props.x !== undefined ? this.props.x : this.dataProvider ? this.dataProvider.x : (this.Document.x || 0); }
+ get Y() { return this._animPos !== undefined ? this._animPos[1] : this.renderScriptDim ? this.renderScriptDim.y : this.props.y !== undefined ? this.props.y : this.dataProvider ? this.dataProvider.y : (this.Document.y || 0); }
+ get width() { return this.renderScriptDim ? this.renderScriptDim.width : this.props.width !== undefined ? this.props.width : this.props.dataProvider && this.dataProvider ? this.dataProvider.width : this.layoutDoc[WidthSym](); }
+ get height() { return this.renderScriptDim ? this.renderScriptDim.height : this.props.height !== undefined ? this.props.height : this.props.dataProvider && this.dataProvider ? this.dataProvider.height : this.layoutDoc[HeightSym](); }
@computed get dataProvider() { return this.props.dataProvider && this.props.dataProvider(this.props.Document, this.props.DataDoc) ? this.props.dataProvider(this.props.Document, this.props.DataDoc) : undefined; }
- @computed get nativeWidth() { return FieldValue(this.Document.nativeWidth, 0); }
- @computed get nativeHeight() { return FieldValue(this.Document.nativeHeight, 0); }
+ @computed get nativeWidth() { return NumCast(this.layoutDoc.nativeWidth); }
+ @computed get nativeHeight() { return NumCast(this.layoutDoc.nativeHeight); }
@computed get renderScriptDim() {
if (this.Document.renderScript) {
@@ -64,7 +56,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
this._disposer = reaction(() => [this.props.Document.animateToPos, this.props.Document.isAnimating],
() => {
const target = this.props.Document.animateToPos ? Array.from(Cast(this.props.Document.animateToPos, listSpec("number"))!) : undefined;
- this._animPos = !target ? undefined : target[2] ? [this.Document.x || 0, this.Document.y || 0] : this.props.ScreenToLocalTransform().transformPoint(target[0], target[1]);
+ this._animPos = !target ? undefined : target[2] ? [NumCast(this.layoutDoc.x), NumCast(this.layoutDoc.y)] : this.props.ScreenToLocalTransform().transformPoint(target[0], target[1]);
}, { fireImmediately: true });
}
@@ -77,7 +69,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
borderRounding = () => {
let ruleRounding = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleRounding_" + this.Document.heading]) : undefined;
- let ld = this.layoutDoc.layout instanceof Doc ? this.layoutDoc.layout : undefined;
+ let ld = this.layoutDoc[StrCast(this.layoutDoc.layoutKey, "layout")] instanceof Doc ? this.layoutDoc[StrCast(this.layoutDoc.layoutKey, "layout")] as Doc : undefined;
let br = StrCast((ld || this.props.Document).borderRounding);
br = !br && ruleRounding ? ruleRounding : br;
if (br.endsWith("%")) {
@@ -92,12 +84,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
clusterColorFunc = (doc: Doc) => this.clusterColor;
- get layoutDoc() {
- // if this document's layout field contains a document (ie, a rendering template), then we will use that
- // to determine the render JSX string, otherwise the layout field should directly contain a JSX layout string.
- return this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document;
- }
-
@observable _animPos: number[] | undefined = undefined;
finalPanelWidth = () => this.dataProvider ? this.dataProvider.width : this.panelWidth();
diff --git a/src/client/views/nodes/ColorBox.scss b/src/client/views/nodes/ColorBox.scss
new file mode 100644
index 000000000..bf334c939
--- /dev/null
+++ b/src/client/views/nodes/ColorBox.scss
@@ -0,0 +1,22 @@
+.colorBox-container, .colorBox-container-interactive {
+ width:100%;
+ height:100%;
+ position: relative;
+ pointer-events: none;
+
+ .sketch-picker {
+ margin:auto;
+ div {
+ cursor: crosshair;
+ }
+ .flexbox-fix {
+ cursor: pointer;
+ div {
+ cursor:pointer;
+ }
+ }
+ }
+}
+.colorBox-container-interactive {
+ pointer-events:all;
+} \ No newline at end of file
diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx
new file mode 100644
index 000000000..fda6d64f4
--- /dev/null
+++ b/src/client/views/nodes/ColorBox.tsx
@@ -0,0 +1,45 @@
+import React = require("react");
+import { observer } from "mobx-react";
+import { SketchPicker } from 'react-color';
+import { FieldView, FieldViewProps } from './FieldView';
+import "./ColorBox.scss";
+import { InkingControl } from "../InkingControl";
+import { DocExtendableComponent } from "../DocComponent";
+import { makeInterface } from "../../../new_fields/Schema";
+import { reaction, observable, action, IReactionDisposer } from "mobx";
+import { SelectionManager } from "../../util/SelectionManager";
+import { StrCast } from "../../../new_fields/Types";
+import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
+import { documentSchema } from "../../../new_fields/documentSchemas";
+
+type ColorDocument = makeInterface<[typeof documentSchema]>;
+const ColorDocument = makeInterface(documentSchema);
+
+@observer
+export class ColorBox extends DocExtendableComponent<FieldViewProps, ColorDocument>(ColorDocument) {
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ColorBox, fieldKey); }
+
+ _selectedDisposer: IReactionDisposer | undefined;
+ _penDisposer: IReactionDisposer | undefined;
+ @observable _startupColor = "black";
+
+ componentDidMount() {
+ this._selectedDisposer = reaction(() => SelectionManager.SelectedDocuments(),
+ action(() => this._startupColor = SelectionManager.SelectedDocuments().length ? StrCast(SelectionManager.SelectedDocuments()[0].Document.backgroundColor, "black") : "black"),
+ { fireImmediately: true });
+ this._penDisposer = reaction(() => CurrentUserUtils.ActivePen,
+ action(() => this._startupColor = CurrentUserUtils.ActivePen ? StrCast(CurrentUserUtils.ActivePen.backgroundColor, "black") : "black"),
+ { fireImmediately: true });
+ }
+ componentWillUnmount() {
+ this._penDisposer && this._penDisposer();
+ this._selectedDisposer && this._selectedDisposer();
+ }
+
+ render() {
+ return <div className={`colorBox-container${this.active() ? "-interactive" : ""}`}
+ onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()}>
+ <SketchPicker color={this._startupColor} onChange={InkingControl.Instance.switchColor} />
+ </div>;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DocuLinkBox.scss b/src/client/views/nodes/DocuLinkBox.scss
new file mode 100644
index 000000000..57c1a66e0
--- /dev/null
+++ b/src/client/views/nodes/DocuLinkBox.scss
@@ -0,0 +1,8 @@
+.docuLinkBox-cont {
+ cursor: default;
+ position: absolute;
+ width: 25px;
+ height: 25px;
+ border-radius: 20px;
+ pointer-events: all;
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DocuLinkBox.tsx b/src/client/views/nodes/DocuLinkBox.tsx
new file mode 100644
index 000000000..f4c7d43aa
--- /dev/null
+++ b/src/client/views/nodes/DocuLinkBox.tsx
@@ -0,0 +1,81 @@
+import { action, observable } from "mobx";
+import { observer } from "mobx-react";
+import { Doc } from "../../../new_fields/Doc";
+import { makeInterface } from "../../../new_fields/Schema";
+import { NumCast, StrCast, Cast } from "../../../new_fields/Types";
+import { Utils } from '../../../Utils';
+import { DocumentManager } from "../../util/DocumentManager";
+import { DragLinksAsDocuments } from "../../util/DragManager";
+import { DocComponent } from "../DocComponent";
+import "./DocuLinkBox.scss";
+import { FieldView, FieldViewProps } from "./FieldView";
+import React = require("react");
+import { DocumentType } from "../../documents/DocumentTypes";
+import { documentSchema } from "../../../new_fields/documentSchemas";
+
+type DocLinkSchema = makeInterface<[typeof documentSchema]>;
+const DocLinkDocument = makeInterface(documentSchema);
+
+@observer
+export class DocuLinkBox extends DocComponent<FieldViewProps, DocLinkSchema>(DocLinkDocument) {
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DocuLinkBox, fieldKey); }
+ _downx = 0;
+ _downy = 0;
+ @observable _x = 0;
+ @observable _y = 0;
+ @observable _selected = false;
+ _ref = React.createRef<HTMLDivElement>();
+
+ onPointerDown = (e: React.PointerEvent) => {
+ this._downx = e.clientX;
+ this._downy = e.clientY;
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ document.addEventListener("pointermove", this.onPointerMove);
+ document.addEventListener("pointerup", this.onPointerUp);
+ (e.button === 0 && !e.ctrlKey) && e.stopPropagation();
+ }
+ onPointerMove = action((e: PointerEvent) => {
+ let cdiv = this._ref && this._ref.current && this._ref.current.parentElement;
+ if (cdiv && (Math.abs(e.clientX - this._downx) > 5 || Math.abs(e.clientY - this._downy) > 5)) {
+ let bounds = cdiv.getBoundingClientRect();
+ let pt = Utils.getNearestPointInPerimeter(bounds.left, bounds.top, bounds.width, bounds.height, e.clientX, e.clientY);
+ let separation = Math.sqrt((pt[0] - e.clientX) * (pt[0] - e.clientX) + (pt[1] - e.clientY) * (pt[1] - e.clientY));
+ let dragdist = Math.sqrt((pt[0] - this._downx) * (pt[0] - this._downx) + (pt[1] - this._downy) * (pt[1] - this._downy));
+ if (separation > 100) {
+ DragLinksAsDocuments(this._ref.current!, pt[0], pt[1], Cast(this.props.Document[this.props.fieldKey], Doc) as Doc, this.props.Document); // Containging collection is the document, not a collection... hack.
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ } else if (dragdist > separation) {
+ this.props.Document[this.props.fieldKey + "_x"] = (pt[0] - bounds.left) / bounds.width * 100;
+ this.props.Document[this.props.fieldKey + "_y"] = (pt[1] - bounds.top) / bounds.height * 100;
+ }
+ }
+ });
+ onPointerUp = (e: PointerEvent) => {
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ if (Math.abs(e.clientX - this._downx) < 3 && Math.abs(e.clientY - this._downy) < 3 && (e.button === 2 || e.ctrlKey || !this.props.Document.isButton)) {
+ this.props.select(false);
+ }
+ }
+ onClick = (e: React.MouseEvent) => {
+ if (Math.abs(e.clientX - this._downx) < 3 && Math.abs(e.clientY - this._downy) < 3 && (e.button !== 2 && !e.ctrlKey && this.props.Document.isButton)) {
+ DocumentManager.Instance.FollowLink(this.props.Document, this.props.Document[this.props.fieldKey] as Doc, document => this.props.addDocTab(document, undefined, "inTab"), false);
+ }
+ e.stopPropagation();
+ }
+
+ render() {
+ let anchorDoc = Cast(this.props.Document[this.props.fieldKey], Doc);
+ let hasAnchor = anchorDoc instanceof Doc && anchorDoc.type === DocumentType.PDFANNO;
+ let y = NumCast(this.props.Document[this.props.fieldKey + "_y"], 100);
+ let x = NumCast(this.props.Document[this.props.fieldKey + "_x"], 100);
+ let c = StrCast(this.props.Document.backgroundColor, "lightblue");
+ return <div className="docuLinkBox-cont" onPointerDown={this.onPointerDown} onClick={this.onClick} title={StrCast((this.props.Document[this.props.fieldKey === "anchor1" ? "anchor2" : "anchor1"]! as Doc).title)}
+ ref={this._ref} style={{
+ background: c, left: `calc(${x}% - 12.5px)`, top: `calc(${y}% - 12.5px)`,
+ transform: `scale(${hasAnchor ? 0.333 : 1 / this.props.ContentScaling()})`
+ }} />;
+ }
+}
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index ee58e4152..60d5a16d7 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -2,15 +2,13 @@ import { computed } from "mobx";
import { observer } from "mobx-react";
import { Doc } from "../../../new_fields/Doc";
import { ScriptField } from "../../../new_fields/ScriptField";
-import { Cast } from "../../../new_fields/Types";
+import { Cast, StrCast } from "../../../new_fields/Types";
import { OmitKeys, Without } from "../../../Utils";
import { HistogramBox } from "../../northstar/dash-nodes/HistogramBox";
import DirectoryImportBox from "../../util/Import & Export/DirectoryImportBox";
import { CollectionDockingView } from "../collections/CollectionDockingView";
import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
-import { CollectionPDFView } from "../collections/CollectionPDFView";
import { CollectionSchemaView } from "../collections/CollectionSchemaView";
-import { CollectionVideoView } from "../collections/CollectionVideoView";
import { CollectionView } from "../collections/CollectionView";
import { LinkFollowBox } from "../linking/LinkFollowBox";
import { YoutubeBox } from "./../../apis/youtube/YoutubeBox";
@@ -18,7 +16,7 @@ import { AudioBox } from "./AudioBox";
import { ButtonBox } from "./ButtonBox";
import { DocumentViewProps } from "./DocumentView";
import "./DocumentView.scss";
-import { DragBox } from "./DragBox";
+import { FontIconBox } from "./FontIconBox";
import { FieldView, FieldViewProps } from "./FieldView";
import { FormattedTextBox } from "./FormattedTextBox";
import { IconBox } from "./IconBox";
@@ -26,11 +24,14 @@ import { ImageBox } from "./ImageBox";
import { KeyValueBox } from "./KeyValueBox";
import { PDFBox } from "./PDFBox";
import { PresBox } from "./PresBox";
+import { QueryBox } from "./QueryBox";
+import { ColorBox } from "./ColorBox";
+import { DocuLinkBox } from "./DocuLinkBox";
import { PresElementBox } from "../presentationview/PresElementBox";
import { VideoBox } from "./VideoBox";
import { WebBox } from "./WebBox";
import React = require("react");
-import { StrCast, NumCast } from "../../../new_fields/Types";
+import { NumCast } from "../../../new_fields/Types";
import { List } from "../../../new_fields/List";
import { fromPromise } from "mobx-utils";
import { DashWebCam } from "../../views/webcam/DashWebCam";
@@ -61,6 +62,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
hideOnLeave?: boolean
}> {
@computed get layout(): string {
+ if (!this.layoutDoc) return "<p>awaiting layout</p>";
const layout = Cast(this.layoutDoc[this.props.layoutKey], "string");
if (layout === undefined) {
return this.props.Document.data ?
@@ -74,45 +76,40 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
}
get dataDoc() {
- if (this.props.DataDoc === undefined && (this.props.Document.layout instanceof Doc || this.props.Document instanceof Promise)) {
- // if there is no dataDoc (ie, we're not rendering a template layout), but this document
- // has a template layout document, then we will render the template layout but use
- // this document as the data document for the layout.
+ if (this.props.DataDoc === undefined && typeof Doc.LayoutField(this.props.Document) !== "string") {
+ // if there is no dataDoc (ie, we're not rendering a template layout), but this document has a layout document (not a layout string),
+ // then we render the layout document as a template and use this document as the data context for the template layout.
return this.props.Document;
}
return this.props.DataDoc;
}
get layoutDoc() {
- // if this document's layout field contains a document (ie, a rendering template), then we will use that
- // to determine the render JSX string, otherwise the layout field should directly contain a JSX layout string.
- return this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document;
+ return this.props.DataDoc === undefined ? Doc.expandTemplateLayout(Doc.Layout(this.props.Document), this.props.Document) : Doc.Layout(this.props.Document);
}
CreateBindings(): JsxBindings {
let list = {
...OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit,
Document: this.layoutDoc,
- DataDoc: this.dataDoc
+ DataDoc: this.dataDoc,
};
return { props: list };
}
- @computed get finalLayout() {
- return this.props.layoutKey === "overlayLayout" ? "<div/>" : this.layout;
- }
-
render() {
- let self = this;
- if (this.props.renderDepth > 7) return (null);
- if (!this.layout && this.props.layoutKey !== "overlayLayout") return (null);
- return <ObserverJsxParser
- blacklistedAttrs={[]}
- components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, DragBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox, PresElementBox, DashWebCam, DashWebRTCVideo }}
- bindings={this.CreateBindings()}
- jsx={this.finalLayout}
- showWarnings={true}
+ return (this.props.renderDepth > 7 || !this.layout) ? (null) :
+ <ObserverJsxParser
+ blacklistedAttrs={[]}
+ components={{
+ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, FontIconBox: FontIconBox, ButtonBox, FieldView,
+ CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox,
+ PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox, PresElementBox, QueryBox, ColorBox, DocuLinkBox, DashWebRTCVideo
+ }}
+ bindings={this.CreateBindings()}
+ jsx={this.layout}
+ showWarnings={true}
- onError={(test: any) => { console.log(test); }}
- />;
+ 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 4ea200e6d..a0bf74990 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -5,6 +5,8 @@
top: 0;
left:0;
border-radius: inherit;
+ transition : outline .3s linear;
+ cursor: grab;
// background: $light-color; //overflow: hidden;
transform-origin: left top;
@@ -45,9 +47,6 @@
}
}
}
-.documentView-node-topmost {
- background: white;
-}
.documentView-styleWrapper {
position: absolute;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index a903f8fe2..11066145b 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,34 +1,39 @@
import { library } from '@fortawesome/fontawesome-svg-core';
import * as fa from '@fortawesome/free-solid-svg-icons';
-import { action, computed, runInAction, trace } from "mobx";
+import { action, computed, runInAction } from "mobx";
import { observer } from "mobx-react";
import * as rp from "request-promise";
import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc";
+import { Document } from '../../../new_fields/documentSchemas';
import { Id } from '../../../new_fields/FieldSymbols';
-import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema";
+import { listSpec } from "../../../new_fields/Schema";
import { ScriptField } from '../../../new_fields/ScriptField';
-import { BoolCast, Cast, NumCast, PromiseValue, StrCast } from "../../../new_fields/Types";
+import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { ImageField } from '../../../new_fields/URLField';
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
-import { emptyFunction, returnTrue, Utils } from "../../../Utils";
+import { emptyFunction, returnTransparent, returnTrue, Utils } from "../../../Utils";
+import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
import { DocServer } from "../../DocServer";
import { Docs, DocUtils } from "../../documents/Documents";
+import { DocumentType } from '../../documents/DocumentTypes';
import { ClientUtils } from '../../util/ClientUtils';
import { DictationManager } from '../../util/DictationManager';
import { DocumentManager } from "../../util/DocumentManager";
import { DragManager, dropActionType } from "../../util/DragManager";
import { LinkManager } from '../../util/LinkManager';
+import { Scripting } from '../../util/Scripting';
import { SelectionManager } from "../../util/SelectionManager";
+import SharingManager from '../../util/SharingManager';
import { Transform } from "../../util/Transform";
import { undoBatch, UndoManager } from "../../util/UndoManager";
+import { CollectionViewType } from '../collections/CollectionView';
import { CollectionDockingView } from "../collections/CollectionDockingView";
-import { CollectionPDFView } from "../collections/CollectionPDFView";
-import { CollectionVideoView } from "../collections/CollectionVideoView";
import { CollectionView } from "../collections/CollectionView";
import { ContextMenu } from "../ContextMenu";
import { ContextMenuProps } from '../ContextMenuItem';
+import { DictationOverlay } from '../DictationOverlay';
import { DocComponent } from "../DocComponent";
import { EditableView } from '../EditableView';
-import { MainView } from '../MainView';
import { OverlayView } from '../OverlayView';
import { ScriptBox } from '../ScriptBox';
import { ScriptingRepl } from '../ScriptingRepl';
@@ -36,12 +41,9 @@ import { DocumentContentsView } from "./DocumentContentsView";
import "./DocumentView.scss";
import { FormattedTextBox } from './FormattedTextBox';
import React = require("react");
-import { DocumentType } from '../../documents/DocumentTypes';
-import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
-import { ImageField } from '../../../new_fields/URLField';
-import SharingManager from '../../util/SharingManager';
-import { Scripting } from '../../util/Scripting';
+import { link } from 'fs';
+library.add(fa.faEdit);
library.add(fa.faTrash);
library.add(fa.faShare);
library.add(fa.faDownload);
@@ -65,13 +67,13 @@ library.add(fa.faLock);
library.add(fa.faLaptopCode, fa.faMale, fa.faCopy, fa.faHandPointRight, fa.faCompass, fa.faSnowflake, fa.faMicrophone);
export interface DocumentViewProps {
- ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
+ ContainingCollectionView: Opt<CollectionView>;
ContainingCollectionDoc: Opt<Doc>;
Document: Doc;
DataDoc?: Doc;
fitToBox?: boolean;
onClick?: ScriptField;
- addDocument?: (doc: Doc, allowDuplicates?: boolean) => boolean;
+ addDocument?: (doc: Doc) => boolean;
removeDocument?: (doc: Doc) => boolean;
moveDocument?: (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
ScreenToLocalTransform: () => Transform;
@@ -92,37 +94,9 @@ export interface DocumentViewProps {
getScale: () => number;
animateBetweenIcon?: (maximize: boolean, target: number[]) => void;
ChromeHeight?: () => number;
+ layoutKey?: string;
}
-export const documentSchema = createSchema({
- // layout: "string", // this should be a "string" or Doc, but can't do that in schemas, so best to leave it out
- title: "string", // document title (can be on either data document or layout)
- nativeWidth: "number", // native width of document which determines how much document contents are scaled when the document's width is set
- nativeHeight: "number", // "
- width: "number", // width of document in its container's coordinate system
- height: "number", // "
- backgroundColor: "string", // background color of document
- opacity: "number", // opacity of document
- onClick: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
- ignoreAspect: "boolean", // whether aspect ratio should be ignored when laying out or manipulating the document
- autoHeight: "boolean", // whether the height of the document should be computed automatically based on its contents
- isTemplate: "boolean", // whether this document acts as a template layout for describing how other documents should be displayed
- isBackground: "boolean", // whether document is a background element and ignores input events (can only selet with marquee)
- type: "string", // enumerated type of document
- maximizeLocation: "string", // flag for where to place content when following a click interaction (e.g., onRight, inPlace, inTab)
- lockedPosition: "boolean", // whether the document can be spatially manipulated
- borderRounding: "string", // border radius rounding of document
- searchFields: "string", // the search fields to display when this document matches a search in its metadata
- heading: "number", // the logical layout 'heading' of this document (used by rule provider to stylize h1 header elements, from h2, etc)
- showCaption: "string", // whether editable caption text is overlayed at the bottom of the document
- showTitle: "string", // whether an editable title banner is displayed at tht top of the document
- isButton: "boolean", // whether document functions as a button (overiding native interactions of its content)
- ignoreClick: "boolean", // whether documents ignores input clicks (but does not ignore manipulation and other events)
-});
-
-
-type Document = makeInterface<[typeof documentSchema]>;
-const Document = makeInterface(documentSchema);
@observer
export class DocumentView extends DocComponent<DocumentViewProps, Document>(Document) {
@@ -137,8 +111,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
public get ContentDiv() { return this._mainCont.current; }
@computed get active() { return SelectionManager.IsSelected(this) || this.props.parentActive(); }
@computed get topMost() { return this.props.renderDepth === 0; }
- @computed get nativeWidth() { return this.Document.nativeWidth || 0; }
- @computed get nativeHeight() { return this.Document.nativeHeight || 0; }
+ @computed get nativeWidth() { return this.layoutDoc.nativeWidth || 0; }
+ @computed get nativeHeight() { return this.layoutDoc.nativeHeight || 0; }
@computed get onClickHandler() { return this.props.onClick ? this.props.onClick : this.Document.onClick; }
@action
@@ -156,6 +130,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@action
componentWillUnmount() {
this._dropDisposer && this._dropDisposer();
+ Doc.UnBrushDoc(this.props.Document);
DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1);
}
@@ -165,13 +140,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
const [left, top] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(0, 0);
dragData.offset = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top);
dragData.dropAction = dropAction;
- dragData.moveDocument = this.props.moveDocument;
+ dragData.moveDocument = this.Document.onDragStart ? undefined : this.props.moveDocument;
dragData.applyAsTemplate = applyAsTemplate;
DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, {
handlers: {
- dragComplete: action(emptyFunction)
+ dragComplete: action((emptyFunction))
},
- hideSource: !dropAction
+ hideSource: !dropAction && !this.Document.onDragStart
});
}
}
@@ -181,17 +156,18 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
(Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) {
e.stopPropagation();
let preventDefault = true;
- if (this._doubleTap && this.props.renderDepth) {
+ if (this._doubleTap && this.props.renderDepth && (!this.onClickHandler || !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
let fullScreenAlias = Doc.MakeAlias(this.props.Document);
- let layoutNative = await PromiseValue(Cast(this.props.Document.layoutNative, Doc));
- if (layoutNative && fullScreenAlias.layout === layoutNative.layout) {
- await swapViews(fullScreenAlias, "layoutCustom", "layoutNative");
+ if (StrCast(fullScreenAlias.layoutKey) !== "layoutCustom" && fullScreenAlias.layoutCustom !== undefined) {
+ fullScreenAlias.layoutKey = "layoutCustom";
}
this.props.addDocTab(fullScreenAlias, undefined, "inTab");
SelectionManager.DeselectAll();
Doc.UnBrushDoc(this.props.Document);
} else if (this.onClickHandler && this.onClickHandler.script) {
- this.onClickHandler.script.run({ this: this.Document.isTemplate && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log);
+ this.onClickHandler.script.run({ this: this.Document.isTemplateField && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log);
+ } else if (this.Document.type === DocumentType.BUTTON) {
+ ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", e.clientX, e.clientY);
} else if (this.Document.isButton) {
SelectionManager.SelectDoc(this, e.ctrlKey); // don't think this should happen if a button action is actually triggered.
this.buttonClick(e.altKey, e.ctrlKey);
@@ -204,9 +180,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
buttonClick = async (altKey: boolean, ctrlKey: boolean) => {
- let maximizedDocs = await DocListCastAsync(this.props.Document.maximizedDocs);
- let summarizedDocs = await DocListCastAsync(this.props.Document.summarizedDocs);
- let linkedDocs = LinkManager.Instance.getAllRelatedLinks(this.props.Document);
+ let maximizedDocs = await DocListCastAsync(this.Document.maximizedDocs);
+ let summarizedDocs = await DocListCastAsync(this.Document.summarizedDocs);
+ let linkDocs = LinkManager.Instance.getAllRelatedLinks(this.props.Document);
let expandedDocs: Doc[] = [];
expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs;
expandedDocs = summarizedDocs ? [...summarizedDocs, ...expandedDocs] : expandedDocs;
@@ -216,40 +192,23 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
let maxLocation = StrCast(this.Document.maximizeLocation, "inPlace");
maxLocation = this.Document.maximizeLocation = (!ctrlKey ? !altKey ? maxLocation : (maxLocation !== "inPlace" ? "inPlace" : "onRight") : (maxLocation !== "inPlace" ? "inPlace" : "inTab"));
if (maxLocation === "inPlace") {
- expandedDocs.forEach(maxDoc => this.props.addDocument && this.props.addDocument(maxDoc, false));
- let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(NumCast(this.Document.width) / 2, NumCast(this.Document.height) / 2);
+ expandedDocs.forEach(maxDoc => this.props.addDocument && this.props.addDocument(maxDoc));
+ let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(NumCast(this.layoutDoc.width) / 2, NumCast(this.layoutDoc.height) / 2);
DocumentManager.Instance.animateBetweenPoint(scrpt, expandedDocs);
} else {
expandedDocs.forEach(maxDoc => (!this.props.addDocTab(maxDoc, undefined, "close") && this.props.addDocTab(maxDoc, undefined, maxLocation)));
}
}
- else if (linkedDocs.length) {
- SelectionManager.DeselectAll();
- let first = linkedDocs.filter(d => Doc.AreProtosEqual(d.anchor1 as Doc, this.props.Document) && !d.anchor1anchored);
- let second = linkedDocs.filter(d => Doc.AreProtosEqual(d.anchor2 as Doc, this.props.Document) && !d.anchor2anchored);
- let firstUnshown = first.filter(d => DocumentManager.Instance.getDocumentViews(d.anchor2 as Doc).length === 0);
- let secondUnshown = second.filter(d => DocumentManager.Instance.getDocumentViews(d.anchor1 as Doc).length === 0);
- if (firstUnshown.length) first = [firstUnshown[0]];
- if (secondUnshown.length) second = [secondUnshown[0]];
- let linkedFwdDocs = first.length ? [first[0].anchor2 as Doc, first[0].anchor1 as Doc] : second.length ? [second[0].anchor1 as Doc, second[0].anchor1 as Doc] : undefined;
-
- // @TODO: shouldn't always follow target context
- let linkedFwdContextDocs = [first.length ? await (first[0].targetContext) as Doc : undefined, undefined];
- let linkedFwdPage = [first.length ? NumCast(first[0].anchor2Page, undefined) : undefined, undefined];
-
- if (linkedFwdDocs && !linkedFwdDocs.some(l => l instanceof Promise)) {
- let maxLocation = StrCast(linkedFwdDocs[0].maximizeLocation, "inTab");
- let targetContext = !Doc.AreProtosEqual(linkedFwdContextDocs[altKey ? 1 : 0], this.props.ContainingCollectionDoc) ? linkedFwdContextDocs[altKey ? 1 : 0] : undefined;
- DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, false,
- // open up target if it's not already in view ... by zooming into the button document first and setting flag to reset zoom afterwards
- doc => this.props.focus(this.props.Document, true, 1, () => this.props.addDocTab(doc, undefined, maxLocation)),
- linkedFwdPage[altKey ? 1 : 0], targetContext);
- }
+ else if (linkDocs.length) {
+ DocumentManager.Instance.FollowLink(undefined, this.props.Document,
+ // open up target if it's not already in view ... by zooming into the button document first and setting flag to reset zoom afterwards
+ (doc: Doc, maxLocation: string) => this.props.focus(this.props.Document, true, 1, () => this.props.addDocTab(doc, undefined, maxLocation)),
+ ctrlKey, altKey, this.props.ContainingCollectionDoc);
}
}
onPointerDown = (e: React.PointerEvent): void => {
- if (e.nativeEvent.cancelBubble) return;
+ if (e.nativeEvent.cancelBubble && e.button === 0) return;
this._downX = e.clientX;
this._downY = e.clientY;
this._hitTemplateDrag = false;
@@ -260,22 +219,24 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
this._hitTemplateDrag = true;
}
}
- if (this.active && e.button === 0 && !this.Document.lockedPosition) e.stopPropagation(); // events stop at the lowest document that is active. if right dragging, we let it go through though to allow for context menu clicks. PointerMove callbacks should remove themselves if the move event gets stopPropagated by a lower-level handler (e.g, marquee drag);
+ if ((this.active || this.Document.onDragStart || this.Document.onClick) && !e.ctrlKey && e.button === 0 && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation(); // events stop at the lowest document that is active. if right dragging, we let it go through though to allow for context menu clicks. PointerMove callbacks should remove themselves if the move event gets stopPropagated by a lower-level handler (e.g, marquee drag);
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
document.addEventListener("pointermove", this.onPointerMove);
document.addEventListener("pointerup", this.onPointerUp);
+ if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); }
}
onPointerMove = (e: PointerEvent): void => {
+ if ((e as any).formattedHandled) { e.stopPropagation(); return; }
if (e.cancelBubble && this.active) {
document.removeEventListener("pointermove", this.onPointerMove); // stop listening to pointerMove if something else has stopPropagated it (e.g., the MarqueeView)
}
- else if (!e.cancelBubble && (SelectionManager.IsSelected(this) || this.props.parentActive()) && !this.Document.lockedPosition) {
+ else if (!e.cancelBubble && (SelectionManager.IsSelected(this) || this.props.parentActive() || this.Document.onDragStart || this.Document.onClick) && !this.Document.lockedPosition && !this.Document.inOverlay) {
if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) {
- if (!e.altKey && !this.topMost && e.buttons === 1) {
+ if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.Document.onClick) && e.buttons === 1) {
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
- this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag);
+ this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag);
}
}
e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
@@ -292,15 +253,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@undoBatch
deleteClicked = (): void => { SelectionManager.DeselectAll(); this.props.removeDocument && this.props.removeDocument(this.props.Document); }
- @undoBatch
- static makeNativeViewClicked = async (doc: Doc): Promise<void> => swapViews(doc, "layoutNative", "layoutCustom")
+ static makeNativeViewClicked = (doc: Doc) => {
+ undoBatch(() => doc.layoutKey = "layout")();
+ }
- static makeCustomViewClicked = async (doc: Doc, dataDoc: Opt<Doc>) => {
+ static makeCustomViewClicked = (doc: Doc, dataDoc: Opt<Doc>) => {
const batch = UndoManager.StartBatch("CustomViewClicked");
if (doc.layoutCustom === undefined) {
- Doc.GetProto(dataDoc || doc).layoutNative = Doc.MakeTitled("layoutNative");
- await swapViews(doc, "", "layoutNative");
-
const width = NumCast(doc.width);
const height = NumCast(doc.height);
const options = { title: "data", width, x: -width / 2, y: - height / 2, };
@@ -316,6 +275,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
case DocumentType.VID:
fieldTemplate = Docs.Create.VideoDocument("http://www.cs.brown.edu", options);
break;
+ case DocumentType.AUDIO:
+ fieldTemplate = Docs.Create.AudioDocument("http://www.cs.brown.edu", options);
+ break;
default:
fieldTemplate = Docs.Create.ImageDocument("http://www.cs.brown.edu", options);
}
@@ -327,10 +289,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
let docTemplate = Docs.Create.FreeformDocument([fieldTemplate], { title: doc.title + "_layout", width: width + 20, height: Math.max(100, height + 45) });
Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(docTemplate), true);
- Doc.ApplyTemplateTo(docTemplate, doc, undefined);
- Doc.GetProto(dataDoc || doc).layoutCustom = Doc.MakeTitled("layoutCustom");
+ Doc.ApplyTemplateTo(docTemplate, dataDoc || doc, "layoutCustom", undefined);
} else {
- await swapViews(doc, "layoutCustom", "layoutNative");
+ doc.layoutKey = "layoutCustom";
}
batch.end();
}
@@ -352,18 +313,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
if (de.data instanceof DragManager.AnnotationDragData) {
/// this whole section for handling PDF annotations looks weird. Need to rethink this to make it cleaner
e.stopPropagation();
- let sourceDoc = de.data.annotationDocument;
- let targetDoc = this.props.Document;
- let annotations = await DocListCastAsync(sourceDoc.annotations);
- sourceDoc.linkedToDoc = true;
- de.data.targetContext = this.props.ContainingCollectionDoc;
- targetDoc.targetContext = de.data.targetContext;
- annotations && annotations.forEach(anno => anno.target = targetDoc);
-
- DocUtils.MakeLink(sourceDoc, targetDoc, this.props.ContainingCollectionDoc, `Link from ${StrCast(sourceDoc.title)}`);
+ (de.data as any).linkedToDoc = true;
+
+ DocUtils.MakeLink({ doc: de.data.annotationDocument }, { doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, `Link from ${StrCast(de.data.annotationDocument.title)}`);
}
if (de.data instanceof DragManager.DocumentDragData && de.data.applyAsTemplate) {
- Doc.ApplyTemplateTo(de.data.draggedDocuments[0], this.props.Document);
+ Doc.ApplyTemplateTo(de.data.draggedDocuments[0], this.props.Document, "layoutCustom");
e.stopPropagation();
}
if (de.data instanceof DragManager.LinkDragData) {
@@ -371,7 +326,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
// const docs = await SearchUtil.Search(`data_l:"${destDoc[Id]}"`, true);
// const views = docs.map(d => DocumentManager.Instance.getDocumentView(d)).filter(d => d).map(d => d as DocumentView);
de.data.linkSourceDocument !== this.props.Document &&
- (de.data.linkDocument = DocUtils.MakeLink(de.data.linkSourceDocument, this.props.Document, this.props.ContainingCollectionDoc, undefined, "in-text link being created")); // TODODO this is where in text links get passed
+ (de.data.linkDocument = DocUtils.MakeLink({ doc: de.data.linkSourceDocument }, { doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, "in-text link being created")); // TODODO this is where in text links get passed
}
}
@@ -379,9 +334,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
onDrop = (e: React.DragEvent) => {
let text = e.dataTransfer.getData("text/plain");
if (!e.isDefaultPrevented() && text && text.startsWith("<div")) {
- let oldLayout = StrCast(this.props.Document.layout);
+ let oldLayout = this.Document.layout || "";
let layout = text.replace("{layout}", oldLayout);
- this.props.Document.layout = layout;
+ this.Document.layout = layout;
e.stopPropagation();
e.preventDefault();
}
@@ -390,24 +345,23 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@undoBatch
@action
freezeNativeDimensions = (): void => {
- let proto = this.Document.isTemplate ? this.props.Document : Doc.GetProto(this.props.Document);
- proto.autoHeight = this.Document.autoHeight = false;
- proto.ignoreAspect = !proto.ignoreAspect;
- if (!proto.ignoreAspect && !proto.nativeWidth) {
- proto.nativeWidth = this.props.PanelWidth();
- proto.nativeHeight = this.props.PanelHeight();
+ this.layoutDoc.autoHeight = this.layoutDoc.autoHeight = false;
+ this.layoutDoc.ignoreAspect = !this.layoutDoc.ignoreAspect;
+ if (!this.layoutDoc.ignoreAspect && !this.layoutDoc.nativeWidth) {
+ this.layoutDoc.nativeWidth = this.props.PanelWidth();
+ this.layoutDoc.nativeHeight = this.props.PanelHeight();
}
}
@undoBatch
@action
makeIntoPortal = async () => {
- let anchors = await Promise.all(DocListCast(this.props.Document.links).map(async (d: Doc) => Cast(d.anchor2, Doc)));
+ let anchors = await Promise.all(DocListCast(this.Document.links).map(async (d: Doc) => Cast(d.anchor2, Doc)));
if (!anchors.find(anchor2 => anchor2 && anchor2.title === this.Document.title + ".portal" ? true : false)) {
let portalID = (this.Document.title + ".portal").replace(/^-/, "").replace(/\([0-9]*\)$/, "");
DocServer.GetRefField(portalID).then(existingPortal => {
- let portal = existingPortal instanceof Doc ? existingPortal : Docs.Create.FreeformDocument([], { width: (this.Document.width || 0) + 10, height: this.Document.height || 0, title: portalID });
- DocUtils.MakeLink(this.props.Document, portal, undefined, portalID);
+ let portal = existingPortal instanceof Doc ? existingPortal : Docs.Create.FreeformDocument([], { width: (this.layoutDoc.width || 0) + 10, height: this.layoutDoc.height || 0, title: portalID });
+ DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: portal }, portalID, "portal link");
this.Document.isButton = true;
});
}
@@ -440,10 +394,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
Doc.GetProto(this.props.Document).transcript = await DictationManager.Controls.listen({
continuous: { indefinite: true },
interimHandler: (results: string) => {
- let main = MainView.Instance;
- main.dictationSuccess = true;
- main.dictatedPhrase = results;
- main.isListening = { interim: true };
+ DictationOverlay.Instance.dictationSuccess = true;
+ DictationOverlay.Instance.dictatedPhrase = results;
+ DictationOverlay.Instance.isListening = { interim: true };
}
});
}
@@ -493,6 +446,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
});
!existingOnClick && cm.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
+ let funcs: ContextMenuProps[] = [];
+ funcs.push({ description: "Drag an Alias", icon: "edit", event: () => this.Document.dragFactory && (this.Document.onDragStart = ScriptField.MakeFunction('getAlias(this.dragFactory)')) });
+ funcs.push({ description: "Drag a Copy", icon: "edit", event: () => this.Document.dragFactory && (this.Document.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')) });
+ funcs.push({ description: "Drag Document", icon: "edit", event: () => this.Document.onDragStart = undefined });
+ ContextMenu.Instance.addItem({ description: "OnDrag...", subitems: funcs, icon: "asterisk" });
+
let existing = ContextMenu.Instance.findByDescription("Layout...");
let layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : [];
layoutItems.push({ description: this.Document.isBackground ? "As Foreground" : "As Background", event: this.makeBackground, icon: this.Document.lockedPosition ? "unlock" : "lock" });
@@ -500,14 +459,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
layoutItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc!), icon: "concierge-bell" });
}
layoutItems.push({ description: `${this.Document.chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document.chromeStatus = (this.Document.chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" });
- layoutItems.push({ description: `${this.Document.autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.Document.autoHeight = !this.Document.autoHeight, icon: "plus" });
+ layoutItems.push({ description: `${this.Document.autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc.autoHeight = !this.layoutDoc.autoHeight, icon: "plus" });
layoutItems.push({ description: this.Document.ignoreAspect || !this.Document.nativeWidth || !this.Document.nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "snowflake" });
layoutItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" });
layoutItems.push({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" });
layoutItems.push({ description: "Zoom to Document", event: () => this.props.focus(this.props.Document, true), icon: "search" });
if (this.Document.type !== DocumentType.COL && this.Document.type !== DocumentType.TEMPLATE) {
layoutItems.push({ description: "Use Custom Layout", event: () => DocumentView.makeCustomViewClicked(this.props.Document, this.props.DataDoc), icon: "concierge-bell" });
- } else if (this.props.Document.layoutNative) {
+ } else {
layoutItems.push({ description: "Use Native Layout", event: () => DocumentView.makeNativeViewClicked(this.props.Document), icon: "concierge-bell" });
}
!existing && cm.addItem({ description: "Layout...", subitems: layoutItems, icon: "compass" });
@@ -579,13 +538,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
});
}
-
- // the document containing the view layout information - will be the Document itself unless the Document has
- // a layout field. In that case, all layout information comes from there unless overriden by Document
- get layoutDoc(): Document {
- return Document(this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document);
- }
-
// does Document set a layout prop
setsLayoutProp = (prop: string) => this.props.Document[prop] !== this.props.Document["default" + prop[0].toUpperCase() + prop.slice(1)];
// get the a layout prop by first choosing the prop from Document, then falling back to the layout doc otherwise.
@@ -601,7 +553,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
return (showTitle ? 25 : 0) + 1;
}
- childScaling = () => (this.props.Document.fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling());
+ childScaling = () => (this.layoutDoc.fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling());
@computed get contents() {
return (<DocumentContentsView ContainingCollectionView={this.props.ContainingCollectionView}
ContainingCollectionDoc={this.props.ContainingCollectionDoc}
@@ -631,11 +583,23 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
isSelected={this.isSelected}
select={this.select}
onClick={this.onClickHandler}
- layoutKey="layout"
+ layoutKey={this.props.layoutKey || "layout"}
DataDoc={this.props.DataDoc} />);
}
+ linkEndpoint = (linkDoc: Doc) => Doc.LinkEndpoint(linkDoc, this.props.Document);
+
+ // used to decide whether a link document 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.
+ // would be good to generalize this some way.
+ isNonTemporalLink = (linkDoc: Doc) => {
+ let anchor = Cast(Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? linkDoc.anchor1 : linkDoc.anchor2, Doc) as Doc;
+ let ept = Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? linkDoc.anchor1Timecode : linkDoc.anchor2Timecode;
+ return anchor.type === DocumentType.AUDIO && NumCast(ept) ? false : true;
+ }
+
render() {
- let animDims = this.props.Document.animateToDimensions ? Array.from(Cast(this.props.Document.animateToDimensions, listSpec("number"))!) : undefined;
+ if (!this.props.Document) return (null);
+ const animDims = this.Document.animateToDimensions ? Array.from(this.Document.animateToDimensions) : undefined;
const ruleColor = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleColor_" + this.Document.heading]) : undefined;
const ruleRounding = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleRounding_" + this.Document.heading]) : undefined;
const colorSet = this.setsLayoutProp("backgroundColor");
@@ -644,8 +608,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
this.props.backgroundColor(this.Document) || StrCast(this.layoutDoc.backgroundColor) :
ruleColor && !colorSet ? ruleColor : StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor(this.Document);
- const nativeWidth = this.props.Document.fitWidth ? this.props.PanelWidth() : this.nativeWidth > 0 && !this.Document.ignoreAspect ? `${this.nativeWidth}px` : "100%";
- const nativeHeight = this.props.Document.fitWidth ? this.props.PanelHeight() : this.Document.ignoreAspect ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%";
+ const nativeWidth = this.layoutDoc.fitWidth ? this.props.PanelWidth() - 2 : this.nativeWidth > 0 && !this.layoutDoc.ignoreAspect ? `${this.nativeWidth}px` : "100%";
+ const nativeHeight = this.layoutDoc.fitWidth ? this.props.PanelHeight() - 2 : this.Document.ignoreAspect ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%";
const showOverlays = this.props.showOverlays ? this.props.showOverlays(this.Document) : undefined;
const showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : this.getLayoutPropStr("showTitle");
const showCaption = showOverlays && "caption" in showOverlays ? showOverlays.caption : this.getLayoutPropStr("showCaption");
@@ -662,7 +626,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
<FormattedTextBox {...this.props}
onClick={this.onClickHandler} DataDoc={this.props.DataDoc} active={returnTrue}
isSelected={this.isSelected} focus={emptyFunction} select={this.select}
- fieldExt={""} hideOnLeave={true} fieldKey={showCaption}
+ hideOnLeave={true} fieldKey={showCaption}
/>
</div>);
const titleView = (!showTitle ? (null) :
@@ -682,26 +646,31 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
let animheight = animDims ? animDims[1] : nativeHeight;
let animwidth = animDims ? animDims[0] : nativeWidth;
+ const highlightColors = ["transparent", "maroon", "maroon", "yellow", "magenta", "cyan", "orange"];
+ const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid", "solid"];
+ let highlighting = fullDegree && this.layoutDoc.type !== DocumentType.FONTICON && this.layoutDoc.viewType !== CollectionViewType.Linear;
return (
<div className={`documentView-node${this.topMost ? "-topmost" : ""}`}
ref={this._mainCont}
style={{
- transition: this.props.Document.isAnimating !== undefined ? ".5s linear" : StrCast(this.Document.transition),
+ transition: this.Document.isAnimating !== undefined ? ".5s linear" : StrCast(this.Document.transition),
pointerEvents: this.Document.isBackground && !this.isSelected() ? "none" : "all",
color: StrCast(this.Document.color),
- outlineColor: ["transparent", "maroon", "maroon", "yellow"][fullDegree],
- outlineStyle: ["none", "dashed", "solid", "solid"][fullDegree],
- outlineWidth: fullDegree && !borderRounding ? `${localScale}px` : "0px",
- border: fullDegree && borderRounding ? `${["none", "dashed", "solid", "solid"][fullDegree]} ${["transparent", "maroon", "maroon", "yellow"][fullDegree]} ${localScale}px` : undefined,
- background: backgroundColor,
+ outline: highlighting && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px",
+ border: highlighting && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined,
+ background: this.layoutDoc.type === DocumentType.FONTICON || this.layoutDoc.viewType === CollectionViewType.Linear ? undefined : backgroundColor,
width: animwidth,
height: animheight,
- transform: `scale(${this.props.Document.fitWidth ? 1 : this.props.ContentScaling()})`,
+ transform: `scale(${this.layoutDoc.fitWidth ? 1 : this.props.ContentScaling()})`,
opacity: this.Document.opacity
}}
onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
onPointerEnter={() => Doc.BrushDoc(this.props.Document)} onPointerLeave={() => Doc.UnBrushDoc(this.props.Document)}
>
+ {this.Document.links && DocListCast(this.Document.links).filter((d) => !DocListCast(this.layoutDoc.hiddenLinks).some(hidden => Doc.AreProtosEqual(hidden, d))).filter(this.isNonTemporalLink).map((d, i) =>
+ <div style={{ pointerEvents: "none", position: "absolute", transformOrigin: "top left", width: "100%", height: "100%", transform: `scale(${this.layoutDoc.fitWidth ? 1 : 1 / this.props.ContentScaling()})` }}>
+ <DocumentView {...this.props} backgroundColor={returnTransparent} Document={d} removeDocument={undoBatch((doc: Doc) => Doc.AddDocToList(this.layoutDoc, "hiddenLinks", doc))} layoutKey={this.linkEndpoint(d)} />
+ </div>)}
{!showTitle && !showCaption ?
this.Document.searchFields ?
(<div className="documentView-searchWrapper">
@@ -725,35 +694,4 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
}
-export async function swapViews(doc: Doc, newLayoutField: string, oldLayoutField: string, oldLayout?: Doc) {
- let oldLayoutExt = oldLayout || await Cast(doc[oldLayoutField], Doc);
- if (oldLayoutExt) {
- oldLayoutExt.autoHeight = doc.autoHeight;
- oldLayoutExt.width = doc.width;
- oldLayoutExt.height = doc.height;
- oldLayoutExt.nativeWidth = doc.nativeWidth;
- oldLayoutExt.nativeHeight = doc.nativeHeight;
- oldLayoutExt.ignoreAspect = doc.ignoreAspect;
- oldLayoutExt.backgroundLayout = doc.backgroundLayout;
- oldLayoutExt.type = doc.type;
- oldLayoutExt.layout = doc.layout;
- }
-
- let newLayoutExt = newLayoutField && await Cast(doc[newLayoutField], Doc);
- if (newLayoutExt) {
- doc.autoHeight = newLayoutExt.autoHeight;
- doc.width = newLayoutExt.width;
- doc.height = newLayoutExt.height;
- doc.nativeWidth = newLayoutExt.nativeWidth;
- doc.nativeHeight = newLayoutExt.nativeHeight;
- doc.ignoreAspect = newLayoutExt.ignoreAspect;
- doc.backgroundLayout = newLayoutExt.backgroundLayout;
- doc.type = newLayoutExt.type;
- doc.layout = await newLayoutExt.layout;
- }
-}
-
-Scripting.addGlobal(function toggleDetail(doc: any) {
- let native = typeof doc.layout === "string";
- swapViews(doc, native ? "layoutCustom" : "layoutNative", native ? "layoutNative" : "layoutCustom");
-}); \ No newline at end of file
+Scripting.addGlobal(function toggleDetail(doc: any) { doc.layoutKey = StrCast(doc.layoutKey, "layout") === "layout" ? "layoutCustom" : "layout"; }); \ No newline at end of file
diff --git a/src/client/views/nodes/DragBox.tsx b/src/client/views/nodes/DragBox.tsx
deleted file mode 100644
index 6c3db18c4..000000000
--- a/src/client/views/nodes/DragBox.tsx
+++ /dev/null
@@ -1,99 +0,0 @@
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faEdit } from '@fortawesome/free-regular-svg-icons';
-import { observer } from 'mobx-react';
-import * as React from 'react';
-import { Doc } from '../../../new_fields/Doc';
-import { createSchema, makeInterface } from '../../../new_fields/Schema';
-import { ScriptField } from '../../../new_fields/ScriptField';
-import { emptyFunction } from '../../../Utils';
-import { CompileScript } from '../../util/Scripting';
-import { ContextMenu } from '../ContextMenu';
-import { DocComponent } from '../DocComponent';
-import { OverlayView } from '../OverlayView';
-import { ScriptBox } from '../ScriptBox';
-import { DocumentIconContainer } from './DocumentIcon';
-import './DragBox.scss';
-import { FieldView, FieldViewProps } from './FieldView';
-import { DragManager } from '../../util/DragManager';
-import { Docs } from '../../documents/Documents';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-
-library.add(faEdit as any);
-
-const DragSchema = createSchema({
- onDragStart: ScriptField,
- text: "string"
-});
-
-type DragDocument = makeInterface<[typeof DragSchema]>;
-const DragDocument = makeInterface(DragSchema);
-@observer
-export class DragBox extends DocComponent<FieldViewProps, DragDocument>(DragDocument) {
- _downX: number = 0;
- _downY: number = 0;
- public static LayoutString() { return FieldView.LayoutString(DragBox); }
- _mainCont = React.createRef<HTMLDivElement>();
- onDragStart = (e: React.PointerEvent) => {
- if (!e.ctrlKey && !e.altKey && !e.shiftKey && !this.props.isSelected() && e.button === 0) {
- document.removeEventListener("pointermove", this.onDragMove);
- document.addEventListener("pointermove", this.onDragMove);
- document.removeEventListener("pointerup", this.onDragUp);
- document.addEventListener("pointerup", this.onDragUp);
- e.stopPropagation();
- e.preventDefault();
- }
- }
-
- onDragMove = (e: MouseEvent) => {
- if (!e.cancelBubble && (Math.abs(this._downX - e.clientX) > 5 || Math.abs(this._downY - e.clientY) > 5)) {
- document.removeEventListener("pointermove", this.onDragMove);
- document.removeEventListener("pointerup", this.onDragUp);
- const onDragStart = this.Document.onDragStart;
- e.stopPropagation();
- e.preventDefault();
- let res = onDragStart && onDragStart.script.run({ this: this.props.Document }).result;
- let doc = (res as Doc) || Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" });
- DragManager.StartDocumentDrag([this._mainCont.current!], new DragManager.DocumentDragData([doc]), e.clientX, e.clientY);
- }
- e.stopPropagation();
- e.preventDefault();
- }
-
- onDragUp = (e: MouseEvent) => {
- document.removeEventListener("pointermove", this.onDragMove);
- document.removeEventListener("pointerup", this.onDragUp);
- }
-
- onContextMenu = () => {
- ContextMenu.Instance.addItem({
- description: "Edit OnClick script", icon: "edit", event: () => {
- let overlayDisposer: () => void = emptyFunction;
- const script = this.Document.onDragStart;
- let originalText: string | undefined = undefined;
- if (script) originalText = script.script.originalScript;
- // tslint:disable-next-line: no-unnecessary-callback-wrapper
- let scriptingBox = <ScriptBox initialText={originalText} onCancel={() => overlayDisposer()} onSave={(text, onError) => {
- const script = CompileScript(text, {
- params: { this: Doc.name },
- typecheck: false,
- editable: true,
- transformer: DocumentIconContainer.getTransformer()
- });
- if (!script.compiled) {
- onError(script.errors.map(error => error.messageText).join("\n"));
- return;
- }
- this.Document.onClick = new ScriptField(script);
- overlayDisposer();
- }} showDocumentIcons />;
- overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, { x: 400, y: 200, width: 500, height: 400, title: `${this.Document.title || ""} OnDragStart` });
- }
- });
- }
-
- render() {
- return (<div className="dragBox-outerDiv" onContextMenu={this.onContextMenu} onPointerDown={this.onDragStart} ref={this._mainCont}>
- <FontAwesomeIcon className="dragBox-icon" icon="folder" size="lg" color="white" />
- </div>);
- }
-} \ No newline at end of file
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index b93c78cfd..5108954bb 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -8,8 +8,6 @@ import { List } from "../../../new_fields/List";
import { RichTextField } from "../../../new_fields/RichTextField";
import { AudioField, ImageField, VideoField } from "../../../new_fields/URLField";
import { Transform } from "../../util/Transform";
-import { CollectionPDFView } from "../collections/CollectionPDFView";
-import { CollectionVideoView } from "../collections/CollectionVideoView";
import { CollectionView } from "../collections/CollectionView";
import { AudioBox } from "./AudioBox";
import { FormattedTextBox } from "./FormattedTextBox";
@@ -26,10 +24,8 @@ import { ScriptField } from "../../../new_fields/ScriptField";
//
export interface FieldViewProps {
fieldKey: string;
- fieldExt: string;
- leaveNativeSize?: boolean;
fitToBox?: boolean;
- ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
+ ContainingCollectionView: Opt<CollectionView>;
ContainingCollectionDoc: Opt<Doc>;
ruleProvider: Doc | undefined;
Document: Doc;
@@ -38,7 +34,7 @@ export interface FieldViewProps {
isSelected: () => boolean;
select: (isCtrlPressed: boolean) => void;
renderDepth: number;
- addDocument?: (document: Doc, allowDuplicates?: boolean) => boolean;
+ addDocument?: (document: Doc) => boolean;
addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
pinToPres: (document: Doc) => void;
removeDocument?: (document: Doc) => boolean;
@@ -56,9 +52,8 @@ export interface FieldViewProps {
@observer
export class FieldView extends React.Component<FieldViewProps> {
- public static LayoutString(fieldType: { name: string }, fieldStr: string = "data", fieldExt: string = "") {
- return `<${fieldType.name} {...props} fieldKey={"${fieldStr}"} fieldExt={"${fieldExt}"} />`;
- //"<ImageBox {...props} />"
+ public static LayoutString(fieldType: { name: string }, fieldStr: string) {
+ return `<${fieldType.name} {...props} fieldKey={"${fieldStr}"}/>`; //e.g., "<ImageBox {...props} fieldKey={"dada} />"
}
@computed
@@ -78,7 +73,7 @@ export class FieldView extends React.Component<FieldViewProps> {
return <FormattedTextBox {...this.props} />;
}
else if (field instanceof ImageField) {
- return <ImageBox {...this.props} leaveNativeSize={true} />;
+ return <ImageBox {...this.props} />;
}
// else if (field instaceof PresBox) {
// return <PresBox {...this.props} />;
diff --git a/src/client/views/nodes/DragBox.scss b/src/client/views/nodes/FontIconBox.scss
index fbb9b9c1c..75d093fcb 100644
--- a/src/client/views/nodes/DragBox.scss
+++ b/src/client/views/nodes/FontIconBox.scss
@@ -1,4 +1,4 @@
-.dragBox-outerDiv {
+.fontIconBox-outerDiv {
width: 100%;
height: 100%;
pointer-events: all;
diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx
new file mode 100644
index 000000000..ae9273639
--- /dev/null
+++ b/src/client/views/nodes/FontIconBox.tsx
@@ -0,0 +1,46 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { createSchema, makeInterface } from '../../../new_fields/Schema';
+import { DocComponent } from '../DocComponent';
+import './FontIconBox.scss';
+import { FieldView, FieldViewProps } from './FieldView';
+import { StrCast } from '../../../new_fields/Types';
+import { Utils } from "../../../Utils";
+import { runInAction, observable, reaction, IReactionDisposer } from 'mobx';
+import { Doc } from '../../../new_fields/Doc';
+const FontIconSchema = createSchema({
+ icon: "string"
+});
+
+type FontIconDocument = makeInterface<[typeof FontIconSchema]>;
+const FontIconDocument = makeInterface(FontIconSchema);
+@observer
+export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>(FontIconDocument) {
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(FontIconBox, fieldKey); }
+ @observable _foregroundColor = "white";
+ _ref: React.RefObject<HTMLButtonElement> = React.createRef();
+ _backgroundReaction: IReactionDisposer | undefined;
+ componentDidMount() {
+ this._backgroundReaction = reaction(() => this.props.Document.backgroundColor,
+ () => {
+ if (this._ref && this._ref.current) {
+ let col = Utils.fromRGBAstr(getComputedStyle(this._ref.current).backgroundColor!);
+ let colsum = (col.r + col.g + col.b);
+ if (colsum / col.a > 600 || col.a < 0.25) runInAction(() => this._foregroundColor = "black");
+ else if (colsum / col.a <= 600 || col.a >= .25) runInAction(() => this._foregroundColor = "white");
+ }
+ }, { fireImmediately: true });
+ }
+ componentWillUnmount() {
+ this._backgroundReaction && this._backgroundReaction();
+ }
+ render() {
+ let referenceDoc = (this.props.Document.dragFactory instanceof Doc ? this.props.Document.dragFactory : this.props.Document);
+ let referenceLayout = Doc.Layout(referenceDoc);
+ return <button className="fontIconBox-outerDiv" title={StrCast(this.props.Document.title)} ref={this._ref}
+ style={{ background: StrCast(referenceLayout.backgroundColor), boxShadow: this.props.Document.unchecked ? undefined : `4px 4px 12px black` }}>
+ <FontAwesomeIcon className="fontIconBox-icon" icon={this.Document.icon as any} color={this._foregroundColor} size="sm" />
+ </button>;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index 45e516015..a4acd3b82 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -10,8 +10,8 @@
outline: none !important;
}
-.formattedTextBox-cont-scroll,
-.formattedTextBox-cont-hidden {
+.formattedTextBox-cont {
+ cursor: text;
background: inherit;
padding: 0;
border-width: 0px;
@@ -25,10 +25,15 @@
color: initial;
height: 100%;
pointer-events: all;
-}
-
-.formattedTextBox-cont-hidden {
- pointer-events: none;
+ overflow-y: auto;
+ max-height: 100%;
+ .formattedTextBox-dictation {
+ height: 20px;
+ width: 20px;
+ top: 0px;
+ left: 0px;
+ position: absolute;
+ }
}
.formattedTextBox-inner-rounded {
@@ -40,9 +45,10 @@
left: 10%;
}
-.formattedTextBox-inner-rounded div,
-.formattedTextBox-inner div {
+.formattedTextBox-inner-rounded ,
+.formattedTextBox-inner {
padding: 10px 10px;
+ height: 100%;
}
.menuicon {
@@ -153,17 +159,18 @@ footnote::after {
content: "...";
}
-ol { counter-reset: deci1 0;}
-.decimal1-ol {counter-reset: deci1; p { display: inline }; font-size: 24 }
-.decimal2-ol {counter-reset: deci2; p { display: inline }; font-size: 18 }
-.decimal3-ol {counter-reset: deci3; p { display: inline }; font-size: 14 }
-.decimal4-ol {counter-reset: deci4; p { display: inline }; font-size: 10 }
-.decimal5-ol {counter-reset: deci5; p { display: inline }; font-size: 10 }
-.decimal6-ol {counter-reset: deci6; p { display: inline }; font-size: 10 }
-.decimal7-ol {counter-reset: deci7; p { display: inline }; font-size: 10 }
-.upper-alpha-ol {counter-reset: ualph; p { display: inline }; font-size: 18 }
+.ProseMirror {
+ol { counter-reset: deci1 0; padding-left: 0px; }
+.decimal1-ol {counter-reset: deci1; p { display: inline }; font-size: 24 ; ul, ol { padding-left:30px; } }
+.decimal2-ol {counter-reset: deci2; p { display: inline }; font-size: 18 ; ul, ol { padding-left:30px; } }
+.decimal3-ol {counter-reset: deci3; p { display: inline }; font-size: 14 ; ul, ol { padding-left:30px; } }
+.decimal4-ol {counter-reset: deci4; p { display: inline }; font-size: 10 ; ul, ol { padding-left:30px; } }
+.decimal5-ol {counter-reset: deci5; p { display: inline }; font-size: 10 ; ul, ol { padding-left:30px; } }
+.decimal6-ol {counter-reset: deci6; p { display: inline }; font-size: 10 ; ul, ol { padding-left:30px; } }
+.decimal7-ol {counter-reset: deci7; p { display: inline }; font-size: 10 ; ul, ol { padding-left:30px; } }
+.upper-alpha-ol {counter-reset: ualph; p { display: inline }; font-size: 18; }
.lower-roman-ol {counter-reset: lroman; p { display: inline }; font-size: 14; }
-.lower-alpha-ol {counter-reset: lalpha; p { display: inline }; font-size: 10;}
+.lower-alpha-ol {counter-reset: lalpha; p { display: inline }; font-size: 10; }
.decimal1:before { content: counter(deci1) ") "; counter-increment: deci1; display:inline-block; min-width: 30;}
.decimal2:before { content: counter(deci1) "." counter(deci2) ") "; counter-increment: deci2; display:inline-block; min-width: 35}
.decimal3:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) ") "; counter-increment: deci3; display:inline-block; min-width: 35}
@@ -174,3 +181,4 @@ ol { counter-reset: deci1 0;}
.upper-alpha:before { content: counter(deci1) "." counter(ualph, upper-alpha) ") "; counter-increment: ualph; display:inline-block; min-width: 35 }
.lower-roman:before { content: counter(deci1) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) ") "; counter-increment: lroman;display:inline-block; min-width: 50 }
.lower-alpha:before { content: counter(deci1) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) "." counter(lalpha, lower-alpha) ") "; counter-increment: lalpha; display:inline-block; min-width: 35}
+}
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 449f56b16..cf55a10ba 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -1,53 +1,56 @@
import { library } from '@fortawesome/fontawesome-svg-core';
import { faEdit, faSmile, faTextHeight, faUpload } from '@fortawesome/free-solid-svg-icons';
-import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction } from "mobx";
+import _ from "lodash";
+import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction, trace } from "mobx";
import { observer } from "mobx-react";
import { baseKeymap } from "prosemirror-commands";
import { history } from "prosemirror-history";
+import { inputRules } from 'prosemirror-inputrules';
import { keymap } from "prosemirror-keymap";
-import { Fragment, Node, Node as ProsNode, NodeType, Slice, Mark, ResolvedPos } from "prosemirror-model";
-import { EditorState, Plugin, Transaction, TextSelection, NodeSelection } from "prosemirror-state";
+import { Fragment, Mark, Node, Node as ProsNode, 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 { Doc, DocListCast, Opt, WidthSym } from "../../../new_fields/Doc";
+import { Doc, DocListCastAsync, Opt, WidthSym, HeightSym } from "../../../new_fields/Doc";
import { Copy, Id } from '../../../new_fields/FieldSymbols';
-import { List } from '../../../new_fields/List';
import { RichTextField } from "../../../new_fields/RichTextField";
-import { BoolCast, Cast, NumCast, StrCast, DateCast, PromiseValue } from "../../../new_fields/Types";
+import { RichTextUtils } from '../../../new_fields/RichTextUtils';
import { createSchema, makeInterface } from "../../../new_fields/Schema";
-import { Utils, numberRange, timenow } from '../../../Utils';
+import { Cast, DateCast, NumCast, StrCast } from "../../../new_fields/Types";
+import { numberRange, Utils, addStyleSheet, addStyleSheetRule, clearStyleSheetRules } 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 { DocumentManager } from '../../util/DocumentManager';
import { DragManager } from "../../util/DragManager";
import buildKeymap from "../../util/ProsemirrorExampleTransfer";
import { inpRules } from "../../util/RichTextRules";
-import { ImageResizeView, schema, SummarizedView, OrderedListView, FootnoteView } from "../../util/RichTextSchema";
+import { FootnoteView, ImageResizeView, DashDocView, OrderedListView, schema, SummarizedView } from "../../util/RichTextSchema";
import { SelectionManager } from "../../util/SelectionManager";
import { TooltipLinkingMenu } from "../../util/TooltipLinkingMenu";
import { TooltipTextMenu } from "../../util/TooltipTextMenu";
import { undoBatch, UndoManager } from "../../util/UndoManager";
-import { DocComponent } from "../DocComponent";
+import { DocExtendableComponent } from "../DocComponent";
+import { DocumentButtonBar } from '../DocumentButtonBar';
+import { DocumentDecorations } from '../DocumentDecorations';
import { InkingControl } from "../InkingControl";
import { FieldView, FieldViewProps } from "./FieldView";
import "./FormattedTextBox.scss";
+import { FormattedTextBoxComment, formattedTextBoxCommentPlugin } from './FormattedTextBoxComment';
import React = require("react");
-import { GoogleApiClientUtils, Pulls, Pushes } from '../../apis/google_docs/GoogleApiClientUtils';
-import { DocumentDecorations } from '../DocumentDecorations';
-import { DictationManager } from '../../util/DictationManager';
-import { ReplaceStep } from 'prosemirror-transform';
-import { DocumentType } from '../../documents/DocumentTypes';
-import { RichTextUtils } from '../../../new_fields/RichTextUtils';
-import _ from "lodash";
-import { formattedTextBoxCommentPlugin, FormattedTextBoxComment } from './FormattedTextBoxComment';
-import { inputRules } from 'prosemirror-inputrules';
-import { DocumentButtonBar } from '../DocumentButtonBar';
+import { ContextMenuProps } from '../ContextMenuItem';
+import { ContextMenu } from '../ContextMenu';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { documentSchema } from '../../../new_fields/documentSchemas';
+import { AudioBox } from './AudioBox';
library.add(faEdit);
library.add(faSmile, faTextHeight, faUpload);
export interface FormattedTextBoxProps {
- isOverlay?: boolean;
hideOnLeave?: boolean;
height?: string;
color?: string;
@@ -61,16 +64,14 @@ const richTextSchema = createSchema({
export const GoogleRef = "googleDocId";
-type RichTextDocument = makeInterface<[typeof richTextSchema]>;
-const RichTextDocument = makeInterface(richTextSchema);
+type RichTextDocument = makeInterface<[typeof richTextSchema, typeof documentSchema]>;
+const RichTextDocument = makeInterface(richTextSchema, documentSchema);
type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void;
@observer
-export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) {
- public static LayoutString(fieldStr: string = "data") {
- return FieldView.LayoutString(FormattedTextBox, fieldStr);
- }
+export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) {
+ public static LayoutString(fieldStr: string) { return FieldView.LayoutString(FormattedTextBox, fieldStr); }
public static blankState = () => EditorState.create(FormattedTextBox.Instance.config);
public static Instance: FormattedTextBox;
private static _toolTipTextMenu: TooltipTextMenu | undefined = undefined;
@@ -78,8 +79,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
private _proseRef?: HTMLDivElement;
private _editorView: Opt<EditorView>;
private _applyingChange: boolean = false;
- private _linkClicked = "";
private _nodeClicked: any;
+ private _searchIndex = 0;
private _undoTyping?: UndoManager.Batch;
private _searchReactionDisposer?: Lambda;
private _scrollToRegionReactionDisposer: Opt<IReactionDisposer>;
@@ -96,9 +97,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
@observable private _fontFamily = "Arial";
@observable private _fontAlign = "";
@observable private _entered = false;
- @observable public static InputBoxOverlay?: FormattedTextBox = undefined;
public static SelectOnLoad = "";
- public static InputBoxOverlayScroll: number = 0;
public static IsFragment(html: string) {
return html.indexOf("data-pm-slice") !== -1;
}
@@ -120,8 +119,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
return "";
}
- public static getToolTip() {
- return this._toolTipTextMenu;
+ public static getToolTip(ev: EditorView) {
+ return this._toolTipTextMenu ? this._toolTipTextMenu : this._toolTipTextMenu = new TooltipTextMenu(ev);
}
@undoBatch
@@ -129,95 +128,20 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
let view = this._editorView!;
if (view.state.selection.from === view.state.selection.to) return false;
if (view.state.selection.to - view.state.selection.from > view.state.doc.nodeSize - 3) {
- this.props.Document.color = color;
+ this.layoutDoc.color = color;
}
let colorMark = view.state.schema.mark(view.state.schema.marks.pFontColor, { color: color });
view.dispatch(view.state.tr.addMark(view.state.selection.from, view.state.selection.to, colorMark));
return true;
}
- constructor(props: FieldViewProps) {
+ constructor(props: any) {
super(props);
- if (this.props.isOverlay) {
- DragManager.StartDragFunctions.push(() => FormattedTextBox.InputBoxOverlay = undefined);
- }
FormattedTextBox.Instance = this;
- this._scrollToRegionReactionDisposer = reaction(
- () => StrCast(this.props.Document.scrollToLinkID),
- async (scrollToLinkID) => {
- let findLinkFrag = (frag: Fragment, editor: EditorView) => {
- const nodes: Node[] = [];
- frag.forEach((node, index) => {
- let examinedNode = findLinkNode(node, editor);
- if (examinedNode && examinedNode.textContent) {
- nodes.push(examinedNode);
- start += index;
- }
- });
- return { frag: Fragment.fromArray(nodes), start: start };
- };
- let findLinkNode = (node: Node, editor: EditorView) => {
- if (!node.isText) {
- const content = findLinkFrag(node.content, editor);
- return node.copy(content.frag);
- }
- const marks = [...node.marks];
- const linkIndex = marks.findIndex(mark => mark.type === editor.state.schema.marks.link);
- return linkIndex !== -1 && scrollToLinkID === marks[linkIndex].attrs.href.replace(/.*\/doc\//, "") ? node : undefined;
- };
-
- let start = -1;
- if (this._editorView && scrollToLinkID) {
- let editor = this._editorView;
- let ret = findLinkFrag(editor.state.doc.content, editor);
-
- if (ret.frag.size > 2 && ((!this.props.isOverlay && !this.props.isSelected()) || (this.props.isSelected() && this.props.isOverlay))) {
- let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start
- if (ret.frag.firstChild) {
- selection = TextSelection.between(editor.state.doc.resolve(ret.start + 2), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected
- }
- editor.dispatch(editor.state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView());
- const mark = editor.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
- setTimeout(() => editor.dispatch(editor.state.tr.addMark(selection.from, selection.to, mark)), 0);
- setTimeout(() => this.unhighlightSearchTerms(), 2000);
- }
- this.props.Document.scrollToLinkID = undefined;
- }
-
- },
- { fireImmediately: true }
- );
}
public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
- @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
-
- @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplate ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
-
- // this should be internal to prosemirror, but is needed
- // here to make sure that footnote view nodes in the overlay editor
- // get removed when they're not selected.
-
- syncNodeSelection(view: any, sel: any) {
- if (sel instanceof NodeSelection) {
- var desc = view.docView.descAt(sel.from);
- if (desc !== view.lastSelectedViewDesc) {
- if (view.lastSelectedViewDesc) {
- view.lastSelectedViewDesc.deselectNode();
- view.lastSelectedViewDesc = null;
- }
- if (desc) { desc.selectNode(); }
- view.lastSelectedViewDesc = desc;
- }
- } else {
- if (view.lastSelectedViewDesc) {
- view.lastSelectedViewDesc.deselectNode();
- view.lastSelectedViewDesc = null;
- }
- }
- }
-
linkOnDeselect: Map<string, string> = new Map();
doLinkOnDeselect() {
@@ -230,7 +154,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
this.dataDoc[key] = doc || Docs.Create.FreeformDocument([], { title: value, width: 500, height: 500 }, value);
DocUtils.Publish(this.dataDoc[key] as Doc, value, this.props.addDocument, this.props.removeDocument);
if (linkDoc) { (linkDoc as Doc).anchor2 = this.dataDoc[key] as Doc; }
- else DocUtils.MakeLink(this.dataDoc, this.dataDoc[key] as Doc, undefined, "Ref:" + value, undefined, undefined, id, true);
+ else DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: this.dataDoc[key] as Doc }, "Ref:" + value, "link to named target", id);
});
});
});
@@ -263,11 +187,12 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
const state = this._editorView.state.apply(tx);
this._editorView.updateState(state);
- this.syncNodeSelection(this._editorView, this._editorView.state.selection); // bcz: ugh -- shouldn't be needed but without this the overlay view's footnote popup doesn't get deselected
if (state.selection.empty && FormattedTextBox._toolTipTextMenu && tx.storedMarks) {
FormattedTextBox._toolTipTextMenu.mark_key_pressed(tx.storedMarks);
}
+ let tsel = this._editorView.state.selection.$from;
+ tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 5000 - 1000)));
this._applyingChange = true;
this.extensionDoc && (this.extensionDoc.text = state.doc.textBetween(0, state.doc.content.size, "\n\n"));
this.extensionDoc && (this.extensionDoc.lastModified = new DateField(new Date(Date.now())));
@@ -286,44 +211,33 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
- public highlightSearchTerms = (terms: String[]) => {
+ public highlightSearchTerms = (terms: string[]) => {
if (this._editorView && (this._editorView as any).docView) {
- const doc = this._editorView.state.doc;
const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
- doc.nodesBetween(0, doc.content.size, (node: ProsNode, pos: number, parent: ProsNode, index: number) => {
- if (node.isLeaf && node.isText && node.text) {
- let nodeText: String = node.text;
- let tokens = nodeText.split(" ");
- let start = pos;
- tokens.forEach((word) => {
- if (terms.includes(word) && this._editorView) {
- this._editorView.dispatch(this._editorView.state.tr.addMark(start, start + word.length, mark).removeStoredMark(mark));
- }
- start += word.length + 1;
- });
- }
- });
+ const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true });
+ let res = terms.map(term => this.findInNode(this._editorView!, this._editorView!.state.doc, term));
+ let tr = this._editorView.state.tr;
+ let flattened: TextSelection[] = [];
+ res.map(r => r.map(h => flattened.push(h)));
+ let lastSel = Math.min(flattened.length - 1, this._searchIndex);
+ flattened.forEach((h: TextSelection, ind: number) => tr = tr.addMark(h.from, h.to, ind === lastSel ? activeMark : mark));
+ this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex;
+ this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(flattened[lastSel].from), tr.doc.resolve(flattened[lastSel].to))).scrollIntoView());
}
}
public unhighlightSearchTerms = () => {
if (this._editorView && (this._editorView as any).docView) {
- const doc = this._editorView.state.doc;
const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
- doc.nodesBetween(0, doc.content.size, (node: ProsNode, pos: number, parent: ProsNode, index: number) => {
- if (node.isLeaf && node.isText && node.text) {
- if (node.marks.includes(mark) && this._editorView) {
- this._editorView.dispatch(this._editorView.state.tr.removeMark(pos, pos + node.nodeSize, mark));
- }
- }
- });
+ const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true });
+ let end = this._editorView.state.doc.nodeSize - 2;
+ this._editorView.dispatch(this._editorView.state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark));
}
}
setAnnotation = (start: number, end: number, mark: Mark, opened: boolean, keep: boolean = false) => {
let view = this._editorView!;
- let mid = view.state.doc.resolve(Math.round((start + end) / 2));
let nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: keep ? Doc.CurrentUserEmail : mark.attrs.userid, opened: opened });
- view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark).setSelection(new TextSelection(mid)));
+ view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark));
}
protected createDropTarget = (ele: HTMLDivElement) => {
this._proseRef = ele;
@@ -334,48 +248,157 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
@undoBatch
@action
drop = async (e: Event, de: DragManager.DropEvent) => {
- // We're dealing with a link to a document
- if (de.data instanceof DragManager.EmbedDragData && de.data.urlField) {
- let target = de.data.embeddableSourceDoc;
- // We're dealing with an internal document drop
- let url = de.data.urlField.url.href;
- let model: NodeType = [".mov", ".mp4"].includes(url) ? schema.nodes.video : schema.nodes.image;
- let pos = this._editorView!.posAtCoords({ left: de.x, top: de.y });
- this._editorView!.dispatch(this._editorView!.state.tr.insert(pos!.pos, model.create({ src: url, docid: target[Id] })));
- DocUtils.MakeLink(this.dataDoc, target, undefined, "ImgRef:" + target.title, undefined, undefined, target[Id]);
- this.tryUpdateHeight();
- e.stopPropagation();
- } else if (de.data instanceof DragManager.DocumentDragData) {
+ if (de.data instanceof DragManager.DocumentDragData) {
const draggedDoc = de.data.draggedDocuments.length && de.data.draggedDocuments[0];
- if (draggedDoc && draggedDoc.type === DocumentType.TEXT && !Doc.AreProtosEqual(draggedDoc, this.props.Document)) {
- if (de.mods === "AltKey") {
- if (draggedDoc.data instanceof RichTextField) {
- Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new RichTextField(draggedDoc.data.Data);
- e.stopPropagation();
- }
- } else {
- draggedDoc.isTemplate = true;
- if (typeof (draggedDoc.layout) === "string") {
- let layoutDelegateToOverrideFieldKey = Doc.MakeDelegate(draggedDoc);
- layoutDelegateToOverrideFieldKey.layout = StrCast(layoutDelegateToOverrideFieldKey.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${this.props.fieldKey}"}`);
- this.props.Document.layout = layoutDelegateToOverrideFieldKey;
- } else {
- this.props.Document.layout = draggedDoc.layout instanceof Doc ? draggedDoc.layout : draggedDoc;
- }
+ // replace text contents whend dragging with Alt
+ if (draggedDoc && draggedDoc.type === DocumentType.TEXT && !Doc.AreProtosEqual(draggedDoc, this.props.Document) && de.mods === "AltKey") {
+ if (draggedDoc.data instanceof RichTextField) {
+ Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new RichTextField(draggedDoc.data.Data);
+ e.stopPropagation();
}
+ // apply as template when dragging with Meta
+ } else if (draggedDoc && draggedDoc.type === DocumentType.TEXT && !Doc.AreProtosEqual(draggedDoc, this.props.Document) && de.mods === "MetaKey") {
+ draggedDoc.isTemplateDoc = true;
+ let newLayout = Doc.Layout(draggedDoc);
+ if (typeof (draggedDoc.layout) === "string") {
+ newLayout = Doc.MakeDelegate(draggedDoc);
+ newLayout.layout = StrCast(newLayout.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${this.props.fieldKey}"}`);
+ }
+ this.Document.layoutCustom = newLayout;
+ this.Document.layoutKey = "layoutCustom";
e.stopPropagation();
+ // embed document when dragging with a userDropAction or an embedDoc flag set
+ } else if (de.data.userDropAction || de.data.embedDoc) {
+ let target = de.data.droppedDocuments[0];
+ const link = DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "Embedded Doc:" + target.title);
+ if (link) {
+ target.fitToBox = true;
+ let node = schema.nodes.dashDoc.create({
+ width: target[WidthSym](), height: target[HeightSym](),
+ title: "dashDoc", docid: target[Id],
+ float: "right"
+ });
+ let view = this._editorView!;
+ view.dispatch(view.state.tr.insert(view.posAtCoords({ left: de.x, top: de.y })!.pos, node));
+ this.tryUpdateHeight();
+ e.stopPropagation();
+ }
+ } // otherwise, fall through to outer collection to handle drop
+ }
+ }
+
+ getNodeEndpoints(context: Node, node: Node): { from: number, to: number } | null {
+ let offset = 0;
+
+ if (context === node) return { from: offset, to: offset + node.nodeSize };
+
+ if (node.isBlock) {
+ for (let i = 0; i < (context.content as any).content.length; i++) {
+ let result = this.getNodeEndpoints((context.content as any).content[i], node);
+ if (result) {
+ return {
+ from: result.from + offset + (context.type.name === "doc" ? 0 : 1),
+ to: result.to + offset + (context.type.name === "doc" ? 0 : 1)
+ };
+ }
+ offset += (context.content as any).content[i].nodeSize;
}
+ return null;
+ } else {
+ return null;
}
}
- recordKeyHandler = (e: KeyboardEvent) => {
- if (SelectionManager.SelectedDocuments().length && this.props.Document === SelectionManager.SelectedDocuments()[0].props.Document) {
- if (e.key === "R" && e.altKey) {
- e.stopPropagation();
- e.preventDefault();
- this.recordBullet();
+
+ //Recursively finds matches within a given node
+ findInNode(pm: EditorView, node: Node, find: string) {
+ let ret: TextSelection[] = [];
+
+ if (node.isTextblock) {
+ let index = 0, foundAt, ep = this.getNodeEndpoints(pm.state.doc, node);
+ while (ep && (foundAt = node.textContent.slice(index).search(RegExp(find, "i"))) > -1) {
+ let sel = new TextSelection(pm.state.doc.resolve(ep.from + index + foundAt + 1), pm.state.doc.resolve(ep.from + index + foundAt + find.length + 1));
+ ret.push(sel);
+ index = index + foundAt + find.length;
}
+ } else {
+ node.content.forEach((child, i) => ret = ret.concat(this.findInNode(pm, child, find)));
}
+ return ret;
+ }
+ static _highlights: string[] = [];
+
+ updateHighlights = () => {
+ clearStyleSheetRules(FormattedTextBox._userStyleSheet);
+ if (FormattedTextBox._highlights.indexOf("Text from Others") !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-remote", { background: "yellow" });
+ }
+ if (FormattedTextBox._highlights.indexOf("My Text") !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { background: "moccasin" });
+ }
+ if (FormattedTextBox._highlights.indexOf("Todo Items") !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "todo", { outline: "black solid 1px" });
+ }
+ if (FormattedTextBox._highlights.indexOf("Important Items") !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "important", { "font-size": "larger" });
+ }
+ if (FormattedTextBox._highlights.indexOf("Disagree Items") !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "disagree", { "text-decoration": "line-through" });
+ }
+ if (FormattedTextBox._highlights.indexOf("Ignore Items") !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "ignore", { "font-size": "0" });
+ }
+ if (FormattedTextBox._highlights.indexOf("By Recent Minute") !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" });
+ let min = Math.round(Date.now() / 1000 / 60);
+ numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-min-" + (min - i), { opacity: ((10 - i - 1) / 10).toString() }));
+ setTimeout(() => this.updateHighlights());
+ }
+ if (FormattedTextBox._highlights.indexOf("By Recent Hour") !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" });
+ let hr = Math.round(Date.now() / 1000 / 60 / 60);
+ numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-hr-" + (hr - i), { opacity: ((10 - i - 1) / 10).toString() }));
+ }
+ }
+
+ specificContextMenu = (e: React.MouseEvent): void => {
+ let funcs: ContextMenuProps[] = [];
+ funcs.push({ description: "Record Bullet", event: () => { e.stopPropagation(); this.recordBullet(); }, icon: "expand-arrows-alt" });
+ ["My Text", "Text from Others", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option =>
+ funcs.push({
+ description: (FormattedTextBox._highlights.indexOf(option) === -1 ? "Highlight " : "Unhighlight ") + option, event: () => {
+ e.stopPropagation();
+ if (FormattedTextBox._highlights.indexOf(option) === -1) {
+ FormattedTextBox._highlights.push(option);
+ } else {
+ FormattedTextBox._highlights.splice(FormattedTextBox._highlights.indexOf(option), 1);
+ }
+ this.updateHighlights();
+ }, icon: "expand-arrows-alt"
+ }));
+
+ ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: funcs, icon: "asterisk" });
+ }
+
+ @observable _recording = false;
+
+ recordDictation = () => {
+ //this._editorView!.focus();
+ if (this._recording) return;
+ runInAction(() => this._recording = true);
+ DictationManager.Controls.listen({
+ interimHandler: this.setCurrentBulletContent,
+ continuous: { indefinite: false },
+ }).then(results => {
+ if (results && [DictationManager.Controls.Infringed].includes(results)) {
+ DictationManager.Controls.stop();
+ }
+ this._editorView!.focus();
+ });
+ }
+ stopDictation = (abort: boolean) => {
+ runInAction(() => this._recording = false);
+ DictationManager.Controls.stop(!abort);
}
recordBullet = async () => {
@@ -408,12 +431,14 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
nextBullet = (pos: number) => {
if (this._editorView) {
let frag = Fragment.fromArray(this.newListItems(2));
- let slice = new Slice(frag, 2, 2);
- let state = this._editorView.state;
- this._editorView.dispatch(state.tr.step(new ReplaceStep(pos, pos, slice)));
- pos += 4;
- state = this._editorView.state;
- this._editorView.dispatch(state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos, pos)));
+ if (this._editorView.state.doc.resolve(pos).depth >= 2) {
+ let slice = new Slice(frag, 2, 2);
+ let state = this._editorView.state;
+ this._editorView.dispatch(state.tr.step(new ReplaceStep(pos, pos, slice)));
+ pos += 4;
+ state = this._editorView.state;
+ this._editorView.dispatch(state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos, pos)));
+ }
}
}
@@ -424,9 +449,10 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
_keymap: any = undefined;
@computed get config() {
this._keymap = buildKeymap(schema);
+ (schema as any).Document = this.props.Document;
return {
schema,
- plugins: this.props.isOverlay ? [
+ plugins: [
inputRules(inpRules),
this.tooltipTextMenuPlugin(),
history(),
@@ -439,26 +465,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}),
formattedTextBoxCommentPlugin
- ] : [
- history(),
- keymap(this._keymap),
- keymap(baseKeymap),
- ]
+ ]
};
}
componentDidMount() {
-
- if (!this.props.isOverlay) {
- this._proxyReactionDisposer = reaction(() => this.props.isSelected(),
- () => {
- if (this.props.isSelected()) {
- FormattedTextBox.InputBoxOverlay = this;
- FormattedTextBox.InputBoxOverlayScroll = this._ref.current!.scrollTop;
- }
- }, { fireImmediately: true });
- }
-
this.pullFromGoogleDoc(this.checkState);
this.dataDoc[GoogleRef] && this.dataDoc.unchanged && runInAction(() => DocumentDecorations.Instance.isAnimatingFetch = true);
@@ -498,14 +509,14 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
);
this._heightReactionDisposer = reaction(
- () => this.props.Document[WidthSym](),
+ () => [this.layoutDoc[WidthSym](), this.layoutDoc.autoHeight],
() => this.tryUpdateHeight()
);
this._textReactionDisposer = reaction(
() => this.extensionDoc,
() => {
- if (this.dataDoc.text || this.dataDoc.lastModified) {
+ if (this.extensionDoc && (this.dataDoc.text || this.dataDoc.lastModified)) {
this.extensionDoc.text = this.dataDoc.text;
this.extensionDoc.lastModified = DateCast(this.dataDoc.lastModified)[Copy]();
this.dataDoc.text = undefined;
@@ -517,13 +528,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
this.setupEditor(this.config, this.dataDoc, this.props.fieldKey);
this._searchReactionDisposer = reaction(() => {
- return StrCast(this.props.Document.search_string);
+ return StrCast(this.layoutDoc.search_string);
}, searchString => {
- const fieldkey = 'preview';
- let preview = false;
- // if (!this._editorView && Object.keys(this.props.Document).indexOf(fieldkey) !== -1) {
- // preview = true;
- // }
if (searchString) {
this.highlightSearchTerms([searchString]);
}
@@ -535,7 +541,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
this._rulesReactionDisposer = reaction(() => {
let ruleProvider = this.props.ruleProvider;
- let heading = NumCast(this.props.Document.heading);
+ let heading = NumCast(this.layoutDoc.heading);
if (ruleProvider instanceof Doc) {
return {
align: StrCast(ruleProvider["ruleAlign_" + heading], ""),
@@ -547,7 +553,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
},
action((rules: any) => {
this._fontFamily = rules ? rules.font : "Arial";
- this._fontSize = rules ? rules.size : 13;
+ this._fontSize = rules ? rules.size : NumCast(this.layoutDoc.fontSize, 13);
rules && setTimeout(() => {
const view = this._editorView!;
if (this._proseRef) {
@@ -563,6 +569,51 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}, 0);
}), { fireImmediately: true }
);
+ this._scrollToRegionReactionDisposer = reaction(
+ () => StrCast(this.layoutDoc.scrollToLinkID),
+ async (scrollToLinkID) => {
+ let findLinkFrag = (frag: Fragment, editor: EditorView) => {
+ const nodes: Node[] = [];
+ frag.forEach((node, index) => {
+ let examinedNode = findLinkNode(node, editor);
+ if (examinedNode && examinedNode.textContent) {
+ nodes.push(examinedNode);
+ start += index;
+ }
+ });
+ return { frag: Fragment.fromArray(nodes), start: start };
+ };
+ let findLinkNode = (node: Node, editor: EditorView) => {
+ if (!node.isText) {
+ const content = findLinkFrag(node.content, editor);
+ return node.copy(content.frag);
+ }
+ const marks = [...node.marks];
+ const linkIndex = marks.findIndex(mark => mark.type === editor.state.schema.marks.link);
+ return linkIndex !== -1 && scrollToLinkID === marks[linkIndex].attrs.href.replace(/.*\/doc\//, "") ? node : undefined;
+ };
+
+ let start = -1;
+ if (this._editorView && scrollToLinkID) {
+ let editor = this._editorView;
+ let ret = findLinkFrag(editor.state.doc.content, editor);
+
+ if (ret.frag.size > 2 && ret.start >= 0) {
+ let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start
+ if (ret.frag.firstChild) {
+ selection = TextSelection.between(editor.state.doc.resolve(ret.start + 2), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected
+ }
+ editor.dispatch(editor.state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView());
+ const mark = editor.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
+ setTimeout(() => editor.dispatch(editor.state.tr.addMark(selection.from, selection.to, mark)), 0);
+ setTimeout(() => this.unhighlightSearchTerms(), 2000);
+ }
+ Doc.SetInPlace(this.layoutDoc, "scrollToLinkID", undefined, false);
+ }
+
+ },
+ { fireImmediately: true }
+ );
setTimeout(() => this.tryUpdateHeight(), 0);
}
@@ -667,55 +718,45 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
handlePaste = (view: EditorView, event: Event, slice: Slice): boolean => {
let cbe = event as ClipboardEvent;
- let docId: string;
- let regionId: string;
- if (!cbe.clipboardData) {
- return false;
- }
- let linkId: string;
- docId = cbe.clipboardData.getData("dash/pdfOrigin");
- regionId = cbe.clipboardData.getData("dash/pdfRegion");
- if (!docId || !regionId) {
- return false;
- }
-
- DocServer.GetRefField(docId).then(doc => {
- DocServer.GetRefField(regionId).then(region => {
- if (!(doc instanceof Doc) || !(region instanceof Doc)) {
- return;
- }
-
- let annotations = DocListCast(region.annotations);
- annotations.forEach(anno => anno.target = this.props.Document);
- let fieldExtDoc = Doc.fieldExtensionDoc(doc, "data");
- let targetAnnotations = DocListCast(fieldExtDoc.annotations);
- if (targetAnnotations) {
- targetAnnotations.push(region);
- fieldExtDoc.annotations = new List<Doc>(targetAnnotations);
- }
+ const pdfDocId = cbe.clipboardData && cbe.clipboardData.getData("dash/pdfOrigin");
+ const pdfRegionId = cbe.clipboardData && cbe.clipboardData.getData("dash/pdfRegion");
+ if (pdfDocId && pdfRegionId) {
+ DocServer.GetRefField(pdfDocId).then(pdfDoc => {
+ DocServer.GetRefField(pdfRegionId).then(pdfRegion => {
+ if ((pdfDoc instanceof Doc) && (pdfRegion instanceof Doc)) {
+ setTimeout(async () => {
+ const extension = Doc.fieldExtensionDoc(pdfDoc, "data");
+ if (extension) {
+ let targetAnnotations = await DocListCastAsync(extension.annotations);// bcz: NO... this assumes the pdf is using its 'data' field. need to have the PDF's view handle updating its own annotations
+ targetAnnotations && targetAnnotations.push(pdfRegion);
+ }
+ });
- let link = DocUtils.MakeLink(this.props.Document, region, doc);
- if (link) {
- cbe.clipboardData!.setData("dash/linkDoc", link[Id]);
- linkId = link[Id];
- let frag = addMarkToFrag(slice.content, (node: Node) => addLinkMark(node, StrCast(doc.title)));
- slice = new Slice(frag, slice.openStart, slice.openEnd);
- var tr = view.state.tr.replaceSelection(slice);
- view.dispatch(tr.scrollIntoView().setMeta("paste", true).setMeta("uiEvent", "paste"));
- }
+ let link = DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: pdfRegion, ctx: pdfDoc }, "note on " + pdfDoc.title, "pasted PDF link");
+ if (link) {
+ cbe.clipboardData!.setData("dash/linkDoc", link[Id]);
+ let linkId = link[Id];
+ let frag = addMarkToFrag(slice.content, (node: Node) => addLinkMark(node, StrCast(pdfDoc.title), linkId));
+ slice = new Slice(frag, slice.openStart, slice.openEnd);
+ var tr = view.state.tr.replaceSelection(slice);
+ view.dispatch(tr.scrollIntoView().setMeta("paste", true).setMeta("uiEvent", "paste"));
+ }
+ }
+ });
});
- });
+ return true;
+ }
+ return false;
- return true;
function addMarkToFrag(frag: Fragment, marker: (node: Node) => Node) {
const nodes: Node[] = [];
frag.forEach(node => nodes.push(marker(node)));
return Fragment.fromArray(nodes);
}
- function addLinkMark(node: Node, title: string) {
+ function addLinkMark(node: Node, title: string, linkId: string) {
if (!node.isText) {
- const content = addMarkToFrag(node.content, (node: Node) => addLinkMark(node, title));
+ const content = addMarkToFrag(node.content, (node: Node) => addLinkMark(node, title, linkId));
return node.copy(content);
}
const marks = [...node.marks];
@@ -754,6 +795,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
},
dispatchTransaction: this.dispatchTransaction,
nodeViews: {
+ dashDoc(node, view, getPos) { return new DashDocView(node, view, getPos, self); },
image(node, view, getPos) { return new ImageResizeView(node, view, getPos, self.props.addDocTab); },
star(node, view, getPos) { return new SummarizedView(node, view, getPos); },
ordered_list(node, view, getPos) { return new OrderedListView(); },
@@ -762,7 +804,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
clipboardTextSerializer: this.clipboardTextSerializer,
handlePaste: this.handlePaste,
});
- (this._editorView as any).isOverlay = this.props.isOverlay;
if (startup) {
Doc.GetProto(doc).documentText = undefined;
this._editorView.dispatch(this._editorView.state.tr.insertText(startup));
@@ -774,9 +815,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
FormattedTextBox.SelectOnLoad = "";
this.props.select(false);
}
- else if (this.props.isOverlay) this._editorView!.focus();
+ this._editorView!.focus();
// add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet.
- this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })];
+ this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.round(Date.now() / 1000 / 5) })];
}
getFont(font: string) {
switch (font) {
@@ -803,8 +844,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
this._searchReactionDisposer && this._searchReactionDisposer();
this._editorView && this._editorView.destroy();
}
-
-
onPointerDown = (e: React.PointerEvent): void => {
let pos = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
pos && (this._nodeClicked = this._editorView!.state.doc.nodeAt(pos.pos));
@@ -814,31 +853,25 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
if (e.button === 0 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) {
e.stopPropagation();
}
- let ctrlKey = e.ctrlKey;
if (e.button === 2 || (e.button === 0 && e.ctrlKey)) {
e.preventDefault();
}
}
onPointerUp = (e: React.PointerEvent): void => {
- FormattedTextBoxComment.textBox = this;
+ if (!(e.nativeEvent as any).formattedHandled) { FormattedTextBoxComment.textBox = this; }
+ (e.nativeEvent as any).formattedHandled = true;
+
if (e.buttons === 1 && this.props.isSelected() && !e.altKey) {
e.stopPropagation();
}
}
+ static InputBoxOverlay: FormattedTextBox | undefined;
@action
onFocused = (e: React.FocusEvent): void => {
- document.removeEventListener("keypress", this.recordKeyHandler);
- document.addEventListener("keypress", this.recordKeyHandler);
+ FormattedTextBox.InputBoxOverlay = this;
this.tryUpdateHeight();
- if (!this.props.isOverlay) {
- FormattedTextBox.InputBoxOverlay = this;
- } else {
- if (this._ref.current) {
- this._ref.current.scrollTop = FormattedTextBox.InputBoxOverlayScroll;
- }
- }
}
onPointerWheel = (e: React.WheelEvent): void => {
// if a text note is not selected and scrollable, this prevents us from being able to scroll and zoom out at the same time
@@ -847,17 +880,18 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
+ static _bulletStyleSheet: any = addStyleSheet();
+ static _userStyleSheet: any = addStyleSheet();
+
onClick = (e: React.MouseEvent): void => {
- let ctrlKey = e.ctrlKey;
+ if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); return; }
+ (e.nativeEvent as any).formattedHandled = true;
if (e.button === 0 && ((!this.props.isSelected() && !e.ctrlKey) || (this.props.isSelected() && e.ctrlKey)) && !e.metaKey && e.target) {
let href = (e.target as any).href;
let location: string;
if ((e.target as any).attributes.location) {
location = (e.target as any).attributes.location.value;
}
- for (let parent = (e.target as any).parentNode; !href && parent; parent = parent.parentNode) {
- href = parent.childNodes[0].href ? parent.childNodes[0].href : parent.href;
- }
let pcords = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
let node = pcords && this._editorView!.state.doc.nodeAt(pcords.pos);
if (node) {
@@ -869,57 +903,43 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
if (href) {
if (href.indexOf(Utils.prepend("/doc/")) === 0) {
- this._linkClicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
- if (this._linkClicked) {
- DocServer.GetRefField(this._linkClicked).then(async linkDoc => {
- if (linkDoc instanceof Doc) {
- let proto = Doc.GetProto(linkDoc);
- let targetContext = await Cast(proto.targetContext, Doc);
- let jumpToDoc = await Cast(linkDoc.anchor2, Doc);
-
- if (jumpToDoc) {
- if (DocumentManager.Instance.getDocumentView(jumpToDoc)) {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, undefined, undefined, NumCast((jumpToDoc === linkDoc.anchor2 ? linkDoc.anchor2Page : linkDoc.anchor1Page)));
- return;
- }
- }
- if (targetContext && (!jumpToDoc || targetContext !== await jumpToDoc.annotationOn)) {
- DocumentManager.Instance.jumpToDocument(jumpToDoc || targetContext, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab"), undefined, targetContext);
- } else if (jumpToDoc) {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab"));
- } else {
- DocumentManager.Instance.jumpToDocument(linkDoc, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab"));
- }
- }
+ let linkClicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+ if (linkClicked) {
+ DocServer.GetRefField(linkClicked).then(async linkDoc => {
+ (linkDoc instanceof Doc) &&
+ DocumentManager.Instance.FollowLink(linkDoc, this.props.Document, document => this.props.addDocTab(document, undefined, location ? location : "inTab"), false);
});
- e.stopPropagation();
- e.preventDefault();
}
} else {
- let webDoc = Docs.Create.WebDocument(href, { x: NumCast(this.props.Document.x, 0) + NumCast(this.props.Document.width, 0), y: NumCast(this.props.Document.y) });
+ let webDoc = Docs.Create.WebDocument(href, { x: NumCast(this.layoutDoc.x, 0) + NumCast(this.layoutDoc.width, 0), y: NumCast(this.layoutDoc.y) });
this.props.addDocument && this.props.addDocument(webDoc);
- this._linkClicked = webDoc[Id];
}
e.stopPropagation();
e.preventDefault();
}
-
}
- // this hackiness handles clicking on the list item bullets to do expand/collapse. the bullets are ::before pseudo elements so there's no real way to hit test against them.
- if (this.props.isSelected() && e.nativeEvent.offsetX < 40) {
- let pos = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
+
+ this.hitBulletTargets(e.clientX, e.clientY, e.nativeEvent.offsetX, e.shiftKey);
+ if (this._recording) setTimeout(() => { this.stopDictation(true); setTimeout(() => this.recordDictation(), 500); }, 500);
+ }
+
+ // this hackiness handles clicking on the list item bullets to do expand/collapse. the bullets are ::before pseudo elements so there's no real way to hit test against them.
+ hitBulletTargets(x: number, y: number, offsetX: number, select: boolean = false) {
+ clearStyleSheetRules(FormattedTextBox._bulletStyleSheet);
+ if (this.props.isSelected() && offsetX < 40) {
+ let pos = this._editorView!.posAtCoords({ left: x, top: y });
if (pos && pos.pos > 0) {
let node = this._editorView!.state.doc.nodeAt(pos.pos);
let node2 = node && node.type === schema.nodes.paragraph ? this._editorView!.state.doc.nodeAt(pos.pos - 1) : undefined;
if (node === this._nodeClicked && node2 && (node2.type === schema.nodes.ordered_list || node2.type === schema.nodes.list_item)) {
- let hit = this._editorView!.domAtPos(pos.pos).node as any;
- let beforeEle = document.querySelector("." + hit.className) as Element;
- let before = beforeEle ? window.getComputedStyle(beforeEle, ':before') : undefined;
+ let hit = this._editorView!.domAtPos(pos.pos).node as any; // let beforeEle = document.querySelector("." + hit.className) as Element;
+ let before = hit ? window.getComputedStyle(hit, ':before') : undefined;
let beforeWidth = before ? Number(before.getPropertyValue('width').replace("px", "")) : undefined;
- if (beforeWidth && e.nativeEvent.offsetX < beforeWidth) {
+ if (beforeWidth && offsetX < beforeWidth) {
let ol = this._editorView!.state.doc.nodeAt(pos.pos - 2) ? this._editorView!.state.doc.nodeAt(pos.pos - 2) : undefined;
- if (ol && ol.type === schema.nodes.ordered_list && !e.shiftKey) {
+ if (ol && ol.type === schema.nodes.ordered_list && select) {
this._editorView!.dispatch(this._editorView!.state.tr.setSelection(new NodeSelection(this._editorView!.state.doc.resolve(pos.pos - 2))));
+ addStyleSheetRule(FormattedTextBox._bulletStyleSheet, hit.className + ":before", { background: "gray" });
} else {
this._editorView!.dispatch(this._editorView!.state.tr.setNodeMarkup(pos.pos - 1, node2.type, { ...node2.attrs, visibility: !node2.attrs.visibility }));
}
@@ -927,25 +947,30 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
}
- this._proseRef!.focus();
- if (this._linkClicked) {
- this._linkClicked = "";
- e.preventDefault();
- e.stopPropagation();
- }
}
- onMouseDown = (e: React.MouseEvent): void => {
- if (!this.props.isSelected()) { // preventing default allows the onClick to be generated instead of being swallowed by the text box itself
- e.preventDefault(); // bcz: this would normally be in OnPointerDown - however, if done there, no mouse move events will be generated which makes transititioning to GoldenLayout's drag interactions impossible
+ onMouseUp = (e: React.MouseEvent): void => {
+ e.stopPropagation();
+
+ let view = this._editorView as any;
+ // this interposes on prosemirror's upHandler to prevent prosemirror's up from invoked multiple times when there
+ // are nested prosemirrors. We only want the lowest level prosemirror to be invoked.
+ if (view.mouseDown) {
+ let originalUpHandler = view.mouseDown.up;
+ view.root.removeEventListener("mouseup", originalUpHandler);
+ view.mouseDown.up = (e: MouseEvent) => {
+ !(e as any).formattedHandled && originalUpHandler(e);
+ // e.stopPropagation();
+ (e as any).formattedHandled = true;
+ };
+ view.root.addEventListener("mouseup", view.mouseDown.up);
}
}
tooltipTextMenuPlugin() {
- let myprops = this.props;
let self = FormattedTextBox;
return new Plugin({
- view(_editorView) {
- return self._toolTipTextMenu = new TooltipTextMenu(_editorView, myprops);
+ view(newView) {
+ return self._toolTipTextMenu = FormattedTextBox.getToolTip(newView);
}
});
}
@@ -959,7 +984,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
});
}
onBlur = (e: any) => {
- document.removeEventListener("keypress", this.recordKeyHandler);
+ //DictationManager.Controls.stop(false);
if (this._undoTyping) {
this._undoTyping.end();
this._undoTyping = undefined;
@@ -974,55 +999,68 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
if (e.key === "Tab" || e.key === "Enter") {
e.preventDefault();
}
- this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })));
+ let mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.round(Date.now() / 1000 / 5) });
+ this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(mark));
if (!this._undoTyping) {
this._undoTyping = UndoManager.StartBatch("undoTyping");
}
+ if (this._recording) { this.stopDictation(true); setTimeout(() => this.recordDictation(), 250); }
}
@action
tryUpdateHeight() {
- const ChromeHeight = this.props.ChromeHeight;
- let sh = this._ref.current ? this._ref.current.scrollHeight : 0;
- if (!this.props.isOverlay && !this.props.Document.isAnimating && this.props.Document.autoHeight && sh !== 0) {
- let nh = this.props.Document.isTemplate ? 0 : NumCast(this.dataDoc.nativeHeight, 0);
- let dh = NumCast(this.props.Document.height, 0);
- this.props.Document.height = Math.max(10, (nh ? dh / nh * sh : sh) + (ChromeHeight ? ChromeHeight() : 0));
- this.dataDoc.nativeHeight = nh ? sh : undefined;
+ let scrollHeight = this._ref.current ? this._ref.current.scrollHeight : 0;
+ if (!this.layoutDoc.isAnimating && this.layoutDoc.autoHeight && scrollHeight !== 0 &&
+ getComputedStyle(this._ref.current!.parentElement!).top === "0px") { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
+ let nh = this.Document.isTemplateField ? 0 : NumCast(this.dataDoc.nativeHeight, 0);
+ let dh = NumCast(this.layoutDoc.height, 0);
+ this.layoutDoc.height = Math.max(10, (nh ? dh / nh * scrollHeight : scrollHeight) + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0));
+ this.dataDoc.nativeHeight = nh ? scrollHeight : undefined;
}
}
render() {
- let style = this.props.isOverlay ? "scroll" : "hidden";
- let rounded = StrCast(this.props.Document.borderRounding) === "100%" ? "-rounded" : "";
- let interactive: "all" | "none" = InkingControl.Instance.selectedTool || this.props.Document.isBackground
+ let rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : "";
+ let interactive: "all" | "none" = InkingControl.Instance.selectedTool || this.layoutDoc.isBackground
? "none" : "all";
- Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
+ if (this.props.isSelected()) {
+ FormattedTextBox._toolTipTextMenu!.updateFromDash(this._editorView!, undefined, this.props);
+ }
return (
- <div className={`formattedTextBox-cont-${style}`} ref={this._ref}
+ <div className={`formattedTextBox-cont`} ref={this._ref}
style={{
- overflowY: this.props.Document.autoHeight ? "hidden" : "auto",
- height: this.props.Document.autoHeight ? "max-content" : this.props.height ? this.props.height : undefined,
+ height: this.layoutDoc.autoHeight ? "max-content" : this.props.height ? this.props.height : undefined,
background: this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : undefined,
- opacity: this.props.hideOnLeave ? (this._entered || this.props.isSelected() || Doc.IsBrushed(this.props.Document) ? 1 : 0.1) : 1,
+ opacity: this.props.hideOnLeave ? (this._entered ? 1 : 0.1) : 1,
color: this.props.color ? this.props.color : this.props.hideOnLeave ? "white" : "inherit",
pointerEvents: interactive,
fontSize: this._fontSize,
fontFamily: this._fontFamily,
}}
+ onContextMenu={this.specificContextMenu}
onKeyDown={this.onKeyPress}
onFocus={this.onFocused}
onClick={this.onClick}
onBlur={this.onBlur}
onPointerUp={this.onPointerUp}
onPointerDown={this.onPointerDown}
- onMouseDown={this.onMouseDown}
+ onMouseUp={this.onMouseUp}
onWheel={this.onPointerWheel}
onPointerEnter={action(() => this._entered = true)}
onPointerLeave={action(() => this._entered = false)}
>
- <div className={`formattedTextBox-inner${rounded}`} style={{ whiteSpace: "pre-wrap", pointerEvents: ((this.props.Document.isButton || this.props.onClick) && !this.props.isSelected()) ? "none" : undefined }} ref={this.createDropTarget} />
+ <div className={`formattedTextBox-inner${rounded}`} style={{ whiteSpace: "pre-wrap", pointerEvents: ((this.Document.isButton || this.props.onClick) && !this.props.isSelected()) ? "none" : undefined }} ref={this.createDropTarget} />
+
+ <div className="formattedTextBox-dictation"
+ onClick={e => {
+ this._recording ? this.stopDictation(true) : this.recordDictation();
+ setTimeout(() => this._editorView!.focus(), 500);
+ e.stopPropagation();
+ }} >
+ <FontAwesomeIcon className="formattedTExtBox-audioFont"
+ style={{ color: this._recording ? "red" : "blue", opacity: this._recording ? 1 : 0.2 }} icon={"file-audio"} size="sm" />
+ </div>
</div>
);
}
diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/FormattedTextBoxComment.tsx
index 2d2fff06d..bde278be3 100644
--- a/src/client/views/nodes/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/FormattedTextBoxComment.tsx
@@ -7,6 +7,7 @@ import { schema } from "../../util/RichTextSchema";
import { DocServer } from "../../DocServer";
import { Utils } from "../../../Utils";
import { StrCast } from "../../../new_fields/Types";
+import { FormattedTextBox } from "./FormattedTextBox";
export let formattedTextBoxCommentPlugin = new Plugin({
view(editorView) { return new FormattedTextBoxComment(editorView); }
@@ -49,7 +50,7 @@ export class FormattedTextBoxComment {
static end: number;
static mark: Mark;
static opened: boolean;
- static textBox: any;
+ static textBox: FormattedTextBox | undefined;
constructor(view: any) {
if (!FormattedTextBoxComment.tooltip) {
const root = document.getElementById("root");
@@ -62,9 +63,9 @@ export class FormattedTextBoxComment {
FormattedTextBoxComment.tooltip.style.pointerEvents = "all";
FormattedTextBoxComment.tooltip.appendChild(input);
FormattedTextBoxComment.tooltip.onpointerdown = (e: PointerEvent) => {
- let keep = e.target && (e.target as any).type === "checkbox";
+ let keep = e.target && (e.target as any).type === "checkbox" ? true : false;
FormattedTextBoxComment.opened = keep || !FormattedTextBoxComment.opened;
- FormattedTextBoxComment.textBox && FormattedTextBoxComment.textBox.setAnnotation(
+ FormattedTextBoxComment.textBox && FormattedTextBoxComment.start !== undefined && FormattedTextBoxComment.textBox.setAnnotation(
FormattedTextBoxComment.start, FormattedTextBoxComment.end, FormattedTextBoxComment.mark,
FormattedTextBoxComment.opened, keep);
};
@@ -92,8 +93,9 @@ export class FormattedTextBoxComment {
if (lastState && lastState.doc.eq(state.doc) &&
lastState.selection.eq(state.selection)) return;
+ if (!FormattedTextBoxComment.textBox || !FormattedTextBoxComment.textBox.props || !FormattedTextBoxComment.textBox.props.isSelected()) return;
let set = "none";
- if (state.selection.$from) {
+ if (FormattedTextBoxComment.textBox && state.selection.$from) {
let nbef = findStartOfMark(state.selection.$from, view, findOtherUserMark);
let naft = findEndOfMark(state.selection.$from, view, findOtherUserMark);
const spos = state.selection.$from.pos - nbef;
diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx
index f3adade58..60f547b1e 100644
--- a/src/client/views/nodes/IconBox.tsx
+++ b/src/client/views/nodes/IconBox.tsx
@@ -24,8 +24,10 @@ library.add(faFilm, faTag, faTextHeight);
@observer
export class IconBox extends React.Component<FieldViewProps> {
- public static LayoutString() { return FieldView.LayoutString(IconBox); }
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(IconBox, fieldKey); }
+ @observable _panelWidth: number = 0;
+ @observable _panelHeight: number = 0;
@computed get layout(): string { const field = Cast(this.props.Document[this.props.fieldKey], IconField); return field ? field.icon : "<p>Error loading icon data</p>"; }
@computed get minimizedIcon() { return IconBox.DocumentIcon(this.layout); }
@@ -69,8 +71,6 @@ export class IconBox extends React.Component<FieldViewProps> {
cm.addItem({ description: "Use Target Title", event: () => IconBox.AutomaticTitle(this.props.Document), icon: "text-height" });
}
}
- @observable _panelWidth: number = 0;
- @observable _panelHeight: number = 0;
render() {
let label = this.props.Document.hideLabel ? "" : this.props.Document.title;
return (
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 71d718b39..57c024bbf 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -1,18 +1,24 @@
-.imageBox-cont {
+.imageBox-cont, .imageBox-cont-interactive {
padding: 0vw;
- position: relative;
+ position: absolute;
text-align: center;
width: 100%;
- height: auto;
+ height: 100%;
max-width: 100%;
max-height: 100%;
pointer-events: none;
+ background:transparent;
+}
+
+.imageBox-container {
+ border-radius: inherit;
+ width:100%;
+ height:100%;
+ position: absolute;
}
.imageBox-cont-interactive {
pointer-events: all;
- width: 100%;
- height: auto;
}
.imageBox-dot {
@@ -47,6 +53,10 @@
padding: 3px;
background: white;
cursor: pointer;
+ opacity:0.15;
+}
+#google-photos:hover{
+ opacity: 1;
}
#google-tags {
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 9e9fe1324..188440292 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -2,10 +2,8 @@ import { library } from '@fortawesome/fontawesome-svg-core';
import { faEye } from '@fortawesome/free-regular-svg-icons';
import { faAsterisk, faFileAudio, faImage, faPaintBrush } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable, runInAction } from 'mobx';
+import { action, computed, observable, runInAction, trace } from 'mobx';
import { observer } from "mobx-react";
-import Lightbox from 'react-image-lightbox';
-import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
import { Doc, DocListCast, HeightSym, WidthSym } from '../../../new_fields/Doc';
import { List } from '../../../new_fields/List';
import { createSchema, listSpec, makeInterface } from '../../../new_fields/Schema';
@@ -13,20 +11,22 @@ import { ComputedField } from '../../../new_fields/ScriptField';
import { BoolCast, Cast, FieldValue, NumCast, StrCast } from '../../../new_fields/Types';
import { AudioField, ImageField } from '../../../new_fields/URLField';
import { RouteStore } from '../../../server/RouteStore';
-import { Utils } from '../../../Utils';
+import { Utils, returnOne, emptyFunction, OmitKeys } from '../../../Utils';
import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices';
import { Docs } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
import { undoBatch } from '../../util/UndoManager';
import { ContextMenu } from "../../views/ContextMenu";
import { ContextMenuProps } from '../ContextMenuItem';
-import { DocComponent } from '../DocComponent';
+import { DocAnnotatableComponent } from '../DocComponent';
import { InkingControl } from '../InkingControl';
-import { documentSchema } from './DocumentView';
import FaceRectangles from './FaceRectangles';
import { FieldView, FieldViewProps } from './FieldView';
import "./ImageBox.scss";
import React = require("react");
+import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
+import { documentSchema } from '../../../new_fields/documentSchemas';
+import { Id } from '../../../new_fields/FieldSymbols';
var requestImageSize = require('../../util/request-image-size');
var path = require('path');
const { Howl } = require('howler');
@@ -38,7 +38,10 @@ library.add(faFileAudio, faAsterisk);
export const pageSchema = createSchema({
curPage: "number",
- fitWidth: "boolean"
+ fitWidth: "boolean",
+ rotation: "number",
+ googlePhotosUrl: "string",
+ googlePhotosTags: "string"
});
interface Window {
@@ -54,33 +57,16 @@ type ImageDocument = makeInterface<[typeof pageSchema, typeof documentSchema]>;
const ImageDocument = makeInterface(pageSchema, documentSchema);
@observer
-export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageDocument) {
-
- public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(ImageBox, fieldKey); }
+export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocument>(ImageDocument) {
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ImageBox, fieldKey); }
private _imgRef: React.RefObject<HTMLImageElement> = React.createRef();
- private _downX: number = 0;
- private _downY: number = 0;
- private _lastTap: number = 0;
- @observable private _isOpen: boolean = false;
- private dropDisposer?: DragManager.DragDropDisposer;
- @observable private hoverActive = false;
-
- @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
-
- @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplate ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
+ private _dropDisposer?: DragManager.DragDropDisposer;
+ @observable private _audioState = 0;
+ @observable static _showControls: boolean;
protected createDropTarget = (ele: HTMLDivElement) => {
- if (this.dropDisposer) {
- this.dropDisposer();
- }
- if (ele) {
- this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
- }
- }
- onDrop = (e: React.DragEvent) => {
- e.stopPropagation();
- e.preventDefault();
- console.log("IMPLEMENT ME PLEASE");
+ this._dropDisposer && this._dropDisposer();
+ ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }));
}
@undoBatch
@@ -92,61 +78,18 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
e.stopPropagation();
}
de.mods === "MetaKey" && de.data.droppedDocuments.forEach(action((drop: Doc) => {
- Doc.AddDocToList(Doc.GetProto(this.extensionDoc), "Alternates", drop);
+ this.extensionDoc && Doc.AddDocToList(Doc.GetProto(this.extensionDoc), "Alternates", drop);
e.stopPropagation();
}));
}
}
- onPointerDown = (e: React.PointerEvent): void => {
- if (e.shiftKey && e.ctrlKey) {
- e.stopPropagation(); // allows default system drag drop of images with shift+ctrl only
- }
- // if (Date.now() - this._lastTap < 300) {
- // if (e.buttons === 1) {
- // this._downX = e.clientX;
- // this._downY = e.clientY;
- // document.removeEventListener("pointerup", this.onPointerUp);
- // document.addEventListener("pointerup", this.onPointerUp);
- // }
- // } else {
- // this._lastTap = Date.now();
- // }
- }
- @action
- onPointerUp = (e: PointerEvent): void => {
- document.removeEventListener("pointerup", this.onPointerUp);
- if (Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2) {
- this._isOpen = true;
- }
- e.stopPropagation();
- }
-
- @action
- lightbox = (images: string[]) => {
- if (this._isOpen) {
- return (<Lightbox
- mainSrc={images[this.Document.curPage || 0]}
- nextSrc={images[((this.Document.curPage || 0) + 1) % images.length]}
- prevSrc={images[((this.Document.curPage || 0) + images.length - 1) % images.length]}
- onCloseRequest={action(() =>
- this._isOpen = false
- )}
- onMovePrevRequest={action(() =>
- this.Document.curPage = ((this.Document.curPage || 0) + images.length - 1) % images.length
- )}
- onMoveNextRequest={action(() =>
- this.Document.curPage = ((this.Document.curPage || 0) + 1) % images.length
- )}
- />);
- }
- }
-
recordAudioAnnotation = () => {
let gumStream: any;
let recorder: any;
let self = this;
- navigator.mediaDevices.getUserMedia({
+ const extensionDoc = this.extensionDoc;
+ extensionDoc && navigator.mediaDevices.getUserMedia({
audio: true
}).then(function (stream) {
gumStream = stream;
@@ -159,13 +102,13 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
body: formData
});
const files = await res.json();
- const url = Utils.prepend(files[0]);
+ const url = Utils.prepend(files[0].path);
// upload to server with known URL
- let audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", x: NumCast(self.props.Document.x), y: NumCast(self.props.Document.y), width: 200, height: 32 });
+ let audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", width: 200, height: 32 });
audioDoc.treeViewExpandedView = "layout";
- let audioAnnos = Cast(self.extensionDoc.audioAnnotations, listSpec(Doc));
+ let audioAnnos = Cast(extensionDoc.audioAnnotations, listSpec(Doc));
if (audioAnnos === undefined) {
- self.extensionDoc.audioAnnotations = new List([audioDoc]);
+ extensionDoc.audioAnnotations = new List([audioDoc]);
} else {
audioAnnos.push(audioDoc);
}
@@ -182,25 +125,22 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
@undoBatch
rotate = action(() => {
- let proto = Doc.GetProto(this.props.Document);
- let nw = this.props.Document.nativeWidth;
- let nh = this.props.Document.nativeHeight;
- let w = this.props.Document.width;
- let h = this.props.Document.height;
- proto.rotation = (NumCast(this.props.Document.rotation) + 90) % 360;
- proto.nativeWidth = nh;
- proto.nativeHeight = nw;
- this.props.Document.width = h;
- this.props.Document.height = w;
+ let nw = this.Document.nativeWidth;
+ let nh = this.Document.nativeHeight;
+ let w = this.Document.width;
+ let h = this.Document.height;
+ this.Document.rotation = ((this.Document.rotation || 0) + 90) % 360;
+ this.Document.nativeWidth = nh;
+ this.Document.nativeHeight = nw;
+ this.Document.width = h;
+ this.Document.height = w;
});
specificContextMenu = (e: React.MouseEvent): void => {
- let field = Cast(this.Document[this.props.fieldKey], ImageField);
+ const field = Cast(this.Document[this.props.fieldKey], ImageField);
if (field) {
- let url = field.url.href;
let funcs: ContextMenuProps[] = [];
- funcs.push({ description: "Copy path", event: () => Utils.CopyText(url), icon: "expand-arrows-alt" });
- funcs.push({ description: "Record 1sec audio", event: this.recordAudioAnnotation, icon: "expand-arrows-alt" });
+ funcs.push({ description: "Copy path", event: () => Utils.CopyText(field.url.href), icon: "expand-arrows-alt" });
funcs.push({ description: "Rotate", event: this.rotate, icon: "expand-arrows-alt" });
let existingAnalyze = ContextMenu.Instance.findByDescription("Analyzers...");
@@ -216,12 +156,10 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
extractFaces = () => {
let converter = (results: any) => {
let faceDocs = new List<Doc>();
- results.map((face: CognitiveServices.Image.Face) => faceDocs.push(Docs.Get.DocumentHierarchyFromJson(face, `Face: ${face.faceId}`)!));
+ results.reduce((face: CognitiveServices.Image.Face, faceDocs: List<Doc>) => faceDocs.push(Docs.Get.DocumentHierarchyFromJson(face, `Face: ${face.faceId}`)!), new List<Doc>());
return faceDocs;
};
- if (this.url) {
- CognitiveServices.Image.Appliers.ProcessImage(this.extensionDoc, ["faces"], this.url, Service.Face, converter);
- }
+ this.url && this.extensionDoc && CognitiveServices.Image.Appliers.ProcessImage(this.extensionDoc, ["faces"], this.url, Service.Face, converter);
}
generateMetadata = (threshold: Confidence = Confidence.Excellent) => {
@@ -233,36 +171,19 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
let sanitized = tag.name.replace(" ", "_");
tagDoc[sanitized] = ComputedField.MakeFunction(`(${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`);
});
- this.extensionDoc.generatedTags = tagsList;
+ this.extensionDoc && (this.extensionDoc.generatedTags = tagsList);
tagDoc.title = "Generated Tags Doc";
tagDoc.confidence = threshold;
return tagDoc;
};
- if (this.url) {
- CognitiveServices.Image.Appliers.ProcessImage(this.extensionDoc, ["generatedTagsDoc"], this.url, Service.ComputerVision, converter);
- }
+ this.url && this.extensionDoc && CognitiveServices.Image.Appliers.ProcessImage(this.extensionDoc, ["generatedTagsDoc"], this.url, Service.ComputerVision, converter);
}
- @action
- onDotDown(index: number) {
- this.Document.curPage = index;
- }
@computed private get url() {
- let data = Cast(Doc.GetProto(this.props.Document)[this.props.fieldKey], ImageField);
+ let data = Cast(this.dataDoc[this.props.fieldKey], ImageField);
return data ? data.url.href : undefined;
}
- dots(paths: string[]) {
- let nativeWidth = FieldValue(this.Document.nativeWidth, 1);
- let dist = Math.min(nativeWidth / paths.length, 40);
- let left = (nativeWidth - paths.length * dist) / 2;
- return paths.map((p, i) =>
- <div className="imageBox-placer" key={i} >
- <div className="imageBox-dot" style={{ background: (i === this.Document.curPage ? "black" : "gray"), transform: `translate(${i * dist + left}px, 0px)` }} onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); this.onDotDown(i); }} />
- </div>
- );
- }
-
choosePath(url: URL) {
const lower = url.href.toLowerCase();
if (url.protocol === "data") {
@@ -293,32 +214,28 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
}
_curSuffix = "_m";
- resize(srcpath: string, layoutdoc: Doc) {
+ resize = (srcpath: string) => {
requestImageSize(srcpath)
.then((size: any) => {
let rotation = NumCast(this.dataDoc.rotation) % 180;
let realsize = rotation === 90 || rotation === 270 ? { height: size.width, width: size.height } : size;
let aspect = realsize.height / realsize.width;
- if (layoutdoc.nativeHeight !== 0 && layoutdoc.nativeWidth !== 0 && (Math.abs(NumCast(layoutdoc.height) - realsize.height) > 1 || Math.abs(NumCast(layoutdoc.width) - realsize.width) > 1)) {
+ if (this.Document.width && (Math.abs(1 - NumCast(this.Document.height) / NumCast(this.Document.width) / (realsize.height / realsize.width)) > 0.1)) {
setTimeout(action(() => {
- layoutdoc.height = layoutdoc[WidthSym]() * aspect;
- layoutdoc.nativeHeight = realsize.height;
- layoutdoc.nativeWidth = realsize.width;
+ this.Document.height = this.Document[WidthSym]() * aspect;
+ this.Document.nativeHeight = realsize.height;
+ this.Document.nativeWidth = realsize.width;
}), 0);
}
})
- .catch((err: any) => {
- console.log(err);
- });
+ .catch((err: any) => console.log(err));
}
- @observable _audioState = 0;
-
@action
onPointerEnter = () => {
let self = this;
- let audioAnnos = DocListCast(this.extensionDoc.audioAnnotations);
- if (audioAnnos.length && this._audioState === 0) {
+ let audioAnnos = this.extensionDoc && DocListCast(this.extensionDoc.audioAnnotations);
+ if (audioAnnos && audioAnnos.length && this._audioState === 0) {
let anno = audioAnnos[Math.floor(Math.random() * audioAnnos.length)];
anno.data instanceof AudioField && new Howl({
src: [anno.data.url.href],
@@ -332,70 +249,40 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
});
this._audioState = 1;
}
- // else {
- // if (this._audioState === 0) {
- // this._audioState = 1;
- // new Howl({
- // src: ["https://www.kozco.com/tech/piano2-CoolEdit.mp3"],
- // autoplay: true,
- // loop: false,
- // volume: 0.5,
- // onend: function () {
- // runInAction(() => self._audioState = 0);
- // }
- // });
- // }
- // }
}
- @action
- audioDown = () => {
- this.recordAudioAnnotation();
- }
+ audioDown = () => this.recordAudioAnnotation();
considerGooglePhotosLink = () => {
- const remoteUrl = StrCast(this.props.Document.googlePhotosUrl);
- if (remoteUrl) {
- return (
- <img
- id={"google-photos"}
- src={"/assets/google_photos.png"}
- style={{ opacity: this.hoverActive ? 1 : 0 }}
- onClick={() => window.open(remoteUrl)}
- />
- );
- }
- return (null);
+ const remoteUrl = this.Document.googlePhotosUrl;
+ return !remoteUrl ? (null) : (<img
+ id={"google-photos"}
+ src={"/assets/google_photos.png"}
+ onClick={() => window.open(remoteUrl)}
+ />);
}
considerGooglePhotosTags = () => {
- const tags = StrCast(this.props.Document.googlePhotosTags);
- if (tags) {
- return (
- <img
- id={"google-tags"}
- src={"/assets/google_tags.png"}
- />
- );
- }
- return (null);
+ const tags = this.Document.googlePhotosTags;
+ return !tags ? (null) : (<img id={"google-tags"} src={"/assets/google_tags.png"} />);
}
- render() {
+ @computed get content() {
+ const extensionDoc = this.extensionDoc;
+ if (!extensionDoc) return (null);
// let transform = this.props.ScreenToLocalTransform().inverse();
let pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50;
// var [sptX, sptY] = transform.transformPoint(0, 0);
// let [bptX, bptY] = transform.transformPoint(pw, this.props.PanelHeight());
// let w = bptX - sptX;
- let nativeWidth = FieldValue(this.Document.nativeWidth, pw);
- let nativeHeight = FieldValue(this.Document.nativeHeight, 0);
- let paths: string[] = [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")];
+ let nativeWidth = (this.Document.nativeWidth || pw);
+ let nativeHeight = (this.Document.nativeHeight || 0);
+ let paths = [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")];
// this._curSuffix = "";
// if (w > 20) {
- Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
- let alts = DocListCast(this.extensionDoc.Alternates);
- let altpaths: string[] = alts.filter(doc => doc.data instanceof ImageField).map(doc => this.choosePath((doc.data as ImageField).url));
+ let alts = DocListCast(extensionDoc.Alternates);
+ let altpaths = alts.filter(doc => doc.data instanceof ImageField).map(doc => this.choosePath((doc.data as ImageField).url));
let field = this.dataDoc[this.props.fieldKey];
// if (w < 100 && this._smallRetryCount < 10) this._curSuffix = "_s";
// else if (w < 600 && this._mediumRetryCount < 10) this._curSuffix = "_m";
@@ -403,21 +290,17 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
if (field instanceof ImageField) paths = [this.choosePath(field.url)];
paths.push(...altpaths);
// }
- let interactive = InkingControl.Instance.selectedTool || this.props.Document.isBackground ? "" : "-interactive";
- let rotation = NumCast(this.dataDoc.rotation, 0);
- let aspect = (rotation % 180) ? this.dataDoc[HeightSym]() / this.dataDoc[WidthSym]() : 1;
+ let interactive = InkingControl.Instance.selectedTool || this.Document.isBackground ? "" : "-interactive";
+ let rotation = NumCast(this.Document.rotation, 0);
+ let aspect = (rotation % 180) ? this.Document[HeightSym]() / this.Document[WidthSym]() : 1;
let shift = (rotation % 180) ? (nativeHeight - nativeWidth / aspect) / 2 : 0;
- let srcpath = paths[Math.min(paths.length - 1, this.Document.curPage || 0)];
+ let srcpath = paths[Math.min(paths.length - 1, (this.Document.curPage || 0))];
let fadepath = paths[Math.min(paths.length - 1, 1)];
- if (!this.props.Document.ignoreAspect && !this.props.leaveNativeSize) this.resize(srcpath, this.props.Document);
+ !this.Document.ignoreAspect && this.resize(srcpath);
return (
- <div className={`imageBox-cont${interactive}`} style={{ background: "transparent" }}
- onPointerDown={this.onPointerDown}
- onPointerEnter={action(() => this.hoverActive = true)}
- onPointerLeave={action(() => this.hoverActive = false)}
- onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}>
+ <div className={`imageBox-cont${interactive}`} key={this.props.Document[Id]} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}>
<div id="cf">
<img
key={this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys
@@ -434,18 +317,43 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
ref={this._imgRef}
onError={this.onError} /></div>}
</div>
- {paths.length > 1 ? this.dots(paths) : (null)}
<div className="imageBox-audioBackground"
onPointerDown={this.audioDown}
onPointerEnter={this.onPointerEnter}
style={{ height: `calc(${.1 * nativeHeight / nativeWidth * 100}%)` }}
>
<FontAwesomeIcon className="imageBox-audioFont"
- style={{ color: [DocListCast(this.extensionDoc.audioAnnotations).length ? "blue" : "gray", "green", "red"][this._audioState] }} icon={faFileAudio} size="sm" />
+ style={{ color: [DocListCast(extensionDoc.audioAnnotations).length ? "blue" : "gray", "green", "red"][this._audioState] }} icon={faFileAudio} size="sm" />
</div>
{this.considerGooglePhotosLink()}
- {/* {this.lightbox(paths)} */}
- <FaceRectangles document={this.extensionDoc} color={"#0000FF"} backgroundColor={"#0000FF"} />
+ <FaceRectangles document={extensionDoc} color={"#0000FF"} backgroundColor={"#0000FF"} />
</div>);
}
+
+ render() {
+ return (<div className={"imageBox-container"} onContextMenu={this.specificContextMenu}>
+ <CollectionFreeFormView {...this.props}
+ PanelHeight={this.props.PanelHeight}
+ PanelWidth={this.props.PanelWidth}
+ annotationsKey={this.annotationsKey}
+ isAnnotationOverlay={true}
+ focus={this.props.focus}
+ isSelected={this.props.isSelected}
+ select={emptyFunction}
+ active={this.active}
+ ContentScaling={returnOne}
+ whenActiveChanged={this.whenActiveChanged}
+ removeDocument={this.removeDocument}
+ moveDocument={this.moveDocument}
+ addDocument={this.addDocument}
+ CollectionView={undefined}
+ ScreenToLocalTransform={this.props.ScreenToLocalTransform}
+ ruleProvider={undefined}
+ renderDepth={this.props.renderDepth + 1}
+ ContainingCollectionDoc={this.props.ContainingCollectionDoc}
+ chromeCollapsed={true}>
+ {() => [this.content]}
+ </CollectionFreeFormView>
+ </div >);
+ }
} \ No newline at end of file
diff --git a/src/client/views/nodes/KeyValueBox.scss b/src/client/views/nodes/KeyValueBox.scss
index 87a9565e8..6e8a36c6a 100644
--- a/src/client/views/nodes/KeyValueBox.scss
+++ b/src/client/views/nodes/KeyValueBox.scss
@@ -9,6 +9,7 @@
box-sizing: border-box;
display: inline-block;
pointer-events: all;
+ cursor: default;
.imageBox-cont img {
width: auto;
}
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 3a9318469..35e9e4862 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -1,7 +1,6 @@
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
-import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
import { Doc, Field, FieldResult } from "../../../new_fields/Doc";
import { List } from "../../../new_fields/List";
import { RichTextField } from "../../../new_fields/RichTextField";
@@ -26,11 +25,12 @@ export type KVPScript = {
@observer
export class KeyValueBox extends React.Component<FieldViewProps> {
+ public static LayoutString(fieldStr: string) { return FieldView.LayoutString(KeyValueBox, fieldStr); }
+
private _mainCont = React.createRef<HTMLDivElement>();
private _keyHeader = React.createRef<HTMLTableHeaderCellElement>();
- @observable private rows: KeyValuePair[] = [];
- public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(KeyValueBox, fieldStr); }
+ @observable private rows: KeyValuePair[] = [];
@observable private _keyInput: string = "";
@observable private _valueInput: string = "";
@computed get splitPercentage() { return NumCast(this.props.Document.schemaSplitPercentage, 50); }
@@ -68,7 +68,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript): boolean {
const { script, type, onDelegate } = kvpScript;
- //const target = onDelegate ? (doc.layout instanceof Doc ? doc.layout : doc) : Doc.GetProto(doc); // bcz: need to be able to set fields on layout templates
+ //const target = onDelegate ? Doc.Layout(doc.layout) : Doc.GetProto(doc); // bcz: TODO need to be able to set fields on layout templates
const target = onDelegate ? doc : Doc.GetProto(doc);
let field: Field;
if (type === "computed") {
@@ -103,6 +103,8 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
e.stopPropagation();
}
+ rowHeight = () => 30;
+
createTable = () => {
let doc = this.fieldDocToLayout;
if (!doc) {
@@ -124,14 +126,15 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
let i = 0;
const self = this;
for (let key of Object.keys(ids).slice().sort()) {
- rows.push(<KeyValuePair doc={realDoc} addDocTab={this.props.addDocTab} ref={(function () {
- let oldEl: KeyValuePair | undefined;
- return (el: KeyValuePair) => {
- if (oldEl) self.rows.splice(self.rows.indexOf(oldEl), 1);
- oldEl = el;
- if (el) self.rows.push(el);
- };
- })()} keyWidth={100 - this.splitPercentage} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} key={key} keyName={key} />);
+ rows.push(<KeyValuePair doc={realDoc} addDocTab={this.props.addDocTab} PanelWidth={this.props.PanelWidth} PanelHeight={this.rowHeight}
+ ref={(function () {
+ let oldEl: KeyValuePair | undefined;
+ return (el: KeyValuePair) => {
+ if (oldEl) self.rows.splice(self.rows.indexOf(oldEl), 1);
+ oldEl = el;
+ if (el) self.rows.push(el);
+ };
+ })()} keyWidth={100 - this.splitPercentage} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} key={key} keyName={key} />);
}
return rows;
}
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 1fed4c8bb..225565964 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -1,6 +1,5 @@
import { action, observable } from 'mobx';
import { observer } from "mobx-react";
-import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
import { Doc, Field, Opt } from '../../../new_fields/Doc';
import { emptyFunction, returnFalse, returnOne, returnZero } from '../../../Utils';
import { Docs } from '../../documents/Documents';
@@ -22,6 +21,8 @@ export interface KeyValuePairProps {
keyName: string;
doc: Doc;
keyWidth: number;
+ PanelHeight: () => number;
+ PanelWidth: () => number;
addDocTab: (doc: Doc, data: Opt<Doc>, where: string) => boolean;
}
@observer
@@ -59,7 +60,6 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
ContainingCollectionDoc: undefined,
ruleProvider: undefined,
fieldKey: this.props.keyName,
- fieldExt: "",
isSelected: returnFalse,
select: emptyFunction,
renderDepth: 1,
@@ -67,8 +67,8 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
whenActiveChanged: emptyFunction,
ScreenToLocalTransform: Transform.Identity,
focus: emptyFunction,
- PanelWidth: returnZero,
- PanelHeight: returnZero,
+ PanelWidth: this.props.PanelWidth,
+ PanelHeight: this.props.PanelHeight,
addDocTab: returnFalse,
pinToPres: returnZero,
ContentScaling: returnOne
diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss
index 1c1d6ec95..8bec4fbe3 100644
--- a/src/client/views/nodes/PDFBox.scss
+++ b/src/client/views/nodes/PDFBox.scss
@@ -6,7 +6,7 @@
width:100%;
overflow: hidden;
position:absolute;
- z-index: -1;
+ cursor:auto;
}
.pdfBox-title-outer {
@@ -15,7 +15,7 @@
width: 100%;
height: 100%;
background: lightslategray;
- .pdfBox-title-cont, .pdfBox-cont-interactive{
+ .pdfBox-cont, .pdfBox-cont-interactive{
width: 150%;
height: 100%;
position: relative;
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 617b580dd..396a5356a 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -1,76 +1,98 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, IReactionDisposer, observable, reaction, runInAction, untracked, trace } from 'mobx';
+import { action, observable, runInAction, reaction, IReactionDisposer } from 'mobx';
import { observer } from "mobx-react";
import * as Pdfjs from "pdfjs-dist";
import "pdfjs-dist/web/pdf_viewer.css";
-import 'react-image-lightbox/style.css';
-import { Doc, Opt, WidthSym } from "../../../new_fields/Doc";
+import { Opt, WidthSym, Doc } from "../../../new_fields/Doc";
import { makeInterface } from "../../../new_fields/Schema";
import { ScriptField } from '../../../new_fields/ScriptField';
-import { Cast, NumCast } from "../../../new_fields/Types";
+import { Cast } from "../../../new_fields/Types";
import { PdfField } from "../../../new_fields/URLField";
+import { Utils } from '../../../Utils';
import { KeyCodes } from '../../northstar/utils/KeyCodes';
+import { undoBatch } from '../../util/UndoManager';
import { panZoomSchema } from '../collections/collectionFreeForm/CollectionFreeFormView';
-import { DocComponent } from "../DocComponent";
-import { InkingControl } from "../InkingControl";
+import { ContextMenu } from '../ContextMenu';
+import { ContextMenuProps } from '../ContextMenuItem';
+import { DocAnnotatableComponent } from "../DocComponent";
import { PDFViewer } from "../pdf/PDFViewer";
-import { documentSchema } from "./DocumentView";
import { FieldView, FieldViewProps } from './FieldView';
import { pageSchema } from "./ImageBox";
import "./PDFBox.scss";
import React = require("react");
-import { undoBatch } from '../../util/UndoManager';
-import { ContextMenuProps } from '../ContextMenuItem';
-import { ContextMenu } from '../ContextMenu';
-import { Utils } from '../../../Utils';
+import { documentSchema } from '../../../new_fields/documentSchemas';
type PdfDocument = makeInterface<[typeof documentSchema, typeof panZoomSchema, typeof pageSchema]>;
const PdfDocument = makeInterface(documentSchema, panZoomSchema, pageSchema);
@observer
-export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocument) {
- public static LayoutString(fieldExt?: string) { return FieldView.LayoutString(PDFBox, "data", fieldExt); }
+export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>(PdfDocument) {
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PDFBox, fieldKey); }
private _keyValue: string = "";
private _valueValue: string = "";
private _scriptValue: string = "";
private _searchString: string = "";
- private _isChildActive = false;
private _everActive = false; // has this box ever had its contents activated -- if so, stop drawing the overlay title
private _pdfViewer: PDFViewer | undefined;
+ private _searchRef: React.RefObject<HTMLInputElement> = React.createRef();
private _keyRef: React.RefObject<HTMLInputElement> = React.createRef();
private _valueRef: React.RefObject<HTMLInputElement> = React.createRef();
private _scriptRef: React.RefObject<HTMLInputElement> = React.createRef();
+ private _selectReaction: IReactionDisposer | undefined;
@observable private _searching: boolean = false;
@observable private _flyout: boolean = false;
@observable private _pdf: Opt<Pdfjs.PDFDocumentProxy>;
@observable private _pageControls = false;
- @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
- @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplate ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
-
+ componentWillUnmount() {
+ this._selectReaction && this._selectReaction();
+ }
componentDidMount() {
const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
if (pdfUrl instanceof PdfField) {
Pdfjs.getDocument(pdfUrl.url.pathname).promise.then(pdf => runInAction(() => this._pdf = pdf));
}
+ this._selectReaction = reaction(() => this.props.isSelected(),
+ () => {
+ if (this.props.isSelected()) {
+ document.removeEventListener("keydown", this.onKeyDown);
+ document.addEventListener("keydown", this.onKeyDown);
+ } else {
+ document.removeEventListener("keydown", this.onKeyDown);
+ }
+ }, { fireImmediately: true });
}
+
loaded = (nw: number, nh: number, np: number) => {
this.dataDoc.numPages = np;
- if (!this.Document.nativeWidth || !this.Document.nativeHeight || !this.Document.scrollHeight) {
- let oldaspect = (this.Document.nativeHeight || 0) / (this.Document.nativeWidth || 1);
- this.Document.nativeWidth = nw * 96 / 72;
- this.Document.nativeHeight = this.Document.nativeHeight ? nw * 96 / 72 * oldaspect : nh * 96 / 72;
- }
+ this.Document.nativeWidth = nw * 96 / 72;
+ this.Document.nativeHeight = nh * 96 / 72;
!this.Document.fitWidth && !this.Document.ignoreAspect && (this.Document.height = this.Document[WidthSym]() * (nh / nw));
}
public search(string: string, fwd: boolean) { this._pdfViewer && this._pdfViewer.search(string, fwd); }
public prevAnnotation() { this._pdfViewer && this._pdfViewer.prevAnnotation(); }
public nextAnnotation() { this._pdfViewer && this._pdfViewer.nextAnnotation(); }
- public backPage() { this._pdfViewer!.gotoPage(NumCast(this.props.Document.curPage) - 1); }
+ public backPage() { this._pdfViewer!.gotoPage((this.Document.curPage || 1) - 1); }
public gotoPage = (p: number) => { this._pdfViewer!.gotoPage(p); };
- public forwardPage() { this._pdfViewer!.gotoPage(NumCast(this.props.Document.curPage) + 1); }
+ public forwardPage() { this._pdfViewer!.gotoPage((this.Document.curPage || 1) + 1); }
+
+ @undoBatch
+ onKeyDown = action((e: KeyboardEvent) => {
+ if (e.key === "f" && e.ctrlKey) {
+ this._searching = true;
+ setTimeout(() => this._searchRef.current && this._searchRef.current.focus(), 100);
+ e.stopImmediatePropagation();
+ e.preventDefault();
+ }
+ if (e.key === "PageDown" || e.key === "ArrowDown" || e.key === "ArrowRight") {
+ this.forwardPage();
+ }
+ if (e.key === "PageUp" || e.key === "ArrowUp" || e.key === "ArrowLeft") {
+ this.backPage();
+ }
+ });
@undoBatch
@action
@@ -92,7 +114,6 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
private newScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => this._scriptValue = e.currentTarget.value;
whenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive);
- active = () => this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0;
setPdfViewer = (pdfViewer: PDFViewer) => { this._pdfViewer = pdfViewer; };
searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => this._searchString = e.currentTarget.value;
@@ -111,12 +132,14 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
<FontAwesomeIcon style={{ color: "white" }} icon={"arrow-right"} size="sm" />
</button>
</>;
- return !this.props.active() ? (null) :
+ return !this.active() ? (null) :
(<div className="pdfBox-ui" onKeyDown={e => e.keyCode === KeyCodes.BACKSPACE || e.keyCode === KeyCodes.DELETE ? e.stopPropagation() : true}
onPointerDown={e => e.stopPropagation()} style={{ display: this.active() ? "flex" : "none", position: "absolute", width: "100%", height: "100%", zIndex: 1, pointerEvents: "none" }}>
<div className="pdfBox-overlayCont" key="cont" onPointerDown={(e) => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}>
<button className="pdfBox-overlayButton" title="Open Search Bar" />
- <input className="pdfBox-searchBar" placeholder="Search" onChange={this.searchStringChanged} onKeyDown={e => e.keyCode === KeyCodes.ENTER && this.search(this._searchString, !e.shiftKey)} />
+ <input className="pdfBox-searchBar" placeholder="Search" ref={this._searchRef} onChange={this.searchStringChanged} onKeyDown={e => {
+ e.keyCode === KeyCodes.ENTER && this.search(this._searchString, !e.shiftKey);
+ }} />
<button title="Search" onClick={e => this.search(this._searchString, !e.shiftKey)}>
<FontAwesomeIcon icon="search" size="sm" color="white" /></button>
<button className="pdfBox-prevIcon " title="Previous Annotation" onClick={e => this.prevAnnotation()} >
@@ -131,7 +154,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
<div className="pdfBox-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}>
<FontAwesomeIcon style={{ color: "white", padding: 5 }} icon={this._searching ? "times" : "search"} size="3x" /></div>
</button>
- <input value={`${NumCast(this.props.Document.curPage)}`}
+ <input value={`${(this.Document.curPage || 1)}`}
onChange={e => this.gotoPage(Number(e.currentTarget.value))}
style={{ left: 20, top: 5, height: "30px", width: "30px", position: "absolute", pointerEvents: "all" }}
onClick={action(() => this._pageControls = !this._pageControls)} />
@@ -177,12 +200,14 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
ContextMenu.Instance.addItem({ description: "Pdf Funcs...", subitems: funcs, icon: "asterisk" });
}
+ _initialScale: number | undefined; // the initial scale of the PDF when first rendered which determines whether the document will be live on startup or not. Getting bigger after startup won't make it automatically be live....
render() {
const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
- let classname = "pdfBox-cont" + (InkingControl.Instance.selectedTool || !this.active ? "" : "-interactive");
+ let classname = "pdfBox-cont" + (this.active() ? "-interactive" : "");
let noPdf = !(pdfUrl instanceof PdfField) || !this._pdf;
- if (!noPdf && (this.props.isSelected() || this.props.ScreenToLocalTransform().Scale < 2.5)) this._everActive = true;
- return (!this._everActive ?
+ if (this._initialScale === undefined) this._initialScale = this.props.ScreenToLocalTransform().Scale;
+ if (this.props.isSelected() || this.props.Document.scrollY !== undefined) this._everActive = true;
+ return (!this.extensionDoc || noPdf || (!this._everActive && this.props.ScreenToLocalTransform().Scale > 2.5) ?
<div className="pdfBox-title-outer" >
<div className={classname} >
<strong className="pdfBox-title" >{` ${this.props.Document.title}`}</strong>
@@ -203,11 +228,11 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
setPdfViewer={this.setPdfViewer} ContainingCollectionView={this.props.ContainingCollectionView}
renderDepth={this.props.renderDepth} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth}
Document={this.props.Document} DataDoc={this.dataDoc} ContentScaling={this.props.ContentScaling}
- addDocTab={this.props.addDocTab} GoToPage={this.gotoPage}
- pinToPres={this.props.pinToPres} addDocument={this.props.addDocument}
+ addDocTab={this.props.addDocTab} GoToPage={this.gotoPage} focus={this.props.focus}
+ pinToPres={this.props.pinToPres} addDocument={this.addDocument}
ScreenToLocalTransform={this.props.ScreenToLocalTransform} select={this.props.select}
isSelected={this.props.isSelected} whenActiveChanged={this.whenActiveChanged}
- fieldKey={this.props.fieldKey} fieldExtensionDoc={this.extensionDoc} startupLive={this.props.ScreenToLocalTransform().Scale < 2.5 ? true : false} />
+ fieldKey={this.props.fieldKey} startupLive={this._initialScale < 2.5 ? true : false} />
{this.settingsPanel()}
</div>);
}
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index f44ca99b9..cbb83b511 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -10,7 +10,7 @@ import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
import { DocumentManager } from "../../util/DocumentManager";
import { undoBatch } from "../../util/UndoManager";
-import { CollectionViewType } from "../collections/CollectionBaseView";
+import { CollectionViewType } from "../collections/CollectionView";
import { CollectionDockingView } from "../collections/CollectionDockingView";
import { CollectionView } from "../collections/CollectionView";
import { ContextMenu } from "../ContextMenu";
@@ -31,7 +31,7 @@ library.add(faEdit);
@observer
export class PresBox extends React.Component<FieldViewProps> {
- public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(PresBox, fieldKey); }
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresBox, fieldKey); }
_docListChangedReaction: IReactionDisposer | undefined;
componentDidMount() {
this._docListChangedReaction = reaction(() => {
@@ -43,8 +43,8 @@ export class PresBox extends React.Component<FieldViewProps> {
value.forEach((item, i) => {
if (item instanceof Doc && item.type !== DocumentType.PRESELEMENT) {
let pinDoc = Docs.Create.PresElementBoxDocument({ backgroundColor: "transparent" });
- Doc.GetProto(pinDoc).target = item;
- Doc.GetProto(pinDoc).title = ComputedField.MakeFunction('(this.target instanceof Doc) && this.target.title.toString()');
+ Doc.GetProto(pinDoc).presentationTargetDoc = item;
+ Doc.GetProto(pinDoc).title = ComputedField.MakeFunction('(this.presentationTargetDoc instanceof Doc) && this.presentationTargetDoc.title.toString()');
value.splice(i, 1, pinDoc);
}
});
@@ -124,13 +124,13 @@ export class PresBox extends React.Component<FieldViewProps> {
this.childDocs.forEach((doc, ind) => {
//the order of cases is aligned based on priority
if (doc.hideTillShownButton && ind <= index) {
- (doc.target as Doc).opacity = 1;
+ (doc.presentationTargetDoc as Doc).opacity = 1;
}
if (doc.hideAfterButton && ind < index) {
- (doc.target as Doc).opacity = 0;
+ (doc.presentationTargetDoc as Doc).opacity = 0;
}
if (doc.fadeButton && ind < index) {
- (doc.target as Doc).opacity = 0.5;
+ (doc.presentationTargetDoc as Doc).opacity = 0.5;
}
});
}
@@ -145,13 +145,13 @@ export class PresBox extends React.Component<FieldViewProps> {
//the order of cases is aligned based on priority
if (key.hideAfterButton && ind >= index) {
- (key.target as Doc).opacity = 1;
+ (key.presentationTargetDoc as Doc).opacity = 1;
}
if (key.fadeButton && ind >= index) {
- (key.target as Doc).opacity = 1;
+ (key.presentationTargetDoc as Doc).opacity = 1;
}
if (key.hideTillShownButton && ind > index) {
- (key.target as Doc).opacity = 0;
+ (key.presentationTargetDoc as Doc).opacity = 0;
}
});
}
@@ -162,7 +162,7 @@ export class PresBox extends React.Component<FieldViewProps> {
* te option open, navigates to that element.
*/
navigateToElement = async (curDoc: Doc, fromDocIndex: number) => {
- let fromDoc = this.childDocs[fromDocIndex].target as Doc;
+ let fromDoc = this.childDocs[fromDocIndex].presentationTargetDoc as Doc;
let docToJump = curDoc;
let willZoom = false;
@@ -190,7 +190,7 @@ export class PresBox extends React.Component<FieldViewProps> {
//docToJump stayed same meaning, it was not in the group or was the last element in the group
if (docToJump === curDoc) {
//checking if curDoc has navigation open
- let target = await curDoc.target as Doc;
+ let target = await curDoc.presentationTargetDoc as Doc;
if (curDoc.navButton) {
DocumentManager.Instance.jumpToDocument(target, false);
} else if (curDoc.showButton) {
@@ -210,8 +210,8 @@ export class PresBox extends React.Component<FieldViewProps> {
let curScale = DocumentManager.Instance.getScaleOfDocView(fromDoc);
//awaiting jump so that new scale can be found, since jumping is async
- await DocumentManager.Instance.jumpToDocument(await docToJump.target as Doc, willZoom);
- let newScale = DocumentManager.Instance.getScaleOfDocView(await curDoc.target as Doc);
+ await DocumentManager.Instance.jumpToDocument(await docToJump.presentationTargetDoc as Doc, willZoom);
+ let newScale = DocumentManager.Instance.getScaleOfDocView(await curDoc.presentationTargetDoc as Doc);
curDoc.viewScale = newScale;
//saving the scale that user was on
if (curScale !== 1) {
@@ -314,12 +314,11 @@ export class PresBox extends React.Component<FieldViewProps> {
}
toggleMinimize = undoBatch(action((e: React.PointerEvent) => {
- if (this.props.Document.minimizedView) {
- this.props.Document.minimizedView = false;
+ if (this.props.Document.inOverlay) {
Doc.RemoveDocFromList((CurrentUserUtils.UserDocument.overlays as Doc), this.props.fieldKey, this.props.Document);
CollectionDockingView.AddRightSplit(this.props.Document, this.props.DataDoc);
+ this.props.Document.inOverlay = false;
} else {
- this.props.Document.minimizedView = true;
this.props.Document.x = e.clientX + 25;
this.props.Document.y = e.clientY - 25;
this.props.addDocTab && this.props.addDocTab(this.props.Document, this.props.DataDoc, "close");
@@ -370,9 +369,9 @@ export class PresBox extends React.Component<FieldViewProps> {
<FontAwesomeIcon icon={this.props.Document.presStatus ? "stop" : "play"} />
</button>
<button className="presBox-button" title="Next" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></button>
- <button className="presBox-button" title={this.props.Document.minimizedView ? "Expand" : "Minimize"} onClick={this.toggleMinimize}><FontAwesomeIcon icon={"eye"} /></button>
+ <button className="presBox-button" title={this.props.Document.inOverlay ? "Expand" : "Minimize"} onClick={this.toggleMinimize}><FontAwesomeIcon icon={"eye"} /></button>
</div>
- {this.props.Document.minimizedView ? (null) :
+ {this.props.Document.inOverlay ? (null) :
<div className="presBox-listCont" >
<CollectionView {...this.props} focus={this.selectElement} ScreenToLocalTransform={this.getTransform} />
</div>
diff --git a/src/client/views/nodes/QueryBox.scss b/src/client/views/nodes/QueryBox.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/client/views/nodes/QueryBox.scss
diff --git a/src/client/views/nodes/QueryBox.tsx b/src/client/views/nodes/QueryBox.tsx
new file mode 100644
index 000000000..99b5810fc
--- /dev/null
+++ b/src/client/views/nodes/QueryBox.tsx
@@ -0,0 +1,35 @@
+import React = require("react");
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faArrowLeft, faArrowRight, faEdit, faMinus, faPlay, faPlus, faStop, faTimes } from '@fortawesome/free-solid-svg-icons';
+import { IReactionDisposer } from "mobx";
+import { observer } from "mobx-react";
+import { FilterBox } from "../search/FilterBox";
+import { FieldView, FieldViewProps } from './FieldView';
+import "./PresBox.scss";
+
+library.add(faArrowLeft);
+library.add(faArrowRight);
+library.add(faPlay);
+library.add(faStop);
+library.add(faPlus);
+library.add(faTimes);
+library.add(faMinus);
+library.add(faEdit);
+
+@observer
+export class QueryBox extends React.Component<FieldViewProps> {
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(QueryBox, fieldKey); }
+ _docListChangedReaction: IReactionDisposer | undefined;
+ componentDidMount() {
+ }
+
+ componentWillUnmount() {
+ this._docListChangedReaction && this._docListChangedReaction();
+ }
+
+ render() {
+ return <div style={{ width: "100%", height: "100%", position: "absolute", pointerEvents: "all" }}>
+ <FilterBox></FilterBox>
+ </div>;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss
index d651a8621..5829c1bd9 100644
--- a/src/client/views/nodes/VideoBox.scss
+++ b/src/client/views/nodes/VideoBox.scss
@@ -1,6 +1,15 @@
+.videoBox-container {
+ pointer-events: all;
+ .inkingCanvas-paths-markers {
+ opacity : 0.4; // we shouldn't have to do this, but since chrome crawls to a halt with z-index unset in videoBox-content, this is a workaround
+ }
+}
+
.videoBox-content-YouTube, .videoBox-content-YouTube-fullScreen,
.videoBox-content, .videoBox-content-interactive, .videoBox-cont-fullScreen {
width: 100%;
+ z-index: -1; // 0; // logically this should be 0 (or unset) which would give us transparent brush strokes over videos. However, this makes Chrome crawl to a halt
+ position: absolute;
}
.videoBox-content, .videoBox-content-interactive, .videoBox-content-fullScreen {
@@ -11,7 +20,52 @@
height: 100%;
}
-.videoBox-content-interactive, .videoBox-content-fullScreen,
-.videoBox-content-YouTube-fullScreen {
+.videoBox-content-interactive, .videoBox-content-fullScreen, .videoBox-content-YouTube-fullScreen {
pointer-events: all;
+}
+
+.videoBox-time{
+ color : white;
+ top :25px;
+ left : 25px;
+ position: absolute;
+ background-color: rgba(50, 50, 50, 0.2);
+ transform-origin: left top;
+ pointer-events:all;
+}
+.videoBox-snapshot{
+ color : white;
+ top :25px;
+ right : 25px;
+ position: absolute;
+ background-color:rgba(50, 50, 50, 0.2);
+ transform-origin: left top;
+ pointer-events:all;
+}
+.videoBox-play {
+ width: 25px;
+ height: 20px;
+ bottom: 25px;
+ left : 25px;
+ position: absolute;
+ color : white;
+ background-color: rgba(50, 50, 50, 0.2);
+ border-radius: 4px;
+ text-align: center;
+ transform-origin: left bottom;
+ pointer-events:all;
+}
+.videoBox-full {
+ width: 25px;
+ height: 20px;
+ bottom: 25px;
+ right : 25px;
+ position: absolute;
+ color : white;
+ background-color: rgba(50, 50, 50, 0.2);
+ border-radius: 4px;
+ text-align: center;
+ transform-origin: right bottom;
+ pointer-events:all;
+
} \ No newline at end of file
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index b7d9a1eab..53baea4ae 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -1,49 +1,54 @@
import React = require("react");
-import { action, computed, IReactionDisposer, observable, reaction, runInAction, untracked, trace } from "mobx";
+import { library } from "@fortawesome/fontawesome-svg-core";
+import { faVideo } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { action, computed, IReactionDisposer, observable, reaction, runInAction, untracked } from "mobx";
import { observer } from "mobx-react";
import * as rp from 'request-promise';
+import { Doc } from "../../../new_fields/Doc";
import { InkTool } from "../../../new_fields/InkField";
-import { makeInterface } from "../../../new_fields/Schema";
-import { Cast, FieldValue, NumCast, BoolCast } from "../../../new_fields/Types";
+import { createSchema, makeInterface } from "../../../new_fields/Schema";
+import { ScriptField } from "../../../new_fields/ScriptField";
+import { Cast, StrCast } from "../../../new_fields/Types";
import { VideoField } from "../../../new_fields/URLField";
import { RouteStore } from "../../../server/RouteStore";
-import { Utils } from "../../../Utils";
+import { emptyFunction, returnOne, Utils } from "../../../Utils";
import { Docs, DocUtils } from "../../documents/Documents";
+import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
import { ContextMenu } from "../ContextMenu";
import { ContextMenuProps } from "../ContextMenuItem";
-import { DocComponent } from "../DocComponent";
+import { DocAnnotatableComponent } from "../DocComponent";
import { DocumentDecorations } from "../DocumentDecorations";
import { InkingControl } from "../InkingControl";
-import { documentSchema } from "./DocumentView";
import { FieldView, FieldViewProps } from './FieldView';
-import { pageSchema } from "./ImageBox";
import "./VideoBox.scss";
-import { library } from "@fortawesome/fontawesome-svg-core";
-import { faVideo } from "@fortawesome/free-solid-svg-icons";
-import { Doc } from "../../../new_fields/Doc";
-import { ScriptField } from "../../../new_fields/ScriptField";
+import { documentSchema, positionSchema } from "../../../new_fields/documentSchemas";
var path = require('path');
-type VideoDocument = makeInterface<[typeof documentSchema, typeof pageSchema]>;
-const VideoDocument = makeInterface(documentSchema, pageSchema);
+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);
library.add(faVideo);
@observer
-export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoDocument) {
+export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocument>(VideoDocument) {
+ static _youtubeIframeCounter: number = 0;
private _reactionDisposer?: IReactionDisposer;
private _youtubeReactionDisposer?: IReactionDisposer;
private _youtubePlayer: YT.Player | undefined = undefined;
private _videoRef: HTMLVideoElement | null = null;
private _youtubeIframeId: number = -1;
private _youtubeContentCreated = false;
- static _youtubeIframeCounter: number = 0;
+ private _isResetClick = 0;
@observable _forceCreateYouTubeIFrame = false;
- @observable static _showControls: boolean;
@observable _playTimer?: NodeJS.Timeout = undefined;
@observable _fullScreen = false;
- @observable public Playing: boolean = false;
- public static LayoutString() { return FieldView.LayoutString(VideoBox); }
+ @observable _playing = false;
+ @observable static _showControls: boolean;
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(VideoBox, fieldKey); }
public get player(): HTMLVideoElement | null {
return this._videoRef;
@@ -51,18 +56,18 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
videoLoad = () => {
let aspect = this.player!.videoWidth / this.player!.videoHeight;
- var nativeWidth = FieldValue(this.Document.nativeWidth, 0);
- var nativeHeight = FieldValue(this.Document.nativeHeight, 0);
+ var nativeWidth = (this.Document.nativeWidth || 0);
+ var nativeHeight = (this.Document.nativeHeight || 0);
if (!nativeWidth || !nativeHeight) {
if (!this.Document.nativeWidth) this.Document.nativeWidth = this.player!.videoWidth;
- this.Document.nativeHeight = this.Document.nativeWidth / aspect;
- this.Document.height = FieldValue(this.Document.width, 0) / aspect;
+ this.Document.nativeHeight = (this.Document.nativeWidth || 0) / aspect;
+ this.Document.height = (this.Document.width || 0) / aspect;
}
if (!this.Document.duration) this.Document.duration = this.player!.duration;
}
@action public Play = (update: boolean = true) => {
- this.Playing = true;
+ this._playing = true;
update && this.player && this.player.play();
update && this._youtubePlayer && this._youtubePlayer.playVideo();
this._youtubePlayer && !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 5));
@@ -75,7 +80,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
}
@action public Pause = (update: boolean = true) => {
- this.Playing = false;
+ this._playing = false;
update && this.player && this.player.pause();
update && this._youtubePlayer && this._youtubePlayer.pauseVideo && this._youtubePlayer.pauseVideo();
this._youtubePlayer && this._playTimer && clearInterval(this._playTimer);
@@ -97,11 +102,11 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
}
@action public Snapshot() {
- let width = NumCast(this.props.Document.width);
- let height = NumCast(this.props.Document.height);
+ let width = this.Document.width || 0;
+ let height = this.Document.height || 0;
var canvas = document.createElement('canvas');
canvas.width = 640;
- canvas.height = 640 * NumCast(this.props.Document.nativeHeight) / NumCast(this.props.Document.nativeWidth);
+ canvas.height = 640 * (this.Document.nativeHeight || 0) / (this.Document.nativeWidth || 1);
var ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions
if (ctx) {
ctx.rect(0, 0, canvas.width, canvas.height);
@@ -112,24 +117,25 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
if (!this._videoRef) { // can't find a way to take snapshots of videos
let b = Docs.Create.ButtonDocument({
- x: NumCast(this.props.Document.x) + width, y: NumCast(this.props.Document.y),
- width: 150, height: 50, title: NumCast(this.props.Document.curPage).toString()
+ x: (this.Document.x || 0) + width, y: (this.Document.y || 0),
+ width: 150, height: 50, title: (this.Document.currentTimecode || 0).toString()
});
- b.onClick = ScriptField.MakeScript(`this.curPage = ${NumCast(this.props.Document.curPage)}`);
+ b.onClick = ScriptField.MakeScript(`this.currentTimecode = ${(this.Document.currentTimecode || 0)}`);
} else {
//convert to desired file format
var dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
// if you want to preview the captured image,
- let filename = encodeURIComponent("snapshot" + this.props.Document.title + "_" + this.props.Document.curPage).replace(/\./g, "");
+ let filename = path.basename(encodeURIComponent("snapshot" + StrCast(this.Document.title).replace(/\..*$/, "") + "_" + (this.Document.currentTimecode || 0).toString().replace(/\./, "_")));
VideoBox.convertDataUri(dataUrl, filename).then(returnedFilename => {
if (returnedFilename) {
let url = this.choosePath(Utils.prepend(returnedFilename));
let imageSummary = Docs.Create.ImageDocument(url, {
- x: NumCast(this.props.Document.x) + width, y: NumCast(this.props.Document.y),
- width: 150, height: height / width * 150, title: "--snapshot" + NumCast(this.props.Document.curPage) + " image-"
+ x: (this.Document.x || 0) + width, y: (this.Document.y || 0),
+ width: 150, height: height / width * 150, title: "--snapshot" + (this.Document.currentTimecode || 0) + " image-"
});
- this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.addDocument && this.props.ContainingCollectionView.props.addDocument(imageSummary, false);
- DocUtils.MakeLink(imageSummary, this.props.Document);
+ imageSummary.isButton = true;
+ this.props.addDocument && this.props.addDocument(imageSummary);
+ DocUtils.MakeLink({ doc: imageSummary }, { doc: this.props.Document }, "snapshot from " + this.Document.title, "video frame snapshot");
}
});
}
@@ -137,8 +143,8 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
@action
updateTimecode = () => {
- this.player && (this.props.Document.curPage = this.player.currentTime);
- this._youtubePlayer && (this.props.Document.curPage = this._youtubePlayer.getCurrentTime());
+ this.player && (this.Document.currentTimecode = this.player.currentTime);
+ this._youtubePlayer && (this.Document.currentTimecode = this._youtubePlayer.getCurrentTime());
}
componentDidMount() {
@@ -146,12 +152,12 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
if (this.youtubeVideoId) {
let youtubeaspect = 400 / 315;
- var nativeWidth = FieldValue(this.Document.nativeWidth, 0);
- var nativeHeight = FieldValue(this.Document.nativeHeight, 0);
+ var nativeWidth = (this.Document.nativeWidth || 0);
+ var nativeHeight = (this.Document.nativeHeight || 0);
if (!nativeWidth || !nativeHeight) {
if (!this.Document.nativeWidth) this.Document.nativeWidth = 600;
- this.Document.nativeHeight = this.Document.nativeWidth / youtubeaspect;
- this.Document.height = FieldValue(this.Document.width, 0) / youtubeaspect;
+ this.Document.nativeHeight = (this.Document.nativeWidth || 0) / youtubeaspect;
+ this.Document.height = (this.Document.width || 0) / youtubeaspect;
}
}
}
@@ -168,10 +174,9 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
if (vref) {
this._videoRef!.ontimeupdate = this.updateTimecode;
vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen);
- if (this._reactionDisposer) this._reactionDisposer();
- this._reactionDisposer = reaction(() => this.props.Document.curPage, () =>
- !this.Playing && (vref.currentTime = this.Document.curPage || 0)
- , { fireImmediately: true });
+ this._reactionDisposer && this._reactionDisposer();
+ this._reactionDisposer = reaction(() => this.Document.currentTimecode || 0,
+ time => !this._playing && (vref.currentTime = time), { fireImmediately: true });
}
}
@@ -208,7 +213,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
let interactive = InkingControl.Instance.selectedTool || !this.props.isSelected() ? "" : "-interactive";
let style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
return !field ? <div>Loading</div> :
- <video className={`${style}`} ref={this.setVideoRef} onCanPlay={this.videoLoad} controls={VideoBox._showControls}
+ <video className={`${style}`} key="video" ref={this.setVideoRef} onCanPlay={this.videoLoad} controls={VideoBox._showControls}
onPlay={() => this.Play()} onSeeked={this.updateTimecode} onPause={() => this.Pause()} onClick={e => e.preventDefault()}>
<source src={field.url.href} type="video/mp4" />
Not supported.
@@ -236,13 +241,13 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
this.Pause();
return;
}
- if (event.data === YT.PlayerState.PLAYING && !this.Playing) this.Play(false);
- if (event.data === YT.PlayerState.PAUSED && this.Playing) this.Pause(false);
+ if (event.data === YT.PlayerState.PLAYING && !this._playing) this.Play(false);
+ if (event.data === YT.PlayerState.PAUSED && this._playing) this.Pause(false);
});
let onYoutubePlayerReady = (event: any) => {
this._reactionDisposer && this._reactionDisposer();
this._youtubeReactionDisposer && this._youtubeReactionDisposer();
- this._reactionDisposer = reaction(() => this.props.Document.curPage, () => !this.Playing && this.Seek(this.Document.curPage || 0));
+ this._reactionDisposer = reaction(() => this.Document.currentTimecode, () => !this._playing && this.Seek(this.Document.currentTimecode || 0));
this._youtubeReactionDisposer = reaction(() => [this.props.isSelected(), DocumentDecorations.Instance.Interacting, InkingControl.Instance.selectedTool], () => {
let interactive = InkingControl.Instance.selectedTool === InkTool.None && this.props.isSelected() && !DocumentDecorations.Instance.Interacting;
iframe.style.pointerEvents = interactive ? "all" : "none";
@@ -256,25 +261,104 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
});
}
+ private get uIButtons() {
+ let scaling = Math.min(1.8, this.props.ScreenToLocalTransform().Scale);
+ let curTime = (this.Document.currentTimecode || 0);
+ return ([<div className="videoBox-time" key="time" onPointerDown={this.onResetDown} style={{ transform: `scale(${scaling})` }}>
+ <span>{"" + Math.round(curTime)}</span>
+ <span style={{ fontSize: 8 }}>{" " + Math.round((curTime - Math.trunc(curTime)) * 100)}</span>
+ </div>,
+ <div className="videoBox-snapshot" key="snap" onPointerDown={this.onSnapshot} style={{ transform: `scale(${scaling})` }}>
+ <FontAwesomeIcon icon="camera" size="lg" />
+ </div>,
+ VideoBox._showControls ? (null) : [
+ <div className="videoBox-play" key="play" onPointerDown={this.onPlayDown} style={{ transform: `scale(${scaling})` }}>
+ <FontAwesomeIcon icon={this._playing ? "pause" : "play"} size="lg" />
+ </div>,
+ <div className="videoBox-full" key="full" onPointerDown={this.onFullDown} style={{ transform: `scale(${scaling})` }}>
+ F
+ </div>
+ ]]);
+ }
+
+ onPlayDown = () => this._playing ? this.Pause() : this.Play();
+
+ onFullDown = (e: React.PointerEvent) => {
+ this.FullScreen();
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ onSnapshot = (e: React.PointerEvent) => {
+ this.Snapshot();
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ onResetDown = (e: React.PointerEvent) => {
+ this.Pause();
+ e.stopPropagation();
+ this._isResetClick = 0;
+ document.addEventListener("pointermove", this.onResetMove, true);
+ document.addEventListener("pointerup", this.onResetUp, true);
+ }
- @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplate ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
+ onResetMove = (e: PointerEvent) => {
+ this._isResetClick += Math.abs(e.movementX) + Math.abs(e.movementY);
+ this.Seek(Math.max(0, (this.Document.currentTimecode || 0) + Math.sign(e.movementX) * 0.0333));
+ e.stopImmediatePropagation();
+ }
+
+ @action
+ onResetUp = (e: PointerEvent) => {
+ document.removeEventListener("pointermove", this.onResetMove, true);
+ document.removeEventListener("pointerup", this.onResetUp, true);
+ this._isResetClick < 10 && (this.Document.currentTimecode = 0);
+ }
@computed get youtubeContent() {
this._youtubeIframeId = VideoBox._youtubeIframeCounter++;
this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true;
let style = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : "");
- let start = untracked(() => Math.round(NumCast(this.props.Document.curPage)));
+ let start = untracked(() => Math.round(this.Document.currentTimecode || 0));
return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`}
- onLoad={this.youtubeIframeLoaded} className={`${style}`} width={NumCast(this.props.Document.nativeWidth, 640)} height={NumCast(this.props.Document.nativeHeight, 390)}
- src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=1&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._showControls ? 1 : 0}`}
- ></iframe>;
+ onLoad={this.youtubeIframeLoaded} className={`${style}`} width={(this.Document.nativeWidth || 640)} height={(this.Document.nativeHeight || 390)}
+ src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=1&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._showControls ? 1 : 0}`} />;
+ }
+
+ @action.bound
+ addDocumentWithTimestamp(doc: Doc): boolean {
+ var curTime = (this.Document.currentTimecode || -1);
+ curTime !== -1 && (doc.displayTimecode = curTime);
+ return this.addDocument(doc);
}
render() {
- Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
- return <div style={{ pointerEvents: "all", width: "100%", height: "100%" }} onContextMenu={this.specificContextMenu}>
- {this.youtubeVideoId ? this.youtubeContent : this.content}
- </div>;
+ return (<div className={"videoBox-container"} onContextMenu={this.specificContextMenu}>
+ <CollectionFreeFormView {...this.props}
+ PanelHeight={this.props.PanelHeight}
+ PanelWidth={this.props.PanelWidth}
+ annotationsKey={this.annotationsKey}
+ focus={this.props.focus}
+ isSelected={this.props.isSelected}
+ isAnnotationOverlay={true}
+ select={emptyFunction}
+ active={this.active}
+ ContentScaling={returnOne}
+ whenActiveChanged={this.whenActiveChanged}
+ removeDocument={this.removeDocument}
+ moveDocument={this.moveDocument}
+ addDocument={this.addDocumentWithTimestamp}
+ CollectionView={undefined}
+ ScreenToLocalTransform={this.props.ScreenToLocalTransform}
+ ruleProvider={undefined}
+ renderDepth={this.props.renderDepth + 1}
+ ContainingCollectionDoc={this.props.ContainingCollectionDoc}
+ chromeCollapsed={true}>
+ {() => [this.youtubeVideoId ? this.youtubeContent : this.content]}
+ </CollectionFreeFormView>
+ {this.uIButtons}
+ </div >);
}
}
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 29eef27a0..5af743859 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -1,34 +1,36 @@
+import { library } from "@fortawesome/fontawesome-svg-core";
+import { faStickyNote } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
-import { FieldResult, Doc, Field } from "../../../new_fields/Doc";
+import { Doc, FieldResult } from "../../../new_fields/Doc";
import { HtmlField } from "../../../new_fields/HtmlField";
+import { InkTool } from "../../../new_fields/InkField";
+import { makeInterface } from "../../../new_fields/Schema";
+import { Cast, NumCast } from "../../../new_fields/Types";
import { WebField } from "../../../new_fields/URLField";
+import { emptyFunction, returnOne, Utils } from "../../../Utils";
+import { Docs } from "../../documents/Documents";
+import { SelectionManager } from "../../util/SelectionManager";
+import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
import { DocumentDecorations } from "../DocumentDecorations";
import { InkingControl } from "../InkingControl";
import { FieldView, FieldViewProps } from './FieldView';
+import { KeyValueBox } from "./KeyValueBox";
import "./WebBox.scss";
import React = require("react");
-import { InkTool } from "../../../new_fields/InkField";
-import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
-import { Utils } from "../../../Utils";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { faStickyNote } from '@fortawesome/free-solid-svg-icons';
-import { observable, action, computed } from "mobx";
-import { listSpec } from "../../../new_fields/Schema";
-import { RefField } from "../../../new_fields/RefField";
-import { ObjectField } from "../../../new_fields/ObjectField";
-import { updateSourceFile } from "typescript";
-import { KeyValueBox } from "./KeyValueBox";
-import { setReactionScheduler } from "mobx/lib/internal";
-import { library } from "@fortawesome/fontawesome-svg-core";
-import { SelectionManager } from "../../util/SelectionManager";
-import { Docs } from "../../documents/Documents";
+import { DocAnnotatableComponent } from "../DocComponent";
+import { documentSchema } from "../../../new_fields/documentSchemas";
library.add(faStickyNote);
+type WebDocument = makeInterface<[typeof documentSchema]>;
+const WebDocument = makeInterface(documentSchema);
+
@observer
-export class WebBox extends React.Component<FieldViewProps> {
+export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>(WebDocument) {
- public static LayoutString() { return FieldView.LayoutString(WebBox); }
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(WebBox, fieldKey); }
@observable private collapsed: boolean = true;
@observable private url: string = "";
@@ -37,12 +39,12 @@ export class WebBox extends React.Component<FieldViewProps> {
let field = Cast(this.props.Document[this.props.fieldKey], WebField);
if (field && field.url.href.indexOf("youtube") !== -1) {
let youtubeaspect = 400 / 315;
- var nativeWidth = NumCast(this.props.Document.nativeWidth, 0);
- var nativeHeight = NumCast(this.props.Document.nativeHeight, 0);
+ var nativeWidth = NumCast(this.layoutDoc.nativeWidth);
+ var nativeHeight = NumCast(this.layoutDoc.nativeHeight);
if (!nativeWidth || !nativeHeight || Math.abs(nativeWidth / nativeHeight - youtubeaspect) > 0.05) {
- if (!nativeWidth) this.props.Document.nativeWidth = 600;
- this.props.Document.nativeHeight = NumCast(this.props.Document.nativeWidth) / youtubeaspect;
- this.props.Document.height = NumCast(this.props.Document.width) / youtubeaspect;
+ if (!nativeWidth) this.layoutDoc.nativeWidth = 600;
+ this.layoutDoc.nativeHeight = NumCast(this.layoutDoc.nativeWidth) / youtubeaspect;
+ this.layoutDoc.height = NumCast(this.layoutDoc.width) / youtubeaspect;
}
}
@@ -91,7 +93,7 @@ export class WebBox extends React.Component<FieldViewProps> {
});
SelectionManager.SelectedDocuments().map(dv => {
- dv.props.addDocument && dv.props.addDocument(newBox, false);
+ dv.props.addDocument && dv.props.addDocument(newBox);
dv.props.removeDocument && dv.props.removeDocument(dv.props.Document);
});
@@ -162,8 +164,10 @@ export class WebBox extends React.Component<FieldViewProps> {
e.stopPropagation();
}
}
- render() {
- let field = this.props.Document[this.props.fieldKey];
+
+ @computed
+ get content() {
+ let field = this.dataDoc[this.props.fieldKey];
let view;
if (field instanceof HtmlField) {
view = <span id="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />;
@@ -189,4 +193,30 @@ export class WebBox extends React.Component<FieldViewProps> {
{!frozen ? (null) : <div className="webBox-overlay" onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer} />}
</>);
}
+ render() {
+ return (<div className={"imageBox-container"} >
+ <CollectionFreeFormView {...this.props}
+ PanelHeight={this.props.PanelHeight}
+ PanelWidth={this.props.PanelWidth}
+ annotationsKey={this.annotationsKey}
+ focus={this.props.focus}
+ isSelected={this.props.isSelected}
+ isAnnotationOverlay={true}
+ select={emptyFunction}
+ active={this.active}
+ ContentScaling={returnOne}
+ whenActiveChanged={this.whenActiveChanged}
+ removeDocument={this.removeDocument}
+ moveDocument={this.moveDocument}
+ addDocument={this.addDocument}
+ CollectionView={undefined}
+ ScreenToLocalTransform={this.props.ScreenToLocalTransform}
+ ruleProvider={undefined}
+ renderDepth={this.props.renderDepth + 1}
+ ContainingCollectionDoc={this.props.ContainingCollectionDoc}
+ chromeCollapsed={true}>
+ {() => [this.content]}
+ </CollectionFreeFormView>
+ </div >);
+ }
} \ No newline at end of file
diff --git a/src/client/views/pdf/Annotation.scss b/src/client/views/pdf/Annotation.scss
index 0c6df74f0..cc326eb93 100644
--- a/src/client/views/pdf/Annotation.scss
+++ b/src/client/views/pdf/Annotation.scss
@@ -2,6 +2,5 @@
pointer-events: all;
user-select: none;
position: absolute;
- background-color: red;
- opacity: 0.1;
+ background-color: rgba(146, 245, 95, 0.467);
} \ No newline at end of file
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index 3ed85f6a5..2d8f47666 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -1,7 +1,7 @@
import React = require("react");
import { action, IReactionDisposer, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
-import { Doc, DocListCast, HeightSym, WidthSym, Opt } from "../../../new_fields/Doc";
+import { Doc, DocListCast, HeightSym, WidthSym, Opt, DocListCastAsync } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
import { List } from "../../../new_fields/List";
import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
@@ -11,9 +11,10 @@ import "./Annotation.scss";
interface IAnnotationProps {
anno: Doc;
- fieldExtensionDoc: Doc;
+ extensionDoc: Doc;
addDocTab: (document: Doc, dataDoc: Opt<Doc>, where: string) => boolean;
pinToPres: (document: Doc) => void;
+ focus: (doc: Doc) => void;
}
export default class Annotation extends React.Component<IAnnotationProps> {
@@ -28,7 +29,7 @@ interface IRegionAnnotationProps {
y: number;
width: number;
height: number;
- fieldExtensionDoc: Doc;
+ extensionDoc: Doc;
addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
pinToPres: (document: Doc) => void;
document: Doc;
@@ -50,26 +51,27 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
);
this._brushDisposer = reaction(
- () => FieldValue(Cast(this.props.document.group, Doc)) && Doc.IsBrushed(FieldValue(Cast(this.props.document.group, Doc))!),
+ () => FieldValue(Cast(this.props.document.group, Doc)) && Doc.isBrushedHighlightedDegree(FieldValue(Cast(this.props.document.group, Doc))!),
(brushed) => {
if (brushed !== undefined) {
- runInAction(() => this._brushed = brushed);
+ runInAction(() => this._brushed = brushed !== 0);
}
}
);
}
componentWillUnmount() {
+ this._brushDisposer && this._brushDisposer();
this._reactionDisposer && this._reactionDisposer();
}
deleteAnnotation = () => {
- let annotation = DocListCast(this.props.fieldExtensionDoc.annotations);
+ let annotation = DocListCast(this.props.extensionDoc.annotations);
let group = FieldValue(Cast(this.props.document.group, Doc));
if (group) {
if (annotation.indexOf(group) !== -1) {
let newAnnotations = annotation.filter(a => a !== FieldValue(Cast(this.props.document.group, Doc)));
- this.props.fieldExtensionDoc.annotations = new List<Doc>(newAnnotations);
+ this.props.extensionDoc.annotations = new List<Doc>(newAnnotations);
}
DocListCast(group.annotations).forEach(anno => anno.delete = true);
@@ -92,20 +94,20 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
PDFMenu.Instance.AddTag = this.addTag.bind(this);
PDFMenu.Instance.PinToPres = this.pinToPres;
PDFMenu.Instance.jumpTo(e.clientX, e.clientY, true);
+ e.stopPropagation();
}
else if (e.button === 0) {
- let targetDoc = await Cast(this.props.document.target, Doc);
- if (targetDoc) {
- let context = await Cast(targetDoc.targetContext, Doc);
- if (context) {
- DocumentManager.Instance.jumpToDocument(targetDoc, false, false,
- ((doc) => this.props.addDocTab(targetDoc!, undefined, e.ctrlKey ? "onRight" : "inTab")),
- undefined, undefined);
- }
+ let annoGroup = await Cast(this.props.document.group, Doc);
+ if (annoGroup) {
+ DocumentManager.Instance.FollowLink(undefined, annoGroup,
+ (doc: Doc, maxLocation: string) => this.props.addDocTab(doc, undefined, e.ctrlKey ? "inTab" : "onRight"),
+ false, false, undefined);
+ e.stopPropagation();
}
}
}
+
addTag = (key: string, value: string): boolean => {
let group = FieldValue(Cast(this.props.document.group, Doc));
if (group) {
@@ -123,7 +125,9 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
left: this.props.x,
width: this.props.width,
height: this.props.height,
- backgroundColor: this._brushed ? "green" : StrCast(this.props.document.color)
+ opacity: this._brushed ? 0.5 : undefined,
+ backgroundColor: this._brushed ? "orange" : StrCast(this.props.document.backgroundColor),
+ transition: "opacity 0.5s",
}} />);
}
} \ No newline at end of file
diff --git a/src/client/views/pdf/PDFMenu.scss b/src/client/views/pdf/PDFMenu.scss
index b06d19c53..44e075153 100644
--- a/src/client/views/pdf/PDFMenu.scss
+++ b/src/client/views/pdf/PDFMenu.scss
@@ -15,7 +15,7 @@
}
.pdfMenu-button:hover {
- background-color: #121212;
+ background-color: #d4d4d4;
}
.pdfMenu-dragger {
diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx
index 2202351ee..517a99a68 100644
--- a/src/client/views/pdf/PDFMenu.tsx
+++ b/src/client/views/pdf/PDFMenu.tsx
@@ -4,7 +4,7 @@ import { observable, action, } from "mobx";
import { observer } from "mobx-react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { emptyFunction, returnFalse } from "../../../Utils";
-import { Doc } from "../../../new_fields/Doc";
+import { Doc, Opt } from "../../../new_fields/Doc";
@observer
export default class PDFMenu extends React.Component {
@@ -31,7 +31,7 @@ export default class PDFMenu extends React.Component {
@observable public Pinned: boolean = false;
public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = emptyFunction;
- public Highlight: (d: Doc | undefined, color: string) => void = emptyFunction;
+ public Highlight: (color: string) => Opt<Doc> = (color: string) => undefined;
public Delete: () => void = emptyFunction;
public Snippet: (marquee: { left: number, top: number, width: number, height: number }) => void = emptyFunction;
public AddTag: (key: string, value: string) => boolean = returnFalse;
@@ -155,12 +155,8 @@ export default class PDFMenu extends React.Component {
@action
highlightClicked = (e: React.MouseEvent) => {
- if (!this.Pinned) {
- this.Highlight(undefined, "#f4f442");
- }
- else {
+ if (!this.Highlight("rgba(245, 230, 95, 0.616)") && this.Pinned) { // yellowish highlight color for a marker type highlight
this.Highlighting = !this.Highlighting;
- this.Highlight(undefined, "#f4f442");
}
}
diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss
index 8027e93a3..f6fedf3da 100644
--- a/src/client/views/pdf/PDFViewer.scss
+++ b/src/client/views/pdf/PDFViewer.scss
@@ -11,10 +11,22 @@
// transform: scale(0.75);
// transform-origin: top left;
// }
- // .textLayer {
- // transform: scale(0.75);
- // transform-origin: top left;
- // }
+ .textLayer {
+
+ mix-blend-mode: multiply;
+ opacity: 0.9;
+ span {
+ padding-right: 5px;
+ padding-bottom: 4px;
+ }
+ }
+ .textLayer ::selection { background: yellow; } // should match the backgroundColor in createAnnotation()
+ .textLayer .highlight {
+ background-color: yellow;
+ }
+ .textLayer .highlight.selected {
+ background-color: orange;
+ }
.page {
position: relative;
@@ -30,7 +42,6 @@
}
.pdfViewer-overlay {
- transform: scale(2.14359);
transform-origin: left top;
position: absolute;
top: 0px;
@@ -43,11 +54,11 @@
top: 0;
width: 100%;
pointer-events: none;
+ mix-blend-mode: multiply;
- .pdfPage-annotationBox {
+ .pdfViewer-annotationBox {
position: absolute;
- background-color: red;
- opacity: 0.1;
+ background-color: rgba(245, 230, 95, 0.616);
}
}
.pdfViewer-waiting {
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 33226eac4..51dc6e8d6 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -6,11 +6,10 @@ import { Dictionary } from "typescript-collections";
import { Doc, DocListCast, FieldResult, WidthSym, Opt, HeightSym } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
import { List } from "../../../new_fields/List";
-import { listSpec } from "../../../new_fields/Schema";
+import { makeInterface, createSchema } from "../../../new_fields/Schema";
import { ScriptField } from "../../../new_fields/ScriptField";
import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import smoothScroll, { Utils, emptyFunction, returnOne } from "../../../Utils";
-import { DocServer } from "../../DocServer";
+import { smoothScroll, Utils, emptyFunction, returnOne, intersectRect, addStyleSheet, addStyleSheetRule, clearStyleSheetRules } from "../../../Utils";
import { Docs, DocUtils } from "../../documents/Documents";
import { DragManager } from "../../util/DragManager";
import { CompiledScript, CompileScript } from "../../util/Scripting";
@@ -19,41 +18,53 @@ import PDFMenu from "./PDFMenu";
import "./PDFViewer.scss";
import React = require("react");
import * as rp from "request-promise";
-import { CollectionPDFView } from "../collections/CollectionPDFView";
-import { CollectionVideoView } from "../collections/CollectionVideoView";
import { CollectionView } from "../collections/CollectionView";
import Annotation from "./Annotation";
import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
import { SelectionManager } from "../../util/SelectionManager";
+import { undoBatch } from "../../util/UndoManager";
+import { DocAnnotatableComponent } from "../DocComponent";
+import { DocumentType } from "../../documents/DocumentTypes";
+import { documentSchema } from "../../../new_fields/documentSchemas";
const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer");
const pdfjsLib = require("pdfjs-dist");
+export const pageSchema = createSchema({
+ curPage: "number",
+ fitWidth: "boolean",
+ rotation: "number",
+ scrollY: "number",
+ scrollHeight: "number",
+ search_string: "string"
+});
+
pdfjsLib.GlobalWorkerOptions.workerSrc = `/assets/pdf.worker.js`;
+type PdfDocument = makeInterface<[typeof documentSchema, typeof pageSchema]>;
+const PdfDocument = makeInterface(documentSchema, pageSchema);
interface IViewerProps {
pdf: Pdfjs.PDFDocumentProxy;
url: string;
+ fieldKey: string;
Document: Doc;
DataDoc?: Doc;
- fieldExtensionDoc: Doc;
- fieldKey: string;
- fieldExt: string;
+ ContainingCollectionView: Opt<CollectionView>;
PanelWidth: () => number;
PanelHeight: () => number;
ContentScaling: () => number;
select: (isCtrlPressed: boolean) => void;
startupLive: boolean;
renderDepth: number;
+ focus: (doc: Doc) => void;
isSelected: () => boolean;
loaded: (nw: number, nh: number, np: number) => void;
active: () => boolean;
GoToPage?: (n: number) => void;
addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
pinToPres: (document: Doc) => void;
- addDocument?: (doc: Doc, allowDuplicates?: boolean) => boolean;
+ addDocument?: (doc: Doc) => boolean;
setPdfViewer: (view: PDFViewer) => void;
ScreenToLocalTransform: () => Transform;
- ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
whenActiveChanged: (isActive: boolean) => void;
}
@@ -61,7 +72,8 @@ interface IViewerProps {
* Handles rendering and virtualization of the pdf
*/
@observer
-export class PDFViewer extends React.Component<IViewerProps> {
+export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument>(PdfDocument) {
+ static _annotationStyle: any = addStyleSheet();
@observable private _pageSizes: { width: number, height: number }[] = [];
@observable private _annotations: Doc[] = [];
@observable private _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>();
@@ -75,16 +87,17 @@ export class PDFViewer extends React.Component<IViewerProps> {
@observable private _showWaiting = true;
@observable private _showCover = false;
@observable private _zoomed = 1;
+ @observable private _scrollTop = 0;
- public pdfViewer: any;
+ private _pdfViewer: any;
private _retries = 0; // number of times tried to create the PDF viewer
- private _isChildActive = false;
private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void);
private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
private _reactionDisposer?: IReactionDisposer;
private _selectionReactionDisposer?: IReactionDisposer;
private _annotationReactionDisposer?: IReactionDisposer;
private _filterReactionDisposer?: IReactionDisposer;
+ private _searchReactionDisposer?: IReactionDisposer;
private _viewer: React.RefObject<HTMLDivElement> = React.createRef();
private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
private _selectionText: string = "";
@@ -95,29 +108,44 @@ export class PDFViewer extends React.Component<IViewerProps> {
private _coverPath: any;
@computed get allAnnotations() {
- return DocListCast(this.props.fieldExtensionDoc.annotations).filter(
- anno => this._script.run({ this: anno }, console.log, true).result);
+ return this.extensionDoc ? DocListCast(this.extensionDoc.annotations).filter(
+ anno => this._script.run({ this: anno }, console.log, true).result) : [];
}
@computed get nonDocAnnotations() {
return this._annotations.filter(anno => this._script.run({ this: anno }, console.log, true).result);
}
+ _lastSearch: string = "";
componentDidMount = async () => {
// change the address to be the file address of the PNG version of each page
// file address of the pdf
- this._coverPath = JSON.parse(await rp.get(Utils.prepend(`/thumbnail${this.props.url.substring("files/".length, this.props.url.length - ".pdf".length)}-${NumCast(this.props.Document.curPage, 1)}.PNG`)));
+ this._coverPath = JSON.parse(await rp.get(Utils.prepend(`/thumbnail${this.props.url.substring("files/".length, this.props.url.length - ".pdf".length)}-${(this.Document.curPage || 1)}.PNG`)));
runInAction(() => this._showWaiting = this._showCover = true);
- this._selectionReactionDisposer = reaction(() => this.props.isSelected(), () => this.props.isSelected() && SelectionManager.SelectedDocuments().length === 1 && this.setupPdfJsViewer(), { fireImmediately: this.props.startupLive });
+ this.props.startupLive && this.setupPdfJsViewer();
+ this._searchReactionDisposer = reaction(() => this.Document.search_string, searchString => {
+ if (searchString) {
+ this.search(searchString, true);
+ this._lastSearch = searchString;
+ }
+ else {
+ setTimeout(() => this._lastSearch === "mxytzlaf" && this.search("mxytzlaf", true), 200); // bcz: how do we clear search highlights?
+ this._lastSearch && (this._lastSearch = "mxytzlaf");
+ }
+ }, { fireImmediately: true });
+
+ this._selectionReactionDisposer = reaction(() => this.props.isSelected(),
+ () => (SelectionManager.SelectedDocuments().length === 1) && this.setupPdfJsViewer(),
+ { fireImmediately: true });
this._reactionDisposer = reaction(
- () => this.props.Document.scrollY,
+ () => this.Document.scrollY,
(scrollY) => {
if (scrollY !== undefined) {
if (this._showCover || this._showWaiting) {
this.setupPdfJsViewer();
}
- this._mainCont.current && smoothScroll(1000, this._mainCont.current, NumCast(this.props.Document.scrollY) || 0);
- this.props.Document.scrollY = undefined;
+ this._mainCont.current && smoothScroll(1000, this._mainCont.current, (this.Document.scrollY || 0));
+ this.Document.scrollY = undefined;
}
},
{ fireImmediately: true }
@@ -129,28 +157,22 @@ export class PDFViewer extends React.Component<IViewerProps> {
this._annotationReactionDisposer && this._annotationReactionDisposer();
this._filterReactionDisposer && this._filterReactionDisposer();
this._selectionReactionDisposer && this._selectionReactionDisposer();
+ this._searchReactionDisposer && this._searchReactionDisposer();
document.removeEventListener("copy", this.copy);
}
copy = (e: ClipboardEvent) => {
if (this.props.active() && e.clipboardData) {
- e.clipboardData.setData("text/plain", this._selectionText);
- e.clipboardData.setData("dash/pdfOrigin", this.props.Document[Id]);
- e.clipboardData.setData("dash/pdfRegion", this.makeAnnotationDocument(undefined, "#0390fc")[Id]);
+ let annoDoc = this.makeAnnotationDocument("rgba(3,144,152,0.3)"); // copied text markup color (blueish)
+ if (annoDoc) {
+ e.clipboardData.setData("text/plain", this._selectionText);
+ e.clipboardData.setData("dash/pdfOrigin", this.props.Document[Id]);
+ e.clipboardData.setData("dash/pdfRegion", annoDoc[Id]);
+ }
e.preventDefault();
}
}
- paste = (e: ClipboardEvent) => {
- if (e.clipboardData && e.clipboardData.getData("dash/pdfOrigin") === this.props.Document[Id]) {
- let linkDocId = e.clipboardData.getData("dash/linkDoc");
- linkDocId && DocServer.GetRefField(linkDocId).then(async (link) =>
- (link instanceof Doc) && (Doc.GetProto(link).anchor2 = this.makeAnnotationDocument(await Cast(Doc.GetProto(link), Doc), "#0390fc", false)));
- }
- }
-
- setSelectionText = (text: string) => this._selectionText = text;
-
@action
initialLoad = async () => {
if (this._pageSizes.length === 0) {
@@ -164,7 +186,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
i === this.props.pdf.numPages - 1 && this.props.loaded((page.view[page.rotate === 0 || page.rotate === 180 ? 2 : 3] - page.view[page.rotate === 0 || page.rotate === 180 ? 0 : 1]),
(page.view[page.rotate === 0 || page.rotate === 180 ? 3 : 2] - page.view[page.rotate === 0 || page.rotate === 180 ? 1 : 0]), i);
}))));
- Doc.GetProto(this.props.Document).scrollHeight = this._pageSizes.reduce((size, page) => size + page.height, 0) * 96 / 72;
+ this.Document.scrollHeight = this._pageSizes.reduce((size, page) => size + page.height, 0) * 96 / 72;
}
}
@@ -177,12 +199,12 @@ export class PDFViewer extends React.Component<IViewerProps> {
await this.initialLoad();
this._annotationReactionDisposer = reaction(
- () => this.props.fieldExtensionDoc && DocListCast(this.props.fieldExtensionDoc.annotations),
+ () => this.extensionDoc && DocListCast(this.extensionDoc.annotations),
annotations => annotations && annotations.length && this.renderAnnotations(annotations, true),
{ fireImmediately: true });
this._filterReactionDisposer = reaction(
- () => ({ scriptField: Cast(this.props.Document.filterScript, ScriptField), annos: this._annotations.slice() }),
+ () => ({ scriptField: Cast(this.Document.filterScript, ScriptField), annos: this._annotations.slice() }),
action(({ scriptField, annos }: { scriptField: FieldResult<ScriptField>, annos: Doc[] }) => {
let oldScript = this._script.originalScript;
this._script = scriptField && scriptField.script.compiled ? scriptField.script : CompileScript("return true") as CompiledScript;
@@ -194,13 +216,6 @@ export class PDFViewer extends React.Component<IViewerProps> {
{ fireImmediately: true }
);
- document.removeEventListener("copy", this.copy);
- document.addEventListener("copy", this.copy);
- document.addEventListener("pagesinit", action(() => {
- this.pdfViewer.currentScaleValue = this._zoomed = 1;
- this.gotoPage(NumCast(this.props.Document.curPage, 1));
- }));
- document.addEventListener("pagerendered", action(() => this._showCover = this._showWaiting = false));
this.createPdfViewer();
}
@@ -212,43 +227,51 @@ export class PDFViewer extends React.Component<IViewerProps> {
}
return;
}
+ document.removeEventListener("copy", this.copy);
+ document.addEventListener("copy", this.copy);
+ document.addEventListener("pagesinit", action(() => {
+ this._pdfViewer.currentScaleValue = this._zoomed = 1;
+ this.gotoPage(this.Document.curPage || 1);
+ }));
+ document.addEventListener("pagerendered", action(() => this._showCover = this._showWaiting = false));
var pdfLinkService = new PDFJSViewer.PDFLinkService();
let pdfFindController = new PDFJSViewer.PDFFindController({
linkService: pdfLinkService,
});
- this.pdfViewer = new PDFJSViewer.PDFViewer({
+ this._pdfViewer = new PDFJSViewer.PDFViewer({
container: this._mainCont.current,
viewer: this._viewer.current,
linkService: pdfLinkService,
findController: pdfFindController,
renderer: "canvas",
});
- pdfLinkService.setViewer(this.pdfViewer);
+ pdfLinkService.setViewer(this._pdfViewer);
pdfLinkService.setDocument(this.props.pdf, null);
- this.pdfViewer.setDocument(this.props.pdf);
+ this._pdfViewer.setDocument(this.props.pdf);
}
+ @undoBatch
@action
- makeAnnotationDocument = (sourceDoc: Doc | undefined, color: string, createLink: boolean = true): Doc => {
+ makeAnnotationDocument = (color: string): Opt<Doc> => {
+ if (this._savedAnnotations.size() === 0) return undefined;
let mainAnnoDoc = Docs.Create.InstanceFromProto(new Doc(), "", {});
let mainAnnoDocProto = Doc.GetProto(mainAnnoDoc);
let annoDocs: Doc[] = [];
+ let maxX = -Number.MAX_VALUE;
let minY = Number.MAX_VALUE;
- if (this._savedAnnotations.size() === 1 && this._savedAnnotations.values()[0].length === 1 && !createLink) {
+ if ((this._savedAnnotations.values()[0][0] as any).marqueeing) {
let anno = this._savedAnnotations.values()[0][0];
- let annoDoc = Docs.Create.FreeformDocument([], { backgroundColor: "rgba(255, 0, 0, 0.1)", title: "Annotation on " + StrCast(this.props.Document.title) });
+ let annoDoc = Docs.Create.FreeformDocument([], { backgroundColor: color, title: "Annotation on " + this.Document.title });
if (anno.style.left) annoDoc.x = parseInt(anno.style.left);
if (anno.style.top) annoDoc.y = parseInt(anno.style.top);
if (anno.style.height) annoDoc.height = parseInt(anno.style.height);
if (anno.style.width) annoDoc.width = parseInt(anno.style.width);
- annoDoc.target = sourceDoc;
annoDoc.group = mainAnnoDoc;
- annoDoc.color = color;
- annoDoc.type = AnnotationTypes.Region;
- annoDocs.push(annoDoc);
annoDoc.isButton = true;
+ annoDocs.push(annoDoc);
anno.remove();
mainAnnoDoc = annoDoc;
+ mainAnnoDocProto.type = DocumentType.COL;
mainAnnoDocProto = Doc.GetProto(mainAnnoDoc);
mainAnnoDocProto.y = annoDoc.y;
} else {
@@ -258,23 +281,21 @@ export class PDFViewer extends React.Component<IViewerProps> {
if (anno.style.top) annoDoc.y = parseInt(anno.style.top);
if (anno.style.height) annoDoc.height = parseInt(anno.style.height);
if (anno.style.width) annoDoc.width = parseInt(anno.style.width);
- annoDoc.target = sourceDoc;
annoDoc.group = mainAnnoDoc;
- annoDoc.color = color;
- annoDoc.type = AnnotationTypes.Region;
+ annoDoc.backgroundColor = color;
annoDocs.push(annoDoc);
anno.remove();
(annoDoc.y !== undefined) && (minY = Math.min(NumCast(annoDoc.y), minY));
+ (annoDoc.x !== undefined) && (maxX = Math.max(NumCast(annoDoc.x) + NumCast(annoDoc.width), maxX));
}));
mainAnnoDocProto.y = Math.max(minY, 0);
+ mainAnnoDocProto.x = Math.max(maxX, 0);
+ mainAnnoDocProto.type = DocumentType.PDFANNO;
mainAnnoDocProto.annotations = new List<Doc>(annoDocs);
}
- mainAnnoDocProto.title = "Annotation on " + StrCast(this.props.Document.title);
+ mainAnnoDocProto.title = "Annotation on " + this.Document.title;
mainAnnoDocProto.annotationOn = this.props.Document;
- if (sourceDoc && createLink) {
- DocUtils.MakeLink(sourceDoc, mainAnnoDocProto, undefined, `Annotation from ${StrCast(this.props.Document.title)}`);
- }
this._savedAnnotations.clear();
this.Index = -1;
return mainAnnoDoc;
@@ -305,36 +326,23 @@ export class PDFViewer extends React.Component<IViewerProps> {
@action
gotoPage = (p: number) => {
- this.pdfViewer && this.pdfViewer.scrollPageIntoView({ pageNumber: Math.min(Math.max(1, p), this._pageSizes.length) });
+ this._pdfViewer && this._pdfViewer.scrollPageIntoView({ pageNumber: Math.min(Math.max(1, p), this._pageSizes.length) });
}
@action
scrollToAnnotation = (scrollToAnnotation: Doc) => {
- this.allAnnotations.forEach(d => Doc.UnBrushDoc(d));
- let windowHgt = this.props.PanelHeight() / this.props.ContentScaling();
- let scrollRange = this._mainCont.current!.scrollHeight - windowHgt;
- let pgScroll = scrollRange / this._pageSizes.length;
- this._mainCont.current!.scrollTo(0, NumCast(scrollToAnnotation.y) - pgScroll / 2);
- Doc.BrushDoc(scrollToAnnotation);
- }
-
- sendAnnotations = (page: number) => {
- return this._savedAnnotations.getValue(page);
- }
-
- receiveAnnotations = (annotations: HTMLDivElement[], page: number) => {
- if (page === -1) {
- this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove()));
- this._savedAnnotations.keys().forEach(k => this._savedAnnotations.setValue(k, annotations));
- }
- else {
- this._savedAnnotations.setValue(page, annotations);
+ if (scrollToAnnotation) {
+ let offset = this.visibleHeight() / 2 * 96 / 72;
+ this._mainCont.current && smoothScroll(500, this._mainCont.current, NumCast(scrollToAnnotation.y) - offset);
+ Doc.linkFollowHighlight(scrollToAnnotation);
}
}
+
@action
onScroll = (e: React.UIEvent<HTMLElement>) => {
- this.pdfViewer && (this.props.Document.curPage = this.pdfViewer.currentPageNumber);
+ this._scrollTop = this._mainCont.current!.scrollTop;
+ this._pdfViewer && (this.Document.curPage = this._pdfViewer.currentPageNumber);
}
// get the page index that the vertical offset passed in is on
@@ -354,6 +362,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
div.style.top = (parseInt(div.style.top)/*+ this.getScrollFromPage(page)*/).toString();
}
this._annotationLayer.current.append(div);
+ div.style.backgroundColor = "yellow";
+ div.style.opacity = "0.5";
let savedPage = this._savedAnnotations.getValue(page);
if (savedPage) {
savedPage.push(div);
@@ -370,8 +380,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
if (!searchString) {
fwd ? this.nextAnnotation() : this.prevAnnotation();
}
- else if (this.pdfViewer._pageViewsReady) {
- this.pdfViewer.findController.executeCommand('findagain', {
+ else if (this._pdfViewer._pageViewsReady) {
+ this._pdfViewer.findController.executeCommand('findagain', {
caseSensitive: false,
findPrevious: !fwd,
highlightAll: true,
@@ -381,7 +391,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
}
else if (this._mainCont.current) {
let executeFind = () => {
- this.pdfViewer.findController.executeCommand('find', {
+ this._pdfViewer.findController.executeCommand('find', {
caseSensitive: false,
findPrevious: !fwd,
highlightAll: true,
@@ -399,36 +409,31 @@ export class PDFViewer extends React.Component<IViewerProps> {
// if alt+left click, drag and annotate
this._downX = e.clientX;
this._downY = e.clientY;
- if (NumCast(this.props.Document.scale, 1) !== 1) return;
+ addStyleSheetRule(PDFViewer._annotationStyle, "pdfAnnotation", { "pointer-events": "none" });
+ if ((this.Document.scale || 1) !== 1) return;
if ((e.button !== 0 || e.altKey) && this.active()) {
this._setPreviewCursor && this._setPreviewCursor(e.clientX, e.clientY, true);
}
this._marqueeing = false;
if (!e.altKey && e.button === 0 && this.active()) {
+ // clear out old marquees and initialize menu for new selection
PDFMenu.Instance.StartDrag = this.startDrag;
PDFMenu.Instance.Highlight = this.highlight;
PDFMenu.Instance.Snippet = this.createSnippet;
PDFMenu.Instance.Status = "pdf";
PDFMenu.Instance.fadeOut(true);
+ this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove()));
+ this._savedAnnotations.keys().forEach(k => this._savedAnnotations.setValue(k, []));
if (e.target && (e.target as any).parentElement.className === "textLayer") {
- if (!e.ctrlKey) {
- this.receiveAnnotations([], -1);
- }
+ // start selecting text if mouse down on textLayer spans
}
- else {
+ else if (this._mainCont.current) {
// set marquee x and y positions to the spatially transformed position
- if (this._mainCont.current) {
- let boundingRect = this._mainCont.current.getBoundingClientRect();
- this._startX = this._marqueeX = (e.clientX - boundingRect.left) * (this._mainCont.current.offsetWidth / boundingRect.width);
- this._startY = this._marqueeY = (e.clientY - boundingRect.top) * (this._mainCont.current.offsetHeight / boundingRect.height) + this._mainCont.current.scrollTop;
- }
+ let boundingRect = this._mainCont.current.getBoundingClientRect();
+ this._startX = this._marqueeX = (e.clientX - boundingRect.left) * (this._mainCont.current.offsetWidth / boundingRect.width);
+ this._startY = this._marqueeY = (e.clientY - boundingRect.top) * (this._mainCont.current.offsetHeight / boundingRect.height) + this._mainCont.current.scrollTop;
+ this._marqueeHeight = this._marqueeWidth = 0;
this._marqueeing = true;
- let marquees = this._mainCont.current!.getElementsByClassName("pdfViewer-dragAnnotationBox");
- if (marquees && marquees.length) { // make a copy of the marquee
- let marquee = marquees[0] as HTMLDivElement;
- marquee.style.opacity = "0.2";
- }
- this.receiveAnnotations([], -1);
}
document.removeEventListener("pointermove", this.onSelectMove);
document.addEventListener("pointermove", this.onSelectMove);
@@ -463,12 +468,13 @@ export class PDFViewer extends React.Component<IViewerProps> {
let clientRects = selRange.getClientRects();
for (let i = 0; i < clientRects.length; i++) {
let rect = clientRects.item(i);
- if (rect/* && rect.width !== this._mainCont.current.getBoundingClientRect().width && rect.height !== this._mainCont.current.getBoundingClientRect().height / this.props.pdf.numPages*/) {
+ if (rect) {
let scaleY = this._mainCont.current.offsetHeight / boundingRect.height;
let scaleX = this._mainCont.current.offsetWidth / boundingRect.width;
- if (rect.width !== this._mainCont.current.clientWidth) {
+ if (rect.width !== this._mainCont.current.clientWidth &&
+ (i === 0 || !intersectRect(clientRects[i], clientRects[i - 1]))) {
let annoBox = document.createElement("div");
- annoBox.className = "pdfPage-annotationBox";
+ annoBox.className = "pdfViewer-annotationBox";
// transforms the positions from screen onto the pdf div
annoBox.style.top = ((rect.top - boundingRect.top) * scaleY + this._mainCont.current.scrollTop).toString();
annoBox.style.left = ((rect.left - boundingRect.left) * scaleX).toString();
@@ -479,8 +485,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
}
}
}
- let text = selRange.cloneContents().textContent;
- text && this.setSelectionText(text);
+ this._selectionText = selRange.cloneContents().textContent || "";
// clear selection
if (sel.empty) { // Chrome
@@ -492,22 +497,23 @@ export class PDFViewer extends React.Component<IViewerProps> {
@action
onSelectEnd = (e: PointerEvent): void => {
+ clearStyleSheetRules(PDFViewer._annotationStyle);
+ this._savedAnnotations.clear();
if (this._marqueeing) {
if (this._marqueeWidth > 10 || this._marqueeHeight > 10) {
let marquees = this._mainCont.current!.getElementsByClassName("pdfViewer-dragAnnotationBox");
- if (marquees && marquees.length) { // make a copy of the marquee
+ if (marquees && marquees.length) { // copy the marquee and convert it to a permanent annotation.
+ let style = (marquees[0] as HTMLDivElement).style;
let copy = document.createElement("div");
- let marquee = marquees[0] as HTMLDivElement;
- let style = marquee.style;
copy.style.left = style.left;
copy.style.top = style.top;
copy.style.width = style.width;
copy.style.height = style.height;
copy.style.border = style.border;
copy.style.opacity = style.opacity;
- copy.className = "pdfPage-annotationBox";
+ (copy as any).marqueeing = true;
+ copy.className = "pdfViewer-annotationBox";
this.createAnnotation(copy, this.getPageFromScroll(this._marqueeY));
- marquee.style.opacity = "0";
}
if (!e.ctrlKey) {
@@ -516,8 +522,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
}
PDFMenu.Instance.jumpTo(e.clientX, e.clientY);
}
-
- this._marqueeHeight = this._marqueeWidth = 0;
+ this._marqueeing = false;
}
else {
let sel = window.getSelection();
@@ -528,8 +533,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
}
}
- if (PDFMenu.Instance.Highlighting) {
- this.highlight(undefined, "goldenrod");
+ if (PDFMenu.Instance.Highlighting) {// when highlighter has been toggled when menu is pinned, we auto-highlight immediately on mouse up
+ this.highlight("rgba(245, 230, 95, 0.616)"); // yellowish highlight color for highlighted text (should match PDFMenu's highlight color)
}
else {
PDFMenu.Instance.StartDrag = this.startDrag;
@@ -540,10 +545,10 @@ export class PDFViewer extends React.Component<IViewerProps> {
}
@action
- highlight = (targetDoc: Doc | undefined, color: string) => {
+ highlight = (color: string) => {
// creates annotation documents for current highlights
- let annotationDoc = this.makeAnnotationDocument(targetDoc, color, false);
- Doc.AddDocToList(this.props.fieldExtensionDoc, this.props.fieldExt, annotationDoc);
+ let annotationDoc = this.makeAnnotationDocument(color);
+ annotationDoc && this.props.addDocument && this.props.addDocument(annotationDoc);
return annotationDoc;
}
@@ -555,23 +560,19 @@ export class PDFViewer extends React.Component<IViewerProps> {
startDrag = (e: PointerEvent, ele: HTMLElement): void => {
e.preventDefault();
e.stopPropagation();
- let targetDoc = Docs.Create.TextDocument({ width: 200, height: 200, title: "New Annotation" });
- targetDoc.targetPage = this.getPageFromScroll(this._marqueeY);
- let annotationDoc = this.highlight(undefined, "red");
- annotationDoc.linkedToDoc = false;
- let dragData = new DragManager.AnnotationDragData(this.props.Document, annotationDoc, targetDoc);
- DragManager.StartAnnotationDrag([ele], dragData, e.pageX, e.pageY, {
- handlers: {
- dragComplete: () => {
- if (!annotationDoc.linkedToDoc) {
- let annotations = DocListCast(annotationDoc.annotations);
- annotations && annotations.forEach(anno => anno.target = targetDoc);
- DocUtils.MakeLink(annotationDoc, targetDoc, dragData.targetContext, `Annotation from ${StrCast(this.props.Document.title)}`);
- }
- }
- },
- hideSource: false
- });
+ let targetDoc = Docs.Create.TextDocument({ width: 200, height: 200, title: "Note linked to " + this.props.Document.title });
+ const annotationDoc = this.highlight("rgba(146, 245, 95, 0.467)"); // yellowish highlight color when dragging out a text selection
+ if (annotationDoc) {
+ let dragData = new DragManager.AnnotationDragData(this.props.Document, annotationDoc, targetDoc);
+ DragManager.StartAnnotationDrag([ele], dragData, e.pageX, e.pageY, {
+ handlers: {
+ dragComplete: () => !(dragData as any).linkedToDoc &&
+ DocUtils.MakeLink({ doc: annotationDoc }, { doc: dragData.dropDocument, ctx: dragData.targetContext }, `Annotation from ${this.Document.title}`, "link from PDF")
+
+ },
+ hideSource: false
+ });
+ }
}
createSnippet = (marquee: { left: number, top: number, width: number, height: number }): void => {
@@ -580,42 +581,15 @@ export class PDFViewer extends React.Component<IViewerProps> {
data.title = StrCast(data.title) + "_snippet";
view.proto = data;
view.nativeHeight = marquee.height;
- view.height = (this.props.Document[WidthSym]() / NumCast(this.props.Document.nativeWidth)) * marquee.height;
- view.nativeWidth = this.props.Document.nativeWidth;
+ view.height = (this.Document[WidthSym]() / (this.Document.nativeWidth || 1)) * marquee.height;
+ view.nativeWidth = this.Document.nativeWidth;
view.startY = marquee.top;
- view.width = this.props.Document[WidthSym]();
+ view.width = this.Document[WidthSym]();
DragManager.StartDocumentDrag([], new DragManager.DocumentDragData([view]), 0, 0);
}
- // this is called with the document that was dragged and the collection to move it into.
- // if the target collection is the same as this collection, then the move will be allowed.
- // otherwise, the document being moved must be able to be removed from its container before
- // moving it into the target.
- @action.bound
- moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean {
- if (Doc.AreProtosEqual(this.props.Document, targetCollection)) {
- return true;
- }
- return this.removeDocument(doc) ? addDocument(doc) : false;
- }
-
-
- @action.bound
- removeDocument(doc: Doc): boolean {
- //TODO This won't create the field if it doesn't already exist
- let targetDataDoc = this.props.fieldExtensionDoc;
- let targetField = this.props.fieldExt;
- let value = Cast(targetDataDoc[targetField], listSpec(Doc), []);
- let index = value.reduce((p, v, i) => (v instanceof Doc && v === doc) ? i : p, -1);
- index = index !== -1 ? index : value.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, doc)) ? i : p, -1);
- index !== -1 && value.splice(index, 1);
- return true;
- }
scrollXf = () => {
- return this._mainCont.current ? this.props.ScreenToLocalTransform().translate(0, this._mainCont.current.scrollTop) : this.props.ScreenToLocalTransform();
- }
- setPreviewCursor = (func?: (x: number, y: number, drag: boolean) => void) => {
- this._setPreviewCursor = func;
+ return this._mainCont.current ? this.props.ScreenToLocalTransform().translate(0, this._scrollTop) : this.props.ScreenToLocalTransform();
}
onClick = (e: React.MouseEvent) => {
this._setPreviewCursor &&
@@ -624,42 +598,62 @@ export class PDFViewer extends React.Component<IViewerProps> {
Math.abs(e.clientY - this._downY) < 3 &&
this._setPreviewCursor(e.clientX, e.clientY, false);
}
- whenActiveChanged = (isActive: boolean) => {
- this._isChildActive = isActive;
- this.props.whenActiveChanged(isActive);
- }
- active = () => {
- return this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0;
- }
+
+ setPreviewCursor = (func?: (x: number, y: number, drag: boolean) => void) => this._setPreviewCursor = func;
+
getCoverImage = () => {
if (!this.props.Document[HeightSym]() || !this.props.Document.nativeHeight) {
- setTimeout(() => {
- this.props.Document.height = this.props.Document[WidthSym]() * this._coverPath.height / this._coverPath.width;
- this.props.Document.nativeHeight = nativeWidth * this._coverPath.height / this._coverPath.width;
- }, 0);
+ setTimeout((() => {
+ this.Document.height = this.Document[WidthSym]() * this._coverPath.height / this._coverPath.width;
+ this.Document.nativeHeight = nativeWidth * this._coverPath.height / this._coverPath.width;
+ }).bind(this), 0);
}
- let nativeWidth = NumCast(this.props.Document.nativeWidth);
- let nativeHeight = NumCast(this.props.Document.nativeHeight);
+ let nativeWidth = (this.Document.nativeWidth || 0);
+ let nativeHeight = (this.Document.nativeHeight || 0);
return <img key={this._coverPath.path} src={this._coverPath.path} onError={action(() => this._coverPath.path = "http://www.cs.brown.edu/~bcz/face.gif")} onLoad={action(() => this._showWaiting = false)}
style={{ position: "absolute", display: "inline-block", top: 0, left: 0, width: `${nativeWidth}px`, height: `${nativeHeight}px` }} />;
}
-
@action
onZoomWheel = (e: React.WheelEvent) => {
e.stopPropagation();
if (e.ctrlKey) {
- let curScale = Number(this.pdfViewer.currentScaleValue);
- this.pdfViewer.currentScaleValue = Math.max(1, Math.min(10, curScale + curScale * e.deltaY / 1000));
- this._zoomed = Number(this.pdfViewer.currentScaleValue);
+ let curScale = Number(this._pdfViewer.currentScaleValue);
+ this._pdfViewer.currentScaleValue = Math.max(1, Math.min(10, curScale + curScale * e.deltaY / 1000));
+ this._zoomed = Number(this._pdfViewer.currentScaleValue);
}
}
@computed get annotationLayer() {
- return <div className="pdfViewer-annotationLayer" style={{ height: NumCast(this.props.Document.nativeHeight) }} ref={this._annotationLayer}>
+ return <div className="pdfViewer-annotationLayer" style={{ height: (this.Document.nativeHeight || 0) }} ref={this._annotationLayer}>
{this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map((anno, index) =>
- <Annotation {...this.props} anno={anno} key={`${anno[Id]}-annotation`} />)}
+ <Annotation {...this.props} focus={this.props.focus} extensionDoc={this.extensionDoc!} anno={anno} key={`${anno[Id]}-annotation`} />)}
+ <div className="pdfViewer-overlay" id="overlay" style={{ transform: `scale(${this._zoomed})` }}>
+ <CollectionFreeFormView {...this.props}
+ annotationsKey={this.annotationsKey}
+ setPreviewCursor={this.setPreviewCursor}
+ PanelHeight={() => (this.Document.scrollHeight || this.Document.nativeHeight || 0)}
+ PanelWidth={() => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : (this.Document.nativeWidth || 0)}
+ VisibleHeight={this.visibleHeight}
+ focus={this.props.focus}
+ isSelected={this.props.isSelected}
+ isAnnotationOverlay={true}
+ select={emptyFunction}
+ active={this.active}
+ ContentScaling={returnOne}
+ whenActiveChanged={this.whenActiveChanged}
+ removeDocument={this.removeDocument}
+ moveDocument={this.moveDocument}
+ addDocument={this.addDocument}
+ CollectionView={undefined}
+ ScreenToLocalTransform={this.scrollXf}
+ ruleProvider={undefined}
+ renderDepth={this.props.renderDepth + 1}
+ ContainingCollectionDoc={this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document}
+ chromeCollapsed={true}>
+ </CollectionFreeFormView>
+ </div>
</div>;
}
@computed get pdfViewerDiv() {
@@ -676,34 +670,14 @@ export class PDFViewer extends React.Component<IViewerProps> {
marqueeX = () => this._marqueeX;
marqueeY = () => this._marqueeY;
marqueeing = () => this._marqueeing;
+ visibleHeight = () => this.props.PanelHeight() / this.props.ContentScaling() * 72 / 96;
render() {
- return (<div className={"pdfViewer-viewer" + (this._zoomed !== 1 ? "-zoomed" : "")} onScroll={this.onScroll} onWheel={this.onZoomWheel} onPointerDown={this.onPointerDown} onClick={this.onClick} ref={this._mainCont}>
+ return !this.extensionDoc ? (null) : (<div className={"pdfViewer-viewer" + (this._zoomed !== 1 ? "-zoomed" : "")}
+ onScroll={this.onScroll} onWheel={this.onZoomWheel} onPointerDown={this.onPointerDown} onClick={this.onClick} ref={this._mainCont}>
{this.pdfViewerDiv}
- <PdfViewerMarquee isMarqueeing={this.marqueeing} width={this.marqueeWidth} height={this.marqueeHeight} x={this.marqueeX} y={this.marqueeY} />
- <div className="pdfViewer-overlay" style={{ transform: `scale(${this._zoomed})` }}>
- {this.annotationLayer}
- <CollectionFreeFormView {...this.props}
- setPreviewCursor={this.setPreviewCursor}
- PanelHeight={() => NumCast(this.props.Document.scrollHeight, NumCast(this.props.Document.nativeHeight))}
- PanelWidth={() => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : NumCast(this.props.Document.nativeWidth)}
- focus={emptyFunction}
- isSelected={this.props.isSelected}
- select={emptyFunction}
- active={this.active}
- ContentScaling={returnOne}
- whenActiveChanged={this.whenActiveChanged}
- removeDocument={this.removeDocument}
- moveDocument={this.moveDocument}
- addDocument={(doc: Doc, allow: boolean | undefined) => { Doc.AddDocToList(this.props.fieldExtensionDoc, this.props.fieldExt, doc); return true; }}
- CollectionView={this.props.ContainingCollectionView}
- ScreenToLocalTransform={this.scrollXf}
- ruleProvider={undefined}
- renderDepth={this.props.renderDepth + 1}
- ContainingCollectionDoc={this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document}
- chromeCollapsed={true}>
- </CollectionFreeFormView>
- </div>
+ {this.annotationLayer}
{this.standinViews}
+ <PdfViewerMarquee isMarqueeing={this.marqueeing} width={this.marqueeWidth} height={this.marqueeHeight} x={this.marqueeX} y={this.marqueeY} />
</div >);
}
}
@@ -723,11 +697,9 @@ class PdfViewerMarquee extends React.Component<PdfViewerMarqueeProps> {
style={{
left: `${this.props.x()}px`, top: `${this.props.y()}px`,
width: `${this.props.width()}px`, height: `${this.props.height()}px`,
- border: `${this.props.width() === 0 ? "" : "2px dashed black"}`
+ border: `${this.props.width() === 0 ? "" : "2px dashed black"}`,
+ opacity: 0.2
}}>
</div>;
}
-}
-
-
-export enum AnnotationTypes { Region }
+} \ No newline at end of file
diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx
index de3242d32..7a1b4f9fb 100644
--- a/src/client/views/presentationview/PresElementBox.tsx
+++ b/src/client/views/presentationview/PresElementBox.tsx
@@ -5,18 +5,19 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { action, computed } from "mobx";
import { observer } from "mobx-react";
import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { documentSchema } from '../../../new_fields/documentSchemas';
import { Id } from "../../../new_fields/FieldSymbols";
-import { BoolCast, NumCast, StrCast } from "../../../new_fields/Types";
-import { emptyFunction, returnEmptyString, returnFalse, returnOne } from "../../../Utils";
+import { createSchema, makeInterface } from '../../../new_fields/Schema';
+import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { emptyFunction, returnFalse } from "../../../Utils";
import { DocumentType } from "../../documents/DocumentTypes";
import { Transform } from "../../util/Transform";
-import { CollectionViewType } from '../collections/CollectionBaseView';
-import { DocumentView } from "../nodes/DocumentView";
+import { CollectionViewType } from '../collections/CollectionView';
+import { CollectionSchemaPreview } from '../collections/CollectionSchemaView';
+import { DocComponent } from '../DocComponent';
import { FieldView, FieldViewProps } from '../nodes/FieldView';
import "./PresElementBox.scss";
import React = require("react");
-import { CollectionSchemaPreview } from '../collections/CollectionSchemaView';
-
library.add(faArrowUp);
library.add(fileSolid);
@@ -24,34 +25,34 @@ library.add(faLocationArrow);
library.add(fileRegular as any);
library.add(faSearch);
library.add(faArrowDown);
+
+export const presSchema = createSchema({
+ presentationTargetDoc: Doc,
+ presBox: Doc,
+ presBoxKey: "string",
+ showButton: "boolean",
+ navButton: "boolean",
+ hideTillShownButton: "boolean",
+ fadeButton: "boolean",
+ hideAfterButton: "boolean",
+ groupButton: "boolean",
+ embedOpen: "boolean"
+});
+
+type PresDocument = makeInterface<[typeof presSchema, typeof documentSchema]>;
+const PresDocument = makeInterface(presSchema, documentSchema);
/**
* This class models the view a document added to presentation will have in the presentation.
* It involves some functionality for its buttons and options.
*/
@observer
-export class PresElementBox extends React.Component<FieldViewProps> {
+export class PresElementBox extends DocComponent<FieldViewProps, PresDocument>(PresDocument) {
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresElementBox, fieldKey); }
- public static LayoutString() { return FieldView.LayoutString(PresElementBox); }
-
- @computed get myIndex() { return DocListCast(this.presentationDoc[this.presentationFieldKey]).indexOf(this.props.Document); }
- @computed get presentationDoc() { return this.props.Document.presBox as Doc; }
- @computed get presentationFieldKey() { return StrCast(this.props.Document.presBoxKey); }
+ @computed get indexInPres() { return DocListCast(this.presentationDoc[this.Document.presBoxKey || ""]).indexOf(this.props.Document); }
+ @computed get presentationDoc() { return Cast(this.Document.presBox, Doc) as Doc; }
+ @computed get targetDoc() { return this.Document.presentationTargetDoc as Doc; }
@computed get currentIndex() { return NumCast(this.presentationDoc.selectedDoc); }
- @computed get showButton() { return BoolCast(this.props.Document.showButton); }
- @computed get navButton() { return BoolCast(this.props.Document.navButton); }
- @computed get hideTillShownButton() { return BoolCast(this.props.Document.hideTillShownButton); }
- @computed get fadeButton() { return BoolCast(this.props.Document.fadeButton); }
- @computed get hideAfterButton() { return BoolCast(this.props.Document.hideAfterButton); }
- @computed get groupButton() { return BoolCast(this.props.Document.groupButton); }
- @computed get embedOpen() { return BoolCast(this.props.Document.embedOpen); }
-
- set embedOpen(value: boolean) { this.props.Document.embedOpen = value; }
- set showButton(val: boolean) { this.props.Document.showButton = val; }
- set navButton(val: boolean) { this.props.Document.navButton = val; }
- set hideTillShownButton(val: boolean) { this.props.Document.hideTillShownButton = val; }
- set fadeButton(val: boolean) { this.props.Document.fadeButton = val; }
- set hideAfterButton(val: boolean) { this.props.Document.hideAfterButton = val; }
- set groupButton(val: boolean) { this.props.Document.groupButton = val; }
/**
* The function that is called on click to turn Hiding document till press option on/off.
@@ -60,16 +61,14 @@ export class PresElementBox extends React.Component<FieldViewProps> {
@action
onHideDocumentUntilPressClick = (e: React.MouseEvent) => {
e.stopPropagation();
- this.hideTillShownButton = !this.hideTillShownButton;
- if (!this.hideTillShownButton) {
- if (this.myIndex >= this.currentIndex) {
- (this.props.Document.target as Doc).opacity = 1;
+ this.Document.hideTillShownButton = !this.Document.hideTillShownButton;
+ if (!this.Document.hideTillShownButton) {
+ if (this.indexInPres >= this.currentIndex && this.targetDoc) {
+ this.targetDoc.opacity = 1;
}
} else {
- if (this.presentationDoc.presStatus) {
- if (this.myIndex > this.currentIndex) {
- (this.props.Document.target as Doc).opacity = 0;
- }
+ if (this.presentationDoc.presStatus && this.indexInPres > this.currentIndex && this.targetDoc) {
+ this.targetDoc.opacity = 0;
}
}
}
@@ -82,17 +81,15 @@ export class PresElementBox extends React.Component<FieldViewProps> {
@action
onHideDocumentAfterPresentedClick = (e: React.MouseEvent) => {
e.stopPropagation();
- this.hideAfterButton = !this.hideAfterButton;
- if (!this.hideAfterButton) {
- if (this.myIndex <= this.currentIndex) {
- (this.props.Document.target as Doc).opacity = 1;
+ this.Document.hideAfterButton = !this.Document.hideAfterButton;
+ if (!this.Document.hideAfterButton) {
+ if (this.indexInPres <= this.currentIndex && this.targetDoc) {
+ this.targetDoc.opacity = 1;
}
} else {
- if (this.fadeButton) this.fadeButton = false;
- if (this.presentationDoc.presStatus) {
- if (this.myIndex < this.currentIndex) {
- (this.props.Document.target as Doc).opacity = 0;
- }
+ if (this.Document.fadeButton) this.Document.fadeButton = false;
+ if (this.presentationDoc.presStatus && this.indexInPres < this.currentIndex && this.targetDoc) {
+ this.targetDoc.opacity = 0;
}
}
}
@@ -105,17 +102,15 @@ export class PresElementBox extends React.Component<FieldViewProps> {
@action
onFadeDocumentAfterPresentedClick = (e: React.MouseEvent) => {
e.stopPropagation();
- this.fadeButton = !this.fadeButton;
- if (!this.fadeButton) {
- if (this.myIndex <= this.currentIndex) {
- (this.props.Document.target as Doc).opacity = 1;
+ this.Document.fadeButton = !this.Document.fadeButton;
+ if (!this.Document.fadeButton) {
+ if (this.indexInPres <= this.currentIndex && this.targetDoc) {
+ this.targetDoc.opacity = 1;
}
} else {
- this.hideAfterButton = false;
- if (this.presentationDoc.presStatus) {
- if (this.myIndex < this.currentIndex) {
- (this.props.Document.target as Doc).opacity = 0.5;
- }
+ this.Document.hideAfterButton = false;
+ if (this.presentationDoc.presStatus && (this.indexInPres < this.currentIndex) && this.targetDoc) {
+ this.targetDoc.opacity = 0.5;
}
}
}
@@ -126,10 +121,10 @@ export class PresElementBox extends React.Component<FieldViewProps> {
@action
onNavigateDocumentClick = (e: React.MouseEvent) => {
e.stopPropagation();
- this.navButton = !this.navButton;
- if (this.navButton) {
- this.showButton = false;
- if (this.currentIndex === this.myIndex) {
+ this.Document.navButton = !this.Document.navButton;
+ if (this.Document.navButton) {
+ this.Document.showButton = false;
+ if (this.currentIndex === this.indexInPres) {
this.props.focus(this.props.Document);
}
}
@@ -142,12 +137,12 @@ export class PresElementBox extends React.Component<FieldViewProps> {
onZoomDocumentClick = (e: React.MouseEvent) => {
e.stopPropagation();
- this.showButton = !this.showButton;
- if (!this.showButton) {
+ this.Document.showButton = !this.Document.showButton;
+ if (!this.Document.showButton) {
this.props.Document.viewScale = 1;
} else {
- this.navButton = false;
- if (this.currentIndex === this.myIndex) {
+ this.Document.navButton = false;
+ if (this.currentIndex === this.indexInPres) {
this.props.focus(this.props.Document);
}
}
@@ -162,21 +157,22 @@ export class PresElementBox extends React.Component<FieldViewProps> {
* presentation element.
*/
renderEmbeddedInline = () => {
- if (!this.embedOpen || !(this.props.Document.target instanceof Doc)) {
+ if (!this.Document.embedOpen || !this.targetDoc) {
return (null);
}
- let propDocWidth = NumCast(this.props.Document.nativeWidth);
- let propDocHeight = NumCast(this.props.Document.nativeHeight);
- let scale = () => 175 / NumCast(this.props.Document.nativeWidth, 175);
+ let propDocWidth = NumCast(this.layoutDoc.nativeWidth);
+ let propDocHeight = NumCast(this.layoutDoc.nativeHeight);
+ let scale = () => 175 / NumCast(this.layoutDoc.nativeWidth, 175);
return (
<div className="presElementBox-embedded" style={{
- height: propDocHeight === 0 ? NumCast(this.props.Document.height) - NumCast(this.props.Document.collapsedHeight) : propDocHeight * scale(),
+ height: propDocHeight === 0 ? NumCast(this.layoutDoc.height) - NumCast(this.layoutDoc.collapsedHeight) : propDocHeight * scale(),
width: propDocWidth === 0 ? "auto" : propDocWidth * scale(),
}}>
<CollectionSchemaPreview
- fitToBox={StrCast(this.props.Document.target.type).indexOf(DocumentType.COL) !== -1}
- Document={this.props.Document.target}
+ fitToBox={StrCast(this.targetDoc.type).indexOf(DocumentType.COL) !== -1}
+ Document={this.targetDoc}
+ fieldKey={this.props.fieldKey}
addDocument={returnFalse}
removeDocument={returnFalse}
ruleProvider={undefined}
@@ -198,30 +194,27 @@ export class PresElementBox extends React.Component<FieldViewProps> {
}
render() {
- let p = this.props;
-
let treecontainer = this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.viewType === CollectionViewType.Tree;
- let className = "presElementBox-item" + (this.currentIndex === this.myIndex ? " presElementBox-selected" : "");
+ let className = "presElementBox-item" + (this.currentIndex === this.indexInPres ? " presElementBox-selected" : "");
let pbi = "presElementBox-interaction";
return (
- <div className={className} key={p.Document[Id] + this.myIndex}
- style={{ outlineWidth: Doc.IsBrushed(p.Document.target as Doc) ? `1px` : "0px", }}
- onClick={e => { p.focus(p.Document); e.stopPropagation(); }}>
+ <div className={className} key={this.props.Document[Id] + this.indexInPres}
+ style={{ outlineWidth: Doc.IsBrushed(this.targetDoc) ? `1px` : "0px", }}
+ onClick={e => { this.props.focus(this.props.Document); e.stopPropagation(); }}>
{treecontainer ? (null) : <>
<strong className="presElementBox-name">
- {`${this.myIndex + 1}. ${p.Document.title}`}
+ {`${this.indexInPres + 1}. ${this.Document.title}`}
</strong>
- <button className="presElementBox-closeIcon" onPointerDown={e => e.stopPropagation()} onClick={e => this.props.removeDocument && this.props.removeDocument(p.Document)}>X</button>
+ <button className="presElementBox-closeIcon" onPointerDown={e => e.stopPropagation()} onClick={e => this.props.removeDocument && this.props.removeDocument(this.props.Document)}>X</button>
<br />
- </>
- }
- <button title="Zoom" className={pbi + (this.showButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onZoomDocumentClick}><FontAwesomeIcon icon={"search"} /></button>
- <button title="Navigate" className={pbi + (this.navButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onNavigateDocumentClick}><FontAwesomeIcon icon={"location-arrow"} /></button>
- <button title="Hide Before" className={pbi + (this.hideTillShownButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onHideDocumentUntilPressClick}><FontAwesomeIcon icon={fileSolid} /></button>
- <button title="Fade After" className={pbi + (this.fadeButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
- <button title="Hide After" className={pbi + (this.hideAfterButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
- <button title="Group With Up" className={pbi + (this.groupButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={action((e: any) => { e.stopPropagation(); this.groupButton = !this.groupButton; })}><FontAwesomeIcon icon={"arrow-up"} /></button>
- <button title="Expand Inline" className={pbi + (this.embedOpen ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={action((e: any) => { e.stopPropagation(); this.embedOpen = !this.embedOpen; })}><FontAwesomeIcon icon={"arrow-down"} /></button>
+ </>}
+ <button title="Zoom" className={pbi + (this.Document.showButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onZoomDocumentClick}><FontAwesomeIcon icon={"search"} /></button>
+ <button title="Navigate" className={pbi + (this.Document.navButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onNavigateDocumentClick}><FontAwesomeIcon icon={"location-arrow"} /></button>
+ <button title="Hide Before" className={pbi + (this.Document.hideTillShownButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onHideDocumentUntilPressClick}><FontAwesomeIcon icon={fileSolid} /></button>
+ <button title="Fade After" className={pbi + (this.Document.fadeButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
+ <button title="Hide After" className={pbi + (this.Document.hideAfterButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
+ <button title="Group With Up" className={pbi + (this.Document.groupButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={e => { e.stopPropagation(); this.Document.groupButton = !this.Document.groupButton; }}><FontAwesomeIcon icon={"arrow-up"} /></button>
+ <button title="Expand Inline" className={pbi + (this.Document.embedOpen ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={e => { e.stopPropagation(); this.Document.embedOpen = !this.Document.embedOpen; }}><FontAwesomeIcon icon={"arrow-down"} /></button>
<br style={{ lineHeight: 0.1 }} />
{this.renderEmbeddedInline()}
diff --git a/src/client/views/search/FilterBox.tsx b/src/client/views/search/FilterBox.tsx
index da733d64b..b841190d4 100644
--- a/src/client/views/search/FilterBox.tsx
+++ b/src/client/views/search/FilterBox.tsx
@@ -33,7 +33,7 @@ export enum Keys {
export class FilterBox extends React.Component {
static Instance: FilterBox;
- public _allIcons: string[] = [DocumentType.AUDIO, DocumentType.COL, DocumentType.HIST, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.TEXT, DocumentType.VID, DocumentType.WEB];
+ public _allIcons: string[] = [DocumentType.AUDIO, DocumentType.COL, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.TEXT, DocumentType.VID, DocumentType.WEB];
//if true, any keywords can be used. if false, all keywords are required.
//this also serves as an indicator if the word status filter is applied
@@ -393,7 +393,7 @@ export class FilterBox extends React.Component {
<div>
<div style={{ display: "flex", flexDirection: "row-reverse" }}>
<SearchBox />
- {this.getActiveFilters()}
+ {/* {this.getActiveFilters()} */}
</div>
{this._filterOpen ? (
<div className="filter-form" onPointerDown={this.stopProp} id="filter-form" style={this._filterOpen ? { display: "flex" } : { display: "none" }}>
diff --git a/src/client/views/search/IconBar.tsx b/src/client/views/search/IconBar.tsx
index c9924222f..cff397407 100644
--- a/src/client/views/search/IconBar.tsx
+++ b/src/client/views/search/IconBar.tsx
@@ -59,23 +59,9 @@ export class IconBar extends React.Component {
render() {
return (
<div className="icon-bar">
- <div className="type-outer">
- <div className={"type-icon all"}
- onClick={this.selectAll}>
- <FontAwesomeIcon className="fontawesome-icon" icon={faCheckCircle} />
- </div>
- <div className="filter-description">Select All</div>
- </div>
{FilterBox.Instance._allIcons.map((type: string) =>
- <IconButton type={type} />
+ <IconButton key={type.toString()} type={type} />
)}
- <div className="type-outer">
- <div className={"type-icon none"}
- onClick={this.resetSelf}>
- <FontAwesomeIcon className="fontawesome-icon" icon={faTimesCircle} />
- </div>
- <div className="filter-description">Clear</div>
- </div>
</div>
);
}
diff --git a/src/client/views/search/IconButton.scss b/src/client/views/search/IconButton.scss
index d1853177e..4a3107676 100644
--- a/src/client/views/search/IconButton.scss
+++ b/src/client/views/search/IconButton.scss
@@ -35,7 +35,7 @@
text-align: center;
height: 15px;
margin-top: 5px;
- opacity: 0;
+ opacity: 1;
-webkit-transition: all 0.2s ease-in-out;
-moz-transition: all 0.2s ease-in-out;
-o-transition: all 0.2s ease-in-out;
diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss
index 5ed33a596..bc11604a5 100644
--- a/src/client/views/search/SearchBox.scss
+++ b/src/client/views/search/SearchBox.scss
@@ -1,6 +1,15 @@
@import "../globalCssVariables";
@import "./NaviconButton.scss";
+.searchBox-container {
+ display: flex;
+ flex-direction: column;
+ width:100%;
+ height:100%;
+ position: absolute;
+ font-size: 10px;
+ line-height: 1;
+}
.searchBox-bar {
height: 32px;
display: flex;
@@ -34,6 +43,9 @@
&.searchBox-filter {
align-self: stretch;
+ }
+
+ &.searchBox-submit {
margin-left: 2px;
margin-right: 2px
}
@@ -45,12 +57,18 @@
}
}
+.searchBox-quickFilter {
+ width: 100%;
+ height: 40px;
+ margin-top: 10px;
+}
+
.searchBox-results {
- margin-right: 136px;
+ display:flex;
+ flex-direction: column;
top: 300px;
display: flex;
flex-direction: column;
- margin-right: 72px;
// height: 560px;
height: 100%;
// overflow: hidden;
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index f53270c64..899a35f48 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -18,6 +18,7 @@ import { FilterBox } from './FilterBox';
import "./FilterBox.scss";
import "./SearchBox.scss";
import { SearchItem } from './SearchItem';
+import { IconBar } from './IconBar';
import { string } from 'prop-types';
library.add(faTimes);
@@ -140,7 +141,7 @@ export class SearchBox extends React.Component {
private get filterQuery() {
const types = FilterBox.Instance.filterTypes;
const includeDeleted = FilterBox.Instance.getDataStatus();
- return "NOT baseProto_b:true" + (includeDeleted ? "" : " AND NOT deleted_b:true") + (types ? ` AND (${types.map(type => `({!join from=id to=proto_i}type_t:"${type}" AND NOT type_t:*) OR type_t:"${type}"`).join(" ")})` : "");
+ return "NOT baseProto_b:true" + (includeDeleted ? "" : " AND NOT deleted_b:true") + (types ? ` AND (${types.map(type => `({!join from=id to=proto_i}type_t:"${type}" AND NOT type_t:*) OR type_t:"${type}" OR type_t:"extension"`).join(" ")})` : "");
}
@@ -210,7 +211,7 @@ export class SearchBox extends React.Component {
});
let x = 0;
let y = 0;
- for (const doc of docs) {
+ for (const doc of docs.map(d => Doc.Layout(d))) {
doc.x = x;
doc.y = y;
const size = 200;
@@ -236,7 +237,7 @@ export class SearchBox extends React.Component {
}
@action.bound
- openSearch(e: React.PointerEvent) {
+ openSearch(e: React.SyntheticEvent) {
e.stopPropagation();
this._openNoResults = false;
FilterBox.Instance.closeFilter();
@@ -342,12 +343,14 @@ export class SearchBox extends React.Component {
<FontAwesomeIcon icon="object-group" size="lg" />
</span>
<input value={this._searchString} onChange={this.onChange} type="text" placeholder="Search..." id="search-input" ref={this.inputRef}
- className="searchBox-barChild searchBox-input" onPointerDown={this.openSearch} onKeyPress={this.enter}
+ className="searchBox-barChild searchBox-input" onPointerDown={this.openSearch} onKeyPress={this.enter} onFocus={this.openSearch}
style={{ width: this._searchbarOpen ? "500px" : "100px" }} />
- <button className="searchBox-barChild searchBox-submit" onClick={this.submitSearch} onPointerDown={FilterBox.Instance.stopProp}>Submit</button>
- <button className="searchBox-barChild searchBox-filter" onClick={FilterBox.Instance.openFilter} onPointerDown={FilterBox.Instance.stopProp}>Filter</button>
- <button className="searchBox-barChild searchBox-close" title={"Close Search Bar"} onPointerDown={MainView.Instance.toggleSearch}><FontAwesomeIcon icon={faTimes} size="lg" /></button>
+ <button className="searchBox-barChild searchBox-filter" title="Advanced Filtering Options" onClick={FilterBox.Instance.openFilter} onPointerDown={FilterBox.Instance.stopProp}><FontAwesomeIcon icon="ellipsis-v" color="white" /></button>
</div>
+ {(this._numTotalResults > 0 || !this._searchbarOpen) ? (null) :
+ (<div className="searchBox-quickFilter" onPointerDown={this.openSearch}>
+ <div className="filter-panel"><IconBar /></div>
+ </div>)}
<div className="searchBox-results" onScroll={this.resultsScrolled} style={{
display: this._resultsOpen ? "flex" : "none",
height: this.resFull ? "560px" : this.resultHeight, overflow: this.resFull ? "auto" : "visible"
diff --git a/src/client/views/search/SearchItem.scss b/src/client/views/search/SearchItem.scss
index 273d49349..9f12994c3 100644
--- a/src/client/views/search/SearchItem.scss
+++ b/src/client/views/search/SearchItem.scss
@@ -2,22 +2,32 @@
.search-overview {
display: flex;
- flex-direction: row-reverse;
+ flex-direction: reverse;
justify-content: flex-end;
- height: 70px;
z-index: 0;
}
+.link-count {
+ background: black;
+ border-radius: 20px;
+ color: white;
+ width: 15px;
+ text-align: center;
+ margin-top: 5px;
+}
.searchBox-placeholder,
.search-overview .search-item {
- width: 500px;
+ width: 100%;
background: $light-color-secondary;
border-color: $intermediate-color;
border-bottom-style: solid;
padding: 10px;
- height: 70px;
+ min-height: 50px;
+ max-height: 150px;
+ height: auto;
z-index: 0;
display: inline-block;
+ overflow: auto;
.main-search-info {
display: flex;
@@ -26,6 +36,7 @@
.search-title-container {
width: 100%;
+ overflow: hidden;
.search-title {
text-transform: uppercase;
@@ -58,16 +69,6 @@
overflow: hidden;
position: relative;
- .link-count {
- opacity: 1;
- position: absolute;
- z-index: 1000;
- text-align: center;
- -webkit-transition: opacity 0.2s ease-in-out;
- -moz-transition: opacity 0.2s ease-in-out;
- -o-transition: opacity 0.2s ease-in-out;
- transition: opacity 0.2s ease-in-out;
- }
.link-extended {
// display: none;
@@ -109,7 +110,7 @@
.icon-icons,
.icon-live {
- height: 50px;
+ height: auto;
margin: auto;
overflow: hidden;
@@ -171,9 +172,7 @@
.searchBox-instances:active {
opacity: 1;
background: $lighter-alt-accent;
- -webkit-transform: scale(1);
- -ms-transform: scale(1);
- transform: scale(1);
+ width:150px
}
.search-item:hover {
@@ -181,16 +180,19 @@
background: $lighter-alt-accent;
}
+.search-highlighting {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: pre;
+}
+
.searchBox-instances {
float: left;
opacity: 1;
- width: 150px;
+ width: 0px;
transition: all 0.2s ease;
color: black;
- transform-origin: top right;
- -webkit-transform: scale(0);
- -ms-transform: scale(0);
- transform: scale(0);
+ overflow: hidden;
}
@@ -199,7 +201,7 @@
}
.searchBox-placeholder {
- min-height: 70px;
+ min-height: 50px;
margin-left: 150px;
text-transform: uppercase;
text-align: left;
diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx
index 4d021216d..f1d825aa0 100644
--- a/src/client/views/search/SearchItem.tsx
+++ b/src/client/views/search/SearchItem.tsx
@@ -4,13 +4,10 @@ import { faCaretUp, faChartBar, faFile, faFilePdf, faFilm, faFingerprint, faGlob
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { action, computed, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
-import { Doc, DocListCast, HeightSym, WidthSym } from "../../../new_fields/Doc";
+import { Doc } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
-import { ObjectField } from "../../../new_fields/ObjectField";
-import { RichTextField } from "../../../new_fields/RichTextField";
import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils } from "../../../Utils";
-import { DocServer } from "../../DocServer";
import { DocumentType } from "../../documents/DocumentTypes";
import { DocumentManager } from "../../util/DocumentManager";
import { DragManager, SetupDrag } from "../../util/DragManager";
@@ -18,7 +15,7 @@ import { LinkManager } from "../../util/LinkManager";
import { SearchUtil } from "../../util/SearchUtil";
import { Transform } from "../../util/Transform";
import { SEARCH_THUMBNAIL_SIZE } from "../../views/globalCssVariables.scss";
-import { CollectionViewType } from "../collections/CollectionBaseView";
+import { CollectionViewType } from "../collections/CollectionView";
import { CollectionDockingView } from "../collections/CollectionDockingView";
import { ContextMenu } from "../ContextMenu";
import { DocumentView } from "../nodes/DocumentView";
@@ -153,7 +150,7 @@ export class SearchItem extends React.Component<SearchItemProps> {
if (!this._useIcons) {
let returnXDimension = () => this._useIcons ? 50 : Number(SEARCH_THUMBNAIL_SIZE);
let returnYDimension = () => this._displayDim;
- let scale = () => returnXDimension() / NumCast(this.props.doc.nativeWidth, returnXDimension());
+ let scale = () => returnXDimension() / NumCast(Doc.Layout(this.props.doc).nativeWidth, returnXDimension());
const docview = <div
onPointerDown={action(() => {
this._useIcons = !this._useIcons;
@@ -216,18 +213,15 @@ export class SearchItem extends React.Component<SearchItemProps> {
@computed
get linkCount() { return LinkManager.Instance.getAllRelatedLinks(this.props.doc).length; }
- @computed
- get linkString(): string {
- let num = this.linkCount;
- if (num === 1) {
- return num.toString() + " link";
- }
- return num.toString() + " links";
- }
-
@action
pointerDown = (e: React.PointerEvent) => { e.preventDefault(); e.button === 0 && SearchBox.Instance.openSearch(e); }
+ nextHighlight = (e: React.PointerEvent) => {
+ e.preventDefault(); e.button === 0 && SearchBox.Instance.openSearch(e);
+ let sstring = StrCast(this.props.doc.search_string);
+ this.props.doc.search_string = "";
+ setTimeout(() => this.props.doc.search_string = sstring, 0);
+ }
highlightDoc = (e: React.PointerEvent) => {
if (this.props.doc.type === DocumentType.LINK) {
if (this.props.doc.anchor1 && this.props.doc.anchor2) {
@@ -240,6 +234,7 @@ export class SearchItem extends React.Component<SearchItemProps> {
} else {
Doc.BrushDoc(this.props.doc);
}
+ e.stopPropagation();
}
unHighlightDoc = (e: React.PointerEvent) => {
@@ -283,23 +278,24 @@ export class SearchItem extends React.Component<SearchItemProps> {
const doc2 = Cast(this.props.doc.anchor2, Doc);
return (
<div className="search-overview" onPointerDown={this.pointerDown} onContextMenu={this.onContextMenu}>
- <div className="search-item" onPointerEnter={this.highlightDoc} onPointerLeave={this.unHighlightDoc} id="result"
- onClick={this.onClick} onPointerDown={this.pointerDown} >
+ <div className="search-item" onPointerDown={this.nextHighlight} onPointerEnter={this.highlightDoc} onPointerLeave={this.unHighlightDoc} id="result"
+ onClick={this.onClick}>
<div className="main-search-info">
- <div title="Drag as document" onPointerDown={this.onPointerDown} style={{ marginRight: "7px" }}> <FontAwesomeIcon icon="file" size="lg" /> </div>
+ <div title="Drag as document" onPointerDown={this.onPointerDown} style={{ marginRight: "7px" }}> <FontAwesomeIcon icon="file" size="lg" />
+ <div className="link-container item">
+ <div className="link-count" title={`${this.linkCount + " links"}`}>{this.linkCount}</div>
+ </div>
+ </div>
<div className="search-title-container">
<div className="search-title">{StrCast(this.props.doc.title)}</div>
- <div className="search-highlighting">{this.props.highlighting.length ? "Matched fields:" + this.props.highlighting.join(", ") : this.props.lines.length ? "Text:" + this.props.lines[0] : ""}</div>
+ <div className="search-highlighting">{this.props.highlighting.length ? "Matched fields:" + this.props.highlighting.join(", ") : this.props.lines.length ? this.props.lines[0] : ""}</div>
+ {this.props.lines.filter((m, i) => i).map((l, i) => <div id={i.toString()} className="search-highlighting">`${l}`</div>)}
</div>
<div className="search-info" style={{ width: this._useIcons ? "15%" : "400px" }}>
<div className={`icon-${this._useIcons ? "icons" : "live"}`}>
<div className="search-type" title="Click to Preview">{this.DocumentIcon()}</div>
<div className="search-label">{this.props.doc.type ? this.props.doc.type : "Other"}</div>
</div>
- <div className="link-container item">
- <div className="link-count">{this.linkCount}</div>
- <div className="link-extended">{this.linkString}</div>
- </div>
</div>
</div>
</div>
diff --git a/src/client/views/webcam/DashWebCam.tsx b/src/client/views/webcam/DashWebCam.tsx
index 351913562..a9669750f 100644
--- a/src/client/views/webcam/DashWebCam.tsx
+++ b/src/client/views/webcam/DashWebCam.tsx
@@ -308,8 +308,7 @@ export class DashWebCam extends React.Component<CollectionFreeFormDocumentViewPr
- public static LayoutString() { return FieldView.LayoutString(DashWebCam); }
-
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DashWebCam, fieldKey); }
render() {
const { state, props } = this;
@@ -328,8 +327,6 @@ export class DashWebCam extends React.Component<CollectionFreeFormDocumentViewPr
videoConstraints,
imageSmoothing,
fieldKey,
- fieldExt,
- leaveNativeSize,
fitToBox,
ContainingCollectionView,
Document,
diff --git a/src/client/views/webcam/DashWebRTCVideo.tsx b/src/client/views/webcam/DashWebRTCVideo.tsx
index dcaa87557..62efd9730 100644
--- a/src/client/views/webcam/DashWebRTCVideo.tsx
+++ b/src/client/views/webcam/DashWebRTCVideo.tsx
@@ -290,7 +290,8 @@ export class DashWebRTCVideo extends React.Component<CollectionFreeFormDocumentV
- public static LayoutString() { return FieldView.LayoutString(DashWebRTCVideo); }
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DashWebRTCVideo, fieldKey); }
+
_ignore = 0;
diff --git a/src/extensions/ArrayExtensions.ts b/src/extensions/ArrayExtensions.ts
index 422a10dbc..8e125766d 100644
--- a/src/extensions/ArrayExtensions.ts
+++ b/src/extensions/ArrayExtensions.ts
@@ -1,13 +1,37 @@
-function Assign() {
+export default class ArrayExtension {
+ private readonly property: string;
+ private readonly body: <T>(this: Array<T>) => any;
+
+ constructor(property: string, body: <T>(this: Array<T>) => any) {
+ this.property = property;
+ this.body = body;
+ }
+
+ assign() {
+ Object.defineProperty(Array.prototype, this.property, {
+ value: this.body,
+ enumerable: false
+ });
+ }
- Array.prototype.lastElement = function <T>() {
+}
+
+/**
+ * IMPORTANT: Any extension you add here *must* have a corresponding type definition
+ * in the Array<T> interface in ./General/ExtensionsTypings.ts. Otherwise,
+ * Typescript will not recognize your new function.
+ */
+const extensions = [
+ new ArrayExtension("lastElement", function () {
if (!this.length) {
return undefined;
}
- const last: T = this[this.length - 1];
- return last;
- };
+ return this[this.length - 1];
+ })
+];
+function Assign() {
+ extensions.forEach(extension => extension.assign());
}
export { Assign }; \ No newline at end of file
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index aeffc81c4..6aad4a6be 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -14,6 +14,7 @@ import { ComputedField } from "./ScriptField";
import { BoolCast, Cast, FieldValue, NumCast, PromiseValue, StrCast, ToConstructor } from "./Types";
import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction } from "./util";
import { intersectRect } from "../Utils";
+import { UndoManager } from "../client/util/UndoManager";
export namespace Field {
export function toKeyValueString(doc: Doc, key: string): string {
@@ -63,6 +64,10 @@ export function DocListCastAsync(field: FieldResult, defaultValue?: Doc[]) {
return list ? Promise.all(list).then(() => list) : Promise.resolve(defaultValue);
}
+export async function DocCastAsync(field: FieldResult): Promise<Opt<Doc>> {
+ return Cast(field, Doc);
+}
+
export function DocListCast(field: FieldResult): Doc[] {
return Cast(field, listSpec(Doc), []).filter(d => d instanceof Doc) as Doc[];
}
@@ -328,8 +333,10 @@ export namespace Doc {
return Array.from(results);
}
- export function IndexOf(toFind: Doc, list: Doc[]) {
- return list.findIndex(doc => doc === toFind || Doc.AreProtosEqual(doc, toFind));
+ export function IndexOf(toFind: Doc, list: Doc[], allowProtos: boolean = true) {
+ let index = list.reduce((p, v, i) => (v instanceof Doc && v === toFind) ? i : p, -1);
+ index = allowProtos && index !== -1 ? index : list.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, toFind)) ? i : p, -1);
+ return index; // list.findIndex(doc => doc === toFind || Doc.AreProtosEqual(doc, toFind));
}
export function RemoveDocFromList(listDoc: Doc, key: string, doc: Doc) {
if (listDoc[key] === undefined) {
@@ -371,8 +378,9 @@ export namespace Doc {
else list.splice(before ? ind : ind + 1, 0, doc);
}
}
+ return true;
}
- return true;
+ return false;
}
//
@@ -390,40 +398,6 @@ export namespace Doc {
return bounds;
}
- //
- // Resolves a reference to a field by returning 'doc' if no field extension is specified,
- // otherwise, it returns the extension document stored in doc.<fieldKey>_ext.
- // This mechanism allows any fields to be extended with an extension document that can
- // be used to capture field-specific metadata. For example, an image field can be extended
- // to store annotations, ink, and other data.
- //
- export function fieldExtensionDoc(doc: Doc, fieldKey: string, fieldExt: string = "yes") {
- return fieldExt && doc[fieldKey + "_ext"] instanceof Doc ? doc[fieldKey + "_ext"] as Doc : doc;
- }
-
- export function CreateDocumentExtensionForField(doc: Doc, fieldKey: string) {
- let docExtensionForField = new Doc(doc[Id] + fieldKey, true);
- docExtensionForField.title = fieldKey + ".ext";
- docExtensionForField.extendsDoc = doc; // this is used by search to map field matches on the extension doc back to the document it extends.
- docExtensionForField.type = DocumentType.EXTENSION;
- let proto: Doc | undefined = doc;
- while (proto && !Doc.IsPrototype(proto) && proto.proto) {
- proto = proto.proto;
- }
- (proto ? proto : doc)[fieldKey + "_ext"] = new PrefetchProxy(docExtensionForField);
- return docExtensionForField;
- }
-
- export function UpdateDocumentExtensionForField(doc: Doc, fieldKey: string, immediate: boolean = false) {
- let docExtensionForField = doc[fieldKey + "_ext"] as Doc;
- if (docExtensionForField === undefined) {
- if (immediate) CreateDocumentExtensionForField(doc, fieldKey);
- else setTimeout(() => CreateDocumentExtensionForField(doc, fieldKey), 0);
- } else if (doc instanceof Doc) { // backward compatibility -- add fields for docs that don't have them already
- docExtensionForField.extendsDoc === undefined && setTimeout(() => docExtensionForField.extendsDoc = doc, 0);
- docExtensionForField.type === undefined && setTimeout(() => docExtensionForField.type = DocumentType.EXTENSION, 0);
- }
- }
export function MakeTitled(title: string) {
let doc = new Doc();
doc.title = title;
@@ -446,7 +420,7 @@ export namespace Doc {
// for individual layout properties to be overridden in the expanded layout.
//
export function WillExpandTemplateLayout(layoutDoc: Doc, dataDoc?: Doc) {
- return BoolCast(layoutDoc.isTemplate) && dataDoc && layoutDoc !== dataDoc && !(layoutDoc.layout instanceof Doc);
+ return BoolCast(layoutDoc.isTemplateField) && dataDoc && layoutDoc !== dataDoc && !(layoutDoc[StrCast(layoutDoc.layoutKey, "layout")] instanceof Doc);
}
//
@@ -462,15 +436,8 @@ export namespace Doc {
// ... which means we change the layout to be an expanded view of the template layout.
// This allows the view override the template's properties and be referenceable as its own document.
- let expandedTemplateLayout = dataDoc[templateLayoutDoc[Id]];
- if (expandedTemplateLayout instanceof Doc) {
- return expandedTemplateLayout;
- }
- if (expandedTemplateLayout instanceof Promise) {
- return undefined;
- }
let expandedLayoutFieldKey = "Layout[" + templateLayoutDoc[Id] + "]";
- expandedTemplateLayout = dataDoc[expandedLayoutFieldKey];
+ let expandedTemplateLayout = dataDoc[expandedLayoutFieldKey];
if (expandedTemplateLayout instanceof Doc) {
return expandedTemplateLayout;
}
@@ -483,15 +450,40 @@ export namespace Doc {
export function GetLayoutDataDocPair(doc: Doc, dataDoc: Doc | undefined, fieldKey: string, childDocLayout: Doc) {
let layoutDoc: Doc | undefined = childDocLayout;
- let resolvedDataDoc = !doc.isTemplate && dataDoc !== doc && dataDoc ? Doc.GetDataDoc(dataDoc) : undefined;
+ let resolvedDataDoc = !doc.isTemplateField && dataDoc !== doc && dataDoc ? Doc.GetDataDoc(dataDoc) : undefined;
if (resolvedDataDoc && Doc.WillExpandTemplateLayout(childDocLayout, resolvedDataDoc)) {
- Doc.UpdateDocumentExtensionForField(resolvedDataDoc, fieldKey);
- let fieldExtensionDoc = Doc.fieldExtensionDoc(resolvedDataDoc, StrCast(childDocLayout.templateField, StrCast(childDocLayout.title)), "dummy");
- layoutDoc = Doc.expandTemplateLayout(childDocLayout, fieldExtensionDoc !== resolvedDataDoc ? fieldExtensionDoc : undefined);
- } else layoutDoc = Doc.expandTemplateLayout(childDocLayout, resolvedDataDoc);
+ let extensionDoc = fieldExtensionDoc(resolvedDataDoc, StrCast(childDocLayout.templateField, StrCast(childDocLayout.title)));
+ layoutDoc = Doc.expandTemplateLayout(childDocLayout, extensionDoc !== resolvedDataDoc ? extensionDoc : undefined);
+ } else layoutDoc = childDocLayout;
return { layout: layoutDoc, data: resolvedDataDoc };
}
+ //
+ // Resolves a reference to a field by returning 'doc' if no field extension is specified,
+ // otherwise, it returns the extension document stored in doc.<fieldKey>_ext.
+ // This mechanism allows any fields to be extended with an extension document that can
+ // be used to capture field-specific metadata. For example, an image field can be extended
+ // to store annotations, ink, and other data.
+ //
+ export function fieldExtensionDoc(doc: Doc, fieldKey: string) {
+ let extension = doc[fieldKey + "_ext"] as Doc;
+ (extension === undefined) && setTimeout(() => CreateDocumentExtensionForField(doc, fieldKey), 0);
+ return extension ? extension : undefined;
+ }
+
+ export function CreateDocumentExtensionForField(doc: Doc, fieldKey: string) {
+ let docExtensionForField = new Doc(doc[Id] + fieldKey, true);
+ docExtensionForField.title = fieldKey + ".ext";
+ docExtensionForField.extendsDoc = doc; // this is used by search to map field matches on the extension doc back to the document it extends.
+ docExtensionForField.type = DocumentType.EXTENSION;
+ let proto: Doc | undefined = doc;
+ while (proto && !Doc.IsPrototype(proto) && proto.proto) {
+ proto = proto.proto;
+ }
+ (proto ? proto : doc)[fieldKey + "_ext"] = new PrefetchProxy(docExtensionForField);
+ return docExtensionForField;
+ }
+
export function Overwrite(doc: Doc, overwrite: Doc, copyProto: boolean = false): Doc {
Object.keys(doc).forEach(key => {
const field = ProxyField.WithoutProxy(() => doc[key]);
@@ -518,6 +510,7 @@ export namespace Doc {
export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string): Doc {
const copy = new Doc(copyProtoId, true);
Object.keys(doc).forEach(key => {
+ let cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
const field = ProxyField.WithoutProxy(() => doc[key]);
if (key === "proto" && copyProto) {
if (doc[key] instanceof Doc) {
@@ -526,6 +519,8 @@ export namespace Doc {
} else {
if (field instanceof RefField) {
copy[key] = field;
+ } else if (cfield instanceof ComputedField) {
+ copy[key] = ComputedField.MakeFunction(cfield.script.originalScript);
} else if (field instanceof ObjectField) {
copy[key] = ObjectField.MakeCopy(field);
} else if (field instanceof Promise) {
@@ -553,9 +548,14 @@ export namespace Doc {
let _applyCount: number = 0;
export function ApplyTemplate(templateDoc: Doc) {
- return !templateDoc ? undefined : ApplyTemplateTo(templateDoc, Doc.MakeDelegate(new Doc()), templateDoc.title + "(..." + _applyCount++ + ")");
+ if (templateDoc) {
+ let applied = ApplyTemplateTo(templateDoc, Doc.MakeDelegate(new Doc()), "layoutCustom", templateDoc.title + "(..." + _applyCount++ + ")");
+ applied && (Doc.GetProto(applied).layout = applied.layout);
+ return applied;
+ }
+ return undefined;
}
- export function ApplyTemplateTo(templateDoc: Doc, target: Doc, titleTarget: string | undefined = undefined) {
+ export function ApplyTemplateTo(templateDoc: Doc, target: Doc, targetKey: string, titleTarget: string | undefined = undefined) {
if (!templateDoc) {
target.layout = undefined;
target.nativeWidth = undefined;
@@ -565,41 +565,28 @@ export namespace Doc {
return;
}
- let layoutCustom = Doc.MakeTitled("layoutCustom");
let layoutCustomLayout = Doc.MakeDelegate(templateDoc);
- titleTarget && (target.title = titleTarget);
+ titleTarget && (Doc.GetProto(target).title = titleTarget);
target.type = DocumentType.TEMPLATE;
- target.width = templateDoc.width;
- target.height = templateDoc.height;
- target.nativeWidth = templateDoc.nativeWidth ? templateDoc.nativeWidth : 0;
- target.nativeHeight = templateDoc.nativeHeight ? templateDoc.nativeHeight : 0;
- target.ignoreAspect = templateDoc.nativeWidth ? true : false;
target.onClick = templateDoc.onClick instanceof ObjectField && templateDoc.onClick[Copy]();
- target.layout = layoutCustomLayout;
- target.backgroundLayout = layoutCustomLayout.backgroundLayout;
- target.layoutNative = Cast(templateDoc.layoutNative, Doc) as Doc;
- target.layoutCustom = layoutCustom;
+ Doc.GetProto(target)[targetKey] = layoutCustomLayout;
+ target.layoutKey = targetKey;
return target;
}
- export function MakeMetadataFieldTemplate(fieldTemplate: Doc, templateDataDoc: Doc, suppressTitle: boolean = false) {
+ export function MakeMetadataFieldTemplate(fieldTemplate: Doc, templateDataDoc: Doc, suppressTitle: boolean = false): boolean {
// move data doc fields to layout doc as needed (nativeWidth/nativeHeight, data, ??)
let metadataFieldName = StrCast(fieldTemplate.title).replace(/^-/, "");
- let backgroundLayout = StrCast(fieldTemplate.backgroundLayout);
let fieldLayoutDoc = fieldTemplate;
if (fieldTemplate.layout instanceof Doc) {
fieldLayoutDoc = Doc.MakeDelegate(fieldTemplate.layout);
}
- if (backgroundLayout) {
- backgroundLayout = backgroundLayout.replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metadataFieldName}"}`);
- }
fieldTemplate.templateField = metadataFieldName;
fieldTemplate.title = metadataFieldName;
- fieldTemplate.isTemplate = true;
- fieldTemplate.backgroundLayout = backgroundLayout;
+ fieldTemplate.isTemplateField = true;
/* move certain layout properties from the original data doc to the template layout to avoid
inheriting them from the template's data doc which may also define these fields for its own use.
*/
@@ -607,6 +594,7 @@ export namespace Doc {
fieldTemplate.singleColumn = BoolCast(fieldTemplate.singleColumn);
fieldTemplate.nativeWidth = Cast(fieldTemplate.nativeWidth, "number");
fieldTemplate.nativeHeight = Cast(fieldTemplate.nativeHeight, "number");
+ fieldTemplate.type = fieldTemplate.type;
fieldTemplate.panX = 0;
fieldTemplate.panY = 0;
fieldTemplate.scale = 1;
@@ -615,29 +603,32 @@ export namespace Doc {
setTimeout(action(() => {
!templateDataDoc[metadataFieldName] && data instanceof ObjectField && (Doc.GetProto(templateDataDoc)[metadataFieldName] = ObjectField.MakeCopy(data));
let layout = StrCast(fieldLayoutDoc.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metadataFieldName}"}`);
- let layoutDelegate = fieldTemplate.layout instanceof Doc ? fieldLayoutDoc : fieldTemplate;
+ let layoutDelegate = Doc.Layout(fieldTemplate);
layoutDelegate.layout = layout;
fieldTemplate.layout = layoutDelegate !== fieldTemplate ? layoutDelegate : layout;
if (fieldTemplate.backgroundColor !== templateDataDoc.defaultBackgroundColor) fieldTemplate.defaultBackgroundColor = fieldTemplate.backgroundColor;
fieldTemplate.proto = templateDataDoc;
}), 0);
+ return true;
}
- export function overlapping(doc: Doc, doc2: Doc, clusterDistance: number) {
+ export function overlapping(doc1: Doc, doc2: Doc, clusterDistance: number) {
+ let doc2Layout = Doc.Layout(doc2);
+ let doc1Layout = Doc.Layout(doc1);
var x2 = NumCast(doc2.x) - clusterDistance;
var y2 = NumCast(doc2.y) - clusterDistance;
- var w2 = NumCast(doc2.width) + clusterDistance;
- var h2 = NumCast(doc2.height) + clusterDistance;
- var x = NumCast(doc.x) - clusterDistance;
- var y = NumCast(doc.y) - clusterDistance;
- var w = NumCast(doc.width) + clusterDistance;
- var h = NumCast(doc.height) + clusterDistance;
- return doc.z === doc2.z && intersectRect({ left: x, top: y, width: w, height: h }, { left: x2, top: y2, width: w2, height: h2 });
+ var w2 = NumCast(doc2Layout.width) + clusterDistance;
+ var h2 = NumCast(doc2Layout.height) + clusterDistance;
+ var x = NumCast(doc1.x) - clusterDistance;
+ var y = NumCast(doc1.y) - clusterDistance;
+ var w = NumCast(doc1Layout.width) + clusterDistance;
+ var h = NumCast(doc1Layout.height) + clusterDistance;
+ return doc1.z === doc2.z && intersectRect({ left: x, top: y, width: w, height: h }, { left: x2, top: y2, width: w2, height: h2 });
}
export function isBrushedHighlightedDegree(doc: Doc) {
if (Doc.IsHighlighted(doc)) {
- return 3;
+ return 6;
}
else {
return Doc.IsBrushedDegree(doc);
@@ -653,6 +644,11 @@ export namespace Doc {
@observable _user_doc: Doc = undefined!;
@observable BrushedDoc: ObservableMap<Doc, boolean> = new ObservableMap();
}
+
+ // the document containing the view layout information - will be the Document itself unless the Document has
+ // a layout field. In that case, all layout information comes from there unless overriden by Document
+ export function Layout(doc: Doc) { return Doc.LayoutField(doc) instanceof Doc ? doc[StrCast(doc.layoutKey, "layout")] as Doc : doc; }
+ export function LayoutField(doc: Doc) { return doc[StrCast(doc.layoutKey, "layout")]; }
const manager = new DocData();
export function UserDoc(): Doc { return manager._user_doc; }
export function SetUserDoc(doc: Doc) { manager._user_doc = doc; }
@@ -665,10 +661,29 @@ export namespace Doc {
export function BrushDoc(doc: Doc) {
brushManager.BrushedDoc.set(doc, true);
brushManager.BrushedDoc.set(Doc.GetDataDoc(doc), true);
+ return doc;
}
export function UnBrushDoc(doc: Doc) {
brushManager.BrushedDoc.delete(doc);
brushManager.BrushedDoc.delete(Doc.GetDataDoc(doc));
+ return doc;
+ }
+
+ export function LinkEndpoint(linkDoc: Doc, anchorDoc: Doc) { return Doc.AreProtosEqual(anchorDoc, Cast(linkDoc.anchor1, Doc) as Doc) ? "layoutKey1" : "layoutKey2"; }
+
+ export function linkFollowUnhighlight() {
+ Doc.UnhighlightAll();
+ document.removeEventListener("pointerdown", linkFollowUnhighlight);
+ }
+
+ let dt = 0;
+ export function linkFollowHighlight(destDoc: Doc) {
+ linkFollowUnhighlight();
+ Doc.HighlightDoc(destDoc);
+ document.removeEventListener("pointerdown", linkFollowUnhighlight);
+ document.addEventListener("pointerdown", linkFollowUnhighlight);
+ let x = dt = Date.now();
+ window.setTimeout(() => dt === x && linkFollowUnhighlight(), 5000);
}
export class HighlightBrush {
@@ -701,12 +716,25 @@ export namespace Doc {
}
export function UnBrushAllDocs() {
- manager.BrushedDoc.clear();
+ brushManager.BrushedDoc.clear();
+ }
+
+ export function setChildLayout(target: Doc, source?: Doc) {
+ target.childLayout = source && source.isTemplateDoc ? source : source &&
+ source.dragFactory instanceof Doc && source.dragFactory.isTemplateDoc ? source.dragFactory :
+ source && source.layout instanceof Doc && source.layout.isTemplateDoc ? source.layout : undefined;
}
}
+
+
Scripting.addGlobal(function renameAlias(doc: any, n: any) { return StrCast(Doc.GetProto(doc).title).replace(/\([0-9]*\)/, "") + `(${n})`; });
Scripting.addGlobal(function getProto(doc: any) { return Doc.GetProto(doc); });
+Scripting.addGlobal(function setChildLayout(target: any, source: any) { Doc.setChildLayout(target, source); });
Scripting.addGlobal(function getAlias(doc: any) { return Doc.MakeAlias(doc); });
+Scripting.addGlobal(function getCopy(doc: any, copyProto: any) { return doc.isTemplateDoc ? Doc.ApplyTemplate(doc) : Doc.MakeCopy(doc, copyProto); });
Scripting.addGlobal(function copyField(field: any) { return ObjectField.MakeCopy(field); });
Scripting.addGlobal(function aliasDocs(field: any) { return new List<Doc>(field.map((d: any) => Doc.MakeAlias(d))); });
-Scripting.addGlobal(function docList(field: any) { return DocListCast(field); }); \ No newline at end of file
+Scripting.addGlobal(function docList(field: any) { return DocListCast(field); });
+Scripting.addGlobal(function sameDocs(doc1: any, doc2: any) { return Doc.AreProtosEqual(doc1, doc2); });
+Scripting.addGlobal(function undo() { return UndoManager.Undo(); });
+Scripting.addGlobal(function redo() { return UndoManager.Redo(); }); \ No newline at end of file
diff --git a/src/new_fields/InkField.ts b/src/new_fields/InkField.ts
index 8f64c1c2e..d94834e91 100644
--- a/src/new_fields/InkField.ts
+++ b/src/new_fields/InkField.ts
@@ -8,7 +8,8 @@ export enum InkTool {
None,
Pen,
Highlighter,
- Eraser
+ Eraser,
+ Scrubber
}
export interface StrokeData {
@@ -16,7 +17,8 @@ export interface StrokeData {
color: string;
width: string;
tool: InkTool;
- page: number;
+ displayTimecode: number;
+ creationTime: number;
}
export type InkData = Map<string, StrokeData>;
diff --git a/src/new_fields/SchemaHeaderField.ts b/src/new_fields/SchemaHeaderField.ts
index 7494c9bd1..92d0aec9a 100644
--- a/src/new_fields/SchemaHeaderField.ts
+++ b/src/new_fields/SchemaHeaderField.ts
@@ -105,5 +105,4 @@ export class SchemaHeaderField extends ObjectField {
[ToScriptString]() {
return `invalid`;
}
-}
-
+} \ No newline at end of file
diff --git a/src/new_fields/URLField.ts b/src/new_fields/URLField.ts
index eebd15ecd..eabd04e0f 100644
--- a/src/new_fields/URLField.ts
+++ b/src/new_fields/URLField.ts
@@ -38,6 +38,8 @@ export abstract class URLField extends ObjectField {
}
}
+export const nullAudio = "https://actions.google.com/sounds/v1/alarms/beep_short.ogg";
+
@scriptingGlobal @Deserializable("audio") export class AudioField extends URLField { }
@scriptingGlobal @Deserializable("image") export class ImageField extends URLField { }
@scriptingGlobal @Deserializable("video") export class VideoField extends URLField { }
diff --git a/src/new_fields/documentSchemas.ts b/src/new_fields/documentSchemas.ts
new file mode 100644
index 000000000..e2730914f
--- /dev/null
+++ b/src/new_fields/documentSchemas.ts
@@ -0,0 +1,59 @@
+import { makeInterface, createSchema, listSpec } from "./Schema";
+import { ScriptField } from "./ScriptField";
+import { Doc } from "./Doc";
+import { DateField } from "./DateField";
+
+export const documentSchema = createSchema({
+ layout: "string", // this is the native layout string for the document. templates can be added using other fields and setting layoutKey below (see layoutCustom as an example)
+ layoutKey: "string", // holds the field key for the field that actually holds the current lyoat
+ layoutCustom: Doc, // used to hold a custom layout (there's nothing special about this field .. any field could hold a custom layout that can be selected by setting 'layoutKey')
+ title: "string", // document title (can be on either data document or layout)
+ nativeWidth: "number", // native width of document which determines how much document contents are scaled when the document's width is set
+ nativeHeight: "number", // "
+ width: "number", // width of document in its container's coordinate system
+ height: "number", // "
+ color: "string", // foreground color of document
+ backgroundColor: "string", // background color of document
+ opacity: "number", // opacity of document
+ creationDate: DateField, // when the document was created
+ links: listSpec(Doc), // computed (readonly) list of links associated with this document
+ dropAction: "string", // override specifying what should happen when this document is dropped (can be "alias" or "copy")
+ removeDropProperties: listSpec("string"), // properties that should be removed from the alias/copy/etc of this document when it is dropped
+ onClick: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
+ 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.
+ ignoreAspect: "boolean", // whether aspect ratio should be ignored when laying out or manipulating the document
+ autoHeight: "boolean", // whether the height of the document should be computed automatically based on its contents
+ isTemplateField: "boolean", // whether this document acts as a template layout for describing how other documents should be displayed
+ isBackground: "boolean", // whether document is a background element and ignores input events (can only selet with marquee)
+ type: "string", // enumerated type of document
+ currentTimecode: "number", // current play back time of a temporal document (video / audio)
+ summarizedDocs: listSpec(Doc), // documents that are summarized by this document (and which will typically be opened by clicking this document)
+ maximizedDocs: listSpec(Doc), // documents to maximize when clicking this document (generally this document will be an icon)
+ maximizeLocation: "string", // flag for where to place content when following a click interaction (e.g., onRight, inPlace, inTab)
+ lockedPosition: "boolean", // whether the document can be spatially manipulated
+ inOverlay: "boolean", // whether the document is rendered in an OverlayView which handles selection/dragging differently
+ borderRounding: "string", // border radius rounding of document
+ searchFields: "string", // the search fields to display when this document matches a search in its metadata
+ heading: "number", // the logical layout 'heading' of this document (used by rule provider to stylize h1 header elements, from h2, etc)
+ showCaption: "string", // whether editable caption text is overlayed at the bottom of the document
+ showTitle: "string", // whether an editable title banner is displayed at tht top of the document
+ isButton: "boolean", // whether document functions as a button (overiding native interactions of its content)
+ ignoreClick: "boolean", // whether documents ignores input clicks (but does not ignore manipulation and other events)
+ isAnimating: "boolean", // whether the document is in the midst of animating between two layouts (used by icons to de/iconify documents).
+ animateToDimensions: listSpec("number"), // layout information about the target rectangle a document is animating towards
+ 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.
+});
+
+export const positionSchema = createSchema({
+ zIndex: "number",
+ x: "number",
+ y: "number",
+ z: "number",
+});
+
+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/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index 4230e9b17..46d897339 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -3,6 +3,8 @@ import { Utils } from '../Utils';
import * as path from 'path';
import * as sharp from 'sharp';
import request = require('request-promise');
+import { ExifData, ExifImage } from 'exif';
+import { Opt } from '../new_fields/Doc';
const uploadDirectory = path.join(__dirname, './public/files/');
@@ -22,7 +24,7 @@ export namespace DashUploadUtils {
const gifs = [".gif"];
const pngs = [".png"];
const jpgs = [".jpg", ".jpeg"];
- const imageFormats = [...pngs, ...jpgs, ...gifs];
+ export const imageFormats = [...pngs, ...jpgs, ...gifs];
const videoFormats = [".mov", ".mp4"];
const size = "content-length";
@@ -31,12 +33,19 @@ export namespace DashUploadUtils {
export interface UploadInformation {
mediaPaths: string[];
fileNames: { [key: string]: string };
+ exifData: EnrichedExifData;
contentSize?: number;
contentType?: string;
}
- const generate = (prefix: string, url: string) => `${prefix}upload_${Utils.GenerateGuid()}${path.extname(url).toLowerCase()}`;
+ const generate = (prefix: string, url: string) => `${prefix}upload_${Utils.GenerateGuid()}${sanitizeExtension(url)}`;
const sanitize = (filename: string) => filename.replace(/\s+/g, "_");
+ const sanitizeExtension = (source: string) => {
+ let extension = path.extname(source);
+ extension = extension.toLowerCase();
+ extension = extension.split("?")[0];
+ return extension;
+ };
/**
* Uploads an image specified by the @param source to Dash's /public/files/
@@ -64,10 +73,16 @@ export namespace DashUploadUtils {
isLocal: boolean;
stream: any;
normalizedUrl: string;
+ exifData: EnrichedExifData;
contentSize?: number;
contentType?: string;
}
+ export interface EnrichedExifData {
+ data: ExifData;
+ error?: string;
+ }
+
/**
* Based on the url's classification as local or remote, gleans
* as much information as possible about the specified image
@@ -76,7 +91,9 @@ export namespace DashUploadUtils {
*/
export const InspectImage = async (source: string): Promise<InspectionResults> => {
const { isLocal, stream, normalized: normalizedUrl } = classify(source);
+ const exifData = await parseExifData(source);
const results = {
+ exifData,
isLocal,
stream,
normalizedUrl
@@ -101,13 +118,13 @@ export namespace DashUploadUtils {
};
export const UploadInspectedImage = async (metadata: InspectionResults, filename?: string, prefix = ""): Promise<UploadInformation> => {
- const { isLocal, stream, normalizedUrl, contentSize, contentType } = metadata;
+ const { isLocal, stream, normalizedUrl, contentSize, contentType, exifData } = metadata;
const resolved = filename ? sanitize(filename) : generate(prefix, normalizedUrl);
- let extension = path.extname(normalizedUrl) || path.extname(resolved);
- extension && (extension = extension.toLowerCase());
+ const extension = sanitizeExtension(normalizedUrl || resolved);
let information: UploadInformation = {
mediaPaths: [],
fileNames: { clean: resolved },
+ exifData,
contentSize,
contentType,
};
@@ -159,6 +176,18 @@ export namespace DashUploadUtils {
};
};
+ const parseExifData = async (source: string): Promise<EnrichedExifData> => {
+ return new Promise<EnrichedExifData>(resolve => {
+ new ExifImage(source, (error, data) => {
+ let reason: Opt<string> = undefined;
+ if (error) {
+ reason = (error as any).code;
+ }
+ resolve({ data, error: reason });
+ });
+ });
+ };
+
export const createIfNotExists = async (path: string) => {
if (await new Promise<boolean>(resolve => fs.exists(path, resolve))) {
return true;
diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts
index ee9cd8a0e..7426ffb39 100644
--- a/src/server/RouteStore.ts
+++ b/src/server/RouteStore.ts
@@ -13,6 +13,8 @@ export enum RouteStore {
upload = "/upload",
dataUriToImage = "/uploadURI",
images = "/images",
+ inspectImage = "/inspectImage",
+ imageHierarchyExport = "/imageHierarchyExport",
// USER AND WORKSPACES
getCurrUser = "/getCurrentUser",
@@ -32,7 +34,8 @@ export enum RouteStore {
// APIS
cognitiveServices = "/cognitiveservices",
googleDocs = "/googleDocs",
- googlePhotosAccessToken = "/googlePhotosAccessToken",
+ readGoogleAccessToken = "/readGoogleAccessToken",
+ writeGoogleAccessToken = "/writeGoogleAccessToken",
googlePhotosMediaUpload = "/googlePhotosMediaUpload",
googlePhotosMediaDownload = "/googlePhotosMediaDownload",
googleDocsGet = "/googleDocsGet"
diff --git a/src/server/RouteSubscriber.ts b/src/server/RouteSubscriber.ts
new file mode 100644
index 000000000..e49be8af5
--- /dev/null
+++ b/src/server/RouteSubscriber.ts
@@ -0,0 +1,26 @@
+export default class RouteSubscriber {
+ private _root: string;
+ private requestParameters: string[] = [];
+
+ constructor(root: string) {
+ this._root = root;
+ }
+
+ add(...parameters: string[]) {
+ this.requestParameters.push(...parameters);
+ return this;
+ }
+
+ public get root() {
+ return this._root;
+ }
+
+ public get build() {
+ let output = this._root;
+ if (this.requestParameters.length) {
+ output = `${output}/:${this.requestParameters.join("/:")}`;
+ }
+ return output;
+ }
+
+} \ No newline at end of file
diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts
index c899c2ef2..5714c9928 100644
--- a/src/server/apis/google/GoogleApiServerUtils.ts
+++ b/src/server/apis/google/GoogleApiServerUtils.ts
@@ -25,7 +25,8 @@ export namespace GoogleApiServerUtils {
'drive.file',
'photoslibrary',
'photoslibrary.appendonly',
- 'photoslibrary.sharing'
+ 'photoslibrary.sharing',
+ 'userinfo.profile'
];
export const parseBuffer = (data: Buffer) => JSON.parse(data.toString());
@@ -75,6 +76,82 @@ export namespace GoogleApiServerUtils {
});
};
+ const RetrieveOAuthClient = async (information: CredentialInformation) => {
+ return new Promise<OAuth2Client>((resolve, reject) => {
+ readFile(information.credentialsPath, async (err, credentials) => {
+ if (err) {
+ reject(err);
+ return console.log('Error loading client secret file:', err);
+ }
+ const { client_secret, client_id, redirect_uris } = parseBuffer(credentials).installed;
+ resolve(new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]));
+ });
+ });
+ };
+
+ export const GenerateAuthenticationUrl = async (information: CredentialInformation) => {
+ const client = await RetrieveOAuthClient(information);
+ return client.generateAuthUrl({
+ access_type: 'offline',
+ scope: SCOPES.map(relative => prefix + relative),
+ });
+ };
+
+ export interface GoogleAuthenticationResult {
+ access_token: string;
+ avatar: string;
+ name: string;
+ }
+ export const ProcessClientSideCode = async (information: CredentialInformation, authenticationCode: string): Promise<GoogleAuthenticationResult> => {
+ const oAuth2Client = await RetrieveOAuthClient(information);
+ return new Promise<GoogleAuthenticationResult>((resolve, reject) => {
+ oAuth2Client.getToken(authenticationCode, async (err, token) => {
+ if (err || !token) {
+ reject(err);
+ return console.error('Error retrieving access token', err);
+ }
+ oAuth2Client.setCredentials(token);
+ const enriched = injectUserInfo(token);
+ await Database.Auxiliary.GoogleAuthenticationToken.Write(information.userId, enriched);
+ const { given_name, picture } = enriched.userInfo;
+ resolve({
+ access_token: enriched.access_token!,
+ avatar: picture,
+ name: given_name
+ });
+ });
+ });
+ };
+
+ /**
+ * It's pretty cool: the credentials id_token is split into thirds by periods.
+ * The middle third contains a base64-encoded JSON string with all the
+ * user info contained in the interface below. So, we isolate that middle third,
+ * base64 decode with atob and parse the JSON.
+ * @param credentials the client credentials returned from OAuth after the user
+ * has executed the authentication routine
+ */
+ const injectUserInfo = (credentials: Credentials): EnrichedCredentials => {
+ const userInfo = JSON.parse(atob(credentials.id_token!.split(".")[1]));
+ return { ...credentials, userInfo };
+ };
+
+ export type EnrichedCredentials = Credentials & { userInfo: UserInfo };
+ export interface UserInfo {
+ at_hash: string;
+ aud: string;
+ azp: string;
+ exp: number;
+ family_name: string;
+ given_name: string;
+ iat: number;
+ iss: string;
+ locale: string;
+ name: string;
+ picture: string;
+ sub: string;
+ }
+
export const RetrieveCredentials = (information: CredentialInformation) => {
return new Promise<TokenResult>((resolve, reject) => {
readFile(information.credentialsPath, async (err, credentials) => {
@@ -107,17 +184,13 @@ export namespace GoogleApiServerUtils {
return new Promise<TokenResult>((resolve, reject) => {
// Attempting to authorize user (${userId})
Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId).then(token => {
- if (!token) {
- // No token registered, so awaiting input from user
- return getNewToken(oAuth2Client, userId).then(resolve, reject);
- }
- if (token.expiry_date! < new Date().getTime()) {
+ if (token!.expiry_date! < new Date().getTime()) {
// Token has expired, so submitting a request for a refreshed access token
- return refreshToken(token, client_id, client_secret, oAuth2Client, userId).then(resolve, reject);
+ return refreshToken(token!, client_id, client_secret, oAuth2Client, userId).then(resolve, reject);
}
// Authentication successful!
- oAuth2Client.setCredentials(token);
- resolve({ token, client: oAuth2Client });
+ oAuth2Client.setCredentials(token!);
+ resolve({ token: token!, client: oAuth2Client });
});
});
}
@@ -145,35 +218,4 @@ export namespace GoogleApiServerUtils {
});
};
- /**
- * Get and store new token after prompting for user authorization, and then
- * execute the given callback with the authorized OAuth2 client.
- * @param {google.auth.OAuth2} oAuth2Client The OAuth2 client to get token for.
- * @param {getEventsCallback} callback The callback for the authorized client.
- */
- function getNewToken(oAuth2Client: OAuth2Client, userId: string) {
- return new Promise<TokenResult>((resolve, reject) => {
- const authUrl = oAuth2Client.generateAuthUrl({
- access_type: 'offline',
- scope: SCOPES.map(relative => prefix + relative),
- });
- console.log('Authorize this app by visiting this url:', authUrl);
- const rl = createInterface({
- input: process.stdin,
- output: process.stdout,
- });
- rl.question('Enter the code from that page here: ', (code) => {
- rl.close();
- oAuth2Client.getToken(code, async (err, token) => {
- if (err || !token) {
- reject(err);
- return console.error('Error retrieving access token', err);
- }
- oAuth2Client.setCredentials(token);
- await Database.Auxiliary.GoogleAuthenticationToken.Write(userId, token);
- resolve({ token, client: oAuth2Client });
- });
- });
- });
- }
} \ No newline at end of file
diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts
index 16c4f6c3a..4a67e57cc 100644
--- a/src/server/apis/google/GooglePhotosUploadUtils.ts
+++ b/src/server/apis/google/GooglePhotosUploadUtils.ts
@@ -4,6 +4,7 @@ import * as path from 'path';
import { MediaItemCreationResult } from './SharedTypes';
import { NewMediaItem } from "../../index";
import { BatchedArray, TimeUnit } from 'array-batcher';
+import { DashUploadUtils } from '../../DashUploadUtils';
export namespace GooglePhotosUploadUtils {
@@ -32,6 +33,9 @@ export namespace GooglePhotosUploadUtils {
};
export const DispatchGooglePhotosUpload = async (url: string) => {
+ if (!DashUploadUtils.imageFormats.includes(path.extname(url))) {
+ return undefined;
+ }
const body = await request(url, { encoding: null });
const parameters = {
method: 'POST',
diff --git a/src/server/authentication/config/passport.ts b/src/server/authentication/config/passport.ts
index 8b20d29b1..8915a4abf 100644
--- a/src/server/authentication/config/passport.ts
+++ b/src/server/authentication/config/passport.ts
@@ -41,11 +41,9 @@ export let isAuthenticated = (req: Request, res: Response, next: NextFunction) =
export let isAuthorized = (req: Request, res: Response, next: NextFunction) => {
const provider = req.path.split("/").slice(-1)[0];
- if (req.user) {
- if (_.find((req.user as any).tokens, { kind: provider })) {
- next();
- } else {
- res.redirect(`/auth/${provider}`);
- }
+ if (_.find((req.user as any).tokens, { kind: provider })) {
+ next();
+ } else {
+ res.redirect(`/auth/${provider}`);
}
}; \ No newline at end of file
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 24bad694d..55390989b 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -1,17 +1,21 @@
-import { action, computed, observable, runInAction } from "mobx";
+import { action, computed, observable, reaction, runInAction } from "mobx";
import * as rp from 'request-promise';
import { DocServer } from "../../../client/DocServer";
import { Docs } from "../../../client/documents/Documents";
import { Attribute, AttributeGroup, Catalog, Schema } from "../../../client/northstar/model/idea/idea";
import { ArrayUtil } from "../../../client/northstar/utils/ArrayUtil";
-import { CollectionViewType } from "../../../client/views/collections/CollectionBaseView";
-import { CollectionView } from "../../../client/views/collections/CollectionView";
+import { UndoManager } from "../../../client/util/UndoManager";
import { Doc, DocListCast } from "../../../new_fields/Doc";
import { List } from "../../../new_fields/List";
import { listSpec } from "../../../new_fields/Schema";
-import { Cast, StrCast, PromiseValue } from "../../../new_fields/Types";
+import { ScriptField, ComputedField } from "../../../new_fields/ScriptField";
+import { Cast, PromiseValue } from "../../../new_fields/Types";
import { Utils } from "../../../Utils";
import { RouteStore } from "../../RouteStore";
+import { InkingControl } from "../../../client/views/InkingControl";
+import { DragManager } from "../../../client/util/DragManager";
+import { nullAudio } from "../../../new_fields/URLField";
+import { LinkManager } from "../../../client/util/LinkManager";
export class CurrentUserUtils {
private static curr_id: string;
@@ -19,104 +23,187 @@ export class CurrentUserUtils {
private static mainDocId: string | undefined;
public static get id() { return this.curr_id; }
- @computed public static get UserDocument() { return Doc.UserDoc(); }
public static get MainDocId() { return this.mainDocId; }
public static set MainDocId(id: string | undefined) { this.mainDocId = id; }
+ @computed public static get UserDocument() { return Doc.UserDoc(); }
+ @computed public static get ActivePen() { return Doc.UserDoc().activePen instanceof Doc && (Doc.UserDoc().activePen as Doc).pen as Doc; }
@observable public static GuestTarget: Doc | undefined;
@observable public static GuestWorkspace: Doc | undefined;
- private static createUserDocument(id: string): Doc {
- let doc = new Doc(id, true);
- doc.viewType = CollectionViewType.Tree;
- doc.dropAction = "alias";
- doc.layout = CollectionView.LayoutString();
- doc.title = Doc.CurrentUserEmail;
- this.updateUserDocument(doc);
- doc.data = new List<Doc>();
- doc.gridGap = 5;
- doc.xMargin = 5;
- doc.yMargin = 5;
- doc.boxShadow = "0 0";
- doc.optionalRightCollection = Docs.Create.StackingDocument([], { title: "New mobile uploads" });
- return doc;
+ // a default set of note types .. not being used yet...
+ static setupNoteTypes(doc: Doc) {
+ return [
+ Docs.Create.TextDocument({ title: "Note", backgroundColor: "yellow", isTemplateDoc: true }),
+ Docs.Create.TextDocument({ title: "Idea", backgroundColor: "pink", isTemplateDoc: true }),
+ Docs.Create.TextDocument({ title: "Topic", backgroundColor: "lightBlue", isTemplateDoc: true }),
+ Docs.Create.TextDocument({ title: "Person", backgroundColor: "lightGreen", isTemplateDoc: true }),
+ Docs.Create.TextDocument({ title: "Todo", backgroundColor: "orange", isTemplateDoc: true })
+ ];
}
- static updateUserDocument(doc: Doc) {
+ // setup the "creator" buttons for the sidebar-- eg. the default set of draggable document creation tools
+ static setupCreatorButtons(doc: Doc) {
+ let notes = CurrentUserUtils.setupNoteTypes(doc);
+ doc.noteTypes = Docs.Create.TreeDocument(notes, { title: "Note Types", height: 75 });
+ doc.activePen = doc;
+ let docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, unchecked?: string, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
+ { title: "collection", icon: "folder", ignoreClick: true, drag: 'Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" })' },
+ { title: "todo item", icon: "check", ignoreClick: true, drag: 'getCopy(this.dragFactory, true)', dragFactory: notes[notes.length - 1] },
+ { title: "web page", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" })' },
+ { title: "cat image", icon: "cat", ignoreClick: true, drag: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { width: 200, title: "an image of a cat" })' },
+ { title: "webcam", icon: "video", ignoreClick: true, drag: 'Docs.Create.WebCamDocument("", { width: 400, height: 400, title: "a test cam" })' },
+ { title: "record", icon: "microphone", ignoreClick: true, drag: `Docs.Create.AudioDocument("${nullAudio}", { width: 200, title: "ready to record audio" })` },
+ { title: "clickable button", icon: "bolt", ignoreClick: true, drag: 'Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" })' },
+ { title: "presentation", icon: "tv", ignoreClick: true, drag: 'Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List<Doc>(), { width: 200, height: 500, title: "a presentation trail" })' },
+ { title: "import folder", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })' },
+ { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", unchecked: `!sameDocs(this.activePen.pen, this)`, activePen: doc },
+ { title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", unchecked: `!sameDocs(this.activePen.pen, this)`, activePen: doc },
+ { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', unchecked: `!sameDocs(this.activePen.pen, this)`, backgroundColor: "pink", activePen: doc },
+ { title: "use scrubber", icon: "eraser", click: 'activateScrubber(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', unchecked: `!sameDocs(this.activePen.pen, this)`, backgroundColor: "green", activePen: doc },
+ { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.pen = this;', unchecked: `!sameDocs(this.activePen.pen, this) && this.activePen.pen !== undefined`, backgroundColor: "white", activePen: doc },
+ ];
+ return docProtoData.map(data => Docs.Create.FontIconDocument({
+ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: data.click ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
+ onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, onClick: data.click ? ScriptField.MakeScript(data.click) : undefined,
+ unchecked: data.unchecked ? ComputedField.MakeFunction(data.unchecked) : undefined, activePen: data.activePen,
+ backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), dragFactory: data.dragFactory,
+ }));
+ }
+
+ // setup the Creator button which will display the creator panel. This panel will include the drag creators and the color picker. when clicked, this panel will be displayed in the target container (ie, sidebarContainer)
+ static setupToolsPanel(sidebarContainer: Doc, doc: Doc) {
+ // setup a masonry view of all he creators
+ const dragCreators = Docs.Create.MasonryDocument(CurrentUserUtils.setupCreatorButtons(doc), {
+ width: 500, autoHeight: true, columnWidth: 35, ignoreClick: true, lockedPosition: true, chromeStatus: "disabled", title: "buttons",
+ dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), yMargin: 5
+ });
+ // setup a color picker
+ const color = Docs.Create.ColorDocument({
+ title: "color picker", width: 400, dropAction: "alias", forceActive: true, removeDropProperties: new List<string>(["dropAction", "forceActive"])
+ });
+
+ return Docs.Create.ButtonDocument({
+ width: 35, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Tools", targetContainer: sidebarContainer,
+ sourcePanel: Docs.Create.StackingDocument([dragCreators, color], {
+ width: 500, height: 800, lockedPosition: true, chromeStatus: "disabled", title: "tools stack"
+ }),
+ onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel"),
+ });
+ }
+ // setup the Library button which will display the library panel. This panel includes a collection of workspaces, documents, and recently closed views
+ static setupLibraryPanel(sidebarContainer: Doc, doc: Doc) {
// setup workspaces library item
- if (doc.workspaces === undefined) {
- const workspaces = Docs.Create.TreeDocument([], { title: "Workspaces", height: 100 });
- workspaces.boxShadow = "0 0";
- doc.workspaces = workspaces;
- }
- PromiseValue(Cast(doc.workspaces, Doc)).then(workspaces => {
- if (workspaces) {
- workspaces.preventTreeViewOpen = true;
- workspaces.forceActive = true;
- workspaces.lockedPosition = true;
- }
+ doc.workspaces = Docs.Create.TreeDocument([], {
+ title: "WORKSPACES", height: 100, forceActive: true, boxShadow: "0 0", lockedPosition: true, backgroundColor: "#eeeeee"
});
- // setup notes list
- if (doc.noteTypes === undefined) {
- let notes = [Docs.Create.TextDocument({ title: "Note", backgroundColor: "yellow", isTemplate: true }),
- Docs.Create.TextDocument({ title: "Idea", backgroundColor: "pink", isTemplate: true }),
- Docs.Create.TextDocument({ title: "Topic", backgroundColor: "lightBlue", isTemplate: true }),
- Docs.Create.TextDocument({ title: "Person", backgroundColor: "lightGreen", isTemplate: true })];
- const noteTypes = Docs.Create.TreeDocument(notes, { title: "Note Types", height: 75 });
- doc.noteTypes = noteTypes;
- }
- PromiseValue(Cast(doc.noteTypes, Doc)).then(noteTypes => noteTypes && PromiseValue(noteTypes.data).then(DocListCast));
+ doc.documents = Docs.Create.TreeDocument([], {
+ title: "DOCUMENTS", height: 42, forceActive: true, boxShadow: "0 0", preventTreeViewOpen: true, lockedPosition: true, backgroundColor: "#eeeeee"
+ });
// setup Recently Closed library item
- if (doc.recentlyClosed === undefined) {
- const recentlyClosed = Docs.Create.TreeDocument([], { title: "Recently Closed", height: 75 });
- recentlyClosed.boxShadow = "0 0";
- doc.recentlyClosed = recentlyClosed;
- }
- PromiseValue(Cast(doc.recentlyClosed, Doc)).then(recent => {
- if (recent) {
- recent.preventTreeViewOpen = true;
- recent.forceActive = true;
- recent.lockedPosition = true;
- }
+ doc.recentlyClosed = Docs.Create.TreeDocument([], {
+ title: "RECENTLY CLOSED", height: 75, forceActive: true, boxShadow: "0 0", preventTreeViewOpen: true, lockedPosition: true, backgroundColor: "#eeeeee"
});
+ return Docs.Create.ButtonDocument({
+ width: 50, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Library",
+ sourcePanel: Docs.Create.TreeDocument([doc.workspaces as Doc, doc.documents as Doc, doc.recentlyClosed as Doc], {
+ title: "Library", xMargin: 5, yMargin: 5, gridGap: 5, forceActive: true, dropAction: "alias", lockedPosition: true
+ }),
+ targetContainer: sidebarContainer,
+ onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel")
+ });
+ }
- if (doc.curPresentation === undefined) {
- const curPresentation = Docs.Create.PresDocument(new List<Doc>(), { title: "Presentation" });
- curPresentation.boxShadow = "0 0";
- doc.curPresentation = curPresentation;
- }
+ // setup the Search button which will display the search panel.
+ static setupSearchPanel(sidebarContainer: Doc) {
+ return Docs.Create.ButtonDocument({
+ width: 50, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Search",
+ sourcePanel: Docs.Create.QueryDocument({
+ title: "search stack", ignoreClick: true
+ }),
+ targetContainer: sidebarContainer,
+ lockedPosition: true,
+ onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel")
+ });
+ }
- if (doc.sidebar === undefined) {
- const sidebar = Docs.Create.StackingDocument([doc.workspaces as Doc, doc, doc.recentlyClosed as Doc], { title: "Sidebar" });
- sidebar.forceActive = true;
- sidebar.lockedPosition = true;
- sidebar.gridGap = 5;
- sidebar.xMargin = 5;
- sidebar.yMargin = 5;
- Doc.GetProto(sidebar).backgroundColor = "#aca3a6";
- sidebar.boxShadow = "1 1 3";
- doc.sidebar = sidebar;
- }
+ // setup the list of sidebar mode buttons which determine what is displayed in the sidebar
+ static setupSidebarButtons(doc: Doc) {
+ doc.sidebarContainer = new Doc();
+ (doc.sidebarContainer as Doc).chromeStatus = "disabled";
+ (doc.sidebarContainer as Doc).onClick = ScriptField.MakeScript("freezeSidebar()");
- if (doc.overlays === undefined) {
- const overlays = Docs.Create.FreeformDocument([], { title: "Overlays" });
- Doc.GetProto(overlays).backgroundColor = "#aca3a6";
- doc.overlays = overlays;
- }
+ doc.ToolsBtn = this.setupToolsPanel(doc.sidebarContainer as Doc, doc);
+ doc.LibraryBtn = this.setupLibraryPanel(doc.sidebarContainer as Doc, doc);
+ doc.SearchBtn = this.setupSearchPanel(doc.sidebarContainer as Doc);
- if (doc.linkFollowBox === undefined) {
- PromiseValue(Cast(doc.overlays, Doc)).then(overlays => overlays && Doc.AddDocToList(overlays, "data", doc.linkFollowBox = Docs.Create.LinkFollowBoxDocument({ x: 250, y: 20, width: 500, height: 370, title: "Link Follower" })));
- }
+ // Finally, setup the list of buttons to display in the sidebar
+ doc.sidebarButtons = Docs.Create.StackingDocument([doc.SearchBtn as Doc, doc.LibraryBtn as Doc, doc.ToolsBtn as Doc], {
+ width: 500, height: 80, boxShadow: "0 0", sectionFilter: "title", hideHeadings: true, ignoreClick: true,
+ backgroundColor: "lightgrey", chromeStatus: "disabled", title: "library stack"
+ });
+ }
- StrCast(doc.title).indexOf("@") !== -1 && (doc.title = StrCast(doc.title).split("@")[0] + "'s Library");
- doc.width = 100;
- doc.preventTreeViewOpen = true;
- doc.forceActive = true;
- doc.lockedPosition = true;
+ /// sets up the default list of buttons to be shown in the expanding button menu at the bottom of the Dash window
+ static setupExpandingButtons(doc: Doc) {
+ doc.undoBtn = Docs.Create.FontIconDocument(
+ { nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: "alias", onClick: ScriptField.MakeScript("undo()"), removeDropProperties: new List<string>(["dropAction"]), title: "undo button", icon: "undo-alt" });
+ doc.redoBtn = Docs.Create.FontIconDocument(
+ { nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: "alias", onClick: ScriptField.MakeScript("redo()"), removeDropProperties: new List<string>(["dropAction"]), title: "redo button", icon: "redo-alt" });
+
+ doc.expandingButtons = Docs.Create.LinearDocument([doc.undoBtn as Doc, doc.redoBtn as Doc], {
+ title: "expanding buttons", gridGap: 5, xMargin: 5, yMargin: 5, height: 42, width: 100, boxShadow: "0 0",
+ backgroundColor: "black", preventTreeViewOpen: true, forceActive: true, lockedPosition: true,
+ dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name })
+ });
+ }
+
+ // sets up the default set of documents to be shown in the Overlay layer
+ static setupOverlays(doc: Doc) {
+ doc.overlays = Docs.Create.FreeformDocument([], { title: "Overlays", backgroundColor: "#aca3a6" });
+ doc.linkFollowBox = Docs.Create.LinkFollowBoxDocument({ x: 250, y: 20, width: 500, height: 370, title: "Link Follower" });
+ Doc.AddDocToList(doc.overlays as Doc, "data", doc.linkFollowBox as Doc);
+ }
+
+ // the initial presentation Doc to use
+ static setupDefaultPresentation(doc: Doc) {
+ doc.curPresentation = Docs.Create.PresDocument(new List<Doc>(), { title: "Presentation", boxShadow: "0 0" });
+ }
+
+ static setupMobileUploads(doc: Doc) {
+ doc.optionalRightCollection = Docs.Create.StackingDocument([], { title: "New mobile uploads" });
+ }
+
+ static updateUserDocument(doc: Doc) {
+ doc.title = Doc.CurrentUserEmail;
+ new InkingControl();
+ (doc.optionalRightCollection === undefined) && CurrentUserUtils.setupMobileUploads(doc);
+ (doc.overlays === undefined) && CurrentUserUtils.setupOverlays(doc);
+ (doc.expandingButtons === undefined) && CurrentUserUtils.setupExpandingButtons(doc);
+ (doc.curPresentation === undefined) && CurrentUserUtils.setupDefaultPresentation(doc);
+ (doc.sidebarButtons === undefined) && CurrentUserUtils.setupSidebarButtons(doc);
+
+ // this is equivalent to using PrefetchProxies to make sure all the sidebarButtons and noteType internal Doc's have been retrieved.
+ PromiseValue(Cast(doc.noteTypes, Doc)).then(noteTypes => noteTypes && PromiseValue(noteTypes.data).then(DocListCast));
+ PromiseValue(Cast(doc.sidebarButtons, Doc)).then(stackingDoc => {
+ stackingDoc && PromiseValue(Cast(stackingDoc.data, listSpec(Doc))).then(sidebarButtons => {
+ sidebarButtons && sidebarButtons.map((sidebarBtn, i) => {
+ sidebarBtn && PromiseValue(Cast(sidebarBtn, Doc)).then(async btn => {
+ btn && btn.sourcePanel && btn.targetContainer && i === 1 && (btn.onClick as ScriptField).script.run({ this: btn });
+ });
+ });
+ });
+ });
+
+ // setup reactions to change the highlights on the undo/redo buttons -- would be better to encode this in the undo/redo buttons, but the undo/redo stacks are not wired up that way yet
+ doc.undoBtn && reaction(() => UndoManager.undoStack.slice(), () => Doc.GetProto(doc.undoBtn as Doc).opacity = UndoManager.CanUndo() ? 1 : 0.4, { fireImmediately: true });
+ doc.redoBtn && reaction(() => UndoManager.redoStack.slice(), () => Doc.GetProto(doc.redoBtn as Doc).opacity = UndoManager.CanRedo() ? 1 : 0.4, { fireImmediately: true });
+
+ return doc;
}
public static loadCurrentUser() {
@@ -135,14 +222,8 @@ export class CurrentUserUtils {
Doc.CurrentUserEmail = email;
await rp.get(Utils.prepend(RouteStore.getUserDocumentId)).then(id => {
if (id && id !== "guest") {
- return DocServer.GetRefField(id).then(async field => {
- if (field instanceof Doc) {
- await this.updateUserDocument(field);
- runInAction(() => Doc.SetUserDoc(field));
- } else {
- runInAction(() => Doc.SetUserDoc(this.createUserDocument(id)));
- }
- });
+ return DocServer.GetRefField(id).then(async field =>
+ Doc.SetUserDoc(await this.updateUserDocument(field instanceof Doc ? field : new Doc(id, true))));
} else {
throw new Error("There should be a user id! Why does Dash think there isn't one?");
}
diff --git a/src/server/database.ts b/src/server/database.ts
index d2375ebd9..db86b472d 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -4,6 +4,7 @@ import { Opt } from '../new_fields/Doc';
import { Utils, emptyFunction } from '../Utils';
import { DashUploadUtils } from './DashUploadUtils';
import { Credentials } from 'google-auth-library';
+import { GoogleApiServerUtils } from './apis/google/GoogleApiServerUtils';
export namespace Database {
@@ -131,7 +132,7 @@ export namespace Database {
}
}
- public getDocument(id: string, fn: (result?: Transferable) => void, collectionName = Database.DocumentsCollection) {
+ public getDocument(id: string, fn: (result?: Transferable) => void, collectionName = "newDocuments") {
if (this.db) {
this.db.collection(collectionName).findOne({ _id: id }, (err, result) => {
if (result) {
@@ -165,7 +166,7 @@ export namespace Database {
}
}
- public async visit(ids: string[], fn: (result: any) => string[], collectionName = "newDocuments"): Promise<void> {
+ public async visit(ids: string[], fn: (result: any) => string[] | Promise<string[]>, collectionName = "newDocuments"): Promise<void> {
if (this.db) {
const visited = new Set<string>();
while (ids.length) {
@@ -179,10 +180,9 @@ export namespace Database {
for (const doc of docs) {
const id = doc.id;
visited.add(id);
- ids.push(...fn(doc));
+ ids.push(...(await fn(doc)));
}
}
-
} else {
return new Promise(res => {
this.onConnect.push(() => {
@@ -260,8 +260,8 @@ export namespace Database {
return SanitizedSingletonQuery<StoredCredentials>({ userId }, GoogleAuthentication, removeId);
};
- export const Write = async (userId: string, token: any) => {
- return Instance.insert({ userId, canAccess: [], ...token }, GoogleAuthentication);
+ export const Write = async (userId: string, enrichedCredentials: GoogleApiServerUtils.EnrichedCredentials) => {
+ return Instance.insert({ userId, canAccess: [], ...enrichedCredentials }, GoogleAuthentication);
};
export const Update = async (userId: string, access_token: string, expiry_date: number) => {
diff --git a/src/server/index.ts b/src/server/index.ts
index b37822eec..428ebace7 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -53,6 +53,9 @@ import { DashUploadUtils } from './DashUploadUtils';
import { BatchedArray, TimeUnit } from 'array-batcher';
import { ParsedPDF } from "./PdfTypes";
import { reject } from 'bluebird';
+import { ExifData } from 'exif';
+import { Result } from '../client/northstar/model/idea/idea';
+import RouteSubscriber from './RouteSubscriber';
const download = (url: string, dest: fs.PathLike) => request.get(url).pipe(fs.createWriteStream(dest));
let youtubeApiKey: string;
@@ -104,6 +107,26 @@ enum Method {
POST
}
+export type ValidationHandler = (user: DashUserModel, req: express.Request, res: express.Response) => any | Promise<any>;
+export type RejectionHandler = (req: express.Request, res: express.Response) => any | Promise<any>;
+export type ErrorHandler = (req: express.Request, res: express.Response, error: any) => any | Promise<any>;
+
+const LoginRedirect: RejectionHandler = (_req, res) => res.redirect(RouteStore.login);
+
+export interface RouteInitializer {
+ method: Method;
+ subscribers: string | RouteSubscriber | (string | RouteSubscriber)[];
+ onValidation: ValidationHandler;
+ onRejection?: RejectionHandler;
+ onError?: ErrorHandler;
+}
+
+const isSharedDocAccess = (target: string) => {
+ const shared = qs.parse(qs.extract(target), { sort: false }).sharing === "true";
+ const docAccess = target.startsWith("/doc/");
+ return shared && docAccess;
+};
+
/**
* Please invoke this function when adding a new route to Dash's server.
* It ensures that any requests leading to or containing user-sensitive information
@@ -113,22 +136,40 @@ enum Method {
* @param onRejection an optional callback invoked on return if no user is found to be logged in
* @param subscribers the forward slash prepended path names (reference and add to RouteStore.ts) that will all invoke the given @param handler
*/
-function addSecureRoute(method: Method,
- handler: (user: DashUserModel, res: express.Response, req: express.Request) => void,
- onRejection: (res: express.Response, req: express.Request) => any = res => res.redirect(RouteStore.login),
- ...subscribers: string[]
-) {
- let abstracted = (req: express.Request, res: express.Response) => {
- let sharing = qs.parse(qs.extract(req.originalUrl), { sort: false }).sharing === "true";
- sharing = sharing && req.originalUrl.startsWith("/doc/");
- if (req.user || sharing) {
- handler(req.user as any, res, req);
+function addSecureRoute(initializer: RouteInitializer) {
+ const { method, subscribers, onValidation, onRejection, onError } = initializer;
+ let abstracted = async (req: express.Request, res: express.Response) => {
+ const { user, originalUrl: target } = req;
+ if (user || isSharedDocAccess(target)) {
+ try {
+ await onValidation(user as any, req, res);
+ } catch (e) {
+ if (onError) {
+ onError(req, res, e);
+ } else {
+ _error(res, `The server encountered an internal error handling ${target}.`, e);
+ }
+ }
} else {
- req.session!.target = req.originalUrl;
- onRejection(res, req);
+ req.session!.target = target;
+ try {
+ await (onRejection || LoginRedirect)(req, res);
+ } catch (e) {
+ if (onError) {
+ onError(req, res, e);
+ } else {
+ _error(res, `The server encountered an internal error when rejecting ${target}.`, e);
+ }
+ }
}
};
- subscribers.forEach(route => {
+ const subscribe = (subscriber: RouteSubscriber | string) => {
+ let route: string;
+ if (typeof subscriber === "string") {
+ route = subscriber;
+ } else {
+ route = subscriber.build;
+ }
switch (method) {
case Method.GET:
app.get(route, abstracted);
@@ -137,7 +178,12 @@ function addSecureRoute(method: Method,
app.post(route, abstracted);
break;
}
- });
+ };
+ if (Array.isArray(subscribers)) {
+ subscribers.forEach(subscribe);
+ } else {
+ subscribe(subscribers);
+ }
}
// STATIC FILE SERVING
@@ -211,7 +257,6 @@ const solrURL = "http://localhost:8983/solr/#/dash";
app.get("/textsearch", async (req, res) => {
let q = req.query.q;
- console.log("TEXTSEARCH " + q);
if (q === undefined) {
res.send([]);
return;
@@ -319,6 +364,80 @@ app.get("/serializeDoc/:docId", async (req, res) => {
res.send({ docs, files: Array.from(files) });
});
+export type Hierarchy = { [id: string]: string | Hierarchy };
+export type ZipMutator = (file: Archiver.Archiver) => void | Promise<void>;
+
+addSecureRoute({
+ method: Method.GET,
+ subscribers: new RouteSubscriber(RouteStore.imageHierarchyExport).add('docId'),
+ onValidation: async (_user, req, res) => {
+ const id = req.params.docId;
+ const hierarchy: Hierarchy = {};
+ await targetedVisitorRecursive(id, hierarchy);
+ BuildAndDispatchZip(res, async zip => {
+ await hierarchyTraverserRecursive(zip, hierarchy);
+ });
+ }
+});
+
+const BuildAndDispatchZip = async (res: Response, mutator: ZipMutator): Promise<void> => {
+ const zip = Archiver('zip');
+ zip.pipe(res);
+ await mutator(zip);
+ return zip.finalize();
+};
+
+const targetedVisitorRecursive = async (seedId: string, hierarchy: Hierarchy): Promise<void> => {
+ const local: Hierarchy = {};
+ const { title, data } = await getData(seedId);
+ const label = `${title} (${seedId})`;
+ if (Array.isArray(data)) {
+ hierarchy[label] = local;
+ await Promise.all(data.map(proxy => targetedVisitorRecursive(proxy.fieldId, local)));
+ } else {
+ hierarchy[label + path.extname(data)] = data;
+ }
+};
+
+const getData = async (seedId: string): Promise<{ data: string | any[], title: string }> => {
+ return new Promise<{ data: string | any[], title: string }>((resolve, reject) => {
+ Database.Instance.getDocument(seedId, async (result: any) => {
+ const { data, proto, title } = result.fields;
+ if (data) {
+ if (data.url) {
+ resolve({ data: data.url, title });
+ } else if (data.fields) {
+ resolve({ data: data.fields, title });
+ } else {
+ reject();
+ }
+ }
+ if (proto) {
+ getData(proto.fieldId).then(resolve, reject);
+ }
+ });
+ });
+};
+
+const hierarchyTraverserRecursive = async (file: Archiver.Archiver, hierarchy: Hierarchy, prefix = "Dash Export"): Promise<void> => {
+ for (const key of Object.keys(hierarchy)) {
+ const result = hierarchy[key];
+ if (typeof result === "string") {
+ let path: string;
+ let matches: RegExpExecArray | null;
+ if ((matches = /\:1050\/files\/(upload\_[\da-z]{32}.*)/g.exec(result)) !== null) {
+ path = `${__dirname}/public/files/${matches[1]}`;
+ } else {
+ const information = await DashUploadUtils.UploadImage(result);
+ path = information.mediaPaths[0];
+ }
+ file.file(path, { name: key, prefix });
+ } else {
+ await hierarchyTraverserRecursive(file, result, `${prefix}/${key}`);
+ }
+ }
+};
+
app.get("/downloadId/:docId", async (req, res) => {
res.set('Content-disposition', `attachment;`);
res.set('Content-Type', "application/zip");
@@ -505,50 +624,49 @@ function LoadPage(file: string, pageNumber: number, res: Response) {
});
}
-// anyone attempting to navigate to localhost at this port will
-// first have to login
-addSecureRoute(
- Method.GET,
- (user, res) => res.redirect(RouteStore.home),
- undefined,
- RouteStore.root
-);
-
-addSecureRoute(
- Method.GET,
- async (_, res) => {
+/**
+ * Anyone attempting to navigate to localhost at this port will
+ * first have to log in.
+ */
+addSecureRoute({
+ method: Method.GET,
+ subscribers: RouteStore.root,
+ onValidation: (_user, _req, res) => res.redirect(RouteStore.home)
+});
+
+addSecureRoute({
+ method: Method.GET,
+ subscribers: RouteStore.getUsers,
+ onValidation: async (_user, _req, res) => {
const cursor = await Database.Instance.query({}, { email: 1, userDocumentId: 1 }, "users");
const results = await cursor.toArray();
res.send(results.map(user => ({ email: user.email, userDocumentId: user.userDocumentId })));
},
- undefined,
- RouteStore.getUsers
-);
+});
-addSecureRoute(
- Method.GET,
- (user, res, req) => {
+addSecureRoute({
+ method: Method.GET,
+ subscribers: [RouteStore.home, RouteStore.openDocumentWithId],
+ onValidation: (_user, req, res) => {
let detector = new mobileDetect(req.headers['user-agent'] || "");
let filename = detector.mobile() !== null ? 'mobile/image.html' : 'index.html';
res.sendFile(path.join(__dirname, '../../deploy/' + filename));
},
- undefined,
- RouteStore.home, RouteStore.openDocumentWithId
-);
-
-addSecureRoute(
- Method.GET,
- (user, res) => res.send(user.userDocumentId),
- (res) => res.send(undefined),
- RouteStore.getUserDocumentId,
-);
-
-addSecureRoute(
- Method.GET,
- (user, res) => { res.send(JSON.stringify({ id: user.id, email: user.email })); },
- (res) => res.send(JSON.stringify({ id: "__guest__", email: "" })),
- RouteStore.getCurrUser
-);
+});
+
+addSecureRoute({
+ method: Method.GET,
+ subscribers: RouteStore.getUserDocumentId,
+ onValidation: (user, _req, res) => res.send(user.userDocumentId),
+ onRejection: (_req, res) => res.send(undefined)
+});
+
+addSecureRoute({
+ method: Method.GET,
+ subscribers: RouteStore.getCurrUser,
+ onValidation: (user, _req, res) => { res.send(JSON.stringify(user)); },
+ onRejection: (_req, res) => res.send(JSON.stringify({ id: "__guest__", email: "" }))
+});
const ServicesApiKeyMap = new Map<string, string | undefined>([
["face", process.env.FACE],
@@ -556,10 +674,14 @@ const ServicesApiKeyMap = new Map<string, string | undefined>([
["handwriting", process.env.HANDWRITING]
]);
-addSecureRoute(Method.GET, (user, res, req) => {
- let service = req.params.requestedservice;
- res.send(ServicesApiKeyMap.get(service));
-}, undefined, `${RouteStore.cognitiveServices}/:requestedservice`);
+addSecureRoute({
+ method: Method.GET,
+ subscribers: new RouteSubscriber(RouteStore.cognitiveServices).add('requestedservice'),
+ onValidation: (_user, req, res) => {
+ let service = req.params.requestedservice;
+ res.send(ServicesApiKeyMap.get(service));
+ }
+});
class NodeCanvasFactory {
create = (width: number, height: number) => {
@@ -590,24 +712,26 @@ const uploadDirectory = __dirname + "/public/files/";
const pdfDirectory = uploadDirectory + "text";
DashUploadUtils.createIfNotExists(pdfDirectory);
-interface FileResponse {
+interface ImageFileResponse {
name: string;
path: string;
type: string;
+ exif: Opt<DashUploadUtils.EnrichedExifData>;
}
-// SETTERS
-app.post(
- RouteStore.upload,
- (req, res) => {
+addSecureRoute({
+ method: Method.POST,
+ subscribers: RouteStore.upload,
+ onValidation: (_user, req, res) => {
let form = new formidable.IncomingForm();
form.uploadDir = uploadDirectory;
form.keepExtensions = true;
form.parse(req, async (_err, _fields, files) => {
- let results: FileResponse[] = [];
+ let results: ImageFileResponse[] = [];
for (const key in files) {
const { type, path: location, name } = files[key];
const filename = path.basename(location);
+ let uploadInformation: Opt<DashUploadUtils.UploadInformation>;
if (filename.endsWith(".pdf")) {
let dataBuffer = fs.readFileSync(uploadDirectory + filename);
const result: ParsedPDF = await pdf(dataBuffer);
@@ -621,20 +745,37 @@ app.post(
}
});
});
+ } else if (type.indexOf("audio") !== -1) {
+ // nothing to be done yet-- although transcribing the audio a la pdfs would make sense.
} else {
- await DashUploadUtils.UploadImage(uploadDirectory + filename, filename).catch(() => console.log(`Unable to process ${filename}`));
+ uploadInformation = await DashUploadUtils.UploadImage(uploadDirectory + filename, filename);
}
- results.push({ name, type, path: `/files/${filename}` });
+ const exif = uploadInformation ? uploadInformation.exifData : undefined;
+ results.push({ name, type, path: `/files/${filename}`, exif });
}
_success(res, results);
});
}
-);
+});
-addSecureRoute(
- Method.POST,
- (user, res, req) => {
+addSecureRoute({
+ method: Method.POST,
+ subscribers: RouteStore.inspectImage,
+ onValidation: async (_user, req, res) => {
+ const { source } = req.body;
+ if (typeof source === "string") {
+ const uploadInformation = await DashUploadUtils.UploadImage(source);
+ return res.send(await DashUploadUtils.InspectImage(uploadInformation.mediaPaths[0]));
+ }
+ res.send({});
+ }
+});
+
+addSecureRoute({
+ method: Method.POST,
+ subscribers: RouteStore.dataUriToImage,
+ onValidation: (_user, req, res) => {
const uri = req.body.uri;
const filename = req.body.name;
if (!uri || !filename) {
@@ -667,10 +808,9 @@ addSecureRoute(
}
res.send("/files/" + filename + ext);
});
- },
- undefined,
- RouteStore.dataUriToImage
-);
+ }
+});
+
// AUTHENTICATION
// Sign Up
@@ -709,29 +849,27 @@ app.use(RouteStore.corsProxy, (req, res) => {
}).pipe(res);
});
-addSecureRoute(
- Method.GET,
- (user, res, req) => {
+addSecureRoute({
+ method: Method.GET,
+ subscribers: RouteStore.delete,
+ onValidation: (_user, _req, res) => {
if (release) {
return _permission_denied(res, deletionPermissionError);
}
deleteFields().then(() => res.redirect(RouteStore.home));
- },
- undefined,
- RouteStore.delete
-);
+ }
+});
-addSecureRoute(
- Method.GET,
- (_user, res, _req) => {
+addSecureRoute({
+ method: Method.GET,
+ subscribers: RouteStore.deleteAll,
+ onValidation: (_user, _req, res) => {
if (release) {
return _permission_denied(res, deletionPermissionError);
}
deleteAll().then(() => res.redirect(RouteStore.home));
- },
- undefined,
- RouteStore.deleteAll
-);
+ }
+});
app.use(wdm(compiler, { publicPath: config.output.publicPath }));
@@ -910,7 +1048,29 @@ app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => {
});
});
-app.get(RouteStore.googlePhotosAccessToken, (req, res) => GoogleApiServerUtils.RetrieveAccessToken({ credentialsPath, userId: req.header("userId")! }).then(token => res.send(token)));
+addSecureRoute({
+ method: Method.GET,
+ subscribers: RouteStore.readGoogleAccessToken,
+ onValidation: async (user, _req, res) => {
+ const userId = user.id;
+ const token = await Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId);
+ const information = { credentialsPath, userId };
+ if (!token) {
+ return res.send(await GoogleApiServerUtils.GenerateAuthenticationUrl(information));
+ }
+ GoogleApiServerUtils.RetrieveAccessToken(information).then(token => res.send(token));
+ }
+});
+
+addSecureRoute({
+ method: Method.POST,
+ subscribers: RouteStore.writeGoogleAccessToken,
+ onValidation: async (user, req, res) => {
+ const userId = user.id;
+ const information = { credentialsPath, userId };
+ res.send(await GoogleApiServerUtils.ProcessClientSideCode(information, req.body.authenticationCode));
+ }
+});
const tokenError = "Unable to successfully upload bytes for all images!";
const mediaError = "Unable to convert all uploaded bytes to media items!";
@@ -923,45 +1083,50 @@ export interface NewMediaItem {
};
}
-app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => {
- const { media } = req.body;
- const userId = req.header("userId");
-
- if (!userId) {
- return _error(res, userIdError);
- }
-
- await GooglePhotosUploadUtils.initialize({ credentialsPath, userId });
-
- let failed = 0;
+addSecureRoute({
+ method: Method.POST,
+ subscribers: RouteStore.googlePhotosMediaUpload,
+ onValidation: async (user, req, res) => {
+ const { media } = req.body;
+ const userId = user.id;
+ if (!userId) {
+ return _error(res, userIdError);
+ }
- const newMediaItems = await BatchedArray.from<GooglePhotosUploadUtils.MediaInput>(media, { batchSize: 25 }).batchedMapPatientInterval(
- { magnitude: 100, unit: TimeUnit.Milliseconds },
- async (batch: GooglePhotosUploadUtils.MediaInput[]) => {
- const newMediaItems: NewMediaItem[] = [];
- for (let element of batch) {
- const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(element.url);
- if (!uploadToken) {
- failed++;
- } else {
- newMediaItems.push({
- description: element.description,
- simpleMediaItem: { uploadToken }
- });
+ await GooglePhotosUploadUtils.initialize({ credentialsPath, userId });
+
+ let failed: number[] = [];
+
+ const newMediaItems = await BatchedArray.from<GooglePhotosUploadUtils.MediaInput>(media, { batchSize: 25 }).batchedMapPatientInterval(
+ { magnitude: 100, unit: TimeUnit.Milliseconds },
+ async (batch: GooglePhotosUploadUtils.MediaInput[]) => {
+ const newMediaItems: NewMediaItem[] = [];
+ for (let index = 0; index < batch.length; index++) {
+ const element = batch[index];
+ const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(element.url);
+ if (!uploadToken) {
+ failed.push(index);
+ } else {
+ newMediaItems.push({
+ description: element.description,
+ simpleMediaItem: { uploadToken }
+ });
+ }
}
+ return newMediaItems;
}
- return newMediaItems;
+ );
+
+ const failedCount = failed.length;
+ if (failedCount) {
+ console.error(`Unable to upload ${failedCount} image${failedCount === 1 ? "" : "s"} to Google's servers`);
}
- );
- if (failed) {
- return _error(res, tokenError);
+ GooglePhotosUploadUtils.CreateMediaItems(newMediaItems, req.body.album).then(
+ result => _success(res, { results: result.newMediaItemResults, failed }),
+ error => _error(res, mediaError, error)
+ );
}
-
- GooglePhotosUploadUtils.CreateMediaItems(newMediaItems, req.body.album).then(
- result => _success(res, result.newMediaItemResults),
- error => _error(res, mediaError, error)
- );
});
interface MediaItem {