aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--package.json4
-rw-r--r--src/Utils.ts26
-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/documents/DocumentTypes.ts6
-rw-r--r--src/client/documents/Documents.ts58
-rw-r--r--src/client/util/DictationManager.ts22
-rw-r--r--src/client/util/DocumentManager.ts68
-rw-r--r--src/client/util/DragManager.ts18
-rw-r--r--src/client/util/Import & Export/DirectoryImportBox.tsx12
-rw-r--r--src/client/util/Import & Export/ImageUtils.ts31
-rw-r--r--src/client/util/ProsemirrorExampleTransfer.ts4
-rw-r--r--src/client/util/RichTextRules.ts78
-rw-r--r--src/client/util/RichTextSchema.tsx235
-rw-r--r--src/client/util/SearchUtil.ts35
-rw-r--r--src/client/util/SelectionManager.ts2
-rw-r--r--src/client/util/SharingManager.scss4
-rw-r--r--src/client/util/SharingManager.tsx137
-rw-r--r--src/client/util/TooltipTextMenu.tsx85
-rw-r--r--src/client/views/CollectionLinearView.scss77
-rw-r--r--src/client/views/CollectionLinearView.tsx115
-rw-r--r--src/client/views/DictationOverlay.tsx71
-rw-r--r--src/client/views/DocComponent.tsx51
-rw-r--r--src/client/views/DocumentButtonBar.tsx3
-rw-r--r--src/client/views/DocumentDecorations.tsx49
-rw-r--r--src/client/views/EditableView.tsx6
-rw-r--r--src/client/views/GlobalKeyHandler.ts35
-rw-r--r--src/client/views/InkingCanvas.tsx6
-rw-r--r--src/client/views/InkingControl.tsx6
-rw-r--r--src/client/views/Main.scss206
-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.scss97
-rw-r--r--src/client/views/MainView.tsx567
-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.tsx11
-rw-r--r--src/client/views/PreviewCursor.tsx18
-rw-r--r--src/client/views/collections/CollectionBaseView.tsx21
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx17
-rw-r--r--src/client/views/collections/CollectionMasonryViewFieldRow.tsx396
-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.tsx8
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx12
-rw-r--r--src/client/views/collections/CollectionStackingView.scss77
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx157
-rw-r--r--src/client/views/collections/CollectionStackingViewFieldColumn.tsx160
-rw-r--r--src/client/views/collections/CollectionSubView.tsx29
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx39
-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.tsx4
-rw-r--r--src/client/views/collections/CollectionViewChromes.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss6
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx33
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.scss7
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx20
-rw-r--r--src/client/views/linking/LinkFollowBox.tsx84
-rw-r--r--src/client/views/nodes/Annotation.tsx117
-rw-r--r--src/client/views/nodes/ButtonBox.scss2
-rw-r--r--src/client/views/nodes/ButtonBox.tsx3
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.scss2
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx14
-rw-r--r--src/client/views/nodes/ColorBox.scss10
-rw-r--r--src/client/views/nodes/ColorBox.tsx16
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx12
-rw-r--r--src/client/views/nodes/DocumentView.scss1
-rw-r--r--src/client/views/nodes/DocumentView.tsx67
-rw-r--r--src/client/views/nodes/DragBox.tsx99
-rw-r--r--src/client/views/nodes/FieldView.tsx6
-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.tsx21
-rw-r--r--src/client/views/nodes/FormattedTextBox.scss29
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx486
-rw-r--r--src/client/views/nodes/FormattedTextBoxComment.tsx10
-rw-r--r--src/client/views/nodes/IconBox.tsx4
-rw-r--r--src/client/views/nodes/ImageBox.scss12
-rw-r--r--src/client/views/nodes/ImageBox.tsx46
-rw-r--r--src/client/views/nodes/PDFBox.tsx66
-rw-r--r--src/client/views/nodes/PresBox.tsx9
-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.scss55
-rw-r--r--src/client/views/nodes/VideoBox.tsx183
-rw-r--r--src/client/views/nodes/WebBox.tsx71
-rw-r--r--src/client/views/pdf/Annotation.scss3
-rw-r--r--src/client/views/pdf/Annotation.tsx20
-rw-r--r--src/client/views/pdf/PDFMenu.tsx9
-rw-r--r--src/client/views/pdf/PDFViewer.scss27
-rw-r--r--src/client/views/pdf/PDFViewer.tsx305
-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.scss17
-rw-r--r--src/client/views/search/SearchBox.tsx4
-rw-r--r--src/client/views/search/SearchItem.scss50
-rw-r--r--src/client/views/search/SearchItem.tsx38
-rw-r--r--src/new_fields/Doc.ts28
-rw-r--r--src/new_fields/InkField.ts2
-rw-r--r--src/new_fields/SchemaHeaderField.ts3
-rw-r--r--src/server/DashUploadUtils.ts39
-rw-r--r--src/server/RouteStore.ts5
-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.ts86
-rw-r--r--src/server/database.ts12
-rw-r--r--src/server/index.ts123
113 files changed, 3564 insertions, 2612 deletions
diff --git a/package.json b/package.json
index 9d12f51b0..8cbbb84af 100644
--- a/package.json
+++ b/package.json
@@ -65,6 +65,7 @@
"@types/cookie-session": "^2.0.36",
"@types/d3-format": "^1.3.1",
"@types/dotenv": "^6.1.1",
+ "@types/exif": "^0.6.0",
"@types/express": "^4.16.1",
"@types/express-flash": "0.0.0",
"@types/express-session": "^1.15.12",
@@ -132,6 +133,7 @@
"crypto-browserify": "^3.11.0",
"d3-format": "^1.3.2",
"dotenv": "^8.0.0",
+ "exif": "^0.6.0",
"express": "^4.16.4",
"express-flash": "0.0.2",
"express-session": "^1.15.6",
@@ -167,7 +169,7 @@
"nodemailer": "^5.1.1",
"nodemon": "^1.18.10",
"normalize.css": "^8.0.1",
- "npm": "^6.11.3",
+ "npm": "^6.12.0",
"p-limit": "^2.2.0",
"passport": "^0.4.0",
"passport-google-oauth20": "^2.0.0",
diff --git a/src/Utils.ts b/src/Utils.ts
index ca134e165..c9d198fd3 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -322,7 +322,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();
@@ -339,4 +339,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/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index e5d5885cd..432e53825 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -17,8 +17,10 @@ export enum DocumentType {
TEMPLATE = "template",
EXTENSION = "extension",
YOUTUBE = "youtube",
- DRAGBOX = "dragbox",
+ FONTICONBOX = "fonticonbox",
PRES = "presentation",
LINKFOLLOW = "linkfollow",
- PRESELEMENT = "preselement"
+ PRESELEMENT = "preselement",
+ QUERY = "search",
+ COLOR = "color",
} \ No newline at end of file
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 2d323ea4b..1f89d2993 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1,7 +1,6 @@
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 { AudioBox } from "../views/nodes/AudioBox";
@@ -38,7 +37,7 @@ 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";
@@ -46,6 +45,8 @@ import { ProxyField } from "../../new_fields/Proxy";
import { DocumentType } from "./DocumentTypes";
import { LinkFollowBox } from "../views/linking/LinkFollowBox";
import { PresElementBox } from "../views/presentationview/PresElementBox";
+import { QueryBox } from "../views/nodes/QueryBox";
+import { ColorBox } from "../views/nodes/ColorBox";
var requestImageSize = require('../util/request-image-size');
var path = require('path');
@@ -63,23 +64,31 @@ export interface DocumentOptions {
panY?: number;
page?: number;
scale?: number;
+ fitWidth?: boolean;
layout?: string | Doc;
isTemplate?: 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;
documentText?: string;
borderRounding?: string;
+ boxShadow?: string;
schemaColumns?: List<SchemaHeaderField>;
dockingConfig?: string;
autoHeight?: boolean;
dbDoc?: Doc;
+ icon?: string;
// [key: string]: Opt<Field>;
}
@@ -118,12 +127,20 @@ export namespace Docs {
layout: { view: HistogramBox, collectionView: [CollectionView, data] as CollectionViewType },
options: { height: 300, backgroundColor: "black" }
}],
+ [DocumentType.QUERY, {
+ layout: { view: QueryBox },
+ options: { width: 400 }
+ }],
+ [DocumentType.COLOR, {
+ layout: { view: ColorBox },
+ options: { nativeWidth: 220, nativeHeight: 300 }
+ }],
[DocumentType.IMG, {
- layout: { view: ImageBox, collectionView: [CollectionView, data, anno] as CollectionViewType },
- options: { curPage: 0 }
+ layout: { view: ImageBox, ext: anno },
+ options: {}
}],
[DocumentType.WEB, {
- layout: { view: WebBox, collectionView: [CollectionView, data, anno] as CollectionViewType },
+ layout: { view: WebBox, ext: anno },
options: { height: 300 }
}],
[DocumentType.COL, {
@@ -135,8 +152,8 @@ export namespace Docs {
options: { height: 150 }
}],
[DocumentType.VID, {
- layout: { view: VideoBox, collectionView: [CollectionVideoView, data, anno] as CollectionViewType },
- options: { curPage: 0 },
+ layout: { view: VideoBox, ext: anno },
+ options: { currentTimecode: 0 },
}],
[DocumentType.AUDIO, {
layout: { view: AudioBox },
@@ -168,8 +185,8 @@ export namespace Docs {
layout: { view: PresBox },
options: {}
}],
- [DocumentType.DRAGBOX, {
- layout: { view: DragBox },
+ [DocumentType.FONTICONBOX, {
+ layout: { view: FontIconBox },
options: { width: 40, height: 40 },
}],
[DocumentType.LINKFOLLOW, {
@@ -254,7 +271,7 @@ 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 options = { title, type, baseProto: true, ...defaultOptions, ...(template.options || {}) };
let primary = layout.view.LayoutString(layout.ext);
let collectionView = layout.collectionView;
if (collectionView) {
@@ -373,6 +390,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);
}
@@ -453,8 +478,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.FONTICONBOX), undefined, { ...(options || {}) });
}
export function LinkFollowBoxDocument(options?: DocumentOptions) {
@@ -653,7 +678,7 @@ export namespace DocUtils {
}
});
}
- export function MakeLink(source: {doc:Doc,ctx?:Doc}, target: {doc:Doc,ctx?:Doc}, title: string = "", description: string = "", id?: string, anchored1?: boolean) {
+ 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;
@@ -662,16 +687,17 @@ export namespace DocUtils {
UndoManager.RunInBatch(() => {
linkDocProto.type = DocumentType.LINK;
- linkDocProto.targetContext = target.ctx;
- linkDocProto.sourceContext = source.ctx;
linkDocProto.title = title === "" ? source.doc.title + " to " + target.doc.title : title;
linkDocProto.linkDescription = description;
linkDocProto.anchor1 = source.doc;
+ linkDocProto.anchor1Context = source.ctx;
+ linkDocProto.anchor1Timecode = source.doc.currentTimecode;
linkDocProto.anchor1Groups = new List<Doc>([]);
- linkDocProto.anchor1anchored = anchored1;
linkDocProto.anchor2 = target.doc;
+ linkDocProto.anchor2Context = target.ctx;
linkDocProto.anchor2Groups = new List<Doc>([]);
+ linkDocProto.anchor2Timecode = target.doc.currentTimecode;
LinkManager.Instance.addLink(linkDocProto);
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index cebb56bbe..182cfb70a 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -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
@@ -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();
@@ -336,7 +335,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 4ebcdf83c..00de39671 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,16 +1,13 @@
import { action, computed, observable } from 'mobx';
import { Doc, DocListCastAsync } from '../../new_fields/Doc';
import { Id } from '../../new_fields/FieldSymbols';
+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';
@@ -56,7 +53,7 @@ export class DocumentManager {
return this.getDocumentViewsById(doc[Id]);
}
- public getDocumentViewById(id: string, preferredCollection?: CollectionView | CollectionPDFView | CollectionVideoView): DocumentView | undefined {
+ public getDocumentViewById(id: string, preferredCollection?: CollectionView): DocumentView | undefined {
let toReturn: DocumentView | undefined;
let passes = preferredCollection ? [preferredCollection, undefined] : [undefined];
@@ -81,7 +78,7 @@ export class DocumentManager {
return toReturn;
}
- public getDocumentView(toFind: Doc, preferredCollection?: CollectionView | CollectionPDFView | CollectionVideoView): DocumentView | undefined {
+ public getDocumentView(toFind: Doc, preferredCollection?: CollectionView): DocumentView | undefined {
return this.getDocumentViewById(toFind[Id], preferredCollection);
}
@@ -131,12 +128,20 @@ export class DocumentManager {
return pairs;
}
- public jumpToDocument = async (targetDoc: Doc, willZoom: boolean, dockFunc?: (doc: Doc) => void, docContext?: Doc, closeContextIfNotFound: boolean = false): Promise<void> => {
+
+
+ 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(targetDoc, willZoom);
+ docView.props.focus(docView.props.Document, willZoom);
+ highlight();
} else {
const contextDocs = docContext ? await DocListCastAsync(docContext.data) : undefined;
const contextDoc = contextDocs && contextDocs.find(doc => Doc.AreProtosEqual(doc, targetDoc)) ? docContext : undefined;
@@ -144,11 +149,12 @@ export class DocumentManager {
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 {
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";
- targetDocContext.scrollY = 0;
targetDocContextView.props.focus(targetDocContextView.props.Document, willZoom);
// now find the target document within the context
@@ -158,40 +164,42 @@ export class DocumentManager {
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);
- (dockFunc || CollectionDockingView.AddRightSplit)(Doc.BrushDoc(Doc.MakeAlias(targetDoc)), undefined); // otherwise create a new view of the target
+ targetDoc.layout && (dockFunc || CollectionDockingView.AddRightSplit)(Doc.BrushDoc(Doc.MakeAlias(targetDoc)), undefined); // otherwise create a new view of the target
}
+ highlight();
}, 0);
} else { // there's no context view so we need to create one first and try again
- targetDocContext.scrollY = 0;
(dockFunc || CollectionDockingView.AddRightSplit)(targetDocContext, undefined);
setTimeout(() => {
- const foundTargetDocContextView = DocumentManager.Instance.getDocumentView(targetDocContext);
- if (foundTargetDocContextView) { // we should always find a target context here....
- this.jumpToDocument(targetDoc, willZoom, dockFunc, undefined, true); // so call jump to doc again and if the doc isn't found, it will be created.
- }
- }, 2000); // the long timeout gives the context view a chance to create its children. think pdf's which need to be activated to render their annotations.
+ 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(doc: Doc, focus: (doc: Doc, maxLocation: string) => void, zoom: boolean = false, reverse: boolean = false, currentContext?: Doc) {
- let linkDocs = LinkManager.Instance.getAllRelatedLinks(doc);
+ 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();
- let firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor1 as Doc, doc) && !linkDoc.anchor1anchored);
- let secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor2 as Doc, doc) && !linkDoc.anchor2anchored);
+ 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);
- let first = firstDocWithoutView ? [firstDocWithoutView] : firstDocs;
- let second = secondDocWithoutView ? [secondDocWithoutView] : secondDocs;
- let linkFollowDocs = first.length ? [first[0].anchor2 as Doc, first[0].anchor1 as Doc] : second.length ? [second[0].anchor1 as Doc, second[0].anchor2 as Doc] : undefined;
- let linkFollowDocContexts = first.length ? [await (first[0].targetContext) as Doc, await (first[0].sourceContext) as Doc] : second.length ? [await (second[0].sourceContext) as Doc, await (second[0].targetContext) as Doc] : [undefined, undefined];
- if (linkFollowDocs && !linkFollowDocs.some(l => l instanceof Promise)) {
- let maxLocation = StrCast(linkFollowDocs[0].maximizeLocation, "inTab");
- let targetContext = !Doc.AreProtosEqual(linkFollowDocContexts[reverse ? 1 : 0], currentContext) ? linkFollowDocContexts[reverse ? 1 : 0] : undefined;
- DocumentManager.Instance.jumpToDocument(linkFollowDocs[reverse ? 1 : 0], zoom,
- // 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) => focus(doc, maxLocation), targetContext);
+ 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]);
}
}
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index ddc8fb62c..2c316ccdf 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,10 @@ 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";
export type dropActionType = "alias" | "copy" | undefined;
export function SetupDrag(
@@ -234,16 +234,18 @@ export namespace DragManager {
export let StartDragFunctions: (() => void)[] = [];
- export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
+ export async function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
runInAction(() => StartDragFunctions.map(func => func()));
+ await dragData.draggedDocuments.map(d => d.dragFactory);
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 => 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));
});
}
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index d3f81b992..d74b51993 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
@@ -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/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 49bd93942..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,62 +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);
- return;
- }
- }
- if (targetContext) {
- DocumentManager.Instance.jumpToDocument(targetContext, e.ctrlKey, document => addDocTab(document, undefined, location ? location : "inTab"));
- } else if (jumpToDoc) {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, e.ctrlKey, document => addDocTab(document, undefined, location ? location : "inTab"));
- } else {
- DocumentManager.Instance.jumpToDocument(linkDoc, e.ctrlKey, 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;
}
@@ -687,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
@@ -726,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";
@@ -740,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..df1b46b33 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,7 +41,6 @@ export namespace SelectionManager {
DeselectAll(): void {
manager.SelectedDocuments.map(dv => dv.props.whenActiveChanged(false));
manager.SelectedDocuments = [];
- FormattedTextBox.InputBoxOverlay = undefined;
}
}
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 1cde2aa8e..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,9 +182,10 @@ 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, context.props.Document);
}
@@ -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..c82d3bc63 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.TextBar && DocumentDecorations.Instance.setTextBar(DocumentDecorations.Instance.TextBar);
+ 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/views/CollectionLinearView.scss b/src/client/views/CollectionLinearView.scss
new file mode 100644
index 000000000..46a226eef
--- /dev/null
+++ b/src/client/views/CollectionLinearView.scss
@@ -0,0 +1,77 @@
+@import "globalCssVariables";
+@import "nodeModuleOverrides";
+
+.collectionLinearView-outer{
+ overflow: hidden;
+ height:100%;
+ padding:5px;
+}
+.collectionLinearView {
+ display:flex;
+ >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;
+ width: 500px;
+ height: 500px;
+ }
+
+ 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;
+ padding: 0;
+ position: relative;
+ .collectionFreeFormDocumentView-container {
+ position: relative;
+ }
+ .collectionLinearView-docBtn {
+ position:relative;
+ margin-right: 10px;
+ }
+ .collectionLinearView-round-button {
+ width: 36px;
+ height: 36px;
+ border-radius: 18px;
+ font-size: 15px;
+ }
+
+ .collectionLinearView-round-button:hover {
+ transform: scale(1.15);
+ }
+
+ }
+
+ .collectionLinearView-add-button {
+ position: relative;
+ margin-right: 10px;
+ }
+}
diff --git a/src/client/views/CollectionLinearView.tsx b/src/client/views/CollectionLinearView.tsx
new file mode 100644
index 000000000..692f940f8
--- /dev/null
+++ b/src/client/views/CollectionLinearView.tsx
@@ -0,0 +1,115 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, observable, computed } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { Doc, DocListCast, Opt } from '../../new_fields/Doc';
+import { InkTool } from '../../new_fields/InkField';
+import { ObjectField } from '../../new_fields/ObjectField';
+import { ScriptField } from '../../new_fields/ScriptField';
+import { NumCast, StrCast } from '../../new_fields/Types';
+import { emptyFunction, returnEmptyString, returnOne, returnTrue, returnFalse, Utils } from '../../Utils';
+import { Docs } from '../documents/Documents';
+import { DragManager } from '../util/DragManager';
+import { Transform } from '../util/Transform';
+import { UndoManager } from '../util/UndoManager';
+import { InkingControl } from './InkingControl';
+import { DocumentView, documentSchema } from './nodes/DocumentView';
+import "./CollectionLinearView.scss";
+import { makeInterface } from '../../new_fields/Schema';
+import { CollectionSubView } from './collections/CollectionSubView';
+
+
+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;
+
+ componentWillUnmount() {
+ this._dropDisposer && this._dropDisposer();
+ }
+
+ 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) } });
+ }
+ }
+
+ drop = action((e: Event, de: DragManager.DropEvent) => {
+ (de.data as DragManager.DocumentDragData).draggedDocuments.map((doc, i) => {
+ let dbox = doc;
+ if (!doc.onDragStart && this.props.Document.convertToButtons) {
+ dbox = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, backgroundColor: StrCast(doc.backgroundColor), title: "Custom", icon: "bolt" });
+ dbox.dragFactory = doc;
+ dbox.removeDropProperties = doc.removeDropProperties instanceof ObjectField ? ObjectField.MakeCopy(doc.removeDropProperties) : undefined;
+ dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)');
+ }
+ (de.data as DragManager.DocumentDragData).droppedDocuments[i] = dbox;
+ });
+ e.stopPropagation();
+ return super.drop(e, de);
+ });
+
+ 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 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) - 5;
+ render() {
+ let guid = Utils.GenerateGuid();
+ return <div className="collectionLinearView-outer"><div className="collectionLinearView" ref={this.createDropTarget} >
+ <input id={`${guid}`} type="checkbox" ref={this.addMenuToggle} />
+ <label htmlFor={`${guid}`} style={{ marginTop: (this.dimension() - 36) / 2, marginBottom: "auto" }} title="Close Menu"><p>+</p></label>
+
+ <div className="collectionLinearView-content">
+ {this.props.showHiddenControls ? <button key="undo" className="collectionLinearView-add-button collectionLinearView-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> : (null)}
+ {this.props.showHiddenControls ? <button key="redo" className="collectionLinearView-add-button collectionLinearView-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> : (null)}
+
+ {this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair =>
+ <div className="collectionLinearView-docBtn" style={{ width: this.dimension(), height: this.dimension() }} key={StrCast(pair.layout.title)} >
+ <DocumentView
+ Document={pair.layout}
+ DataDoc={pair.data}
+ addDocument={this.props.addDocument}
+ addDocTab={returnFalse}
+ pinToPres={emptyFunction}
+ removeDocument={this.props.removeDocument}
+ ruleProvider={undefined}
+ onClick={undefined}
+ ScreenToLocalTransform={Transform.Identity}
+ ContentScaling={() => this.dimension() / (10 + NumCast(pair.layout.nativeWidth, this.dimension()))} // ugh - need to get rid of this inline function to avoid recomputing
+ PanelWidth={this.dimension}
+ PanelHeight={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> */}
+ {this.props.showHiddenControls ? <>
+ <button className="collectionLinearView-toolbar-button collectionLinearView-round-button" title="Ink" onClick={() => InkingControl.Instance.toggleDisplay()}><FontAwesomeIcon icon="pen-nib" size="sm" /> </button>
+ <button key="pen" onClick={() => InkingControl.Instance.switchTool(InkTool.Pen)} title="Pen" style={this.selected(InkTool.Pen)}><FontAwesomeIcon icon="pen" size="lg" /></button>
+ <button key="marker" onClick={() => InkingControl.Instance.switchTool(InkTool.Highlighter)} title="Highlighter" style={this.selected(InkTool.Highlighter)}><FontAwesomeIcon icon="highlighter" size="lg" /></button>
+ <button key="eraser" onClick={() => InkingControl.Instance.switchTool(InkTool.Eraser)} title="Eraser" style={this.selected(InkTool.Eraser)}><FontAwesomeIcon icon="eraser" size="lg" /></button>
+ <InkingControl />
+ </> : (null)}
+ </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 6d51122d4..0ed443a99 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -1,7 +1,9 @@
import * as React from 'react';
import { Doc } from '../../new_fields/Doc';
-import { computed } from 'mobx';
import { Touchable } from './Touchable';
+import { computed, action } from 'mobx';
+import { Cast } from '../../new_fields/Types';
+import { listSpec } from '../../new_fields/Schema';
export function DocComponent<P extends { Document: Doc }, T>(schemaCtor: (doc: Doc) => T) {
class Component extends Touchable<P> {
@@ -12,4 +14,51 @@ export function DocComponent<P extends { Document: Doc }, T>(schemaCtor: (doc: D
}
}
return Component;
+}
+
+interface DocAnnotatableProps {
+ Document: Doc;
+ DataDoc?: Doc;
+ fieldKey: string;
+ fieldExt: 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> {
+ //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);
+ }
+ _isChildActive = false;
+ @action.bound
+ removeDocument(doc: Doc): boolean {
+ Doc.GetProto(doc).annotationOn = undefined;
+ let value = Cast(this.extensionDoc[this.props.fieldExt], listSpec(Doc), []);
+ let index = value ? Doc.IndexOf(doc, value.map(d => d as Doc), true) : -1;
+ return index !== -1 && value.splice(index, 1) ? true : false;
+ }
+
+ @computed get dataDoc() { return (this.props.DataDoc && this.props.Document.isTemplate ? this.props.DataDoc : Doc.GetProto(this.props.Document)) as Doc; }
+
+ @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
+
+ // 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 Doc.AddDocToList(this.extensionDoc, this.props.fieldExt, doc);
+ }
+ whenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive);
+ active = () => this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0;
+ }
+ return Component;
} \ No newline at end of file
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 338f6b83e..9e2d41621 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -140,7 +140,6 @@ 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");
DragManager.StartLinkDrag(this._linkerButton.current, dragData, e.pageX, e.pageY, {
handlers: {
@@ -203,7 +202,7 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
considerEmbed = () => {
let thisDoc = this.props.views[0].props.Document;
let canEmbed = thisDoc.data && thisDoc.data instanceof URLField;
- if (!canEmbed) return (null);
+ // if (!canEmbed) return (null);
return (
<div className="linkButtonWrapper">
<div title="Drag Embed" className="linkButton-linker" ref={this._embedButton} onPointerDown={this.onEmbedButtonDown}>
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 9d42eb719..1d9f0c74b 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -24,6 +24,7 @@ import { FieldView } from "./nodes/FieldView";
import { FormattedTextBox } from "./nodes/FormattedTextBox";
import { IconBox } from "./nodes/IconBox";
import React = require("react");
+import { TooltipTextMenu } from '../util/TooltipTextMenu';
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -48,7 +49,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;
@@ -333,7 +333,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
@@ -414,41 +414,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,7 +467,6 @@ 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);
@@ -582,6 +546,13 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
return "-unset-";
}
+ TextBar: HTMLDivElement | undefined;
+ setTextBar = (ele: HTMLDivElement) => {
+ if (ele) {
+ this.TextBar = ele;
+ TooltipTextMenu.Toolbar && Array.from(ele.childNodes).indexOf(TooltipTextMenu.Toolbar) === -1 && ele.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..f31a29bdc 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -7,6 +7,9 @@ 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";
const modifiers = ["control", "meta", "shift", "alt"];
type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise<KeyControlInfo>;
@@ -70,7 +73,6 @@ export default class KeyManager {
SelectionManager.DeselectAll();
}
}
- main.toggleColorPicker(true);
SelectionManager.DeselectAll();
DictationManager.Controls.stop();
SharingManager.Instance.close();
@@ -120,10 +122,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 +162,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.tsx b/src/client/views/InkingCanvas.tsx
index 0eb4c66a2..7651060af 100644
--- a/src/client/views/InkingCanvas.tsx
+++ b/src/client/views/InkingCanvas.tsx
@@ -87,7 +87,7 @@ 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)
});
this.inkData = data;
}
@@ -150,9 +150,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}
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index a10df0e75..38734a03d 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -10,7 +10,6 @@ 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 { List } from "../../new_fields/List";
import { Utils } from "../../Utils";
@@ -19,7 +18,7 @@ library.add(faPen, faHighlighter, faEraser, faBan);
@observer
export class InkingControl extends React.Component {
- static Instance: InkingControl = new InkingControl({});
+ @observable static Instance: InkingControl;
@observable private _selectedTool: InkTool = InkTool.None;
@observable private _selectedColor: string = "rgb(244, 67, 54)";
@observable private _selectedWidth: string = "5";
@@ -27,7 +26,7 @@ export class InkingControl extends React.Component {
constructor(props: Readonly<{}>) {
super(props);
- InkingControl.Instance = this;
+ runInAction(() => InkingControl.Instance = this);
}
@action
@@ -46,7 +45,6 @@ export class InkingControl extends React.Component {
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);
diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss
index 4cd860da2..0335e12a5 100644
--- a/src/client/views/Main.scss
+++ b/src/client/views/Main.scss
@@ -21,7 +21,6 @@ div {
}
-
.jsx-parser {
width: 100%;
height: 100%;
@@ -64,215 +63,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;
-}
-
-.expandFlyoutButton {
- position: absolute;
- top: 30px;
- right: 30px;
- cursor: pointer;
-}
-
-.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..e61494e71
--- /dev/null
+++ b/src/client/views/MainView.scss
@@ -0,0 +1,97 @@
+@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%;
+ border: black 1px solid;
+ .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: 30px;
+ right: 30px;
+ 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 53951224c..b329717c4 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,135 +1,57 @@
-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, faChevronRight, faEllipsisV, faCompressArrowsAlt } from '@fortawesome/free-solid-svg-icons';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { 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, faChevronRight, faEllipsisV, faCompressArrowsAlt } 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 { CollectionLinearView } from './CollectionLinearView';
import { CollectionBaseView, CollectionViewType } from './collections/CollectionBaseView';
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 MarqueeOptionsMenu from './collections/collectionFreeForm/MarqueeOptionsMenu';
@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;
- @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');
@@ -139,29 +61,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);
}
@@ -169,7 +72,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) {
@@ -232,7 +135,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
@@ -250,25 +152,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();
@@ -281,39 +175,36 @@ export class MainView extends React.Component {
let freeformOptions: DocumentOptions = {
x: 0,
y: 400,
- width: this.pwidth * .7,
- height: this.pheight,
+ width: this._panelWidth * .7,
+ height: this._panelHeight,
title: "My Blank Collection"
};
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 {
@@ -332,21 +223,16 @@ export class MainView extends React.Component {
}
CollectionBaseView.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;
}
@@ -359,27 +245,20 @@ 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;
- @observable flyoutTranslate: boolean = true;
@computed get dockingContent() {
- let flyoutWidth = this.flyoutWidth;
- let countWidth = this.flyoutTranslate;
- let mainCont = this.mainContainer;
+ const mainContainer = this.mainContainer;
return <Measure offset onResize={this.onResize}>
{({ measureRef }) =>
- <div ref={measureRef} id="mainContent-div" style={{ width: `calc(100% - ${countWidth ? flyoutWidth : 0}px`, transform: `translate(${countWidth ? flyoutWidth : 0}px, 0px)` }} onDrop={this.onDrop}>
- {!mainCont ? (null) :
- <DocumentView Document={mainCont}
+ <div ref={measureRef} className="mainView-mainDiv" onDrop={this.onDrop}>
+ {!mainContainer ? (null) :
+ <DocumentView Document={mainContainer}
DataDoc={undefined}
addDocument={undefined}
addDocTab={this.addDocTabFunc}
@@ -407,9 +286,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);
@@ -422,15 +300,15 @@ export class MainView extends React.Component {
pointerOverDragger = () => {
if (this.flyoutWidth === 0) {
this.flyoutWidth = 250;
- this.flyoutTranslate = false;
+ this._flyoutTranslate = false;
}
}
@action
pointerLeaveDragger = () => {
- if (!this.flyoutTranslate) {
+ if (!this._flyoutTranslate) {
this.flyoutWidth = 0;
- this.flyoutTranslate = true;
+ this._flyoutTranslate = true;
}
}
@@ -440,9 +318,8 @@ export class MainView extends React.Component {
}
@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);
@@ -461,65 +338,93 @@ export class MainView extends React.Component {
}
@computed
get flyout() {
- let sidebar: FieldResult<Field>;
- if (!this.userDoc || !((sidebar = this.userDoc.sidebar) instanceof Doc)) {
+ let sidebarContent = this.userDoc && this.userDoc.sidebarContainer;
+ if (!(sidebarContent 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>;
+ let libraryButtonDoc = Cast(CurrentUserUtils.UserDocument.libraryButtons, Doc) as Doc;
+ libraryButtonDoc.columnWidth = this.flyoutWidth / 3 - 30;
+ return <div className="mainView-flyoutContainer">
+ <div className="mainView-tabButtons" style={{ height: `${this._buttonBarHeight}px` }}>
+ <DocumentView
+ Document={libraryButtonDoc}
+ 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>
+ <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={Transform.Identity}
+ 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() {
- if (!this.userDoc) {
- return (<div>{this.dockingContent}</div>);
- }
- let sidebar = this.userDoc.sidebar;
- if (!(sidebar instanceof Doc)) {
- return (null);
- }
- return (
- <div className="mainContent" style={{ width: "100%", height: "100%", position: "absolute" }}>
- <div onPointerLeave={this.pointerLeaveDragger}>
+ 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")}` }}
+ 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",
+ 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"
+ 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"
+ 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}
@@ -530,168 +435,66 @@ export class MainView extends React.Component {
}
@computed get expandButton() {
- return !this.flyoutTranslate ? (<div className="expandFlyoutButton" title="Re-attach sidebar" onPointerDown={() => {
+ return !this._flyoutTranslate ? (<div className="mainView-expandFlyoutButton" title="Re-attach sidebar" onPointerDown={() => {
runInAction(() => {
this.flyoutWidth = 250;
- this.flyoutTranslate = true;
+ this._flyoutTranslate = true;
});
}}><FontAwesomeIcon icon="chevron-right" color="grey" size="lg" /></div>) : (null);
}
- 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%" };
- }
-
- 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();
- }
- }
-
- 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 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>(), "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.flyoutTranslate ? this.flyoutWidth : 0) + 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,
- ];
-
+ addButtonDoc = (doc: Doc) => {
+ Doc.AddDocToList(CurrentUserUtils.UserDocument, "docButtons", doc);
+ return true;
}
-
- @observable isSearchVisible = false;
- @action.bound
- toggleSearch = () => {
- this.isSearchVisible = !this.isSearchVisible;
+ remButtonDoc = (doc: Doc) => {
+ Doc.RemoveDocFromList(CurrentUserUtils.UserDocument, "docButtons", doc);
+ return true;
}
-
- @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}
- />
- );
+ @computed get docButtons() {
+ return <div className="mainView-docButtons" style={{ left: (this._flyoutTranslate ? this.flyoutWidth : 0) + 20 }} >
+ <MainViewNotifs />
+ <CollectionLinearView Document={CurrentUserUtils.UserDocument} DataDoc={undefined}
+ fieldKey={"docButtons"}
+ fieldExt={""}
+ showHiddenControls={true}
+ select={emptyFunction}
+ chromeCollapsed={true}
+ active={returnFalse}
+ isSelected={returnFalse}
+ moveDocument={returnFalse}
+ CollectionView={undefined}
+ addDocument={this.addButtonDoc}
+ addDocTab={this.addDocTabFunc}
+ pinToPres={emptyFunction}
+ removeDocument={this.remButtonDoc}
+ ruleProvider={undefined}
+ onClick={undefined}
+ ScreenToLocalTransform={Transform.Identity}
+ ContentScaling={returnOne}
+ PanelWidth={this.flyoutWidthFunc}
+ PanelHeight={this.getContentsHeight}
+ renderDepth={0}
+ focus={emptyFunction}
+ whenActiveChanged={emptyFunction}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined} />
+ </div>;
}
render() {
- return (
- <div id="main-div">
- {this.dictationOverlay}
- <SharingManager />
- <DocumentDecorations />
- {this.mainContent}
- <PreviewCursor />
- <ContextMenu />
- {this.nodesMenu()}
- {this.miscButtons}
- <PDFMenu />
- <MarqueeOptionsMenu />
- <MainOverlayTextBox firstinstance={true} />
- <OverlayView />
- </div >
- );
+ return (<div className="mainView-container">
+ <DictationOverlay />
+ <SharingManager />
+ <GoogleAuthenticationManager />
+ <DocumentDecorations />
+ {this.mainContent}
+ <PreviewCursor />
+ <ContextMenu />
+ {this.docButtons}
+ <PDFMenu />
+ <MarqueeOptionsMenu />
+ <OverlayView />
+ </div >);
}
}
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..95c7b2683 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -145,6 +145,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 +171,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}
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/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
index f8988338f..58f1f2883 100644
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -24,6 +24,7 @@ export enum CollectionViewType {
Stacking,
Masonry,
Pivot,
+ Linear,
}
export namespace CollectionViewType {
@@ -36,7 +37,8 @@ export namespace CollectionViewType {
["tree", CollectionViewType.Tree],
["stacking", CollectionViewType.Stacking],
["masonry", CollectionViewType.Masonry],
- ["pivot", CollectionViewType.Pivot]
+ ["pivot", CollectionViewType.Pivot],
+ ["linear", CollectionViewType.Linear]
]);
export const valueOf = (value: string) => {
@@ -46,7 +48,7 @@ export namespace CollectionViewType {
}
export interface CollectionRenderProps {
- addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
+ addDocument: (document: Doc) => boolean;
removeDocument: (document: Doc) => boolean;
moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
active: () => boolean;
@@ -101,22 +103,13 @@ export class CollectionBaseView extends Touchable<CollectionViewProps> {
@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;
+ addDocument(doc: Doc): boolean {
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.AddDocToList(targetDataDoc, targetField, doc);
Doc.GetProto(doc).lastOpened = new DateField;
return true;
}
@@ -187,7 +180,7 @@ export class CollectionBaseView extends Touchable<CollectionViewProps> {
<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")}`
+ boxShadow: this.props.Document.isBackground || viewtype === CollectionViewType.Linear ? 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}>
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 14e513157..1f78c8c97 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) => {
diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
new file mode 100644
index 000000000..1709b9c99
--- /dev/null
+++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
@@ -0,0 +1,396 @@
+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 { Doc, WidthSym } from "../../../new_fields/Doc";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { PastelSchemaPalette, SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
+import { ScriptField } from "../../../new_fields/ScriptField";
+import { NumCast, StrCast } from "../../../new_fields/Types";
+import { Utils, 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 Measure from "react-measure";
+
+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;
+
+ private _dropRef: HTMLDivElement | null = null;
+ 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;
+
+ @observable _heading = this.props.headingObject ? this.props.headingObject.heading : this.props.heading;
+ @observable _color = this.props.headingObject ? this.props.headingObject.color : "#f1efeb";
+
+ createRowDropRef = (ele: HTMLDivElement | null) => {
+ this._dropRef = ele;
+ 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) {
+ let key = StrCast(this.props.parent.props.Document.sectionFilter);
+ let castedValue = this.getValue(this._heading);
+ if (castedValue) {
+ de.data.droppedDocuments.forEach(d => d[key] = castedValue);
+ }
+ else {
+ de.data.droppedDocuments.forEach(d => d[key] = undefined);
+ }
+ 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;
+ }
+ }
+
+ @action
+ pointerEnteredRow = () => {
+ if (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);
+ }
+
+ @action
+ deleteRow = () => {
+ 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);
+ }
+
+ 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>
+ );
+ }
+
+ @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;
+
+ handleResize = (size: any) => {
+ this.counter += 1;
+ if (this.counter !== 1) {
+ this.getTrueHeight();
+ }
+ }
+
+ private counter: number = 0;
+
+ render() {
+ let cols = this.props.rows();
+ 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 templatecols = "";
+ let headings = this.props.headings();
+ let heading = this._heading;
+ let style = this.props.parent;
+ let uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
+ 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.headingObject ?
+ <div className="collectionStackingView-sectionHeader" ref={this._headerRef} >
+ <div className={"collectionStackingView-collapseBar" + (this.props.headingObject.collapsed === true ? " active" : "")} onClick={this.collapseSection}></div>
+ <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>
+ }
+ {evContents === `NO ${key.toUpperCase()} VALUE` ?
+ (null) :
+ <button className="collectionStackingView-sectionDelete" onClick={this.deleteRow}>
+ <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);
+ 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 >
+ <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..79c032723 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;
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 52a41fc84..3218f630a 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
@@ -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;
@@ -905,11 +903,11 @@ interface CollectionSchemaPreviewProps {
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;
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index 74da4ef85..b31f0b8e3 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -1,19 +1,27 @@
@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;
+
.collectionSchemaView-previewDoc {
height: 100%;
position: absolute;
@@ -40,10 +48,12 @@
top: 0;
left: 0;
}
+
.collectionStackingView-masonrySingle {
height: 100%;
position: absolute;
}
+
.collectionStackingView-masonryGrid {
margin: auto;
height: max-content;
@@ -91,17 +101,31 @@
height: 100%;
margin: auto;
}
-
+
.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-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 +156,7 @@
.editableView-input:hover,
.editableView-container-editing:hover,
.editableView-container-editing-oneLine:hover {
- cursor: text
+ cursor: text;
}
.editableView-input {
@@ -182,11 +206,46 @@
}
}
- .collectionStackingView-sectionDelete {
+ .collectionStackingView-sectionOptions {
position: absolute;
right: 0;
top: 0;
height: 100%;
+
+ [class*="css"] {
+ max-width: 102px;
+ }
+
+ .collectionStackingView-sectionOptionButton {
+ height: 35px;
+ }
+
+ .collectionStackingView-optionPicker {
+ width: 78px;
+
+ .optionOptions {
+ display: inline;
+ }
+
+ .optionPicker {
+ cursor: pointer;
+ width: 20px;
+ height: 20px;
+ border-radius: 10px;
+ margin: 3px;
+
+ &.active {
+ color: red;
+ }
+ }
+ }
+ }
+
+ .collectionStackingView-sectionDelete {
+ position: absolute;
+ right: 25px;
+ top: 0;
+ height: 100%;
}
}
@@ -288,6 +347,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..57722b9b7 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); }
@@ -47,9 +49,31 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
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(); }
childDocHeight(child: Doc) { return this.getDocHeight(Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, child).layout); }
+ children(docs: Doc[]) {
+ this._docXfs.length = 0;
+ return docs.map((d, i) => {
+ let pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, d);
+ let width = () => Math.min(d.nativeWidth && !d.ignoreAspect && !this.props.Document.fillColumn ? d[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns);
+ let height = () => this.getDocHeight(pair.layout);
+ let dref = React.createRef<HTMLDivElement>();
+ let dxf = () => this.getDocTransform(pair.layout!, 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 as Doc, pair.data, dxf, width)}
+ </div>;
+ });
+ }
+ @action
+ setDocHeight = (key: string, sectionHeight: number) => {
+ this._heightMap.set(key, sectionHeight);
+ }
+
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.
@@ -88,11 +112,16 @@ 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).
this._heightDisposer = reaction(() => {
- if (this.isStackingView && BoolCast(this.props.Document.autoHeight)) {
+ if (BoolCast(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) {
+ 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);
+ } else {
+ let sum = Array.from(this._heightMap.values()).reduce((acc: number, curr: number) => acc += curr, 0);
+ sum += 30;
+ return this.props.ContentScaling() * (sum + (this.Sections.size ? 50 : 0));
+ }
}
return -1;
},
@@ -120,7 +149,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) => {
@@ -162,19 +191,19 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
if (!d) return 0;
let nw = NumCast(d.nativeWidth);
let nh = NumCast(d.nativeHeight);
+ let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1);
if (!d.ignoreAspect && !d.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);
return wid * aspect;
}
- return d.fitWidth ? this.props.PanelHeight() - 2 * this.yMargin : d[HeightSym]();
+ return d.fitWidth ? !d.nativeHeight ? this.props.PanelHeight() - 2 * this.yMargin : Math.min(wid * NumCast(d.scrollHeight, NumCast(d.nativeHeight)) / NumCast(d.nativeWidth, 1), this.props.PanelHeight() - 2 * this.yMargin) : d[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];
@@ -189,13 +218,13 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
@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 +315,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
@@ -383,26 +389,27 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
sections = entries.sort(this.sortFunc);
}
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.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>
);
}
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 240adf428..7e54b0f29 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -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);
+ }
+ 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,12 +275,20 @@ 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}
@@ -268,6 +297,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
((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)
}}>
+ <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 28d1eb384..6e8e4fa12 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -18,15 +18,14 @@ 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;
@@ -34,25 +33,26 @@ export interface CollectionViewProps extends FieldViewProps {
VisibleHeight?: () => number;
chromeCollapsed: boolean;
setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void;
+ showHiddenControls?: boolean; // hack for showing the undo/redo/ink controls in a linear view -- needs to be redone
}
export interface SubCollectionViewProps extends CollectionViewProps {
- CollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
+ CollectionView: Opt<CollectionView>;
ruleProvider: Doc | undefined;
+ children?: never | (() => JSX.Element[]) | React.ReactNode;
}
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);
}
@@ -176,14 +176,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;
}
@@ -194,7 +194,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/";
@@ -203,12 +204,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;
}
@@ -252,7 +253,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);
@@ -275,7 +276,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.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 882a0f144..abaa9662c 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -28,6 +28,7 @@ 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 {
@@ -187,7 +188,7 @@ 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" });
@@ -198,6 +199,7 @@ class TreeView extends React.Component<TreeViewProps> {
} 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,7 +219,7 @@ class TreeView extends React.Component<TreeViewProps> {
if (de.data instanceof DragManager.LinkDragData) {
let sourceDoc = de.data.linkSourceDocument;
let destDoc = this.props.document;
- DocUtils.MakeLink({doc:sourceDoc}, {doc:destDoc});
+ DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc });
e.stopPropagation();
}
if (de.data instanceof DragManager.DocumentDragData) {
@@ -258,7 +260,10 @@ class TreeView extends React.Component<TreeViewProps> {
let aspect = NumCast(this.props.document.nativeHeight) / NumCast(this.props.document.nativeWidth);
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 this.props.document.fitWidth ? (!this.props.document.nativeHeight ? NumCast(this.props.containingCollection.height) :
+ Math.min(this.docWidth() * NumCast(this.props.document.scrollHeight, NumCast(this.props.document.nativeHeight)) / NumCast(this.props.document.nativeWidth,
+ NumCast(this.props.containingCollection.height)))) :
+ NumCast(this.props.document.height) ? NumCast(this.props.document.height) : 50;
})());
}
@@ -512,8 +517,6 @@ 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; }
protected createTreeDropTarget = (ele: HTMLDivElement) => {
@@ -538,7 +541,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 +555,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>
@@ -609,7 +591,6 @@ export class CollectionTreeView extends CollectionSubView(Document) {
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" }} >
{
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/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 534246326..3d5b4e562 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -18,6 +18,8 @@ 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';
export const COLLECTION_BORDER_WIDTH = 2;
library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faEllipsisV, faImage, faEye as any, faCopy);
@@ -65,6 +67,7 @@ export class CollectionView extends React.Component<FieldViewProps> {
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} />); }
+ case CollectionViewType.Linear: { return (<CollectionLinearView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); }
case CollectionViewType.Freeform:
default:
this.props.Document.freeformLayoutEngine = undefined;
@@ -123,6 +126,7 @@ export class CollectionView extends React.Component<FieldViewProps> {
let layoutItems: ContextMenuProps[] = 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) });
}
}
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
index 23001f0f5..3a66c05f4 100644
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -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"
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index df089eb00..fe92eed10 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -10,7 +10,7 @@ export interface CollectionFreeFormLinkViewProps {
A: Doc;
B: Doc;
LinkDocs: Doc[];
- addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
+ addDocument: (document: Doc) => boolean;
removeDocument: (document: Doc) => boolean;
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index 9b608a576..db36c4391 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -50,12 +50,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 915126742..440a0a8e5 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";
@@ -95,10 +95,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;
@@ -107,7 +107,7 @@ 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);
@@ -269,6 +269,7 @@ 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()) {
document.removeEventListener("pointermove", this.onPointerMove);
@@ -341,7 +342,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
return;
}
- 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();
@@ -437,7 +438,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();
}
@@ -449,7 +450,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
setPan(panX: number, panY: number) {
- if (!this.props.Document.lockedPosition) {
+ if (!this.props.Document.lockedPosition || this.props.Document.inOverlay) {
this.props.Document.panTransformType = "None";
var scale = this.getLocalTransform().inverse().Scale;
const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX));
@@ -565,7 +566,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
PanelWidth: layoutDoc[WidthSym],
PanelHeight: layoutDoc[HeightSym],
ContentScaling: returnOne,
- ContainingCollectionView: this.props.CollectionView,
+ ContainingCollectionView: this.props.ContainingCollectionView,
focus: this.focusDocument,
backgroundColor: returnEmptyString,
parentActive: this.props.active,
@@ -747,7 +748,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);
}
}
}
@@ -769,10 +770,14 @@ 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 [
+ <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />,
+ ...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;
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 4a7fa629c..e9f4c40a6 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -25,7 +25,7 @@ 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;
@@ -86,7 +86,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;
});
})();
@@ -95,7 +95,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);
}
@@ -149,7 +149,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
@@ -220,7 +220,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;
@@ -235,7 +235,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 => {
@@ -344,12 +344,12 @@ 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 newCollection = this.getCollection(selected);
- this.props.addDocument(newCollection, false);
+ this.props.addDocument(newCollection);
this.props.selectDocuments([newCollection]);
MarqueeOptionsMenu.Instance.fadeOut(true);
this.hideMarquee();
@@ -506,8 +506,8 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
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)` }} >
+ return <div className="marqueeView" onScroll={(e) => e.currentTarget.scrollLeft = 0} style={{ borderRadius: "inherit" }} onClick={this.onClick} onPointerDown={this.onPointerDown}>
+ <div style={{ position: "relative", transform: `translate(${p[0]}px, ${p[1]}px)` }} onScroll={(e) => e.currentTarget.scrollLeft = 0} >
{this._visible ? this.marqueeDiv : null}
<div ref={this._mainCont} style={{ transform: `translate(${-p[0]}px, ${-p[1]}px)` }} >
{this.props.children}
diff --git a/src/client/views/linking/LinkFollowBox.tsx b/src/client/views/linking/LinkFollowBox.tsx
index 53b720a9e..32ebe7c61 100644
--- a/src/client/views/linking/LinkFollowBox.tsx
+++ b/src/client/views/linking/LinkFollowBox.tsx
@@ -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,21 +152,7 @@ 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 = () => {
@@ -235,44 +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, async document => dockingFunc(document), targetContext);
- }
- else if (LinkFollowBox.destinationDoc === LinkFollowBox.linkDoc.anchor1 && sourceContext) {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, 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);
-
- }
- else {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, 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);
}
}
@@ -310,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/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..3cf8c3eb3 100644
--- a/src/client/views/nodes/ButtonBox.tsx
+++ b/src/client/views/nodes/ButtonBox.tsx
@@ -70,7 +70,8 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt
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-outerDiv" ref={this.createDropTarget} onContextMenu={this.specificContextMenu}
+ style={{ boxShadow: this.Document.opacity === 0 ? undefined : StrCast(this.Document.boxShadow, "") }}>
<div className="buttonBox-mainButton" style={{ background: StrCast(this.props.Document.backgroundColor), color: StrCast(this.props.Document.color, "black") }} >
<div className="buttonBox-mainButtonCenter">
{(this.Document.text || this.Document.title)}
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.scss b/src/client/views/nodes/CollectionFreeFormDocumentView.scss
index 55404d818..da287649e 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.scss
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.scss
@@ -3,4 +3,6 @@
position: absolute;
background-color: transparent;
touch-action: manipulation;
+ 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..93f6dc468 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,15 +1,15 @@
-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 { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema";
+import { Cast, FieldValue, 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 { documentSchema, DocumentView, DocumentViewProps } from "./DocumentView";
import React = require("react");
-import { Doc, WidthSym, HeightSym } from "../../../new_fields/Doc";
-import { random } from "animejs";
export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
dataProvider?: (doc: Doc, dataDoc?: Doc) => { x: number, y: number, width: number, height: number, z: number, transition?: string } | undefined;
diff --git a/src/client/views/nodes/ColorBox.scss b/src/client/views/nodes/ColorBox.scss
new file mode 100644
index 000000000..8df617fca
--- /dev/null
+++ b/src/client/views/nodes/ColorBox.scss
@@ -0,0 +1,10 @@
+.colorBox-container {
+ width:100%;
+ height:100%;
+ position: relative;
+ pointer-events:all;
+
+ .sketch-picker {
+ margin:auto;
+ }
+} \ 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..4aff770f9
--- /dev/null
+++ b/src/client/views/nodes/ColorBox.tsx
@@ -0,0 +1,16 @@
+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";
+
+@observer
+export class ColorBox extends React.Component<FieldViewProps> {
+ public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(ColorBox, fieldKey); }
+ render() {
+ return <div className="colorBox-container" >
+ <SketchPicker color={InkingControl.Instance.selectedColor} onChange={InkingControl.Instance.switchColor} />
+ </div>;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 75dd27f46..19ffdf0cd 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -8,9 +8,7 @@ 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,6 +24,8 @@ 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 { PresElementBox } from "../presentationview/PresElementBox";
import { VideoBox } from "./VideoBox";
import { WebBox } from "./WebBox";
@@ -101,7 +101,11 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
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 }}
+ 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
+ }}
bindings={this.CreateBindings()}
jsx={this.finalLayout}
showWarnings={true}
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 4ea200e6d..b3e7898c1 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -5,6 +5,7 @@
top: 0;
left:0;
border-radius: inherit;
+ transition : outline .3s linear;
// background: $light-color; //overflow: hidden;
transform-origin: left top;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index f74023f42..ca1cdbd9d 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -7,7 +7,7 @@ import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc
import { Id } from '../../../new_fields/FieldSymbols';
import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema";
import { ScriptField } from '../../../new_fields/ScriptField';
-import { BoolCast, Cast, NumCast, PromiseValue, StrCast } from "../../../new_fields/Types";
+import { BoolCast, Cast, NumCast, PromiseValue, StrCast, FieldValue } from "../../../new_fields/Types";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
import { emptyFunction, returnTrue, Utils } from "../../../Utils";
import { DocServer } from "../../DocServer";
@@ -21,14 +21,11 @@ import { SelectionManager } from "../../util/SelectionManager";
import { Transform } from "../../util/Transform";
import { undoBatch, UndoManager } from "../../util/UndoManager";
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 { DocComponent } from "../DocComponent";
import { EditableView } from '../EditableView';
-import { MainView } from '../MainView';
import { OverlayView } from '../OverlayView';
import { ScriptBox } from '../ScriptBox';
import { ScriptingRepl } from '../ScriptingRepl';
@@ -42,7 +39,9 @@ import { ImageField } from '../../../new_fields/URLField';
import SharingManager from '../../util/SharingManager';
import { Scripting } from '../../util/Scripting';
import { InteractionUtils } from '../../util/InteractionUtils';
+import { DictationOverlay } from '../DictationOverlay';
+library.add(fa.faEdit);
library.add(fa.faTrash);
library.add(fa.faShare);
library.add(fa.faDownload);
@@ -66,13 +65,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;
@@ -104,7 +103,11 @@ export const documentSchema = createSchema({
height: "number", // "
backgroundColor: "string", // background color of document
opacity: "number", // opacity of 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 an Doc to drag.
+ dragFactory: Doc, // the document that serves as the "template" for the onDragStart script
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
@@ -112,6 +115,7 @@ export const documentSchema = createSchema({
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
+ 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)
@@ -164,15 +168,15 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
if (this._mainCont.current) {
let dragData = new DragManager.DocumentDragData([this.props.Document]);
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.offset = this.Document.onDragStart ? [0, 0] : 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
});
}
}
@@ -217,7 +221,7 @@ 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));
+ expandedDocs.forEach(maxDoc => this.props.addDocument && this.props.addDocument(maxDoc));
let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(NumCast(this.Document.width) / 2, NumCast(this.Document.height) / 2);
DocumentManager.Instance.animateBetweenPoint(scrpt, expandedDocs);
} else {
@@ -225,14 +229,15 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
}
else if (linkDocs.length) {
- DocumentManager.Instance.FollowLink(this.props.Document,
+ 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;
@@ -243,23 +248,25 @@ 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) && 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.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) && 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
@@ -419,10 +426,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 };
}
});
}
@@ -472,6 +478,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" });
@@ -615,6 +627,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
render() {
+ if (!this.props.Document) return (null);
let animDims = this.props.Document.animateToDimensions ? Array.from(Cast(this.props.Document.animateToDimensions, listSpec("number"))!) : 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;
@@ -624,8 +637,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.props.Document.fitWidth ? this.props.PanelWidth() - 2 : this.nativeWidth > 0 && !this.Document.ignoreAspect ? `${this.nativeWidth}px` : "100%";
+ const nativeHeight = this.props.Document.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,6 +675,8 @@ 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"];
return (
<div className={`documentView-node${this.topMost ? "-topmost" : ""}`}
ref={this._mainCont}
@@ -669,10 +684,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
transition: this.props.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,
+ outline: fullDegree && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px",
+ border: fullDegree && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined,
background: backgroundColor,
width: animwidth,
height: animheight,
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..074efaf6c 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";
@@ -29,7 +27,7 @@ export interface FieldViewProps {
fieldExt: string;
leaveNativeSize?: boolean;
fitToBox?: boolean;
- ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
+ ContainingCollectionView: Opt<CollectionView>;
ContainingCollectionDoc: Opt<Doc>;
ruleProvider: Doc | undefined;
Document: Doc;
@@ -38,7 +36,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;
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..3b580d851
--- /dev/null
+++ b/src/client/views/nodes/FontIconBox.tsx
@@ -0,0 +1,21 @@
+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';
+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() { return FieldView.LayoutString(FontIconBox); }
+
+ render() {
+ return <div className="fontIconBox-outerDiv" > <FontAwesomeIcon className="fontIconBox-icon" icon={this.Document.icon as any} size="lg" color="white" /> </div>;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index 45e516015..29e8b14a8 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -28,7 +28,7 @@
}
.formattedTextBox-cont-hidden {
- pointer-events: none;
+ // pointer-events: none;
}
.formattedTextBox-inner-rounded {
@@ -40,9 +40,10 @@
left: 10%;
}
-.formattedTextBox-inner-rounded div,
-.formattedTextBox-inner div {
+.formattedTextBox-inner-rounded ,
+.formattedTextBox-inner {
padding: 10px 10px;
+ height: 100%;
}
.menuicon {
@@ -153,17 +154,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 +176,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 749886d9a..b05d0046c 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -1,53 +1,54 @@
import { library } from '@fortawesome/fontawesome-svg-core';
import { faEdit, faSmile, faTextHeight, faUpload } from '@fortawesome/free-solid-svg-icons';
+import _ from "lodash";
import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction } 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, DocListCastAsync } 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 { 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 { TextShadowProperty } from 'csstype';
library.add(faEdit);
library.add(faSmile, faTextHeight, faUpload);
export interface FormattedTextBoxProps {
- isOverlay?: boolean;
hideOnLeave?: boolean;
height?: string;
color?: string;
@@ -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
@@ -138,55 +137,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
constructor(props: FieldViewProps) {
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!; }
@@ -195,29 +146,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
@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 +158,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({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: this.dataDoc[key] as Doc }, "Ref:" + value, "link to named target", 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,7 +191,6 @@ 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);
}
@@ -286,44 +213,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;
@@ -335,14 +251,19 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
@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) {
+ if (de.data instanceof DragManager.EmbedDragData) {
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;
+ const link = DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "ImgRef:" + target.title);
+ let alias = Doc.MakeAlias(target);
+ alias.fitToBox = true;
+ let node = schema.nodes.dashDoc.create({
+ width: target[WidthSym](), height: target[HeightSym](),
+ title: "dashDoc", docid: alias[Id],
+ float: "right"
+ });
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({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "ImgRef:" + target.title);
+ link && this._editorView!.dispatch(this._editorView!.state.tr.insert(pos!.pos, node));
this.tryUpdateHeight();
e.stopPropagation();
} else if (de.data instanceof DragManager.DocumentDragData) {
@@ -353,7 +274,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new RichTextField(draggedDoc.data.Data);
e.stopPropagation();
}
- } else {
+ } else if (de.mods === "CtrlKey") {
draggedDoc.isTemplate = true;
if (typeof (draggedDoc.layout) === "string") {
let layoutDelegateToOverrideFieldKey = Doc.MakeDelegate(draggedDoc);
@@ -368,16 +289,99 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
- 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();
+ 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;
}
}
+
+ //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: "Dictate", 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" });
+ }
+
recordBullet = async () => {
let completedCue = "end session";
let results = await DictationManager.Controls.listen({
@@ -408,12 +412,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 +430,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 +446,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);
@@ -519,11 +511,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
this._searchReactionDisposer = reaction(() => {
return StrCast(this.props.Document.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]);
}
@@ -547,7 +534,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.props.Document.fontSize, 13);
rules && setTimeout(() => {
const view = this._editorView!;
if (this._proseRef) {
@@ -563,6 +550,51 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}, 0);
}), { fireImmediately: true }
);
+ 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 && 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);
+ }
+ this.props.Document.scrollToLinkID = undefined;
+ }
+
+ },
+ { fireImmediately: true }
+ );
setTimeout(() => this.tryUpdateHeight(), 0);
}
@@ -741,6 +773,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(); },
@@ -749,7 +782,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));
@@ -761,9 +793,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) {
@@ -790,8 +822,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));
@@ -801,31 +831,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
@@ -834,17 +858,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) {
@@ -856,57 +881,42 @@ 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);
- return;
- }
- }
- if (targetContext && (!jumpToDoc || targetContext !== await jumpToDoc.annotationOn)) {
- DocumentManager.Instance.jumpToDocument(jumpToDoc || targetContext, ctrlKey, document => this.props.addDocTab(document, undefined, location ? location : "inTab"), targetContext);
- } else if (jumpToDoc) {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, ctrlKey, document => this.props.addDocTab(document, undefined, location ? location : "inTab"));
- } else {
- DocumentManager.Instance.jumpToDocument(linkDoc, ctrlKey, 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) });
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);
+ }
+
+ // 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 }));
}
@@ -914,25 +924,28 @@ 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();
+
+ // 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 ((this._editorView as any).mouseDown) {
+ let originalUpHandler = (this._editorView as any).mouseDown.up;
+ (this._editorView as any).root.removeEventListener("mouseup", originalUpHandler);
+ (this._editorView as any).mouseDown.up = (e: MouseEvent) => {
+ !(e as any).formattedHandled && originalUpHandler(e);
+ e.stopPropagation();
+ (e as any).formattedHandled = true;
+ };
+ (this._editorView as any).root.addEventListener("mouseup", (this._editorView as any).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);
}
});
}
@@ -946,7 +959,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;
@@ -961,7 +974,8 @@ 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");
@@ -972,7 +986,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
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) {
+ if (!this.props.Document.isAnimating && this.props.Document.autoHeight && sh !== 0 && getComputedStyle(this._ref.current!.parentElement!).top === "0px") {
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));
@@ -981,30 +995,34 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
render() {
- let style = this.props.isOverlay ? "scroll" : "hidden";
+ let style = "hidden";
let rounded = StrCast(this.props.Document.borderRounding) === "100%" ? "-rounded" : "";
let interactive: "all" | "none" = InkingControl.Instance.selectedTool || this.props.Document.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}
style={{
overflowY: this.props.Document.autoHeight ? "hidden" : "auto",
height: this.props.Document.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)}
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..4971f61b7 100644
--- a/src/client/views/nodes/IconBox.tsx
+++ b/src/client/views/nodes/IconBox.tsx
@@ -26,6 +26,8 @@ library.add(faFilm, faTag, faTextHeight);
export class IconBox extends React.Component<FieldViewProps> {
public static LayoutString() { return FieldView.LayoutString(IconBox); }
+ @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..2b81c16c0 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -1,18 +1,20 @@
-.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;
}
+.imageBox-container {
+ border-radius: inherit;
+}
+
.imageBox-cont-interactive {
pointer-events: all;
- width: 100%;
- height: auto;
}
.imageBox-dot {
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index fe4f75cad..9f39eccea 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -13,20 +13,21 @@ 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 } 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';
var requestImageSize = require('../../util/request-image-size');
var path = require('path');
const { Howl } = require('howler');
@@ -54,9 +55,10 @@ type ImageDocument = makeInterface<[typeof pageSchema, typeof documentSchema]>;
const ImageDocument = makeInterface(pageSchema, documentSchema);
@observer
-export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageDocument) {
+export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocument>(ImageDocument) {
- public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(ImageBox, fieldKey); }
+ public static LayoutString(fieldExt?: string) { return FieldView.LayoutString(ImageBox, "data", fieldExt); }
+ @observable static _showControls: boolean;
private _imgRef: React.RefObject<HTMLImageElement> = React.createRef();
private _downX: number = 0;
private _downY: number = 0;
@@ -65,10 +67,6 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
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); }
-
protected createDropTarget = (ele: HTMLDivElement) => {
if (this.dropDisposer) {
this.dropDisposer();
@@ -299,7 +297,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
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(1 - NumCast(layoutdoc.nativeHeight) / NumCast(layoutdoc.nativeWidth) / (realsize.height / realsize.width)) > 0.1)) {
+ if (layoutdoc.width && (Math.abs(1 - NumCast(layoutdoc.height) / NumCast(layoutdoc.width) / (realsize.height / realsize.width)) > 0.1)) {
setTimeout(action(() => {
layoutdoc.height = layoutdoc[WidthSym]() * aspect;
layoutdoc.nativeHeight = realsize.height;
@@ -381,7 +379,8 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
return (null);
}
- render() {
+ @computed
+ get content() {
// 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);
@@ -393,7 +392,6 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
let paths: string[] = [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 field = this.dataDoc[this.props.fieldKey];
@@ -448,4 +446,30 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
<FaceRectangles document={this.extensionDoc} color={"#0000FF"} backgroundColor={"#0000FF"} />
</div>);
}
+
+ render() {
+ Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
+ return (<div className={"imageBox-container"} onContextMenu={this.specificContextMenu}>
+ <CollectionFreeFormView {...this.props}
+ PanelHeight={this.props.PanelHeight}
+ PanelWidth={this.props.PanelWidth}
+ 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/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 88cd2cdc4..63b412a23 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -1,17 +1,21 @@
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 } 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 { ContextMenu } from '../ContextMenu';
+import { ContextMenuProps } from '../ContextMenuItem';
+import { DocAnnotatableComponent } from "../DocComponent";
import { InkingControl } from "../InkingControl";
import { PDFViewer } from "../pdf/PDFViewer";
import { documentSchema } from "./DocumentView";
@@ -19,41 +23,47 @@ 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';
type PdfDocument = makeInterface<[typeof documentSchema, typeof panZoomSchema, typeof pageSchema]>;
const PdfDocument = makeInterface(documentSchema, panZoomSchema, pageSchema);
@observer
-export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocument) {
+export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>(PdfDocument) {
public static LayoutString(fieldExt?: string) { return FieldView.LayoutString(PDFBox, "data", fieldExt); }
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;
@@ -65,9 +75,25 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
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
@@ -113,7 +139,9 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
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" autoFocus={true} 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()} >
@@ -128,7 +156,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)} />
@@ -174,7 +202,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
ContextMenu.Instance.addItem({ description: "Pdf Funcs...", subitems: funcs, icon: "asterisk" });
}
- _initialScale: number | undefined;
+ _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");
@@ -206,7 +234,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
pinToPres={this.props.pinToPres} addDocument={this.props.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._initialScale < 2.5 ? true : false} />
+ fieldKey={this.props.fieldKey} extensionDoc={this.extensionDoc} 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 180ed9032..15fafb022 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -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..ced597b59
--- /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..48623eaaf 100644
--- a/src/client/views/nodes/VideoBox.scss
+++ b/src/client/views/nodes/VideoBox.scss
@@ -1,6 +1,12 @@
+.videoBox-container {
+ pointer-events: all;
+}
+
.videoBox-content-YouTube, .videoBox-content-YouTube-fullScreen,
.videoBox-content, .videoBox-content-interactive, .videoBox-cont-fullScreen {
width: 100%;
+ z-index: 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 +17,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 573197117..aa9b28118 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -3,47 +3,53 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction,
import { observer } from "mobx-react";
import * as rp from 'request-promise';
import { InkTool } from "../../../new_fields/InkField";
-import { makeInterface } from "../../../new_fields/Schema";
-import { Cast, FieldValue, NumCast, BoolCast } from "../../../new_fields/Types";
+import { makeInterface, createSchema, listSpec } from "../../../new_fields/Schema";
+import { Cast, FieldValue, NumCast, BoolCast, StrCast } from "../../../new_fields/Types";
import { VideoField } from "../../../new_fields/URLField";
import { RouteStore } from "../../../server/RouteStore";
-import { Utils } from "../../../Utils";
+import { Utils, emptyFunction, returnOne } from "../../../Utils";
import { Docs, DocUtils } from "../../documents/Documents";
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 { positionSchema } from "./CollectionFreeFormDocumentView";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
var path = require('path');
-type VideoDocument = makeInterface<[typeof documentSchema, typeof pageSchema]>;
-const VideoDocument = makeInterface(documentSchema, pageSchema);
+export const timeSchema = createSchema({
+ currentTimecode: "number",
+});
+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(fieldExt?: string) { return FieldView.LayoutString(VideoBox, "data", fieldExt); }
public get player(): HTMLVideoElement | null {
return this._videoRef;
@@ -62,7 +68,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
}
@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 +81,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 +103,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 +118,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({doc:imageSummary}, {doc: this.props.Document}, "snapshot from " + this.props.Document.title, "video frame snapshot");
+ 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 +144,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 +153,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.height = (this.Document.width || 0) / youtubeaspect;
}
}
}
@@ -168,10 +175,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 +214,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 +242,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 +262,112 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
});
}
+ private get uIButtons() {
+ let scaling = Math.min(1.8, this.props.ScreenToLocalTransform().Scale);
+ let curTime = NumCast(this.props.Document.currentTimecode);
+ 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>
+ ]]);
+ }
+
+ @action
+ onPlayDown = () => this._playing ? this.Pause() : this.Play()
+
+ @action
+ onFullDown = (e: React.PointerEvent) => {
+ this.FullScreen();
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ @action
+ onSnapshot = (e: React.PointerEvent) => {
+ this.Snapshot();
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ @action
+ onResetDown = (e: React.PointerEvent) => {
+ this.Pause();
+ e.stopPropagation();
+ this._isResetClick = 0;
+ document.addEventListener("pointermove", this.onResetMove, true);
+ document.addEventListener("pointerup", this.onResetUp, true);
+ InkingControl.Instance.switchTool(InkTool.Eraser);
+ }
+ @action
+ onResetMove = (e: PointerEvent) => {
+ this._isResetClick += Math.abs(e.movementX) + Math.abs(e.movementY);
+ this.Seek(Math.max(0, NumCast(this.props.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);
+ InkingControl.Instance.switchTool(InkTool.None);
+ this._isResetClick < 10 && (this.props.Document.currentTimecode = 0);
+ }
+ @computed get fieldExtensionDoc() { 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); }
@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 {
+ Doc.GetProto(doc).annotationOn = this.props.Document;
+ var curTime = NumCast(this.props.Document.currentTimecode, -1);
+ curTime !== -1 && (doc.displayTimecode = curTime);
+ return Doc.AddDocToList(this.fieldExtensionDoc, this.props.fieldExt, 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}
+ 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.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..7c7f9fb83 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 { documentSchema } from "./DocumentView";
+import { DocAnnotatableComponent } from "../DocComponent";
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(fieldExt?: string) { return FieldView.LayoutString(WebBox, "data", fieldExt); }
@observable private collapsed: boolean = true;
@observable private url: string = "";
@@ -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,29 @@ 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() {
+ Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
+ return (<div className={"imageBox-container"} >
+ <CollectionFreeFormView {...this.props}
+ PanelHeight={this.props.PanelHeight}
+ PanelWidth={this.props.PanelWidth}
+ 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/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 f7f52b3ef..ad6240c70 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -11,7 +11,7 @@ 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;
@@ -29,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;
@@ -51,10 +51,10 @@ 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);
}
}
);
@@ -66,12 +66,12 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
}
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);
@@ -94,17 +94,19 @@ 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 annoGroup = await Cast(this.props.document.group, Doc);
if (annoGroup) {
- DocumentManager.Instance.FollowLink(annoGroup,
+ DocumentManager.Instance.FollowLink(undefined, annoGroup,
(doc: Doc, maxLocation: string) => this.props.addDocTab(doc, undefined, e.ctrlKey ? "onRight" : "inTab"),
false, false, undefined);
}
}
}
+
addTag = (key: string, value: string): boolean => {
let group = FieldValue(Cast(this.props.document.group, Doc));
if (group) {
@@ -122,7 +124,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.tsx b/src/client/views/pdf/PDFMenu.tsx
index 1997ee0f5..c64741769 100644
--- a/src/client/views/pdf/PDFMenu.tsx
+++ b/src/client/views/pdf/PDFMenu.tsx
@@ -5,6 +5,7 @@ import { observer } from "mobx-react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { unimplementedFunction, returnFalse } from "../../../Utils";
import AntimodeMenu from "../AntimodeMenu";
+import { Doc, Opt } from "../../../new_fields/Doc";
@observer
export default class PDFMenu extends AntimodeMenu {
@@ -21,7 +22,7 @@ export default class PDFMenu extends AntimodeMenu {
@observable public Status: "pdf" | "annotation" | "snippet" | "" = "";
public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction;
- public Highlight: (color: string) => void = unimplementedFunction;
+ public Highlight: (color: string) => Opt<Doc> = (color: string) => undefined;
public Delete: () => void = unimplementedFunction;
public Snippet: (marquee: { left: number, top: number, width: number, height: number }) => void = unimplementedFunction;
public AddTag: (key: string, value: string) => boolean = returnFalse;
@@ -70,12 +71,8 @@ export default class PDFMenu extends AntimodeMenu {
@action
highlightClicked = (e: React.MouseEvent) => {
- if (!this.Pinned) {
- this.Highlight("#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("#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 20dfc4d8c..1bae6128c 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -6,10 +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 } 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 { 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";
@@ -18,25 +18,29 @@ 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 { documentSchema } from "../nodes/DocumentView";
const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer");
const pdfjsLib = require("pdfjs-dist");
pdfjsLib.GlobalWorkerOptions.workerSrc = `/assets/pdf.worker.js`;
+type PdfDocument = makeInterface<[typeof documentSchema]>;
+const PdfDocument = makeInterface(documentSchema);
interface IViewerProps {
pdf: Pdfjs.PDFDocumentProxy;
url: string;
- Document: Doc;
- DataDoc?: Doc;
- fieldExtensionDoc: Doc;
fieldKey: string;
fieldExt: string;
+ Document: Doc;
+ DataDoc?: Doc;
+ ContainingCollectionView: Opt<CollectionView>;
+ extensionDoc: Doc;
PanelWidth: () => number;
PanelHeight: () => number;
ContentScaling: () => number;
@@ -50,10 +54,9 @@ interface IViewerProps {
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 +64,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 +79,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,7 +100,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
private _coverPath: any;
@computed get allAnnotations() {
- return DocListCast(this.props.fieldExtensionDoc.annotations).filter(
+ return DocListCast(this.props.extensionDoc.annotations).filter(
anno => this._script.run({ this: anno }, console.log, true).result);
}
@@ -103,13 +108,27 @@ export class PDFViewer extends React.Component<IViewerProps> {
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`)));
runInAction(() => this._showWaiting = this._showCover = true);
this.props.startupLive && this.setupPdfJsViewer();
- this._selectionReactionDisposer = reaction(() => this.props.isSelected(), () => (this.props.isSelected() && SelectionManager.SelectedDocuments().length === 1) && this.setupPdfJsViewer(), { fireImmediately: true });
+ this._searchReactionDisposer = reaction(() => StrCast(this.props.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,
(scrollY) => {
@@ -130,20 +149,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("#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();
}
}
- setSelectionText = (text: string) => this._selectionText = text;
-
@action
initialLoad = async () => {
if (this._pageSizes.length === 0) {
@@ -170,7 +191,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
await this.initialLoad();
this._annotationReactionDisposer = reaction(
- () => this.props.fieldExtensionDoc && DocListCast(this.props.fieldExtensionDoc.annotations),
+ () => this.props.extensionDoc && DocListCast(this.props.extensionDoc.annotations),
annotations => annotations && annotations.length && this.renderAnnotations(annotations, true),
{ fireImmediately: true });
@@ -201,7 +222,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
document.removeEventListener("copy", this.copy);
document.addEventListener("copy", this.copy);
document.addEventListener("pagesinit", action(() => {
- this.pdfViewer.currentScaleValue = this._zoomed = 1;
+ this._pdfViewer.currentScaleValue = this._zoomed = 1;
this.gotoPage(NumCast(this.props.Document.curPage, 1));
}));
document.addEventListener("pagerendered", action(() => this._showCover = this._showWaiting = false));
@@ -209,36 +230,36 @@ export class PDFViewer extends React.Component<IViewerProps> {
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 = (color: string): 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 minY = Number.MAX_VALUE;
- if (this._savedAnnotations.size() === 1 && this._savedAnnotations.values()[0].length === 1) {
+ 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 " + StrCast(this.props.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.group = mainAnnoDoc;
- annoDoc.color = color;
- annoDoc.type = AnnotationTypes.Region;
- annoDocs.push(annoDoc);
annoDoc.isButton = true;
+ annoDocs.push(annoDoc);
anno.remove();
mainAnnoDoc = annoDoc;
mainAnnoDocProto = Doc.GetProto(mainAnnoDoc);
@@ -251,8 +272,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
if (anno.style.height) annoDoc.height = parseInt(anno.style.height);
if (anno.style.width) annoDoc.width = parseInt(anno.style.width);
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));
@@ -293,36 +313,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.props.Document.curPage = this._pdfViewer.currentPageNumber);
}
// get the page index that the vertical offset passed in is on
@@ -342,6 +349,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);
@@ -358,8 +367,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,
@@ -369,7 +378,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,
@@ -387,36 +396,31 @@ export class PDFViewer extends React.Component<IViewerProps> {
// if alt+left click, drag and annotate
this._downX = e.clientX;
this._downY = e.clientY;
+ addStyleSheetRule(PDFViewer._annotationStyle, "pdfAnnotation", { "pointer-events": "none" });
if (NumCast(this.props.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);
@@ -451,12 +455,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();
@@ -467,8 +472,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
@@ -480,22 +484,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) {
@@ -504,8 +509,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();
@@ -516,8 +520,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
}
}
- if (PDFMenu.Instance.Highlighting) {
- this.highlight("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;
@@ -531,7 +535,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
highlight = (color: string) => {
// creates annotation documents for current highlights
let annotationDoc = this.makeAnnotationDocument(color);
- Doc.AddDocToList(this.props.fieldExtensionDoc, this.props.fieldExt, annotationDoc);
+ annotationDoc && Doc.AddDocToList(this.props.extensionDoc, this.props.fieldExt, annotationDoc);
return annotationDoc;
}
@@ -544,16 +548,18 @@ export class PDFViewer extends React.Component<IViewerProps> {
e.preventDefault();
e.stopPropagation();
let targetDoc = Docs.Create.TextDocument({ width: 200, height: 200, title: "Note linked to " + this.props.Document.title });
- let annotationDoc = this.highlight("red");
- 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 ${StrCast(this.props.Document.title)}`, "link from PDF")
-
- },
- hideSource: false
- });
+ 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 ${StrCast(this.props.Document.title)}`, "link from PDF")
+
+ },
+ hideSource: false
+ });
+ }
}
createSnippet = (marquee: { left: number, top: number, width: number, height: number }): void => {
@@ -569,36 +575,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
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 {
- Doc.GetProto(doc).annotationOn = undefined;
- //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 &&
@@ -607,13 +585,9 @@ 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) {
@@ -628,14 +602,13 @@ export class PDFViewer extends React.Component<IViewerProps> {
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);
}
}
@@ -643,33 +616,12 @@ export class PDFViewer extends React.Component<IViewerProps> {
return <div className="pdfViewer-annotationLayer" style={{ height: NumCast(this.props.Document.nativeHeight) }} ref={this._annotationLayer}>
{this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map((anno, index) =>
<Annotation {...this.props} focus={this.props.focus} anno={anno} key={`${anno[Id]}-annotation`} />)}
- </div>;
- }
- @computed get pdfViewerDiv() {
- return <div className="pdfViewer-text" ref={this._viewer} style={{ transformOrigin: "left top" }} />;
- }
- @computed get standinViews() {
- return <>
- {this._showCover ? this.getCoverImage() : (null)}
- {this._showWaiting ? <img className="pdfViewer-waiting" key="waiting" src={"/assets/loading.gif"} /> : (null)}
- </>;
- }
- marqueeWidth = () => this._marqueeWidth;
- marqueeHeight = () => this._marqueeHeight;
- marqueeX = () => this._marqueeX;
- marqueeY = () => this._marqueeY;
- marqueeing = () => this._marqueeing;
- 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}>
- {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}
+ <div className="pdfViewer-overlay" id="overlay" style={{ transform: `scale(${this._zoomed})` }}>
<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)}
- VisibleHeight={() => this.props.PanelHeight() / this.props.ContentScaling() * 72 / 96}
+ VisibleHeight={this.visibleHeight}
focus={this.props.focus}
isSelected={this.props.isSelected}
select={emptyFunction}
@@ -678,8 +630,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
whenActiveChanged={this.whenActiveChanged}
removeDocument={this.removeDocument}
moveDocument={this.moveDocument}
- addDocument={(doc: Doc, allow: boolean | undefined) => { Doc.GetProto(doc).annotationOn = this.props.Document; Doc.AddDocToList(this.props.fieldExtensionDoc, this.props.fieldExt, doc); return true; }}
- CollectionView={this.props.ContainingCollectionView}
+ addDocument={this.addDocument}
+ CollectionView={undefined}
ScreenToLocalTransform={this.scrollXf}
ruleProvider={undefined}
renderDepth={this.props.renderDepth + 1}
@@ -687,7 +639,30 @@ export class PDFViewer extends React.Component<IViewerProps> {
chromeCollapsed={true}>
</CollectionFreeFormView>
</div>
+ </div>;
+ }
+ @computed get pdfViewerDiv() {
+ return <div className="pdfViewer-text" ref={this._viewer} style={{ transformOrigin: "left top" }} />;
+ }
+ @computed get standinViews() {
+ return <>
+ {this._showCover ? this.getCoverImage() : (null)}
+ {this._showWaiting ? <img className="pdfViewer-waiting" key="waiting" src={"/assets/loading.gif"} /> : (null)}
+ </>;
+ }
+ marqueeWidth = () => this._marqueeWidth;
+ marqueeHeight = () => this._marqueeHeight;
+ 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}>
+ {this.pdfViewerDiv}
+ {this.annotationLayer}
{this.standinViews}
+ <PdfViewerMarquee isMarqueeing={this.marqueeing} width={this.marqueeWidth} height={this.marqueeHeight} x={this.marqueeX} y={this.marqueeY} />
</div >);
}
}
@@ -707,11 +682,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/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 0dd4d3dc5..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;
@@ -49,17 +58,17 @@
}
.searchBox-quickFilter {
- width: 500px;
- margin-left: 25px;
+ 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 be75a29e0..b728ffaa9 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -141,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(" ")})` : "");
}
@@ -346,8 +346,6 @@ export class SearchBox extends React.Component {
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-filter" title="Advanced Filtering Options" onClick={FilterBox.Instance.openFilter} onPointerDown={FilterBox.Instance.stopProp}><FontAwesomeIcon icon="ellipsis-v" color="white" /></button>
- <button className="searchBox-barChild searchBox-submit" onClick={this.submitSearch} onPointerDown={FilterBox.Instance.stopProp}>Submit</button>
- <button className="searchBox-barChild searchBox-close" title={"Close Search Bar"} onPointerDown={MainView.Instance.toggleSearch}><FontAwesomeIcon icon={faTimes} size="lg" /></button>
</div>
{(this._numTotalResults > 0 || !this._searchbarOpen) ? (null) :
(<div className="searchBox-quickFilter" onPointerDown={this.openSearch}>
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..b8cff16f2 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";
@@ -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/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 58304cebb..66036f673 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -63,6 +63,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 +332,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) {
@@ -637,7 +643,7 @@ export namespace Doc {
export function isBrushedHighlightedDegree(doc: Doc) {
if (Doc.IsHighlighted(doc)) {
- return 3;
+ return 6;
}
else {
return Doc.IsBrushedDegree(doc);
@@ -673,6 +679,21 @@ export namespace Doc {
return doc;
}
+ 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 {
@observable HighlightedDoc: Map<Doc, boolean> = new Map();
}
@@ -709,6 +730,7 @@ export namespace Doc {
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 getAlias(doc: any) { return Doc.MakeAlias(doc); });
+Scripting.addGlobal(function getCopy(doc: any, copyProto: any) { return 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
diff --git a/src/new_fields/InkField.ts b/src/new_fields/InkField.ts
index 8f64c1c2e..e381d0218 100644
--- a/src/new_fields/InkField.ts
+++ b/src/new_fields/InkField.ts
@@ -16,7 +16,7 @@ export interface StrokeData {
color: string;
width: string;
tool: InkTool;
- page: number;
+ displayTimecode: 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/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/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 b2509a4f1..3858907ba 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -12,6 +12,7 @@ import { listSpec } from "../../../new_fields/Schema";
import { Cast, StrCast, PromiseValue } from "../../../new_fields/Types";
import { Utils } from "../../../Utils";
import { RouteStore } from "../../RouteStore";
+import { ScriptField } from "../../../new_fields/ScriptField";
export class CurrentUserUtils {
private static curr_id: string;
@@ -29,7 +30,6 @@ export class CurrentUserUtils {
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);
@@ -37,7 +37,9 @@ export class CurrentUserUtils {
doc.gridGap = 5;
doc.xMargin = 5;
doc.yMargin = 5;
+ doc.height = 42;
doc.boxShadow = "0 0";
+ doc.convertToButtons = true; // for CollectionLinearView used as the docButton layout
doc.optionalRightCollection = Docs.Create.StackingDocument([], { title: "New mobile uploads" });
return doc;
}
@@ -46,7 +48,7 @@ export class CurrentUserUtils {
// setup workspaces library item
if (doc.workspaces === undefined) {
- const workspaces = Docs.Create.TreeDocument([], { title: "Workspaces".toUpperCase(), height: 100 });
+ const workspaces = Docs.Create.TreeDocument([], { title: "WORKSPACES", height: 100 });
workspaces.boxShadow = "0 0";
doc.workspaces = workspaces;
}
@@ -98,21 +100,70 @@ export class CurrentUserUtils {
doc.curPresentation = curPresentation;
}
- 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;
- sidebar.boxShadow = "1 1 3";
- doc.sidebar = sidebar;
- }
- PromiseValue(Cast(doc.sidebar, Doc)).then(sidebar => {
- if (sidebar) {
- sidebar.backgroundColor = "lightgrey";
+ if (doc.Library === undefined) {
+ let Search = Docs.Create.ButtonDocument({ width: 50, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Search" });
+ let Library = Docs.Create.ButtonDocument({ width: 50, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Library" });
+ let Create = Docs.Create.ButtonDocument({ width: 35, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Create" });
+ if (doc.sidebarContainer === undefined) {
+ doc.sidebarContainer = new Doc();
+ (doc.sidebarContainer as Doc).chromeStatus = "disabled";
}
- })
+
+ const library = Docs.Create.TreeDocument([doc.workspaces as Doc, doc, doc.recentlyClosed as Doc], { title: "Library" });
+ library.forceActive = true;
+ library.lockedPosition = true;
+ library.gridGap = 5;
+ library.xMargin = 5;
+ library.yMargin = 5;
+ library.dropAction = "alias";
+ Library.targetContainer = doc.sidebarContainer;
+ Library.library = library;
+ Library.onClick = ScriptField.MakeScript("this.targetContainer.proto = this.library");
+
+ const searchBox = Docs.Create.QueryDocument({ title: "search stack" });
+ searchBox.ignoreClick = true;
+ Search.searchBox = searchBox;
+ Search.targetContainer = doc.sidebarContainer;
+ Search.onClick = ScriptField.MakeScript("this.targetContainer.proto = this.searchBox");
+
+ let createCollection = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Collection", icon: "folder" });
+ createCollection.onDragStart = ScriptField.MakeFunction('Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" })');
+ let createWebPage = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Web Page", icon: "globe-asia" });
+ createWebPage.onDragStart = ScriptField.MakeFunction('Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" })');
+ let createCatImage = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Image", icon: "cat" });
+ createCatImage.onDragStart = ScriptField.MakeFunction('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" })');
+ let createButton = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Button", icon: "bolt" });
+ createButton.onDragStart = ScriptField.MakeFunction('Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" })');
+ let createPresentation = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Presentation", icon: "tv" });
+ createPresentation.onDragStart = ScriptField.MakeFunction('Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List<Doc>(), { width: 200, height: 500, title: "a presentation trail" })');
+ let createFolderImport = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Import Folder", icon: "cloud-upload-alt" });
+ createFolderImport.onDragStart = ScriptField.MakeFunction('Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })');
+ const dragCreators = Docs.Create.MasonryDocument([createCollection, createWebPage, createCatImage, createButton, createPresentation, createFolderImport], { width: 500, autoHeight: true, columnWidth: 35, ignoreClick: true, lockedPosition: true, chromeStatus: "disabled", title: "buttons" });
+ const color = Docs.Create.ColorDocument({ title: "color picker", width: 400 });
+ color.dropAction = "alias";
+ color.ignoreClick = true;
+ color.removeDropProperties = new List<string>(["dropAction", "ignoreClick"]);
+ const creators = Docs.Create.StackingDocument([dragCreators, color], { width: 500, height: 800, chromeStatus: "disabled", title: "creator stack" });
+ Create.targetContainer = doc.sidebarContainer;
+ Create.creators = creators;
+ Create.onClick = ScriptField.MakeScript("this.targetContainer.proto = this.creators");
+
+ const libraryButtons = Docs.Create.StackingDocument([Search, Library, Create], { width: 500, height: 80, chromeStatus: "disabled", title: "library stack" });
+ libraryButtons.sectionFilter = "title";
+ libraryButtons.boxShadow = "0 0";
+ libraryButtons.ignoreClick = true;
+ libraryButtons.hideHeadings = true;
+ libraryButtons.backgroundColor = "lightgrey";
+
+ doc.libraryButtons = libraryButtons;
+ doc.Library = Library;
+ doc.Create = Create;
+ doc.Search = Search;
+ }
+ PromiseValue(Cast(doc.libraryButtons, Doc)).then(libraryButtons => { });
+ PromiseValue(Cast(doc.Library, Doc)).then(library => library && library.library && library.targetContainer && (library.onClick as ScriptField).script.run({ this: library }));
+ PromiseValue(Cast(doc.Create, Doc)).then(async create => create && create.creators && create.targetContainer);
+ PromiseValue(Cast(doc.Search, Doc)).then(async search => search && search.searchBox && search.targetContainer);
if (doc.overlays === undefined) {
const overlays = Docs.Create.FreeformDocument([], { title: "Overlays" });
@@ -124,8 +175,7 @@ export class CurrentUserUtils {
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" })));
}
- StrCast(doc.title).indexOf("@") !== -1 && (doc.title = (StrCast(doc.title).split("@")[0] + "'s Library").toUpperCase());
- StrCast(doc.title).indexOf("'s Library") !== -1 && (doc.title = StrCast(doc.title).toUpperCase());
+ doc.title = "DOCUMENTS";
doc.backgroundColor = "#eeeeee";
doc.width = 100;
doc.preventTreeViewOpen = true;
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 690836fff..010a851bc 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -53,6 +53,8 @@ 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';
const download = (url: string, dest: fs.PathLike) => request.get(url).pipe(fs.createWriteStream(dest));
let youtubeApiKey: string;
@@ -211,7 +213,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 +320,76 @@ 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>;
+
+app.get(`${RouteStore.imageHierarchyExport}/:docId`, async (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");
@@ -590,10 +661,11 @@ 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
@@ -604,10 +676,11 @@ app.post(
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);
@@ -622,9 +695,10 @@ app.post(
});
});
} 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);
@@ -632,6 +706,15 @@ app.post(
}
);
+app.post(RouteStore.inspectImage, async (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.POST,
(user, res, req) => {
@@ -862,7 +945,21 @@ 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)));
+app.get(RouteStore.readGoogleAccessToken, async (req, res) => {
+ const userId = req.header("userId")!;
+ 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));
+});
+
+app.post(RouteStore.writeGoogleAccessToken, async (req, res) => {
+ const userId = req.header("userId")!;
+ 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!";
@@ -885,16 +982,17 @@ app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => {
await GooglePhotosUploadUtils.initialize({ credentialsPath, userId });
- let failed = 0;
+ 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 element of batch) {
+ for (let index = 0; index < batch.length; index++) {
+ const element = batch[index];
const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(element.url);
if (!uploadToken) {
- failed++;
+ failed.push(index);
} else {
newMediaItems.push({
description: element.description,
@@ -906,12 +1004,13 @@ app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => {
}
);
- if (failed) {
- return _error(res, tokenError);
+ const failedCount = failed.length;
+ if (failedCount) {
+ console.log(`Unable to upload ${failedCount} image${failedCount === 1 ? "" : "s"} to Google's servers`);
}
GooglePhotosUploadUtils.CreateMediaItems(newMediaItems, req.body.album).then(
- result => _success(res, result.newMediaItemResults),
+ result => _success(res, { results: result.newMediaItemResults, failed }),
error => _error(res, mediaError, error)
);
});