aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Utils.ts29
-rw-r--r--src/client/DocServer.ts8
-rw-r--r--src/client/cognitive_services/CognitiveServices.ts135
-rw-r--r--src/client/documents/Documents.ts89
-rw-r--r--src/client/goldenLayout.js8
-rw-r--r--src/client/util/History.ts2
-rw-r--r--src/client/util/Import & Export/DirectoryImportBox.tsx4
-rw-r--r--src/client/util/Scripting.ts69
-rw-r--r--src/client/util/SearchUtil.ts3
-rw-r--r--src/client/util/TooltipTextMenu.tsx8
-rw-r--r--src/client/views/DocumentDecorations.tsx14
-rw-r--r--src/client/views/EditableView.scss1
-rw-r--r--src/client/views/EditableView.tsx3
-rw-r--r--src/client/views/GlobalKeyHandler.ts4
-rw-r--r--src/client/views/MainView.tsx4
-rw-r--r--src/client/views/OverlayView.scss42
-rw-r--r--src/client/views/OverlayView.tsx111
-rw-r--r--src/client/views/ScriptingRepl.scss50
-rw-r--r--src/client/views/ScriptingRepl.tsx256
-rw-r--r--src/client/views/SearchBox.tsx3
-rw-r--r--src/client/views/collections/CollectionBaseView.tsx6
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx2
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx12
-rw-r--r--src/client/views/collections/CollectionStackingView.scss13
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx102
-rw-r--r--src/client/views/collections/CollectionSubView.tsx11
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx79
-rw-r--r--src/client/views/collections/CollectionVideoView.tsx40
-rw-r--r--src/client/views/collections/CollectionView.tsx27
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx19
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx2
-rw-r--r--src/client/views/nodes/DocumentView.tsx41
-rw-r--r--src/client/views/nodes/FaceRectangle.tsx29
-rw-r--r--src/client/views/nodes/FaceRectangles.tsx46
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx11
-rw-r--r--src/client/views/nodes/ImageBox.scss18
-rw-r--r--src/client/views/nodes/ImageBox.tsx53
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx2
-rw-r--r--src/client/views/nodes/KeyValuePair.tsx29
-rw-r--r--src/client/views/nodes/VideoBox.tsx11
-rw-r--r--src/client/views/nodes/WebBox.tsx16
-rw-r--r--src/client/views/pdf/PDFViewer.tsx55
-rw-r--r--src/client/views/presentationview/PresentationElement.tsx464
-rw-r--r--src/client/views/presentationview/PresentationList.tsx50
-rw-r--r--src/client/views/presentationview/PresentationView.scss8
-rw-r--r--src/client/views/presentationview/PresentationView.tsx50
-rw-r--r--src/client/views/search/SearchBox.tsx6
-rw-r--r--src/mobile/ImageUpload.tsx5
-rw-r--r--src/new_fields/Doc.ts60
-rw-r--r--src/new_fields/InkField.ts4
-rw-r--r--src/new_fields/ScriptField.ts32
-rw-r--r--src/new_fields/util.ts28
-rw-r--r--[-rwxr-xr-x]src/scraping/acm/chromedriverbin10256192 -> 10256192 bytes
-rw-r--r--src/server/RouteStore.ts3
-rw-r--r--src/server/authentication/controllers/user_controller.ts3
-rw-r--r--src/server/authentication/models/current_user_utils.ts5
-rw-r--r--src/server/index.ts51
57 files changed, 1855 insertions, 381 deletions
diff --git a/src/Utils.ts b/src/Utils.ts
index e8a80bdc3..8df67df5d 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -2,6 +2,7 @@ import v4 = require('uuid/v4');
import v5 = require("uuid/v5");
import { Socket } from 'socket.io';
import { Message } from './server/Message';
+import { RouteStore } from './server/RouteStore';
export class Utils {
@@ -27,6 +28,18 @@ export class Utils {
return { scale, translateX, translateY };
}
+ /**
+ * A convenience method. Prepends the full path (i.e. http://localhost:1050) to the
+ * requested extension
+ * @param extension the specified sub-path to append to the window origin
+ */
+ public static prepend(extension: string): string {
+ return window.location.origin + extension;
+ }
+ public static CorsProxy(url: string): string {
+ return this.prepend(RouteStore.corsProxy + "/") + encodeURIComponent(url);
+ }
+
public static CopyText(text: string) {
var textArea = document.createElement("textarea");
textArea.value = text;
@@ -133,7 +146,7 @@ export type Without<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
export type Predicate<K, V> = (entry: [K, V]) => boolean;
-export function deepCopy<K, V>(source: Map<K, V>, predicate?: Predicate<K, V>) {
+export function DeepCopy<K, V>(source: Map<K, V>, predicate?: Predicate<K, V>) {
let deepCopy = new Map<K, V>();
let entries = source.entries(), next = entries.next();
while (!next.done) {
@@ -144,4 +157,18 @@ export function deepCopy<K, V>(source: Map<K, V>, predicate?: Predicate<K, V>) {
next = entries.next();
}
return deepCopy;
+}
+
+export namespace JSONUtils {
+
+ export function tryParse(source: string) {
+ let results: any;
+ try {
+ results = JSON.parse(source);
+ } catch (e) {
+ results = source;
+ }
+ return results;
+ }
+
} \ No newline at end of file
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index 6737657c8..8c64d2b2f 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -47,14 +47,6 @@ export namespace DocServer {
Utils.AddServerHandler(_socket, MessageStore.DeleteField, respondToDelete);
Utils.AddServerHandler(_socket, MessageStore.DeleteFields, respondToDelete);
}
- /**
- * A convenience method. Prepends the full path (i.e. http://localhost:1050) to the
- * requested extension
- * @param extension the specified sub-path to append to the window origin
- */
- export function prepend(extension: string): string {
- return window.location.origin + extension;
- }
function errorFunc(): never {
throw new Error("Can't use DocServer without calling init first");
diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts
new file mode 100644
index 000000000..d4085cf76
--- /dev/null
+++ b/src/client/cognitive_services/CognitiveServices.ts
@@ -0,0 +1,135 @@
+import * as request from "request-promise";
+import { Doc, Field } from "../../new_fields/Doc";
+import { Cast } from "../../new_fields/Types";
+import { ImageField } from "../../new_fields/URLField";
+import { List } from "../../new_fields/List";
+import { Docs } from "../documents/Documents";
+import { RouteStore } from "../../server/RouteStore";
+import { Utils } from "../../Utils";
+import { CompileScript } from "../util/Scripting";
+import { ComputedField } from "../../new_fields/ScriptField";
+
+export enum Services {
+ ComputerVision = "vision",
+ Face = "face"
+}
+
+export enum Confidence {
+ Yikes = 0.0,
+ Unlikely = 0.2,
+ Poor = 0.4,
+ Fair = 0.6,
+ Good = 0.8,
+ Excellent = 0.95
+}
+
+export type Tag = { name: string, confidence: number };
+export type Rectangle = { top: number, left: number, width: number, height: number };
+export type Face = { faceAttributes: any, faceId: string, faceRectangle: Rectangle };
+export type Converter = (results: any) => Field;
+
+/**
+ * A file that handles all interactions with Microsoft Azure's Cognitive
+ * Services APIs. These machine learning endpoints allow basic data analytics for
+ * various media types.
+ */
+export namespace CognitiveServices {
+
+ export namespace Image {
+
+ export const analyze = async (imageUrl: string, service: Services) => {
+ return fetch(Utils.prepend(`${RouteStore.cognitiveServices}/${service}`)).then(async response => {
+ let apiKey = await response.text();
+ if (!apiKey) {
+ return undefined;
+ }
+ let uriBase;
+ let parameters;
+
+ switch (service) {
+ case Services.Face:
+ uriBase = 'face/v1.0/detect';
+ parameters = {
+ 'returnFaceId': 'true',
+ 'returnFaceLandmarks': 'false',
+ 'returnFaceAttributes': 'age,gender,headPose,smile,facialHair,glasses,' +
+ 'emotion,hair,makeup,occlusion,accessories,blur,exposure,noise'
+ };
+ break;
+ case Services.ComputerVision:
+ uriBase = 'vision/v2.0/analyze';
+ parameters = {
+ 'visualFeatures': 'Categories,Description,Color,Objects,Tags,Adult',
+ 'details': 'Celebrities,Landmarks',
+ 'language': 'en',
+ };
+ break;
+ }
+
+ const options = {
+ uri: 'https://eastus.api.cognitive.microsoft.com/' + uriBase,
+ qs: parameters,
+ body: `{"url": "${imageUrl}"}`,
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Ocp-Apim-Subscription-Key': apiKey
+ }
+ };
+
+ let results: any;
+ try {
+ results = await request.post(options).then(response => JSON.parse(response));
+ } catch (e) {
+ results = undefined;
+ }
+ return results;
+ });
+ };
+
+ const analyzeDocument = async (target: Doc, service: Services, converter: Converter, storageKey: string) => {
+ let imageData = Cast(target.data, ImageField);
+ if (!imageData || await Cast(target[storageKey], Doc)) {
+ return;
+ }
+ let toStore: any;
+ let results = await analyze(imageData.url.href, service);
+ if (!results) {
+ toStore = "Cognitive Services could not process the given image URL.";
+ } else {
+ if (!results.length) {
+ toStore = converter(results);
+ } else {
+ toStore = results.length > 0 ? converter(results) : "Empty list returned.";
+ }
+ }
+ target[storageKey] = toStore;
+ };
+
+ export const generateMetadata = async (target: Doc, threshold: Confidence = Confidence.Excellent) => {
+ let converter = (results: any) => {
+ let tagDoc = new Doc;
+ results.tags.map((tag: Tag) => {
+ let sanitized = tag.name.replace(" ", "_");
+ let script = `return (${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`;
+ let computed = CompileScript(script, { params: { this: "Doc" } });
+ computed.compiled && (tagDoc[sanitized] = new ComputedField(computed));
+ });
+ tagDoc.title = "Generated Tags";
+ tagDoc.confidence = threshold;
+ return tagDoc;
+ };
+ analyzeDocument(target, Services.ComputerVision, converter, "generatedTags");
+ };
+
+ export const extractFaces = async (target: Doc) => {
+ let converter = (results: any) => {
+ let faceDocs = new List<Doc>();
+ results.map((face: Face) => faceDocs.push(Docs.Get.DocumentHierarchyFromJson(face, `Face: ${face.faceId}`)!));
+ return faceDocs;
+ };
+ analyzeDocument(target, Services.Face, converter, "faces");
+ };
+
+ }
+
+} \ No newline at end of file
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index af2b95659..7563fda20 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -21,7 +21,7 @@ import { AggregateFunction } from "../northstar/model/idea/idea";
import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss";
import { IconBox } from "../views/nodes/IconBox";
import { Field, Doc, Opt } from "../../new_fields/Doc";
-import { OmitKeys } from "../../Utils";
+import { OmitKeys, JSONUtils } from "../../Utils";
import { ImageField, VideoField, AudioField, PdfField, WebField } from "../../new_fields/URLField";
import { HtmlField } from "../../new_fields/HtmlField";
import { List } from "../../new_fields/List";
@@ -55,7 +55,8 @@ export enum DocumentType {
ICON = "icon",
IMPORT = "import",
LINK = "link",
- LINKDOC = "linkdoc"
+ LINKDOC = "linkdoc",
+ TEMPLATE = "template"
}
export interface DocumentOptions {
@@ -311,8 +312,9 @@ export namespace Docs {
}
export function ImageDocument(url: string, options: DocumentOptions = {}) {
- let inst = InstanceFromProto(Prototypes.get(DocumentType.IMG), new ImageField(new URL(url)), { title: path.basename(url), ...options });
- requestImageSize(window.origin + RouteStore.corsProxy + "/" + url)
+ let imgField = new ImageField(new URL(url));
+ let inst = InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: path.basename(url), ...options });
+ requestImageSize(imgField.url.href)
.then((size: any) => {
let aspect = size.height / size.width;
if (!inst.proto!.nativeWidth) {
@@ -438,6 +440,85 @@ export namespace Docs {
export namespace Get {
+ const primitives = ["string", "number", "boolean"];
+
+ /**
+ * This function takes any valid JSON(-like) data, i.e. parsed or unparsed, and at arbitrarily
+ * deep levels of nesting, converts the data and structure into nested documents with the appropriate fields.
+ *
+ * After building a hierarchy within / below a top-level document, it then returns that top-level parent.
+ *
+ * If we've received a string, treat it like valid JSON and try to parse it into an object. If this fails, the
+ * string is invalid JSON, so we should assume that the input is the result of a JSON.parse()
+ * call that returned a regular string value to be stored as a Field.
+ *
+ * If we've received something other than a string, since the caller might also pass in the results of a
+ * JSON.parse() call, valid input might be an object, an array (still typeof object), a boolean or a number.
+ * Anything else (like a function, etc. passed in naively as any) is meaningless for this operation.
+ *
+ * All TS/JS objects get converted directly to documents, directly preserving the key value structure. Everything else,
+ * lacking the key value structure, gets stored as a field in a wrapper document.
+ *
+ * @param input for convenience and flexibility, either a valid JSON string to be parsed,
+ * or the result of any JSON.parse() call.
+ * @param title an optional title to give to the highest parent document in the hierarchy
+ */
+ export function DocumentHierarchyFromJson(input: any, title?: string): Opt<Doc> {
+ if (input === null || ![...primitives, "object"].includes(typeof input)) {
+ return undefined;
+ }
+ let parsed: any = typeof input === "string" ? JSONUtils.tryParse(input) : input;
+ let converted: Doc;
+ if (typeof parsed === "object" && !(parsed instanceof Array)) {
+ converted = convertObject(parsed, title);
+ } else {
+ (converted = new Doc).json = toField(parsed);
+ }
+ title && (converted.title = title);
+ return converted;
+ }
+
+ /**
+ * For each value of the object, recursively convert it to its appropriate field value
+ * and store the field at the appropriate key in the document if it is not undefined
+ * @param object the object to convert
+ * @returns the object mapped from JSON to field values, where each mapping
+ * might involve arbitrary recursion (since toField might itself call convertObject)
+ */
+ const convertObject = (object: any, title?: string): Doc => {
+ let target = new Doc(), result: Opt<Field>;
+ Object.keys(object).map(key => (result = toField(object[key], key)) && (target[key] = result));
+ title && (target.title = title);
+ return target;
+ };
+
+ /**
+ * For each element in the list, recursively convert it to a document or other field
+ * and push the field to the list if it is not undefined
+ * @param list the list to convert
+ * @returns the list mapped from JSON to field values, where each mapping
+ * might involve arbitrary recursion (since toField might itself call convertList)
+ */
+ const convertList = (list: Array<any>): List<Field> => {
+ let target = new List(), result: Opt<Field>;
+ list.map(item => (result = toField(item)) && target.push(result));
+ return target;
+ };
+
+
+ const toField = (data: any, title?: string): Opt<Field> => {
+ if (data === null || data === undefined) {
+ return undefined;
+ }
+ if (primitives.includes(typeof data)) {
+ return data;
+ }
+ if (typeof data === "object") {
+ return data instanceof Array ? convertList(data) : convertObject(data, title);
+ }
+ throw new Error(`How did ${data} of type ${typeof data} end up in JSON?`);
+ };
+
export async function DocumentFromType(type: string, path: string, options: DocumentOptions): Promise<Opt<Doc>> {
let ctor: ((path: string, options: DocumentOptions) => (Doc | Promise<Doc | undefined>)) | undefined = undefined;
if (type.indexOf("image") !== -1) {
diff --git a/src/client/goldenLayout.js b/src/client/goldenLayout.js
index 28c009645..ad78139c1 100644
--- a/src/client/goldenLayout.js
+++ b/src/client/goldenLayout.js
@@ -3995,9 +3995,11 @@
lm.items.AbstractContentItem.prototype.removeChild.call(this, contentItem, keepChild);
if (this.contentItems.length === 1 && this.config.isClosable === true) {
- childItem = this.contentItems[0];
- this.contentItems = [];
- this.parent.replaceChild(this, childItem, true);
+ // bcz: this has the effect of removing children from the DOM and then re-adding them above where they were before.
+ // in the case of things like an iFrame with a YouTube video, the video will reload for now reason. So let's try leaving these "empty" rows alone.
+ // childItem = this.contentItems[0];
+ // this.contentItems = [];
+ // this.parent.replaceChild(this, childItem, true);
} else {
this.callDownwards('setSize');
this.emitBubblingEvent('stateChanged');
diff --git a/src/client/util/History.ts b/src/client/util/History.ts
index cbf5b3fc8..e9ff21b22 100644
--- a/src/client/util/History.ts
+++ b/src/client/util/History.ts
@@ -129,7 +129,7 @@ export namespace HistoryUtil {
function addStringifier(type: string, keys: string[], customStringifier?: (state: ParsedUrl, current: string) => string) {
stringifiers[type] = state => {
- let path = DocServer.prepend(`/${type}`);
+ let path = Utils.prepend(`/${type}`);
if (customStringifier) {
path = customStringifier(state, path);
}
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index c096e9ceb..75b0b52a7 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx
@@ -98,12 +98,12 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
runInAction(() => this.remaining++);
- let prom = fetch(DocServer.prepend(RouteStore.upload), {
+ let prom = fetch(Utils.prepend(RouteStore.upload), {
method: 'POST',
body: formData
}).then(async (res: Response) => {
(await res.json()).map(action((file: any) => {
- let docPromise = Docs.Get.DocumentFromType(type, DocServer.prepend(file), { nativeWidth: 300, width: 300, title: dropFileName });
+ let docPromise = Docs.Get.DocumentFromType(type, Utils.prepend(file), { nativeWidth: 300, width: 300, title: dropFileName });
docPromise.then(doc => {
doc && docs.push(doc) && runInAction(() => this.remaining--);
});
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index 62c2cfe85..46dc320b0 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -1,5 +1,7 @@
-// import * as ts from "typescript"
-let ts = (window as any).ts;
+import * as ts from "typescript";
+export { ts };
+// export const ts = (window as any).ts;
+
// // @ts-ignore
// import * as typescriptlib from '!!raw-loader!../../../node_modules/typescript/lib/lib.d.ts'
// // @ts-ignore
@@ -55,13 +57,35 @@ export namespace Scripting {
}
scriptingGlobals[n] = obj;
}
+
+ export function makeMutableGlobalsCopy(globals?: { [name: string]: any }) {
+ return { ..._scriptingGlobals, ...(globals || {}) };
+ }
+
+ export function setScriptingGlobals(globals: { [key: string]: any }) {
+ scriptingGlobals = globals;
+ }
+
+ export function resetScriptingGlobals() {
+ scriptingGlobals = _scriptingGlobals;
+ }
+
+ // const types = Object.keys(ts.SyntaxKind).map(kind => ts.SyntaxKind[kind]);
+ export function printNodeType(node: any, indentation = "") {
+ console.log(indentation + ts.SyntaxKind[node.kind]);
+ }
+
+ export function getGlobals() {
+ return Object.keys(scriptingGlobals);
+ }
}
export function scriptingGlobal(constructor: { new(...args: any[]): any }) {
Scripting.addGlobal(constructor);
}
-const scriptingGlobals: { [name: string]: any } = {};
+const _scriptingGlobals: { [name: string]: any } = {};
+let scriptingGlobals: { [name: string]: any } = _scriptingGlobals;
function Run(script: string | undefined, customParams: string[], diagnostics: any[], originalScript: string, options: ScriptOptions): CompileResult {
const errors = diagnostics.some(diag => diag.category === ts.DiagnosticCategory.Error);
@@ -162,6 +186,8 @@ class ScriptingCompilerHost {
}
}
+export type Traverser = (node: ts.Node, indentation: string) => boolean | void;
+export type TraverserParam = Traverser | { onEnter: Traverser, onLeave: Traverser };
export interface ScriptOptions {
requiredType?: string;
addReturn?: boolean;
@@ -169,10 +195,23 @@ export interface ScriptOptions {
capturedVariables?: { [name: string]: Field };
typecheck?: boolean;
editable?: boolean;
+ traverser?: TraverserParam;
+ transformer?: ts.TransformerFactory<ts.SourceFile>;
+ globals?: { [name: string]: any };
+}
+
+// function forEachNode(node:ts.Node, fn:(node:any) => void);
+function forEachNode(node: ts.Node, onEnter: Traverser, onExit?: Traverser, indentation = "") {
+ return onEnter(node, indentation) || ts.forEachChild(node, (n: any) => {
+ forEachNode(n, onEnter, onExit, indentation + " ");
+ }) || (onExit && onExit(node, indentation));
}
export function CompileScript(script: string, options: ScriptOptions = {}): CompileResult {
const { requiredType = "", addReturn = false, params = {}, capturedVariables = {}, typecheck = true } = options;
+ if (options.globals) {
+ Scripting.setScriptingGlobals(options.globals);
+ }
let host = new ScriptingCompilerHost;
let paramNames: string[] = [];
if ("this" in params || "this" in capturedVariables) {
@@ -192,10 +231,27 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
paramList.push(`${key}: ${capturedVariables[key].constructor.name}`);
}
let paramString = paramList.join(", ");
+ if (options.traverser) {
+ const sourceFile = ts.createSourceFile('script.ts', script, ts.ScriptTarget.ES2015, true);
+ const onEnter = typeof options.traverser === "object" ? options.traverser.onEnter : options.traverser;
+ const onLeave = typeof options.traverser === "object" ? options.traverser.onLeave : undefined;
+ forEachNode(sourceFile, onEnter, onLeave);
+ }
+ if (options.transformer) {
+ const sourceFile = ts.createSourceFile('script.ts', script, ts.ScriptTarget.ES2015, true);
+ const result = ts.transform(sourceFile, [options.transformer]);
+ const transformed = result.transformed;
+ const printer = ts.createPrinter({
+ newLine: ts.NewLineKind.LineFeed
+ });
+ script = printer.printFile(transformed[0]);
+ result.dispose();
+ }
let funcScript = `(function(${paramString})${requiredType ? `: ${requiredType}` : ''} {
${addReturn ? `return ${script};` : script}
})`;
host.writeFile("file.ts", funcScript);
+
if (typecheck) host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib);
let program = ts.createProgram(["file.ts"], {}, host);
let testResult = program.emit();
@@ -203,7 +259,12 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
let diagnostics = ts.getPreEmitDiagnostics(program).concat(testResult.diagnostics);
- return Run(outputText, paramNames, diagnostics, script, options);
+ const result = Run(outputText, paramNames, diagnostics, script, options);
+
+ if (options.globals) {
+ Scripting.resetScriptingGlobals();
+ }
+ return result;
}
Scripting.addGlobal(CompileScript); \ No newline at end of file
diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts
index abf1a7c32..ee5a83710 100644
--- a/src/client/util/SearchUtil.ts
+++ b/src/client/util/SearchUtil.ts
@@ -2,6 +2,7 @@ import * as rp from 'request-promise';
import { DocServer } from '../DocServer';
import { Doc } from '../../new_fields/Doc';
import { Id } from '../../new_fields/FieldSymbols';
+import { Utils } from '../../Utils';
export namespace SearchUtil {
export type HighlightingResult = { [id: string]: { [key: string]: string[] } };
@@ -29,7 +30,7 @@ export namespace SearchUtil {
export function Search(query: string, returnDocs: false, options?: SearchParams): Promise<IdSearchResult>;
export async function Search(query: string, returnDocs: boolean, options: SearchParams = {}) {
query = query || "*"; //If we just have a filter query, search for * as the query
- const result: IdSearchResult = JSON.parse(await rp.get(DocServer.prepend("/search"), {
+ const result: IdSearchResult = JSON.parse(await rp.get(Utils.prepend("/search"), {
qs: { ...options, q: query },
}));
if (!returnDocs) {
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 309d19016..a4c053de2 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -19,7 +19,7 @@ import { CollectionDockingView } from "../views/collections/CollectionDockingVie
import { DocumentManager } from "./DocumentManager";
import { Id } from "../../new_fields/FieldSymbols";
import { FormattedTextBoxProps } from "../views/nodes/FormattedTextBox";
-import { SelectionManager } from "./SelectionManager";
+import { Utils } from "../../Utils";
//appears above a selection of text in a RichTextBox to give user options such as Bold, Italics, etc.
export class TooltipTextMenu {
@@ -213,8 +213,8 @@ export class TooltipTextMenu {
let link = node && node.marks.find(m => m.type.name === "link");
if (link) {
let href: string = link.attrs.href;
- if (href.indexOf(DocServer.prepend("/doc/")) === 0) {
- let docid = href.replace(DocServer.prepend("/doc/"), "");
+ if (href.indexOf(Utils.prepend("/doc/")) === 0) {
+ let docid = href.replace(Utils.prepend("/doc/"), "");
DocServer.GetRefField(docid).then(action((f: Opt<Field>) => {
if (f instanceof Doc) {
if (DocumentManager.Instance.getDocumentView(f)) {
@@ -254,7 +254,7 @@ export class TooltipTextMenu {
if (docView && docView.props.ContainingCollectionView) {
proto.sourceContext = docView.props.ContainingCollectionView.props.Document;
}
- linkDoc instanceof Doc && this.makeLink(DocServer.prepend("/doc/" + linkDoc[Id]), ctrlKey ? "onRight" : "inTab");
+ linkDoc instanceof Doc && this.makeLink(Utils.prepend("/doc/" + linkDoc[Id]), ctrlKey ? "onRight" : "inTab");
}),
},
hideSource: false
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index fb5104915..989b35581 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -28,6 +28,7 @@ import { RichTextField } from '../../new_fields/RichTextField';
import { LinkManager } from '../util/LinkManager';
import { ObjectField } from '../../new_fields/ObjectField';
import { MetadataEntryMenu } from './MetadataEntryMenu';
+import { ImageBox } from './nodes/ImageBox';
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -85,8 +86,13 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
SelectionManager.DeselectAll();
let fieldTemplate = fieldTemplateView.props.Document;
let docTemplate = fieldTemplateView.props.ContainingCollectionView!.props.Document;
- let metaKey = text.slice(1, text.length);
- Doc.MakeTemplate(fieldTemplate, metaKey, Doc.GetProto(docTemplate));
+ let metaKey = text.startsWith(">>") ? text.slice(2, text.length) : text.slice(1, text.length);
+ let proto = Doc.GetProto(docTemplate);
+ Doc.MakeTemplate(fieldTemplate, metaKey, proto);
+ if (text.startsWith(">>")) {
+ proto.detailedLayout = proto.layout;
+ proto.miniLayout = ImageBox.LayoutString(metaKey);
+ }
}
else {
if (SelectionManager.SelectedDocuments().length > 0) {
@@ -519,8 +525,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
let actualdH = Math.max(height + (dH * scale), 20);
doc.x = (doc.x || 0) + dX * (actualdW - width);
doc.y = (doc.y || 0) + dY * (actualdH - height);
- let proto = Doc.GetProto(element.props.Document);
- let fixedAspect = e.ctrlKey || (!BoolCast(proto.ignoreAspect, false) && nwidth && nheight);
+ let proto = doc.isTemplate ? doc : Doc.GetProto(element.props.Document); // bcz: 'doc' didn't work here...
+ let fixedAspect = e.ctrlKey || (!BoolCast(proto.ignoreAspect) && nwidth && nheight);
if (fixedAspect && (!nwidth || !nheight)) {
proto.nativeWidth = nwidth = doc.width || 0;
proto.nativeHeight = nheight = doc.height || 0;
diff --git a/src/client/views/EditableView.scss b/src/client/views/EditableView.scss
index dfa110f8d..a5150cd66 100644
--- a/src/client/views/EditableView.scss
+++ b/src/client/views/EditableView.scss
@@ -17,4 +17,5 @@
}
.editableView-input {
width: 100%;
+ background: inherit;
} \ No newline at end of file
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index 9471ae98a..c5886f552 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -76,6 +76,7 @@ export class EditableView extends React.Component<EditableProps> {
@action
onClick = (e: React.MouseEvent) => {
+ e.nativeEvent.stopPropagation();
if (!this.props.onClick || !this.props.onClick(e)) {
this._editing = true;
this.props.isEditingCallback && this.props.isEditingCallback(true);
@@ -102,7 +103,7 @@ export class EditableView extends React.Component<EditableProps> {
<div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`}
style={{ display: this.props.display, height: "auto", maxHeight: `${this.props.height}` }}
onClick={this.onClick} >
- <span style={{ fontStyle: this.props.fontStyle }}>{this.props.contents}</span>
+ <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize }}>{this.props.contents}</span>
</div>
);
}
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index f378b6c0c..e8a588e58 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -4,6 +4,7 @@ import { CollectionDockingView } from "./collections/CollectionDockingView";
import { MainView } from "./MainView";
import { DragManager } from "../util/DragManager";
import { action } from "mobx";
+import { Doc } from "../../new_fields/Doc";
const modifiers = ["control", "meta", "shift", "alt"];
type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo;
@@ -82,6 +83,9 @@ export default class KeyManager {
});
}, "delete");
break;
+ case "enter":
+ SelectionManager.SelectedDocuments().map(selected => Doc.ToggleDetailLayout(selected.props.Document));
+ break;
}
return {
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index ce7369220..94a4835a1 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -16,7 +16,7 @@ import { listSpec } from '../../new_fields/Schema';
import { Cast, FieldValue, NumCast, BoolCast, StrCast } from '../../new_fields/Types';
import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
import { RouteStore } from '../../server/RouteStore';
-import { emptyFunction, returnOne, returnTrue } from '../../Utils';
+import { emptyFunction, returnOne, returnTrue, Utils } from '../../Utils';
import { DocServer } from '../DocServer';
import { Docs } from '../documents/Documents';
import { SetupDrag } from '../util/DragManager';
@@ -433,7 +433,7 @@ export class MainView extends React.Component {
return [
this.isSearchVisible ? <div className="main-searchDiv" key="search" style={{ top: '34px', right: '1px', position: 'absolute' }} > <FilterBox /> </div> : null,
<div className="main-buttonDiv" key="logout" style={{ bottom: '0px', right: '1px', position: 'absolute' }} ref={logoutRef}>
- <button onClick={() => window.location.assign(DocServer.prepend(RouteStore.logout))}>Log Out</button></div>
+ <button onClick={() => window.location.assign(Utils.prepend(RouteStore.logout))}>Log Out</button></div>
];
}
diff --git a/src/client/views/OverlayView.scss b/src/client/views/OverlayView.scss
new file mode 100644
index 000000000..4d1e8cf0b
--- /dev/null
+++ b/src/client/views/OverlayView.scss
@@ -0,0 +1,42 @@
+.overlayWindow-outerDiv {
+ border-radius: 5px;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+}
+
+.overlayWindow-outerDiv,
+.overlayView-wrapperDiv {
+ position: absolute;
+ z-index: 1;
+}
+
+.overlayWindow-titleBar {
+ flex: 0 1 30px;
+ background: darkslategray;
+ color: whitesmoke;
+ text-align: center;
+ cursor: move;
+}
+
+.overlayWindow-content {
+ flex: 1 1 auto;
+ display: flex;
+ flex-direction: column;
+}
+
+.overlayWindow-closeButton {
+ float: right;
+ height: 30px;
+ width: 30px;
+}
+
+.overlayWindow-resizeDragger {
+ background-color: red;
+ position: absolute;
+ right: 0px;
+ bottom: 0px;
+ width: 10px;
+ height: 10px;
+ cursor: nwse-resize;
+} \ No newline at end of file
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index f8fc94274..2f2579057 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -3,6 +3,8 @@ import { observer } from "mobx-react";
import { observable, action } from "mobx";
import { Utils } from "../../Utils";
+import './OverlayView.scss';
+
export type OverlayDisposer = () => void;
export type OverlayElementOptions = {
@@ -10,13 +12,92 @@ export type OverlayElementOptions = {
y: number;
width?: number;
height?: number;
+ title?: string;
};
+export interface OverlayWindowProps {
+ children: JSX.Element;
+ overlayOptions: OverlayElementOptions;
+ onClick: () => void;
+}
+
+@observer
+export class OverlayWindow extends React.Component<OverlayWindowProps> {
+ @observable x: number;
+ @observable y: number;
+ @observable width: number;
+ @observable height: number;
+ constructor(props: OverlayWindowProps) {
+ super(props);
+
+ const opts = props.overlayOptions;
+ this.x = opts.x;
+ this.y = opts.y;
+ this.width = opts.width || 200;
+ this.height = opts.height || 200;
+ }
+
+ onPointerDown = (_: React.PointerEvent) => {
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ document.addEventListener("pointermove", this.onPointerMove);
+ document.addEventListener("pointerup", this.onPointerUp);
+ }
+
+ onResizerPointerDown = (_: React.PointerEvent) => {
+ document.removeEventListener("pointermove", this.onResizerPointerMove);
+ document.removeEventListener("pointerup", this.onResizerPointerUp);
+ document.addEventListener("pointermove", this.onResizerPointerMove);
+ document.addEventListener("pointerup", this.onResizerPointerUp);
+ }
+
+ @action
+ onPointerMove = (e: PointerEvent) => {
+ this.x += e.movementX;
+ this.x = Math.max(Math.min(this.x, window.innerWidth - this.width), 0);
+ this.y += e.movementY;
+ this.y = Math.max(Math.min(this.y, window.innerHeight - this.height), 0);
+ }
+
+ @action
+ onResizerPointerMove = (e: PointerEvent) => {
+ this.width += e.movementX;
+ this.width = Math.max(this.width, 30);
+ this.height += e.movementY;
+ this.height = Math.max(this.height, 30);
+ }
+
+ onPointerUp = (e: PointerEvent) => {
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ }
+
+ onResizerPointerUp = (e: PointerEvent) => {
+ document.removeEventListener("pointermove", this.onResizerPointerMove);
+ document.removeEventListener("pointerup", this.onResizerPointerUp);
+ }
+
+ render() {
+ return (
+ <div className="overlayWindow-outerDiv" style={{ transform: `translate(${this.x}px, ${this.y}px)`, width: this.width, height: this.height }}>
+ <div className="overlayWindow-titleBar" onPointerDown={this.onPointerDown} >
+ {this.props.overlayOptions.title || "Untitled"}
+ <button onClick={this.props.onClick} className="overlayWindow-closeButton">X</button>
+ </div>
+ <div className="overlayWindow-content">
+ {this.props.children}
+ </div>
+ <div className="overlayWindow-resizeDragger" onPointerDown={this.onResizerPointerDown}></div>
+ </div>
+ );
+ }
+}
+
@observer
export class OverlayView extends React.Component {
public static Instance: OverlayView;
@observable.shallow
- private _elements: { ele: JSX.Element, id: string, options: OverlayElementOptions }[] = [];
+ private _elements: JSX.Element[] = [];
constructor(props: any) {
super(props);
@@ -27,20 +108,34 @@ export class OverlayView extends React.Component {
@action
addElement(ele: JSX.Element, options: OverlayElementOptions): OverlayDisposer {
- const eleWithPosition = { ele, options, id: Utils.GenerateGuid() };
- this._elements.push(eleWithPosition);
- return action(() => {
- const index = this._elements.indexOf(eleWithPosition);
+ const remove = action(() => {
+ const index = this._elements.indexOf(ele);
+ if (index !== -1) this._elements.splice(index, 1);
+ });
+ ele = <div key={Utils.GenerateGuid()} className="overlayView-wrapperDiv" style={{
+ transform: `translate(${options.x}px, ${options.y}px)`,
+ width: options.width,
+ height: options.height
+ }}>{ele}</div>;
+ this._elements.push(ele);
+ return remove;
+ }
+
+ @action
+ addWindow(contents: JSX.Element, options: OverlayElementOptions): OverlayDisposer {
+ const remove = action(() => {
+ const index = this._elements.indexOf(contents);
if (index !== -1) this._elements.splice(index, 1);
});
+ contents = <OverlayWindow onClick={remove} key={Utils.GenerateGuid()} overlayOptions={options}>{contents}</OverlayWindow>;
+ this._elements.push(contents);
+ return remove;
}
render() {
return (
<div>
- {this._elements.map(({ ele, options: { x, y, width, height }, id }) => (
- <div key={id} style={{ position: "absolute", transform: `translate(${x}px, ${y}px)`, width, height }}>{ele}</div>
- ))}
+ {this._elements}
</div>
);
}
diff --git a/src/client/views/ScriptingRepl.scss b/src/client/views/ScriptingRepl.scss
new file mode 100644
index 000000000..f1ef64193
--- /dev/null
+++ b/src/client/views/ScriptingRepl.scss
@@ -0,0 +1,50 @@
+.scriptingRepl-outerContainer {
+ background-color: whitesmoke;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
+.scriptingRepl-resultContainer {
+ padding-bottom: 5px;
+}
+
+.scriptingRepl-commandInput {
+ width: 100%;
+}
+
+.scriptingRepl-commandResult,
+.scriptingRepl-commandString {
+ overflow-wrap: break-word;
+}
+
+.scriptingRepl-commandsContainer {
+ flex: 1 1 auto;
+ overflow-y: scroll;
+}
+
+.documentIcon-outerDiv {
+ background-color: white;
+ border-width: 1px;
+ border-style: solid;
+ border-radius: 25%;
+ padding: 2px;
+}
+
+.scriptingObject-icon {
+ padding: 3px;
+ cursor: pointer;
+}
+
+.scriptingObject-iconCollapsed {
+ padding-left: 4px;
+ padding-right: 5px;
+}
+
+.scriptingObject-fields {
+ padding-left: 10px;
+}
+
+.scriptingObject-leaf {
+ margin-left: 15px;
+} \ No newline at end of file
diff --git a/src/client/views/ScriptingRepl.tsx b/src/client/views/ScriptingRepl.tsx
new file mode 100644
index 000000000..6eabc7b70
--- /dev/null
+++ b/src/client/views/ScriptingRepl.tsx
@@ -0,0 +1,256 @@
+import * as React from 'react';
+import { observer } from 'mobx-react';
+import { observable, action } from 'mobx';
+import './ScriptingRepl.scss';
+import { Scripting, CompileScript, ts } from '../util/Scripting';
+import { DocumentManager } from '../util/DocumentManager';
+import { DocumentView } from './nodes/DocumentView';
+import { OverlayView } from './OverlayView';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faCaretDown, faCaretRight } from '@fortawesome/free-solid-svg-icons';
+
+library.add(faCaretDown);
+library.add(faCaretRight);
+
+@observer
+export class DocumentIcon extends React.Component<{ view: DocumentView, index: number }> {
+ render() {
+ this.props.view.props.ScreenToLocalTransform();
+ this.props.view.props.Document.width;
+ this.props.view.props.Document.height;
+ const screenCoords = this.props.view.screenRect();
+
+ return (
+ <div className="documentIcon-outerDiv" style={{
+ position: "absolute",
+ transform: `translate(${screenCoords.left + screenCoords.width / 2}px, ${screenCoords.top}px)`,
+ }}>
+ <p >${this.props.index}</p>
+ </div>
+ );
+ }
+}
+
+@observer
+export class DocumentIconContainer extends React.Component {
+ render() {
+ return DocumentManager.Instance.DocumentViews.map((dv, i) => <DocumentIcon key={i} index={i} view={dv} />);
+ }
+}
+
+@observer
+export class ScriptingObjectDisplay extends React.Component<{ scrollToBottom: () => void, value: { [key: string]: any }, name?: string }> {
+ @observable collapsed = true;
+
+ @action
+ toggle = () => {
+ this.collapsed = !this.collapsed;
+ this.props.scrollToBottom();
+ }
+
+ render() {
+ const val = this.props.value;
+ const proto = Object.getPrototypeOf(val);
+ const name = (proto && proto.constructor && proto.constructor.name) || String(val);
+ const title = this.props.name ? <><b>{this.props.name} : </b>{name}</> : name;
+ if (this.collapsed) {
+ return (
+ <div className="scriptingObject-collapsed">
+ <span onClick={this.toggle} className="scriptingObject-icon scriptingObject-iconCollapsed"><FontAwesomeIcon icon="caret-right" size="sm" /></span>{title} (+{Object.keys(val).length})
+ </div>
+ );
+ } else {
+ return (
+ <div className="scriptingObject-open">
+ <div>
+ <span onClick={this.toggle} className="scriptingObject-icon"><FontAwesomeIcon icon="caret-down" size="sm" /></span>{title}
+ </div>
+ <div className="scriptingObject-fields">
+ {Object.keys(val).map(key => <ScriptingValueDisplay {...this.props} name={key} />)}
+ </div>
+ </div>
+ );
+ }
+ }
+}
+
+@observer
+export class ScriptingValueDisplay extends React.Component<{ scrollToBottom: () => void, value: any, name?: string }> {
+ render() {
+ const val = this.props.name ? this.props.value[this.props.name] : this.props.value;
+ if (typeof val === "object") {
+ return <ScriptingObjectDisplay scrollToBottom={this.props.scrollToBottom} value={val} name={this.props.name} />;
+ } else if (typeof val === "function") {
+ const name = "[Function]";
+ const title = this.props.name ? <><b>{this.props.name} : </b>{name}</> : name;
+ return <div className="scriptingObject-leaf">{title}</div>;
+ } else {
+ const name = String(val);
+ const title = this.props.name ? <><b>{this.props.name} : </b>{name}</> : name;
+ return <div className="scriptingObject-leaf">{title}</div>;
+ }
+ }
+}
+
+@observer
+export class ScriptingRepl extends React.Component {
+ @observable private commands: { command: string, result: any }[] = [];
+
+ @observable private commandString: string = "";
+ private commandBuffer: string = "";
+
+ @observable private historyIndex: number = -1;
+
+ private commandsRef = React.createRef<HTMLDivElement>();
+
+ private args: any = {};
+
+ getTransformer: ts.TransformerFactory<ts.SourceFile> = context => {
+ const knownVars: { [name: string]: number } = {};
+ const usedDocuments: number[] = [];
+ Scripting.getGlobals().forEach(global => knownVars[global] = 1);
+ return root => {
+ function visit(node: ts.Node) {
+ node = ts.visitEachChild(node, visit, context);
+
+ if (ts.isIdentifier(node)) {
+ const isntPropAccess = !ts.isPropertyAccessExpression(node.parent) || node.parent.expression === node;
+ const isntPropAssign = !ts.isPropertyAssignment(node.parent) || node.parent.name !== node;
+ if (isntPropAccess && isntPropAssign && !(node.text in knownVars) && !(node.text in globalThis)) {
+ const match = node.text.match(/\$([0-9]+)/);
+ if (match) {
+ const m = parseInt(match[1]);
+ usedDocuments.push(m);
+ } else {
+ return ts.createPropertyAccess(ts.createIdentifier("args"), node);
+ }
+ }
+ }
+
+ return node;
+ }
+ return ts.visitNode(root, visit);
+ };
+ }
+
+ @action
+ onKeyDown = (e: React.KeyboardEvent) => {
+ let stopProp = true;
+ switch (e.key) {
+ case "Enter": {
+ const docGlobals: { [name: string]: any } = {};
+ DocumentManager.Instance.DocumentViews.forEach((dv, i) => docGlobals[`$${i}`] = dv.props.Document);
+ const globals = Scripting.makeMutableGlobalsCopy(docGlobals);
+ const script = CompileScript(this.commandString, { typecheck: false, addReturn: true, editable: true, params: { args: "any" }, transformer: this.getTransformer, globals });
+ if (!script.compiled) {
+ return;
+ }
+ const result = script.run({ args: this.args });
+ if (!result.success) {
+ return;
+ }
+ this.commands.push({ command: this.commandString, result: result.result });
+
+ this.maybeScrollToBottom();
+
+ this.commandString = "";
+ this.commandBuffer = "";
+ this.historyIndex = -1;
+ break;
+ }
+ case "ArrowUp": {
+ if (this.historyIndex < this.commands.length - 1) {
+ this.historyIndex++;
+ if (this.historyIndex === 0) {
+ this.commandBuffer = this.commandString;
+ }
+ this.commandString = this.commands[this.commands.length - 1 - this.historyIndex].command;
+ }
+ break;
+ }
+ case "ArrowDown": {
+ if (this.historyIndex >= 0) {
+ this.historyIndex--;
+ if (this.historyIndex === -1) {
+ this.commandString = this.commandBuffer;
+ this.commandBuffer = "";
+ } else {
+ this.commandString = this.commands[this.commands.length - 1 - this.historyIndex].command;
+ }
+ }
+ break;
+ }
+ default:
+ stopProp = false;
+ break;
+ }
+
+ if (stopProp) {
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ }
+
+ @action
+ onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this.commandString = e.target.value;
+ }
+
+ private shouldScroll: boolean = false;
+ private maybeScrollToBottom = () => {
+ const ele = this.commandsRef.current;
+ if (ele && ele.scrollTop === (ele.scrollHeight - ele.offsetHeight)) {
+ this.shouldScroll = true;
+ this.forceUpdate();
+ }
+ }
+
+ private scrollToBottom() {
+ const ele = this.commandsRef.current;
+ ele && ele.scroll({ behavior: "auto", top: ele.scrollHeight });
+ }
+
+ componentDidUpdate() {
+ if (this.shouldScroll) {
+ this.shouldScroll = false;
+ this.scrollToBottom();
+ }
+ }
+
+ overlayDisposer?: () => void;
+ onFocus = () => {
+ if (this.overlayDisposer) {
+ this.overlayDisposer();
+ }
+ this.overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 });
+ }
+
+ onBlur = () => {
+ this.overlayDisposer && this.overlayDisposer();
+ }
+
+ render() {
+ return (
+ <div className="scriptingRepl-outerContainer">
+ <div className="scriptingRepl-commandsContainer" ref={this.commandsRef}>
+ {this.commands.map(({ command, result }, i) => {
+ return (
+ <div className="scriptingRepl-resultContainer" key={i}>
+ <div className="scriptingRepl-commandString">{command || <br />}</div>
+ <div className="scriptingRepl-commandResult">{<ScriptingValueDisplay scrollToBottom={this.maybeScrollToBottom} value={result} />}</div>
+ </div>
+ );
+ })}
+ </div>
+ <input
+ className="scriptingRepl-commandInput"
+ onFocus={this.onFocus}
+ onBlur={this.onBlur}
+ value={this.commandString}
+ onChange={this.onChange}
+ onKeyDown={this.onKeyDown}></input>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/SearchBox.tsx b/src/client/views/SearchBox.tsx
index 1b9be841f..33cb63df5 100644
--- a/src/client/views/SearchBox.tsx
+++ b/src/client/views/SearchBox.tsx
@@ -13,6 +13,7 @@ import { Docs } from '../documents/Documents';
import { SetupDrag } from '../util/DragManager';
import { SearchItem } from './search/SearchItem';
import "./SearchBox.scss";
+import { Utils } from '../../Utils';
library.add(faSearch);
library.add(faObjectGroup);
@@ -47,7 +48,7 @@ export class SearchBox extends React.Component {
@action
getResults = async (query: string) => {
- let response = await rp.get(DocServer.prepend('/search'), {
+ let response = await rp.get(Utils.prepend('/search'), {
qs: {
query
}
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
index eba69b448..72faf52c4 100644
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -18,7 +18,8 @@ export enum CollectionViewType {
Schema,
Docking,
Tree,
- Stacking
+ Stacking,
+ Masonry
}
export interface CollectionRenderProps {
@@ -78,7 +79,6 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
@action.bound
addDocument(doc: Doc, allowDuplicates: boolean = false): boolean {
- let self = this;
var curPage = NumCast(this.props.Document.curPage, -1);
Doc.GetProto(doc).page = curPage;
if (curPage >= 0) {
@@ -146,7 +146,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
const viewtype = this.collectionViewType;
return (
<div id="collectionBaseView"
- style={{ boxShadow: `#9c9396 ${StrCast(this.props.Document.boxShadow, "0.2vw 0.2vw 0.8vw")}` }}
+ style={{ overflow: "auto", boxShadow: `#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}>
{viewtype !== undefined ? this.props.children(viewtype, props) : (null)}
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index a193ff677..ba7903419 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -465,7 +465,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
.off('click') //unbind the current click handler
.click(action(function () {
stack.config.fixed = !stack.config.fixed;
- // var url = DocServer.prepend("/doc/" + stack.contentItems[0].tab.contentItem.config.props.documentId);
+ // var url = Utils.prepend("/doc/" + stack.contentItems[0].tab.contentItem.config.props.documentId);
// let win = window.open(url, stack.contentItems[0].tab.title, "width=300,height=400");
}));
}
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 6d71823c2..39844dea2 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -31,11 +31,17 @@ import { CollectionVideoView } from "./CollectionVideoView";
import { CollectionView } from "./CollectionView";
import { undoBatch } from "../../util/UndoManager";
import { timesSeries } from "async";
+<<<<<<< HEAD
import { CollectionSchemaHeader, CollectionSchemaAddColumnHeader } from "./CollectionSchemaHeaders";
import { CellProps, CollectionSchemaCell, CollectionSchemaNumberCell, CollectionSchemaStringCell, CollectionSchemaBooleanCell, CollectionSchemaCheckboxCell, CollectionSchemaDocCell } from "./CollectionSchemaCells";
import { MovableColumn, MovableRow } from "./CollectionSchemaMovableTableHOC";
import { SelectionManager } from "../../util/SelectionManager";
import { DocumentManager } from "../../util/DocumentManager";
+=======
+import { ImageBox } from "../nodes/ImageBox";
+import { ComputedField } from "../../../new_fields/ScriptField";
+
+>>>>>>> 86971952237b8bd01a23b52db662740126bd8477
library.add(faCog);
library.add(faPlus);
@@ -729,8 +735,12 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre
drop = (e: Event, de: DragManager.DropEvent) => {
if (de.data instanceof DragManager.DocumentDragData) {
let docDrag = de.data;
+ let computed = CompileScript("return this.image_data[0]", { params: { this: "Doc" } });
this.props.childDocs && this.props.childDocs.map(otherdoc => {
- Doc.GetProto(otherdoc).layout = Doc.MakeDelegate(docDrag.draggedDocuments[0]);
+ let doc = docDrag.draggedDocuments[0];
+ let target = Doc.GetProto(otherdoc);
+ target.layout = target.detailedLayout = Doc.MakeDelegate(doc);
+ computed.compiled && (target.miniLayout = new ComputedField(computed));
});
e.stopPropagation();
}
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index 7e886304d..7ebf5f77c 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -1,7 +1,9 @@
@import "../globalCssVariables";
.collectionStackingView {
+ height: 100%;
+ width: 100%;
+ position: absolute;
overflow-y: auto;
-
.collectionStackingView-docView-container {
width: 45%;
margin: 5% 2.5%;
@@ -71,4 +73,13 @@
grid-column-end: span 1;
height: 100%;
}
+ .collectionStackingView-sectionHeader {
+ width: 90%;
+ background: gray;
+ text-align: center;
+ margin-left: 5%;
+ margin-right: 5%;
+ color: white;
+ margin-top: 10px;
+ }
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index fe01103d6..54b0e37b5 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -1,12 +1,11 @@
import React = require("react");
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, IReactionDisposer, reaction, untracked } from "mobx";
+import { action, computed, IReactionDisposer, reaction, untracked, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
-import { BoolCast, NumCast, Cast } from "../../../new_fields/Types";
-import { emptyFunction, Utils } from "../../../Utils";
-import { ContextMenu } from "../ContextMenu";
+import { BoolCast, NumCast, Cast, StrCast } from "../../../new_fields/Types";
+import { emptyFunction, Utils, returnTrue } from "../../../Utils";
import { CollectionSchemaPreview } from "./CollectionSchemaView";
import "./CollectionStackingView.scss";
import { CollectionSubView } from "./CollectionSubView";
@@ -14,14 +13,16 @@ import { undoBatch } from "../../util/UndoManager";
import { DragManager } from "../../util/DragManager";
import { DocumentType } from "../../documents/Documents";
import { Transform } from "../../util/Transform";
+import { CursorProperty } from "csstype";
@observer
export class CollectionStackingView extends CollectionSubView(doc => doc) {
_masonryGridRef: HTMLDivElement | null = null;
_draggerRef = React.createRef<HTMLDivElement>();
_heightDisposer?: IReactionDisposer;
- _gridSize = 1;
_docXfs: any[] = [];
+ _columnStart: number = 0;
+ @observable private cursor: CursorProperty = "grab";
@computed get xMargin() { return NumCast(this.props.Document.xMargin, 2 * this.gridGap); }
@computed get yMargin() { return NumCast(this.props.Document.yMargin, 2 * this.gridGap); }
@computed get gridGap() { return NumCast(this.props.Document.gridGap, 10); }
@@ -29,15 +30,25 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
@computed get columnWidth() { return this.singleColumn ? (this.props.PanelWidth() / (this.props as any).ContentScaling() - 2 * this.xMargin) : Math.min(this.props.PanelWidth() - 2 * this.xMargin, NumCast(this.props.Document.columnWidth, 250)); }
@computed get filteredChildren() { return this.childDocs.filter(d => !d.isMinimized); }
+ @computed get Sections() {
+ let sectionFilter = StrCast(this.props.Document.sectionFilter);
+ let fields = new Map<object, Doc[]>();
+ sectionFilter && this.filteredChildren.map(d => {
+ let sectionValue = (d[sectionFilter] ? d[sectionFilter] : "-undefined-") as object;
+ if (!fields.has(sectionValue)) fields.set(sectionValue, [d]);
+ else fields.get(sectionValue)!.push(d);
+ });
+ return fields;
+ }
componentDidMount() {
this._heightDisposer = reaction(() => [this.yMargin, this.gridGap, this.columnWidth, this.childDocs.map(d => [d.height, d.width, d.zoomBasis, d.nativeHeight, d.nativeWidth, d.isMinimized])],
() => this.singleColumn &&
- (this.props.Document.height = this.filteredChildren.reduce((height, d, i) =>
+ (this.props.Document.height = this.Sections.size * 50 + this.filteredChildren.reduce((height, d, i) =>
height + this.getDocHeight(d) + (i === this.filteredChildren.length - 1 ? this.yMargin : this.gridGap), this.yMargin))
, { fireImmediately: true });
}
componentWillUnmount() {
- if (this._heightDisposer) this._heightDisposer();
+ this._heightDisposer && this._heightDisposer();
}
@action
@@ -63,6 +74,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
DataDocument={resolvedDataDoc}
showOverlays={this.overlays}
renderDepth={this.props.renderDepth}
+ fitToBox={true}
width={width}
height={height}
getTransform={finalDxf}
@@ -85,7 +97,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
return (nw && nh) ? wid * aspect : d[HeightSym]();
}
-
offsetTransform(doc: Doc, translateX: number, translateY: number) {
let outerXf = Utils.GetScreenTransform(this._masonryGridRef!);
let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY);
@@ -95,6 +106,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
let { scale, translateX, translateY } = Utils.GetScreenTransform(dref);
return this.offsetTransform(doc, translateX, translateY);
}
+
getSingleDocTransform(doc: Doc, ind: number, width: number) {
let localY = this.filteredChildren.reduce((height, d, i) =>
height + (i < ind ? this.getDocHeight(Doc.expandTemplateLayout(d, this.props.DataDoc)) + this.gridGap : 0), this.yMargin);
@@ -102,24 +114,24 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
return this.offsetTransform(doc, translate[0], translate[1]);
}
- @computed
- get children() {
+ children(docs: Doc[]) {
this._docXfs.length = 0;
- return this.filteredChildren.map((d, i) => {
+ return docs.map((d, i) => {
let layoutDoc = Doc.expandTemplateLayout(d, this.props.DataDoc);
let width = () => d.nativeWidth ? Math.min(layoutDoc[WidthSym](), this.columnWidth) : this.columnWidth;
let height = () => this.getDocHeight(layoutDoc);
if (this.singleColumn) {
+ //have to add the height of all previous single column sections or the doc decorations will be in the wrong place.
let dxf = () => this.getSingleDocTransform(layoutDoc, i, width());
- let rowHgtPcnt = height() / (this.props.Document[HeightSym]() - 2 * this.yMargin) * 100;
+ let rowHgtPcnt = height();
this._docXfs.push({ dxf: dxf, width: width, height: height });
- return <div className="collectionStackingView-columnDoc" key={d[Id]} style={{ width: width(), marginTop: i === 0 ? 0 : this.gridGap, height: `${rowHgtPcnt}%` }} >
+ return <div className="collectionStackingView-columnDoc" key={d[Id]} style={{ width: width(), marginTop: i === 0 ? 0 : this.gridGap, height: `${rowHgtPcnt}` }} >
{this.getDisplayDoc(layoutDoc, d, dxf)}
</div>;
} else {
let dref = React.createRef<HTMLDivElement>();
let dxf = () => this.getDocTransform(layoutDoc, dref.current!);
- let rowSpan = Math.ceil((height() + this.gridGap) / (this._gridSize + this.gridGap));
+ let rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap);
this._docXfs.push({ dxf: dxf, width: width, height: height });
return <div className="collectionStackingView-masonryDoc" key={d[Id]} ref={dref} style={{ gridRowEnd: `span ${rowSpan}` }} >
{this.getDisplayDoc(layoutDoc, d, dxf)}
@@ -128,10 +140,10 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
});
}
- _columnStart: number = 0;
columnDividerDown = (e: React.PointerEvent) => {
e.stopPropagation();
e.preventDefault();
+ 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];
@@ -141,29 +153,21 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
let dragPos = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0];
let delta = dragPos - this._columnStart;
this._columnStart = dragPos;
-
this.props.Document.columnWidth = this.columnWidth + delta;
}
@action
onDividerUp = (e: PointerEvent): void => {
+ 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={{ left: `${this.columnWidth + this.xMargin}px` }} >
- <FontAwesomeIcon icon={"caret-down"} />
+ 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>;
}
- 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: "Toggle multi-column",
- event: () => this.props.Document.singleColumn = !BoolCast(this.props.Document.singleColumn, true), icon: "file-pdf"
- });
- }
- }
@undoBatch
@action
@@ -215,28 +219,40 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
});
}
- render() {
+ section(heading: string, docList: Doc[]) {
let cols = this.singleColumn ? 1 : Math.max(1, Math.min(this.filteredChildren.length,
Math.floor((this.props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap))));
let templatecols = "";
for (let i = 0; i < cols; i++) templatecols += `${this.columnWidth}px `;
+ return <div key={heading}>
+ {heading ? <div key={`${heading}`} className="collectionStackingView-sectionHeader">{heading}</div> : (null)}
+ <div key={`${heading}-stack`} className={`collectionStackingView-masonry${this.singleColumn ? "Single" : "Grid"}`}
+ style={{
+ padding: this.singleColumn ? `${this.yMargin}px ${this.xMargin}px ${this.yMargin}px ${this.xMargin}px` : `${this.yMargin}px ${this.xMargin}px`,
+ margin: "auto",
+ width: this.singleColumn ? undefined : `${cols * (this.columnWidth + this.gridGap) + 2 * this.xMargin - this.gridGap}px`,
+ height: 'max-content',
+ position: "relative",
+ gridGap: this.gridGap,
+ gridTemplateColumns: this.singleColumn ? undefined : templatecols,
+ gridAutoRows: this.singleColumn ? undefined : "0px"
+ }}
+ >
+ {this.children(docList)}
+ {this.singleColumn ? (null) : this.columnDragger}
+ </div></div>;
+ }
+ render() {
return (
- <div className="collectionStackingView" ref={this.createRef} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu} onWheel={(e: React.WheelEvent) => e.stopPropagation()} >
- <div className={`collectionStackingView-masonry${this.singleColumn ? "Single" : "Grid"}`}
- style={{
- padding: this.singleColumn ? `${this.yMargin}px ${this.xMargin}px ${this.yMargin}px ${this.xMargin}px` : `${this.yMargin}px ${this.xMargin}px`,
- margin: "auto",
- width: this.singleColumn ? undefined : `${cols * (this.columnWidth + this.gridGap) + 2 * this.xMargin - this.gridGap}px`,
- height: "100%",
- position: "relative",
- gridGap: this.gridGap,
- gridTemplateColumns: this.singleColumn ? undefined : templatecols,
- gridAutoRows: this.singleColumn ? undefined : `${this._gridSize}px`
- }}
- >
- {this.children}
- {this.singleColumn ? (null) : this.columnDragger}
- </div>
+ <div className="collectionStackingView"
+ ref={this.createRef} onDrop={this.onDrop.bind(this)} onWheel={(e: React.WheelEvent) => e.stopPropagation()} >
+ {/* {sectionFilter as boolean ? [
+ ["width > height", this.filteredChildren.filter(f => f[WidthSym]() >= 1 + f[HeightSym]())],
+ ["width = height", this.filteredChildren.filter(f => Math.abs(f[WidthSym]() - f[HeightSym]()) < 1)],
+ ["height > width", this.filteredChildren.filter(f => f[WidthSym]() + 1 <= f[HeightSym]())]]. */}
+ {this.props.Document.sectionFilter ? Array.from(this.Sections.entries()).
+ map(section => this.section(section[0].toString(), section[1] as Doc[])) :
+ this.section("", this.filteredChildren)}
</div>
);
}
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 4fd11add4..2ddefb3c0 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -9,7 +9,7 @@ import { BoolCast, Cast } from "../../../new_fields/Types";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
import { RouteStore } from "../../../server/RouteStore";
import { DocServer } from "../../DocServer";
-import { Docs, DocumentOptions } from "../../documents/Documents";
+import { Docs, DocumentOptions, DocumentType } from "../../documents/Documents";
import { DragManager } from "../../util/DragManager";
import { undoBatch, UndoManager } from "../../util/UndoManager";
import { DocComponent } from "../DocComponent";
@@ -20,6 +20,7 @@ import { CollectionVideoView } from "./CollectionVideoView";
import { CollectionView } from "./CollectionView";
import React = require("react");
import { MainView } from "../MainView";
+import { Utils } from "../../../Utils";
export interface CollectionViewProps extends FieldViewProps {
addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
@@ -74,7 +75,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
return;
}
// The following conditional detects a recurring bug we've seen on the server
- if (proto[Id] === "collectionProto") {
+ if (proto[Id] === Docs.Prototypes.get(DocumentType.COL)[Id]) {
alert("COLLECTION PROTO CURSOR ISSUE DETECTED! Check console for more info...");
console.log(doc);
console.log(proto);
@@ -164,7 +165,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
} else {
let path = window.location.origin + "/doc/";
if (text.startsWith(path)) {
- let docid = text.replace(DocServer.prepend("/doc/"), "").split("?")[0];
+ let docid = text.replace(Utils.prepend("/doc/"), "").split("?")[0];
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
@@ -193,7 +194,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
if (item.kind === "string" && item.type.indexOf("uri") !== -1) {
let str: string;
let prom = new Promise<string>(resolve => e.dataTransfer.items[i].getAsString(resolve))
- .then(action((s: string) => rp.head(DocServer.prepend(RouteStore.corsProxy + "/" + (str = s)))))
+ .then(action((s: string) => rp.head(Utils.CorsProxy(str = s))))
.then(result => {
let type = result["content-type"];
if (type) {
@@ -219,7 +220,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
}).then(async (res: Response) => {
(await res.json()).map(action((file: any) => {
let full = { ...options, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300, width: 300, title: dropFileName };
- let path = DocServer.prepend(file);
+ let path = Utils.prepend(file);
Docs.Get.DocumentFromType(type, path, full).then(doc => doc && this.props.addDocument(doc));
}));
});
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index c212cc97c..f98629c5b 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -1,5 +1,5 @@
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faAngleRight, faCamera, faExpand, faTrash, faBell, faCaretDown, faCaretRight, faCaretSquareDown, faCaretSquareRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
+import { faAngleRight, faCamera, faExpand, faTrash, faBell, faCaretDown, faCaretRight, faArrowsAltH, faCaretSquareDown, faCaretSquareRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, observable, trace, untracked } from "mobx";
import { observer } from "mobx-react";
@@ -58,6 +58,7 @@ library.add(faCaretDown);
library.add(faCaretRight);
library.add(faCaretSquareDown);
library.add(faCaretSquareRight);
+library.add(faArrowsAltH);
@observer
/**
@@ -73,15 +74,15 @@ class TreeView extends React.Component<TreeViewProps> {
@observable _collapsed: boolean = true;
@computed get fieldKey() {
- let keys = Array.from(Object.keys(this.resolvedDataDoc)); // bcz: Argh -- make untracked to avoid this rerunning whenever 'libraryBrush' is set
- if (this.resolvedDataDoc.proto instanceof Doc) {
- let arr = Array.from(Object.keys(this.resolvedDataDoc.proto));// bcz: Argh -- make untracked to avoid this rerunning whenever 'libraryBrush' is set
+ let keys = Array.from(Object.keys(this.dataDoc)); // bcz: Argh -- make untracked to avoid this rerunning whenever 'libraryBrush' is set
+ if (this.dataDoc.proto instanceof Doc) {
+ let arr = Array.from(Object.keys(this.dataDoc.proto));// bcz: Argh -- make untracked to avoid this rerunning whenever 'libraryBrush' is set
keys.push(...arr);
while (keys.indexOf("proto") !== -1) keys.splice(keys.indexOf("proto"), 1);
}
let keyList: string[] = [];
keys.map(key => {
- let docList = Cast(this.resolvedDataDoc[key], listSpec(Doc));
+ let docList = Cast(this.dataDoc[key], listSpec(Doc));
if (docList && docList.length > 0) {
keyList.push(key);
}
@@ -93,7 +94,16 @@ class TreeView extends React.Component<TreeViewProps> {
return keyList.length ? keyList[0] : "data";
}
- @computed get resolvedDataDoc() { return BoolCast(this.props.document.isTemplate) && this.props.dataDoc ? this.props.dataDoc : this.props.document; }
+ @computed get dataDoc() { return this.resolvedDataDoc ? this.resolvedDataDoc : this.props.document; }
+ @computed get resolvedDataDoc() {
+ if (this.props.dataDoc === undefined && this.props.document.layout instanceof Doc) {
+ // if there is no dataDoc (ie, we're not rendering a template layout), but this document
+ // has a template layout document, then we will render the template layout but use
+ // this document as the data document for the layout.
+ return this.props.document;
+ }
+ return this.props.dataDoc ? this.props.dataDoc : undefined;
+ }
protected createTreeDropTarget = (ele: HTMLDivElement) => {
this._treedropDisposer && this._treedropDisposer();
@@ -102,7 +112,7 @@ class TreeView extends React.Component<TreeViewProps> {
}
}
- @undoBatch delete = () => this.props.deleteDoc(this.resolvedDataDoc);
+ @undoBatch delete = () => this.props.deleteDoc(this.dataDoc);
@undoBatch openRight = async () => this.props.addDocTab(this.props.document, undefined, "onRight");
onPointerDown = (e: React.PointerEvent) => e.stopPropagation();
@@ -134,7 +144,7 @@ class TreeView extends React.Component<TreeViewProps> {
@action
remove = (document: Document, key: string): boolean => {
- let children = Cast(this.resolvedDataDoc[key], listSpec(Doc), []);
+ let children = Cast(this.dataDoc[key], listSpec(Doc), []);
if (children.indexOf(document) !== -1) {
children.splice(children.indexOf(document), 1);
return true;
@@ -150,8 +160,8 @@ class TreeView extends React.Component<TreeViewProps> {
indent = () => this.props.addDocument(this.props.document) && this.delete()
renderBullet() {
- let docList = Cast(this.resolvedDataDoc[this.fieldKey], listSpec(Doc));
- let doc = Cast(this.resolvedDataDoc[this.fieldKey], Doc);
+ let docList = Cast(this.dataDoc[this.fieldKey], listSpec(Doc));
+ let doc = Cast(this.dataDoc[this.fieldKey], Doc);
let isDoc = doc instanceof Doc || docList;
let c;
return <div className="bullet" onClick={action(() => this._collapsed = !this._collapsed)} style={{ color: StrCast(this.props.document.color, "black"), opacity: 0.4 }}>
@@ -163,16 +173,17 @@ class TreeView extends React.Component<TreeViewProps> {
editableView = (key: string, style?: string) => (<EditableView
oneLine={true}
display={"inline"}
- editing={this.resolvedDataDoc[Id] === TreeView.loadId}
+ editing={this.dataDoc[Id] === TreeView.loadId}
contents={StrCast(this.props.document[key])}
height={36}
fontStyle={style}
fontSize={12}
GetValue={() => StrCast(this.props.document[key])}
- SetValue={(value: string) => (Doc.GetProto(this.resolvedDataDoc)[key] = value) ? true : true}
+ SetValue={(value: string) => (Doc.GetProto(this.dataDoc)[key] = value) ? true : true}
OnFillDown={(value: string) => {
- Doc.GetProto(this.resolvedDataDoc)[key] = value;
- let doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
+ Doc.GetProto(this.dataDoc)[key] = value;
+ let doc = this.props.document.detailedLayout instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.document.detailedLayout)) : undefined;
+ if (!doc) doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
TreeView.loadId = doc[Id];
return this.props.addDocument(doc);
}}
@@ -180,16 +191,16 @@ class TreeView extends React.Component<TreeViewProps> {
/>)
@computed get keyList() {
- let keys = Array.from(Object.keys(this.resolvedDataDoc));
- if (this.resolvedDataDoc.proto instanceof Doc) {
- keys.push(...Array.from(Object.keys(this.resolvedDataDoc.proto)));
+ let keys = Array.from(Object.keys(this.dataDoc));
+ if (this.dataDoc.proto instanceof Doc) {
+ keys.push(...Array.from(Object.keys(this.dataDoc.proto)));
}
let keyList: string[] = keys.reduce((l, key) => {
- let listspec = DocListCast(this.resolvedDataDoc[key]);
+ let listspec = DocListCast(this.dataDoc[key]);
if (listspec && listspec.length) return [...l, key];
return l;
}, [] as string[]);
- keys.map(key => Cast(this.resolvedDataDoc[key], Doc) instanceof Doc && keyList.push(key));
+ keys.map(key => Cast(this.dataDoc[key], Doc) instanceof Doc && (Cast(this.dataDoc[key], Doc) as Doc).type !== undefined && keyList.push(key));
if (LinkManager.Instance.getAllRelatedLinks(this.props.document).length > 0) keyList.push("links");
if (keyList.indexOf(this.fieldKey) !== -1) {
keyList.splice(keyList.indexOf(this.fieldKey), 1);
@@ -202,7 +213,7 @@ class TreeView extends React.Component<TreeViewProps> {
*/
renderTitle() {
let reference = React.createRef<HTMLDivElement>();
- let onItemDown = SetupDrag(reference, () => this.resolvedDataDoc, this.move, this.props.dropAction, this.props.treeViewId, true);
+ let onItemDown = SetupDrag(reference, () => this.dataDoc, this.move, this.props.dropAction, this.props.treeViewId, true);
let headerElements = (
<span className="collectionTreeView-keyHeader" key={this._chosenKey + "chosen"}
@@ -214,7 +225,7 @@ class TreeView extends React.Component<TreeViewProps> {
{this._chosenKey}
</span>);
let dataDocs = CollectionDockingView.Instance ? Cast(CollectionDockingView.Instance.props.Document[this.fieldKey], listSpec(Doc), []) : [];
- let openRight = dataDocs && dataDocs.indexOf(this.resolvedDataDoc) !== -1 ? (null) : (
+ let openRight = dataDocs && dataDocs.indexOf(this.dataDoc) !== -1 ? (null) : (
<div className="treeViewItem-openRight" onPointerDown={this.onPointerDown} onClick={this.openRight}>
<FontAwesomeIcon icon="angle-right" size="lg" />
</div>);
@@ -240,12 +251,12 @@ class TreeView extends React.Component<TreeViewProps> {
if (NumCast(this.props.document.viewType) !== CollectionViewType.Docking) {
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" });
- if (DocumentManager.Instance.getDocumentViews(this.resolvedDataDoc).length) {
- ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.resolvedDataDoc).map(view => view.props.focus(this.props.document, true)), icon: "camera" });
+ if (DocumentManager.Instance.getDocumentViews(this.dataDoc).length) {
+ ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.dataDoc).map(view => view.props.focus(this.props.document, true)), icon: "camera" });
}
ContextMenu.Instance.addItem({ description: "Delete Item", event: undoBatch(() => this.props.deleteDoc(this.props.document)), icon: "trash-alt" });
} else {
- ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => MainView.Instance.openWorkspace(this.resolvedDataDoc)), icon: "caret-square-right" });
+ ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => MainView.Instance.openWorkspace(this.dataDoc)), icon: "caret-square-right" });
ContextMenu.Instance.addItem({ description: "Delete Workspace", event: undoBatch(() => this.props.deleteDoc(this.props.document)), icon: "trash-alt" });
}
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" });
@@ -273,7 +284,7 @@ class TreeView extends React.Component<TreeViewProps> {
if (de.data.draggedDocuments[0] === this.props.document) return true;
let addDoc = (doc: Doc) => this.props.addDocument(doc, this.resolvedDataDoc, before);
if (inside) {
- let docList = Cast(this.resolvedDataDoc.data, listSpec(Doc));
+ let docList = Cast(this.dataDoc.data, listSpec(Doc));
if (docList !== undefined) {
addDoc = (doc: Doc) => { docList && docList.push(doc); return true; };
}
@@ -325,7 +336,7 @@ class TreeView extends React.Component<TreeViewProps> {
@computed get boundsOfCollectionDocument() {
if (StrCast(this.props.document.type).indexOf(DocumentType.COL) === -1) return undefined;
- let layoutDoc = Doc.expandTemplateLayout(this.props.document, this.props.dataDoc);
+ let layoutDoc = this.props.document;
return Doc.ComputeContentBounds(DocListCast(layoutDoc.data));
}
docWidth = () => {
@@ -343,27 +354,30 @@ class TreeView extends React.Component<TreeViewProps> {
})());
}
+ noOverlays = (doc: Doc) => { return { title: "", caption: "" } };
+
render() {
let contentElement: (JSX.Element | null) = null;
- let docList = Cast(this.resolvedDataDoc[this._chosenKey], listSpec(Doc));
+ let docList = Cast(this.dataDoc[this._chosenKey], listSpec(Doc));
let remDoc = (doc: Doc) => this.remove(doc, this._chosenKey);
- let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.resolvedDataDoc, this._chosenKey, doc, addBefore, before);
- let doc = Cast(this.resolvedDataDoc[this._chosenKey], Doc);
+ let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, this._chosenKey, doc, addBefore, before);
+ let doc = Cast(this.dataDoc[this._chosenKey], Doc);
if (!this._collapsed) {
if (!this.props.document.embed) {
contentElement = <ul key={this._chosenKey + "more"}>
{this._chosenKey === "links" ? this.renderLinks() :
- TreeView.GetChildElements(doc instanceof Doc ? [doc] : DocListCast(docList), this.props.treeViewId, this.props.document, this.props.dataDoc, this._chosenKey, addDoc, remDoc, this.move,
+ TreeView.GetChildElements(doc instanceof Doc ? [doc] : DocListCast(docList), this.props.treeViewId, this.props.document, this.resolvedDataDoc, this._chosenKey, addDoc, remDoc, this.move,
this.props.dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth)}
</ul >;
} else {
- let layoutDoc = Doc.expandTemplateLayout(this.props.document, this.props.dataDoc);
+ let layoutDoc = this.props.document;
contentElement = <div ref={this._dref} style={{ display: "inline-block", height: this.docHeight() }} key={this.props.document[Id] + this.props.document.title}>
<CollectionSchemaPreview
Document={layoutDoc}
DataDocument={this.resolvedDataDoc}
renderDepth={this.props.renderDepth}
+ showOverlays={this.noOverlays}
fitToBox={this.boundsOfCollectionDocument !== undefined}
width={this.docWidth}
height={this.docHeight}
@@ -546,7 +560,8 @@ export class CollectionTreeView extends CollectionSubView(Document) {
SetValue={(value: string) => (Doc.GetProto(this.resolvedDataDoc).title = value) ? true : true}
OnFillDown={(value: string) => {
Doc.GetProto(this.props.Document).title = value;
- let doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
+ let doc = this.props.Document.detailedLayout instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.Document.detailedLayout)) : undefined;
+ if (!doc) doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
TreeView.loadId = doc[Id];
Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, this.childDocs.length ? this.childDocs[0] : undefined, true);
}} />
diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx
index d7d5773ba..31a8a93e0 100644
--- a/src/client/views/collections/CollectionVideoView.tsx
+++ b/src/client/views/collections/CollectionVideoView.tsx
@@ -7,6 +7,8 @@ import { CollectionBaseView, CollectionRenderProps, CollectionViewType } from ".
import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";
import "./CollectionVideoView.scss";
import React = require("react");
+import { InkingControl } from "../InkingControl";
+import { InkTool } from "../../../new_fields/InkField";
@observer
@@ -19,18 +21,19 @@ export class CollectionVideoView extends React.Component<FieldViewProps> {
private get uIButtons() {
let scaling = Math.min(1.8, this.props.ScreenToLocalTransform().Scale);
let curTime = NumCast(this.props.Document.curPage);
- return (VideoBox._showControls ? [] : [
- <div className="collectionVideoView-time" key="time" onPointerDown={this.onResetDown} style={{ transform: `scale(${scaling}, ${scaling})` }}>
- <span>{"" + Math.round(curTime)}</span>
- <span style={{ fontSize: 8 }}>{" " + Math.round((curTime - Math.trunc(curTime)) * 100)}</span>
- </div>,
+ return ([<div className="collectionVideoView-time" key="time" onPointerDown={this.onResetDown} style={{ transform: `scale(${scaling}, ${scaling})` }}>
+ <span>{"" + Math.round(curTime)}</span>
+ <span style={{ fontSize: 8 }}>{" " + Math.round((curTime - Math.trunc(curTime)) * 100)}</span>
+ </div>,
+ VideoBox._showControls ? (null) : [
<div className="collectionVideoView-play" key="play" onPointerDown={this.onPlayDown} style={{ transform: `scale(${scaling}, ${scaling})` }}>
{this._videoBox && this._videoBox.Playing ? "\"" : ">"}
</div>,
<div className="collectionVideoView-full" key="full" onPointerDown={this.onFullDown} style={{ transform: `scale(${scaling}, ${scaling})` }}>
F
</div>
- ]);
+
+ ]]);
}
@action
@@ -53,12 +56,33 @@ export class CollectionVideoView extends React.Component<FieldViewProps> {
}
}
+ _isclick = 0;
@action
- onResetDown = () => {
+ onResetDown = (e: React.PointerEvent) => {
if (this._videoBox) {
this._videoBox.Pause();
- this.props.Document.curPage = 0;
+ 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; };
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 56750668d..4a51a1f58 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -1,11 +1,10 @@
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree } from '@fortawesome/free-solid-svg-icons';
+import { faProjectDiagram, faSignature, faColumns, faSquare, faTh, faImage, faThList, faTree, faEllipsisV } from '@fortawesome/free-solid-svg-icons';
import { observer } from "mobx-react";
import * as React from 'react';
-import { Doc } from '../../../new_fields/Doc';
+import { Doc, DocListCast, WidthSym, HeightSym } from '../../../new_fields/Doc';
import { Id } from '../../../new_fields/FieldSymbols';
import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
-import { Docs } from '../../documents/Documents';
import { undoBatch } from '../../util/UndoManager';
import { ContextMenu } from "../ContextMenu";
import { ContextMenuProps } from '../ContextMenuItem';
@@ -16,6 +15,8 @@ import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormV
import { CollectionSchemaView } from "./CollectionSchemaView";
import { CollectionStackingView } from './CollectionStackingView';
import { CollectionTreeView } from "./CollectionTreeView";
+import { StrCast, PromiseValue } from '../../../new_fields/Types';
+import { DocumentType } from '../../documents/Documents';
export const COLLECTION_BORDER_WIDTH = 2;
library.add(faTh);
@@ -24,6 +25,9 @@ library.add(faSquare);
library.add(faProjectDiagram);
library.add(faSignature);
library.add(faThList);
+library.add(faColumns);
+library.add(faEllipsisV);
+library.add(faImage);
@observer
export class CollectionView extends React.Component<FieldViewProps> {
@@ -35,7 +39,8 @@ export class CollectionView extends React.Component<FieldViewProps> {
case CollectionViewType.Schema: return (<CollectionSchemaView {...props} CollectionView={this} />);
case CollectionViewType.Docking: return (<CollectionDockingView {...props} CollectionView={this} />);
case CollectionViewType.Tree: return (<CollectionTreeView {...props} CollectionView={this} />);
- case CollectionViewType.Stacking: return (<CollectionStackingView {...props} CollectionView={this} />);
+ case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView {...props} CollectionView={this} />); }
+ case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView {...props} CollectionView={this} />); }
case CollectionViewType.Freeform:
default:
return (<CollectionFreeFormView {...props} CollectionView={this} />);
@@ -54,18 +59,10 @@ export class CollectionView extends React.Component<FieldViewProps> {
}
subItems.push({ description: "Schema", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Schema), icon: "th-list" });
subItems.push({ description: "Treeview", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Tree), icon: "tree" });
- subItems.push({ description: "Stacking", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Stacking), icon: "th-list" });
+ subItems.push({ description: "Stacking", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Stacking), icon: "ellipsis-v" });
+ subItems.push({ description: "Masonry", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Masonry), icon: "columns" });
ContextMenu.Instance.addItem({ description: "View Modes...", subitems: subItems });
- ContextMenu.Instance.addItem({
- description: "Apply Template", event: undoBatch(() => {
- let otherdoc = new Doc();
- otherdoc.width = 100;
- otherdoc.height = 50;
- Doc.GetProto(otherdoc).title = "applied(" + this.props.Document.title + ")";
- Doc.GetProto(otherdoc).layout = Doc.MakeDelegate(this.props.Document);
- this.props.addDocTab && this.props.addDocTab(otherdoc, undefined, "onRight");
- }), icon: "project-diagram"
- });
+ ContextMenu.Instance.addItem({ description: "Apply Template", event: undoBatch(() => this.props.addDocTab && this.props.addDocTab(Doc.ApplyTemplate(this.props.Document)!, undefined, "onRight")), icon: "project-diagram" });
}
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index b91aaedee..6bb082b66 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -4,7 +4,7 @@ import { Doc, DocListCastAsync, HeightSym, WidthSym, DocListCast } from "../../.
import { Id } from "../../../../new_fields/FieldSymbols";
import { InkField, StrokeData } from "../../../../new_fields/InkField";
import { createSchema, makeInterface } from "../../../../new_fields/Schema";
-import { BoolCast, Cast, FieldValue, NumCast } from "../../../../new_fields/Types";
+import { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../../../new_fields/Types";
import { emptyFunction, returnOne } from "../../../../Utils";
import { DocumentManager } from "../../../util/DocumentManager";
import { DragManager } from "../../../util/DragManager";
@@ -195,10 +195,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
this._pheight / this.zoomScaling());
let panelwidth = panelDim[0];
let panelheight = panelDim[1];
- if (ranges[0][0] - dx > (this.panX() + panelwidth / 2)) x = ranges[0][1] + panelwidth / 2;
- if (ranges[0][1] - dx < (this.panX() - panelwidth / 2)) x = ranges[0][0] - panelwidth / 2;
- if (ranges[1][0] - dy > (this.panY() + panelheight / 2)) y = ranges[1][1] + panelheight / 2;
- if (ranges[1][1] - dy < (this.panY() - panelheight / 2)) y = ranges[1][0] - panelheight / 2;
+ // if (ranges[0][0] - dx > (this.panX() + panelwidth / 2)) x = ranges[0][1] + panelwidth / 2;
+ // if (ranges[0][1] - dx < (this.panX() - panelwidth / 2)) x = ranges[0][0] - panelwidth / 2;
+ // if (ranges[1][0] - dy > (this.panY() + panelheight / 2)) y = ranges[1][1] + panelheight / 2;
+ // if (ranges[1][1] - dy < (this.panY() - panelheight / 2)) y = ranges[1][0] - panelheight / 2;
}
this.setPan(x - dx, y - dy);
this._lastX = e.pageX;
@@ -263,7 +263,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX));
const newPanY = Math.min((1 - 1 / scale) * this.nativeHeight, Math.max(0, panY));
this.props.Document.panX = this.isAnnotationOverlay ? newPanX : panX;
- this.props.Document.panY = this.isAnnotationOverlay ? newPanY : panY;
+ this.props.Document.panY = this.isAnnotationOverlay && StrCast(this.props.Document.backgroundLayout).indexOf("PDFBox") === -1 ? newPanY : panY;
// this.props.Document.panX = panX;
// this.props.Document.panY = panY;
if (this.props.Document.scrollY) {
@@ -358,6 +358,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
getChildDocumentViewProps(childDocLayout: Doc): DocumentViewProps {
let self = this;
let resolvedDataDoc = !this.props.Document.isTemplate && this.props.DataDoc !== this.props.Document ? this.props.DataDoc : undefined;
+ resolvedDataDoc && Doc.UpdateDocumentExtensionForField(resolvedDataDoc, this.props.fieldKey);
let layoutDoc = Doc.expandTemplateLayout(childDocLayout, resolvedDataDoc);
return {
DataDoc: resolvedDataDoc !== layoutDoc && resolvedDataDoc ? resolvedDataDoc : undefined,
@@ -502,10 +503,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
overlayDisposer();
setTimeout(() => docs.map(d => d.transition = undefined), 1200);
}} />;
- overlayDisposer = OverlayView.Instance.addElement(scriptingBox, options);
+ overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, options);
};
- addOverlay("arrangeInit", { x: 400, y: 100, width: 400, height: 300 }, { collection: "Doc", docs: "Doc[]" }, undefined);
- addOverlay("arrangeScript", { x: 400, y: 500, width: 400, height: 300 }, { doc: "Doc", index: "number", collection: "Doc", state: "any", docs: "Doc[]" }, "{x: number, y: number, width?: number, height?: number}");
+ addOverlay("arrangeInit", { x: 400, y: 100, width: 400, height: 300, title: "Layout Initialization" }, { collection: "Doc", docs: "Doc[]" }, undefined);
+ addOverlay("arrangeScript", { x: 400, y: 500, width: 400, height: 300, title: "Layout Script" }, { doc: "Doc", index: "number", collection: "Doc", state: "any", docs: "Doc[]" }, "{x: number, y: number, width?: number, height?: number}");
}
});
}
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index ed6b224a7..ef65c12cf 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -64,7 +64,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
get dataDoc() {
if (this.props.DataDoc === undefined && this.props.Document.layout instanceof Doc) {
- // if there is no dataDoc (ie, we're not rendering a temlplate layout), but this document
+ // if there is no dataDoc (ie, we're not rendering a template layout), but this document
// has a template layout document, then we will render the template layout but use
// this document as the data document for the layout.
return this.props.Document;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 245dd319d..09a1b49fe 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -35,6 +35,9 @@ import { list, object, createSimpleSchema } from 'serializr';
import { LinkManager } from '../../util/LinkManager';
import { RouteStore } from '../../../server/RouteStore';
import { FormattedTextBox } from './FormattedTextBox';
+import { OverlayView } from '../OverlayView';
+import { ScriptingRepl } from '../ScriptingRepl';
+import { EditableView } from '../EditableView';
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
library.add(fa.faTrash);
@@ -285,6 +288,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
onClick = async (e: React.MouseEvent) => {
+ if (e.nativeEvent.cancelBubble) return; // needed because EditableView may stopPropagation which won't apparently stop this event from firing.
e.stopPropagation();
let altKey = e.altKey;
let ctrlKey = e.ctrlKey;
@@ -347,7 +351,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
// @TODO: shouldn't always follow target context
let linkedFwdContextDocs = [first.length ? await (first[0].targetContext) as Doc : undefined, undefined];
- let linkedFwdPage = [first.length ? NumCast(first[0].linkedToPage, undefined) : undefined, undefined];
+ let linkedFwdPage = [first.length ? NumCast(first[0].anchor2Page, undefined) : undefined, undefined];
if (!linkedFwdDocs.some(l => l instanceof Promise)) {
let maxLocation = StrCast(linkedFwdDocs[0].maximizeLocation, "inTab");
@@ -499,7 +503,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@undoBatch
@action
freezeNativeDimensions = (): void => {
- let proto = Doc.GetProto(this.props.Document);
+ let proto = this.props.Document.isTemplate ? this.props.Document : Doc.GetProto(this.props.Document);
if (proto.ignoreAspect === undefined && !proto.nativeWidth) {
proto.nativeWidth = this.props.PanelWidth();
proto.nativeHeight = this.props.PanelHeight();
@@ -555,15 +559,19 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
this.props.addDocTab && this.props.addDocTab(Docs.Create.SchemaDocument(["title"], aliases, {}), undefined, "onRight"); // bcz: dataDoc?
}, icon: "search"
});
+ if (this.props.Document.detailedLayout && !this.props.Document.isTemplate) {
+ cm.addItem({ description: "Toggle detail", event: () => Doc.ToggleDetailLayout(this.props.Document), icon: "image" });
+ }
+ cm.addItem({ description: "Add Repl", event: () => OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" }) });
cm.addItem({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" });
cm.addItem({ description: "Zoom to Document", event: () => this.props.focus(this.props.Document, true), icon: "search" });
- cm.addItem({ description: "Copy URL", event: () => Utils.CopyText(DocServer.prepend("/doc/" + this.props.Document[Id])), icon: "link" });
+ cm.addItem({ description: "Copy URL", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "link" });
cm.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" });
cm.addItem({ description: "Delete", event: this.deleteClicked, icon: "trash" });
type User = { email: string, userDocumentId: string };
let usersMenu: ContextMenuProps[] = [];
try {
- let stuff = await rp.get(DocServer.prepend(RouteStore.getUsers));
+ let stuff = await rp.get(Utils.prepend(RouteStore.getUsers));
const users: User[] = JSON.parse(stuff);
usersMenu = users.filter(({ email }) => email !== CurrentUserUtils.email).map(({ email, userDocumentId }) => ({
description: email, event: async () => {
@@ -621,10 +629,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
var nativeWidth = this.nativeWidth > 0 ? `${this.nativeWidth}px` : "100%";
var nativeHeight = BoolCast(this.props.Document.ignoreAspect) ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%";
let showOverlays = this.props.showOverlays ? this.props.showOverlays(this.props.Document) : undefined;
- let showTitle = showOverlays && showOverlays.title ? showOverlays.title : StrCast(this.props.Document.showTitle);
- let showCaption = showOverlays && showOverlays.caption ? showOverlays.caption : StrCast(this.props.Document.showCaption);
+ let showTitle = showOverlays && showOverlays.title !== "undefined" ? showOverlays.title : StrCast(this.props.Document.showTitle);
+ let showCaption = showOverlays && showOverlays.caption !== "undefined" ? showOverlays.caption : StrCast(this.props.Document.showCaption);
let templates = Cast(this.props.Document.templates, listSpec("string"));
- if (templates instanceof List) {
+ if (!showOverlays && templates instanceof List) {
templates.map(str => {
if (str.indexOf("{props.Document.title}") !== -1) showTitle = "title";
if (str.indexOf("fieldKey={\"caption\"}") !== -1) showCaption = "caption";
@@ -654,13 +662,25 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
>
{!showTitle && !showCaption ? this.contents :
<div style={{ position: "absolute", display: "inline-block", width: "100%", height: "100%", pointerEvents: "none" }}>
+
+ <div style={{ width: "100%", height: showTextTitle ? "calc(100% - 33px)" : "100%", display: "inline-block", position: "absolute", top: showTextTitle ? "29px" : undefined }}>
+ {this.contents}
+ </div>
{!showTitle ? (null) :
<div style={{
- position: showTextTitle ? "relative" : "absolute", top: 0, textAlign: "center", textOverflow: "ellipsis", whiteSpace: "pre",
+ position: showTextTitle ? "relative" : "absolute", top: 0, padding: "4px", textAlign: "center", textOverflow: "ellipsis", whiteSpace: "pre",
+ pointerEvents: "all",
overflow: "hidden", width: `${100 * this.props.ContentScaling()}%`, height: 25, background: "rgba(0, 0, 0, .4)", color: "white",
transformOrigin: "top left", transform: `scale(${1 / this.props.ContentScaling()})`
}}>
- <span>{this.props.Document[showTitle]}</span>
+ <EditableView
+ contents={this.props.Document[showTitle]}
+ display={"block"}
+ height={72}
+ fontSize={12}
+ GetValue={() => StrCast(this.props.Document[showTitle!])}
+ SetValue={(value: string) => (Doc.GetProto(this.props.Document)[showTitle!] = value) ? true : true}
+ />
</div>
}
{!showCaption ? (null) :
@@ -668,9 +688,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
<FormattedTextBox {...this.props} DataDoc={this.dataDoc} active={returnTrue} isSelected={this.isSelected} focus={emptyFunction} select={this.select} selectOnLoad={this.props.selectOnLoad} fieldExt={""} hideOnLeave={true} fieldKey={showCaption} />
</div>
}
- <div style={{ width: "100%", height: showTextTitle ? "calc(100% - 25px)" : "100%", display: "inline-block", position: showTextTitle ? "relative" : "absolute" }}>
- {this.contents}
- </div>
</div>
}
</div>
diff --git a/src/client/views/nodes/FaceRectangle.tsx b/src/client/views/nodes/FaceRectangle.tsx
new file mode 100644
index 000000000..887efc0d5
--- /dev/null
+++ b/src/client/views/nodes/FaceRectangle.tsx
@@ -0,0 +1,29 @@
+import React = require("react");
+import { observer } from "mobx-react";
+import { observable, runInAction } from "mobx";
+import { RectangleTemplate } from "./FaceRectangles";
+
+@observer
+export default class FaceRectangle extends React.Component<{ rectangle: RectangleTemplate }> {
+ @observable private opacity = 0;
+
+ componentDidMount() {
+ setTimeout(() => runInAction(() => this.opacity = 1), 500);
+ }
+
+ render() {
+ let rectangle = this.props.rectangle;
+ return (
+ <div
+ style={{
+ ...rectangle.style,
+ opacity: this.opacity,
+ transition: "1s ease opacity",
+ position: "absolute",
+ borderRadius: 5
+ }}
+ />
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/nodes/FaceRectangles.tsx b/src/client/views/nodes/FaceRectangles.tsx
new file mode 100644
index 000000000..3570531b2
--- /dev/null
+++ b/src/client/views/nodes/FaceRectangles.tsx
@@ -0,0 +1,46 @@
+import React = require("react");
+import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { Cast, NumCast } from "../../../new_fields/Types";
+import { observer } from "mobx-react";
+import { Id } from "../../../new_fields/FieldSymbols";
+import FaceRectangle from "./FaceRectangle";
+
+interface FaceRectanglesProps {
+ document: Doc;
+ color: string;
+ backgroundColor: string;
+}
+
+export interface RectangleTemplate {
+ id: string;
+ style: Partial<React.CSSProperties>;
+}
+
+@observer
+export default class FaceRectangles extends React.Component<FaceRectanglesProps> {
+
+ render() {
+ let faces = DocListCast(Doc.GetProto(this.props.document).faces);
+ let templates: RectangleTemplate[] = faces.map(faceDoc => {
+ let rectangle = Cast(faceDoc.faceRectangle, Doc) as Doc;
+ let style = {
+ top: NumCast(rectangle.top),
+ left: NumCast(rectangle.left),
+ width: NumCast(rectangle.width),
+ height: NumCast(rectangle.height),
+ backgroundColor: `${this.props.backgroundColor}33`,
+ border: `solid 2px ${this.props.color}`,
+ } as React.CSSProperties;
+ return {
+ id: rectangle[Id],
+ style: style
+ };
+ });
+ return (
+ <div>
+ {templates.map(rectangle => <FaceRectangle key={rectangle.id} rectangle={rectangle} />)}
+ </div>
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 066cc40e2..0a79677e2 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -35,6 +35,7 @@ import "./FormattedTextBox.scss";
import React = require("react");
import { DateField } from '../../../new_fields/DateField';
import { thisExpression } from 'babel-types';
+import { Utils } from '../../../Utils';
library.add(faEdit);
library.add(faSmile);
@@ -232,10 +233,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
return field ? field.Data : `{"doc":{"type":"doc","content":[]},"selection":{"type":"text","anchor":0,"head":0}}`;
},
field2 => {
- if (StrCast(this.props.Document.layout).indexOf("\"" + this.props.fieldKey + "\"") !== -1) { // bcz: UGH! why is this needed... something is happening out of order. test with making a collection, then adding a text note and converting that to a template field.
- this._editorView && !this._applyingChange &&
- this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field2)));
- }
+ this._editorView && !this._applyingChange &&
+ this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field2)));
}
);
@@ -311,8 +310,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
href = parent.childNodes[0].href ? parent.childNodes[0].href : parent.href;
}
if (href) {
- if (href.indexOf(DocServer.prepend("/doc/")) === 0) {
- this._linkClicked = href.replace(DocServer.prepend("/doc/"), "").split("?")[0];
+ 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) {
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index f1b73a676..697f19f0d 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -38,4 +38,22 @@
border: none;
width: 100%;
height: 100%;
+}
+
+.imageBox-audioBackground {
+ display: inline-block;
+ width: 10%;
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ border-radius: 25px;
+ background: white;
+ opacity: 0.3;
+ svg {
+ width: 90% !important;
+ height: 70%;
+ position: absolute;
+ left: 5%;
+ top: 15%;
+ }
} \ No newline at end of file
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index a3e098fd8..0f60bd0fb 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -25,6 +25,8 @@ import { Docs } from '../../documents/Documents';
import { DocServer } from '../../DocServer';
import { Font } from '@react-pdf/renderer';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { CognitiveServices } from '../../cognitive_services/CognitiveServices';
+import FaceRectangles from './FaceRectangles';
var requestImageSize = require('../../util/request-image-size');
var path = require('path');
const { Howl, Howler } = require('howler');
@@ -165,12 +167,12 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
recorder.ondataavailable = async function (e: any) {
const formData = new FormData();
formData.append("file", e.data);
- const res = await fetch(DocServer.prepend(RouteStore.upload), {
+ const res = await fetch(Utils.prepend(RouteStore.upload), {
method: 'POST',
body: formData
});
const files = await res.json();
- const url = DocServer.prepend(files[0]);
+ const url = Utils.prepend(files[0]);
// upload to server with known URL
let audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", x: NumCast(self.props.Document.x), y: NumCast(self.props.Document.y), width: 200, height: 32 });
audioDoc.embed = true;
@@ -195,10 +197,10 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
let field = Cast(this.Document[this.props.fieldKey], ImageField);
if (field) {
let url = field.url.href;
- let subitems: ContextMenuProps[] = [];
- subitems.push({ description: "Copy path", event: () => Utils.CopyText(url), icon: "expand-arrows-alt" });
- subitems.push({ description: "Record 1sec audio", event: this.recordAudioAnnotation, icon: "expand-arrows-alt" });
- subitems.push({
+ let funcs: ContextMenuProps[] = [];
+ funcs.push({ description: "Copy path", event: () => Utils.CopyText(url), icon: "expand-arrows-alt" });
+ funcs.push({ description: "Record 1sec audio", event: this.recordAudioAnnotation, icon: "expand-arrows-alt" });
+ funcs.push({
description: "Rotate", event: action(() => {
let proto = Doc.GetProto(this.props.Document);
let nw = this.props.Document.nativeWidth;
@@ -212,7 +214,14 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
this.props.Document.height = w;
}), icon: "expand-arrows-alt"
});
- ContextMenu.Instance.addItem({ description: "Image Funcs...", subitems: subitems });
+
+ let modes: ContextMenuProps[] = [];
+ let dataDoc = Doc.GetProto(this.Document);
+ modes.push({ description: "Generate Tags", event: () => CognitiveServices.Image.generateMetadata(dataDoc), icon: "tag" });
+ modes.push({ description: "Find Faces", event: () => CognitiveServices.Image.extractFaces(dataDoc), icon: "camera" });
+
+ ContextMenu.Instance.addItem({ description: "Image Funcs...", subitems: funcs });
+ ContextMenu.Instance.addItem({ description: "Analyze...", subitems: modes });
}
}
@@ -259,7 +268,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
_curSuffix = "_m";
resize(srcpath: string, layoutdoc: Doc) {
- requestImageSize(window.origin + RouteStore.corsProxy + "/" + srcpath)
+ requestImageSize(srcpath)
.then((size: any) => {
let aspect = size.height / size.width;
let rotation = NumCast(this.dataDoc.rotation) % 180;
@@ -284,15 +293,17 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
let self = this;
let audioAnnos = DocListCast(this.extensionDoc.audioAnnotations);
if (audioAnnos.length && this._audioState === 0) {
- audioAnnos.map(anno => anno.data instanceof AudioField && new Howl({
+ let anno = audioAnnos[Math.floor(Math.random() * audioAnnos.length)];
+ anno.data instanceof AudioField && new Howl({
src: [anno.data.url.href],
+ format: ["mp3"],
autoplay: true,
loop: false,
volume: 0.5,
onend: function () {
runInAction(() => self._audioState = 0);
}
- }));
+ });
this._audioState = 1;
}
// else {
@@ -327,7 +338,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
let id = (this.props as any).id; // bcz: used to set id = "isExpander" in templates.tsx
let nativeWidth = FieldValue(this.Document.nativeWidth, pw);
let nativeHeight = FieldValue(this.Document.nativeHeight, 0);
- let paths: string[] = [window.origin + RouteStore.corsProxy + "/" + "http://www.cs.brown.edu/~bcz/noImage.png"];
+ 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);
@@ -350,30 +361,26 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
return (
<div id={id} className={`imageBox-cont${interactive}`} style={{ background: "transparent" }}
- onPointerEnter={this.onPointerEnter}
onPointerDown={this.onPointerDown}
onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}>
<img id={id}
key={this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys
src={srcpath}
style={{ transform: `translate(0px, ${shift}px) rotate(${rotation}deg) scale(${aspect})` }}
- // style={{ objectFit: (this.Document.curPage === 0 ? undefined : "contain") }}
width={nativeWidth}
ref={this._imgRef}
onError={this.onError} />
{paths.length > 1 ? this.dots(paths) : (null)}
- <div onPointerDown={this.audioDown} style={{
- display: DocListCast(this.extensionDoc.audioAnnotations).length ? "inline" : "inline",
- width: nativeWidth * 0.1,
- height: nativeWidth * 0.1,
- position: "absolute",
- top: 0,
- left: 0,
- }}>
- <FontAwesomeIcon
- style={{ width: "100%", height: "100%", color: [DocListCast(this.extensionDoc.audioAnnotations).length ? "blue" : "gray", "green", "red"][this._audioState] }} icon={faFileAudio} size="sm" />
+ <div className="imageBox-audioBackground"
+ onPointerDown={this.audioDown}
+ onPointerEnter={this.onPointerEnter}
+ style={{ height: `calc(${.1 * nativeHeight / nativeWidth * 100}%)` }}
+ >
+ <FontAwesomeIcon className="imageBox-audioFont"
+ style={{ color: [DocListCast(this.extensionDoc.audioAnnotations).length ? "blue" : "gray", "green", "red"][this._audioState] }} icon={faFileAudio} size="sm" />
</div>
{/* {this.lightbox(paths)} */}
+ <FaceRectangles document={this.props.Document} color={"#0000FF"} backgroundColor={"#0000FF"} />
</div>);
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index c9dd9a64e..9fc0f2080 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -114,7 +114,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
let protos = Doc.GetAllPrototypes(doc);
for (const proto of protos) {
Object.keys(proto).forEach(key => {
- if (!(key in ids)) {
+ if (!(key in ids) && realDoc[key] !== ComputedField.undefined) {
ids[key] = key;
}
});
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 209782242..064f3edcc 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -13,6 +13,9 @@ import { Doc, Opt, Field } from '../../../new_fields/Doc';
import { FieldValue } from '../../../new_fields/Types';
import { KeyValueBox } from './KeyValueBox';
import { DragManager, SetupDrag } from '../../util/DragManager';
+import { ContextMenu } from '../ContextMenu';
+import { Docs } from '../../documents/Documents';
+import { CollectionDockingView } from '../collections/CollectionDockingView';
// Represents one row in a key value plane
@@ -39,6 +42,16 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
this.isChecked = false;
}
+ onContextMenu = (e: React.MouseEvent) => {
+ const value = this.props.doc[this.props.keyName];
+ if (value instanceof Doc) {
+ e.stopPropagation();
+ e.preventDefault();
+ ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { let kvp = Docs.Create.KVPDocument(value, { width: 300, height: 300 }); CollectionDockingView.Instance.AddRightSplit(kvp, undefined); }, icon: "layer-group" });
+ ContextMenu.Instance.displayMenu(e.clientX, e.clientY);
+ }
+ }
+
render() {
let props: FieldViewProps = {
Document: this.props.doc,
@@ -60,7 +73,17 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
};
let contents = <FieldView {...props} />;
// let fieldKey = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? props.fieldKey : "(" + props.fieldKey + ")";
- let keyStyle = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? "black" : "blue";
+ let protoCount = 0;
+ let doc: Doc | undefined = props.Document;
+ while (doc) {
+ if (Object.keys(doc).includes(props.fieldKey)) {
+ break;
+ }
+ protoCount++;
+ doc = doc.proto;
+ }
+ const parenCount = Math.max(0, protoCount - 1);
+ let keyStyle = protoCount === 0 ? "black" : "blue";
let hover = { transition: "0.3s ease opacity", opacity: this.isPointerOver || this.isChecked ? 1 : 0 };
@@ -83,10 +106,10 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
onChange={this.handleCheck}
ref={this.checkbox}
/>
- <div className="keyValuePair-keyField" style={{ color: keyStyle }}>{props.fieldKey}</div>
+ <div className="keyValuePair-keyField" style={{ color: keyStyle }}>{"(".repeat(parenCount)}{props.fieldKey}{")".repeat(parenCount)}</div>
</div>
</td>
- <td className="keyValuePair-td-value" style={{ width: `${100 - this.props.keyWidth}%` }}>
+ <td className="keyValuePair-td-value" style={{ width: `${100 - this.props.keyWidth}%` }} onContextMenu={this.onContextMenu}>
<div className="keyValuePair-td-value-container">
<EditableView
contents={contents}
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 8b8e500c4..30ad75000 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -52,25 +52,27 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
this.Document.nativeHeight = this.Document.nativeWidth / aspect;
this.Document.height = FieldValue(this.Document.width, 0) / aspect;
}
+ if (!this.Document.duration) this.Document.duration = this.player!.duration;
}
@action public Play = (update: boolean = true) => {
this.Playing = true;
update && this.player && this.player.play();
update && this._youtubePlayer && this._youtubePlayer.playVideo();
- !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 500));
+ this._youtubePlayer && !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 5));
this.updateTimecode();
}
@action public Seek(time: number) {
this._youtubePlayer && this._youtubePlayer.seekTo(Math.round(time), true);
+ this.player && (this.player.currentTime = time);
}
@action public Pause = (update: boolean = true) => {
this.Playing = false;
update && this.player && this.player.pause();
update && this._youtubePlayer && this._youtubePlayer.pauseVideo();
- this._playTimer && clearInterval(this._playTimer);
+ this._youtubePlayer && this._playTimer && clearInterval(this._playTimer);
this._playTimer = undefined;
this.updateTimecode();
}
@@ -112,6 +114,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
setVideoRef = (vref: HTMLVideoElement | null) => {
this._videoRef = vref;
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, () =>
@@ -122,7 +125,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
public static async convertDataUri(imageUri: string, returnedFilename: string) {
try {
- let posting = DocServer.prepend(RouteStore.dataUriToImage);
+ let posting = Utils.prepend(RouteStore.dataUriToImage);
const returnedUri = await rp.post(posting, {
body: {
uri: imageUri,
@@ -164,7 +167,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
let filename = encodeURIComponent("snapshot" + this.props.Document.title + "_" + this.props.Document.curPage).replace(/\./g, "");
VideoBox.convertDataUri(dataUrl, filename).then(returnedFilename => {
if (returnedFilename) {
- let url = DocServer.prepend(returnedFilename);
+ let url = 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-"
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index f0a9ec6d8..162ac1d98 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -9,22 +9,6 @@ import React = require("react");
import { InkTool } from "../../../new_fields/InkField";
import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
-export function onYouTubeIframeAPIReady() {
- console.log("player");
- return;
- let player = new YT.Player('player', {
- events: {
- 'onReady': onPlayerReady
- }
- });
-}
-// must cast as any to set property on window
-const _global = (window /* browser */ || global /* node */) as any;
-_global.onYouTubeIframeAPIReady = onYouTubeIframeAPIReady;
-
-function onPlayerReady(event: any) {
- event.target.playVideo();
-}
@observer
export class WebBox extends React.Component<FieldViewProps> {
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 699a1ffd7..b7ded7e06 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -4,23 +4,18 @@ import * as Pdfjs from "pdfjs-dist";
import "pdfjs-dist/web/pdf_viewer.css";
import * as rp from "request-promise";
import { Dictionary } from "typescript-collections";
-import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc";
+import { Doc, DocListCast, Opt } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
import { List } from "../../../new_fields/List";
-import { BoolCast, Cast, NumCast, StrCast, FieldValue } from "../../../new_fields/Types";
-import { emptyFunction } from "../../../Utils";
-import { DocServer } from "../../DocServer";
-import { Docs, DocUtils, DocumentOptions } from "../../documents/Documents";
-import { DocumentManager } from "../../util/DocumentManager";
+import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { emptyFunction, Utils } from "../../../Utils";
+import { Docs, DocUtils } from "../../documents/Documents";
import { DragManager } from "../../util/DragManager";
-import { DocumentView } from "../nodes/DocumentView";
-import { PDFBox, handleBackspace } from "../nodes/PDFBox";
+import { PDFBox } from "../nodes/PDFBox";
import Page from "./Page";
import "./PDFViewer.scss";
import React = require("react");
-import PDFMenu from "./PDFMenu";
-import { UndoManager } from "../../util/UndoManager";
-import { CompileScript, CompiledScript, CompileResult } from "../../util/Scripting";
+import { CompileScript, CompileResult } from "../../util/Scripting";
import { ScriptField } from "../../../new_fields/ScriptField";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Annotation from "./Annotation";
@@ -90,16 +85,12 @@ export class Viewer extends React.Component<IViewerProps> {
private _annotationReactionDisposer?: IReactionDisposer;
private _dropDisposer?: DragManager.DragDropDisposer;
private _filterReactionDisposer?: IReactionDisposer;
- private _activeReactionDisposer?: IReactionDisposer;
private _viewer: React.RefObject<HTMLDivElement>;
private _mainCont: React.RefObject<HTMLDivElement>;
private _pdfViewer: any;
// private _textContent: Pdfjs.TextContent[] = [];
private _pdfFindController: any;
private _searchString: string = "";
- private _rendered: boolean = false;
- private _pageIndex: number = -1;
- private _matchIndex: number = 0;
constructor(props: IViewerProps) {
super(props);
@@ -135,23 +126,6 @@ export class Viewer extends React.Component<IViewerProps> {
},
{ fireImmediately: true });
- this._activeReactionDisposer = reaction(
- () => this.props.parent.props.active(),
- () => {
- runInAction(() => {
- if (!this.props.parent.props.active()) {
- this._searching = false;
- this._pdfFindController = null;
- if (this._viewer.current) {
- let cns = this._viewer.current.childNodes;
- for (let i = cns.length - 1; i >= 0; i--) {
- cns.item(i).remove();
- }
- }
- }
- });
- }
- );
if (this.props.parent.props.ContainingCollectionView) {
this._filterReactionDisposer = reaction(
@@ -346,7 +320,7 @@ export class Viewer extends React.Component<IViewerProps> {
this._isPage[page] = "image";
const address = this.props.url;
try {
- let res = JSON.parse(await rp.get(DocServer.prepend(`/thumbnail${address.substring("files/".length, address.length - ".pdf".length)}-${page + 1}.PNG`)));
+ let res = JSON.parse(await rp.get(Utils.prepend(`/thumbnail${address.substring("files/".length, address.length - ".pdf".length)}-${page + 1}.PNG`)));
runInAction(() => this._visibleElements[page] =
<img key={res.path} src={res.path} onError={handleError}
style={{ width: `${parseInt(res.width) * scale}px`, height: `${parseInt(res.height) * scale}px` }} />);
@@ -476,7 +450,6 @@ export class Viewer extends React.Component<IViewerProps> {
phraseSearch: true,
query: searchString
});
- this._rendered = true;
});
container.addEventListener("pagerendered", () => {
console.log("rendered");
@@ -488,7 +461,6 @@ export class Viewer extends React.Component<IViewerProps> {
phraseSearch: true,
query: searchString
});
- this._rendered = true;
});
}
}
@@ -563,7 +535,6 @@ export class Viewer extends React.Component<IViewerProps> {
});
container.addEventListener("pagerendered", () => {
console.log("rendered");
- this._rendered = true;
});
this._pdfViewer.setDocument(this.props.pdf);
this._pdfFindController = new PDFJSViewer.PDFFindController(this._pdfViewer);
@@ -703,17 +674,17 @@ class SimpleLinkService {
externalLinkRel: any = null;
pdf: any = null;
- navigateTo(dest: any) { }
+ navigateTo() { }
- getDestinationHash(dest: any) { return "#"; }
+ getDestinationHash() { return "#"; }
- getAnchorUrl(hash: any) { return "#"; }
+ getAnchorUrl() { return "#"; }
- setHash(hash: any) { }
+ setHash() { }
- executeNamedAction(action: any) { }
+ executeNamedAction() { }
- cachePageRef(pageNum: any, pageRef: any) { }
+ cachePageRef() { }
get pagesCount() {
return this.pdf ? this.pdf.numPages : 0;
diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx
index a16d7bc76..329630875 100644
--- a/src/client/views/presentationview/PresentationElement.tsx
+++ b/src/client/views/presentationview/PresentationElement.tsx
@@ -12,6 +12,10 @@ import { faFile as fileSolid, faFileDownload, faLocationArrow, faArrowUp, faSear
import { faFile as fileRegular } from '@fortawesome/free-regular-svg-icons';
import { List } from "../../../new_fields/List";
import { listSpec } from "../../../new_fields/Schema";
+import { DragManager, SetupDrag, dropActionType } from "../../util/DragManager";
+import { SelectionManager } from "../../util/SelectionManager";
+import { indexOf } from "typescript-collections/dist/lib/arrays";
+import { map } from "bluebird";
library.add(faArrowUp);
library.add(fileSolid);
@@ -30,6 +34,8 @@ interface PresentationElementProps {
presStatus: boolean;
presButtonBackUp: Doc;
presGroupBackUp: Doc;
+ removeDocByRef(doc: Doc): boolean;
+ PresElementsMappings: Map<Doc, PresentationElement>;
}
@@ -53,13 +59,28 @@ export enum buttonIndex {
export default class PresentationElement extends React.Component<PresentationElementProps> {
@observable private selectedButtons: boolean[];
+ private header?: HTMLDivElement | undefined;
+ private listdropDisposer?: DragManager.DragDropDisposer;
+ private presElRef: React.RefObject<HTMLDivElement>;
+ private backUpDoc: Doc | undefined;
+
+
+
constructor(props: PresentationElementProps) {
super(props);
this.selectedButtons = new Array(6);
+
+ this.presElRef = React.createRef();
}
+
+ componentWillUnmount() {
+ this.listdropDisposer && this.listdropDisposer();
+ }
+
+
/**
* Getter to get the status of the buttons.
*/
@@ -71,12 +92,18 @@ export default class PresentationElement extends React.Component<PresentationEle
//Lifecycle function that makes sure that button BackUp is received when mounted.
async componentDidMount() {
this.receiveButtonBackUp();
-
+ if (this.presElRef.current) {
+ this.header = this.presElRef.current;
+ this.createListDropTarget(this.presElRef.current);
+ }
}
//Lifecycle function that makes sure button BackUp is received when not re-mounted bu re-rendered.
async componentDidUpdate() {
- this.receiveButtonBackUp();
+ if (this.presElRef.current) {
+ this.header = this.presElRef.current;
+ this.createListDropTarget(this.presElRef.current);
+ }
}
receiveButtonBackUp = async () => {
@@ -86,19 +113,32 @@ export default class PresentationElement extends React.Component<PresentationEle
if (!castedList) {
this.props.presButtonBackUp.selectedButtonDocs = castedList = new List<Doc>();
}
+
+ let foundDoc: boolean = false;
+
//if this is the first time this doc mounts, push a doc for it to store
- if (castedList.length <= this.props.index) {
+
+ for (let doc of castedList) {
+ let curDoc = await doc;
+ let curDocId = StrCast(curDoc.docId);
+ if (curDocId === this.props.document[Id]) {
+ let selectedButtonOfDoc = Cast(curDoc.selectedButtons, listSpec("boolean"), null);
+ if (selectedButtonOfDoc !== undefined) {
+ runInAction(() => this.selectedButtons = selectedButtonOfDoc);
+ foundDoc = true;
+ this.backUpDoc = curDoc;
+ break;
+ }
+ }
+ }
+
+ if (!foundDoc) {
let newDoc = new Doc();
let defaultBooleanArray: boolean[] = new Array(6);
newDoc.selectedButtons = new List(defaultBooleanArray);
+ newDoc.docId = this.props.document[Id];
castedList.push(newDoc);
- //otherwise update the selected buttons depending on storage.
- } else {
- let curDoc: Doc = await castedList[this.props.index];
- let selectedButtonOfDoc = Cast(curDoc.selectedButtons, listSpec("boolean"), null);
- if (selectedButtonOfDoc !== undefined) {
- runInAction(() => this.selectedButtons = selectedButtonOfDoc);
- }
+ this.backUpDoc = newDoc;
}
}
@@ -244,9 +284,9 @@ export default class PresentationElement extends React.Component<PresentationEle
*/
@action
autoSaveButtonChange = async (index: buttonIndex) => {
- let castedList = (await DocListCastAsync(this.props.presButtonBackUp.selectedButtonDocs))!;
- castedList[this.props.index].selectedButtons = new List(this.selectedButtons);
-
+ if (this.backUpDoc) {
+ this.backUpDoc.selectedButtons = new List(this.selectedButtons);
+ }
}
/**
@@ -356,6 +396,380 @@ export default class PresentationElement extends React.Component<PresentationEle
}
+ /**
+ * Creating a drop target for drag and drop when called.
+ */
+ protected createListDropTarget = (ele: HTMLDivElement) => {
+ this.listdropDisposer && this.listdropDisposer();
+ if (ele) {
+ this.listdropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.listDrop.bind(this) } });
+ }
+ }
+
+ /**
+ * Returns a local transformed coordinate array for given coordinates.
+ */
+ ScreenToLocalListTransform = (xCord: number, yCord: number) => {
+ return [xCord, yCord];
+ }
+
+ /**
+ * This method is called when a element is dropped on a already esstablished target.
+ * It makes sure to do appropirate action depending on if the item is dropped before
+ * or after the target.
+ */
+ listDrop = async (e: Event, de: DragManager.DropEvent) => {
+ let x = this.ScreenToLocalListTransform(de.x, de.y);
+ let rect = this.header!.getBoundingClientRect();
+ let bounds = this.ScreenToLocalListTransform(rect.left, rect.top + rect.height / 2);
+ let before = x[1] < bounds[1];
+ if (de.data instanceof DragManager.DocumentDragData) {
+ let addDoc = (doc: Doc) => Doc.AddDocToList(this.props.mainDocument, "data", doc, this.props.document, before);
+ e.stopPropagation();
+ //where does treeViewId come from
+ let movedDocs = (de.data.options === this.props.mainDocument[Id] ? de.data.draggedDocuments : de.data.droppedDocuments);
+ //console.log("How is this causing an issue");
+ let droppedDoc: Doc = de.data.droppedDocuments[0];
+ await this.updateGroupsOnDrop(droppedDoc, de);
+ document.removeEventListener("pointermove", this.onDragMove, true);
+ return (de.data.dropAction || de.data.userDropAction) ?
+ de.data.droppedDocuments.reduce((added: boolean, d: Doc) => Doc.AddDocToList(this.props.mainDocument, "data", d, this.props.document, before) || added, false)
+ : (de.data.moveDocument) ?
+ movedDocs.reduce((added: boolean, d: Doc) => de.data.moveDocument(d, this.props.document, addDoc) || added, false)
+ : de.data.droppedDocuments.reduce((added: boolean, d: Doc) => Doc.AddDocToList(this.props.mainDocument, "data", d, this.props.document, before), false);
+ }
+ document.removeEventListener("pointermove", this.onDragMove, true);
+
+ return false;
+ }
+
+ /**
+ * This method is called to update groups when the user drags and drops an
+ * element to a different place. It follows the default behaviour and reconstructs
+ * the groups in the way they would appear if clicked by user.
+ */
+ updateGroupsOnDrop = async (droppedDoc: Doc, de: DragManager.DropEvent) => {
+
+ let x = this.ScreenToLocalListTransform(de.x, de.y);
+ let rect = this.header!.getBoundingClientRect();
+ let bounds = this.ScreenToLocalListTransform(rect.left, rect.top + rect.height / 2);
+ let before = x[1] < bounds[1];
+
+ let droppedDocIndex = this.props.allListElements.indexOf(droppedDoc);
+
+ let dropIndexDiff = droppedDocIndex - this.props.index;
+
+ //checking if the position it's dropped corresponds to current location with 3 cases.
+ if (droppedDocIndex === this.props.index) {
+ return;
+ }
+
+ if (dropIndexDiff === 1 && !before) {
+ return;
+ }
+ if (dropIndexDiff === -1 && before) {
+ return;
+ }
+
+ let p = this.props;
+ let droppedDocSelectedButtons: boolean[] = await this.getSelectedButtonsOfDoc(droppedDoc);
+ let curDocGuid = StrCast(droppedDoc.presentId, null);
+
+ //Splicing the doc from its current group, since it's moved
+ if (p.groupMappings.has(curDocGuid)) {
+ let groupArray = this.props.groupMappings.get(curDocGuid)!;
+
+ if (droppedDocSelectedButtons[buttonIndex.Group]) {
+ let groupIndexOfDrop = groupArray.indexOf(droppedDoc);
+ let firstPart = groupArray.splice(0, groupIndexOfDrop);
+
+ if (firstPart.length > 1) {
+ let newGroupGuid = Utils.GenerateGuid();
+ firstPart.forEach((doc: Doc) => doc.presentId = newGroupGuid);
+ this.props.groupMappings.set(newGroupGuid, firstPart);
+ }
+ }
+
+ groupArray.splice(groupArray.indexOf(droppedDoc), 1);
+ if (groupArray.length === 0) {
+ this.props.groupMappings.delete(curDocGuid);
+ }
+ droppedDoc.presentId = Utils.GenerateGuid();
+
+ //making sure to correct to groups after splicing, in case the dragged element
+ //had the grouping on.
+ let indexOfBelow = droppedDocIndex + 1;
+ if (indexOfBelow < this.props.allListElements.length && indexOfBelow > 1) {
+ let selectedButtonsOrigBelow: boolean[] = await this.getSelectedButtonsOfDoc(this.props.allListElements[indexOfBelow]);
+ let aboveBelowDoc: Doc = this.props.allListElements[droppedDocIndex - 1];
+ let aboveBelowDocSelectedButtons: boolean[] = await this.getSelectedButtonsOfDoc(aboveBelowDoc);
+ let belowDoc: Doc = this.props.allListElements[indexOfBelow];
+ let belowDocPresId = StrCast(belowDoc.presentId);
+
+ if (selectedButtonsOrigBelow[buttonIndex.Group]) {
+ let belowDocGroup: Doc[] = this.props.groupMappings.get(belowDocPresId)!;
+ if (aboveBelowDocSelectedButtons[buttonIndex.Group]) {
+ let aboveBelowDocPresId = StrCast(aboveBelowDoc.presentId);
+ if (this.props.groupMappings.has(aboveBelowDocPresId)) {
+ let aboveBelowDocGroup: Doc[] = this.props.groupMappings.get(aboveBelowDocPresId)!;
+ aboveBelowDocGroup.push(...belowDocGroup);
+ this.props.groupMappings.delete(belowDocPresId);
+ belowDocGroup.forEach((doc: Doc) => doc.presentId = aboveBelowDocPresId);
+
+ }
+ } else {
+ belowDocGroup.unshift(aboveBelowDoc);
+ aboveBelowDoc.presentId = belowDocPresId;
+ }
+
+
+ }
+ }
+
+ }
+
+ //Case, when the dropped doc had the group button clicked.
+ if (droppedDocSelectedButtons[buttonIndex.Group]) {
+ if (before) {
+ if (this.props.index > 0) {
+ let aboveDoc = this.props.allListElements[this.props.index - 1];
+ let aboveDocGuid = StrCast(aboveDoc.presentId);
+ if (this.props.groupMappings.has(aboveDocGuid)) {
+ this.protectOrderAndPush(aboveDocGuid, aboveDoc, droppedDoc);
+ } else {
+ this.createNewGroup(aboveDoc, droppedDoc, aboveDocGuid);
+ }
+ } else {
+ let propsPresId = StrCast(this.props.document.presentId);
+ if (this.selectedButtons[buttonIndex.Group]) {
+ let propsArray = this.props.groupMappings.get(propsPresId)!;
+ propsArray.unshift(droppedDoc);
+ droppedDoc.presentId = propsPresId;
+ }
+ }
+ } else {
+ let propsDocGuid = StrCast(this.props.document.presentId);
+ if (this.props.groupMappings.has(propsDocGuid)) {
+ this.protectOrderAndPush(propsDocGuid, this.props.document, droppedDoc);
+
+ } else {
+ this.createNewGroup(this.props.document, droppedDoc, propsDocGuid);
+ }
+ }
+
+
+ //if the group button of the element was not clicked.
+ } else {
+ if (before) {
+ if (this.props.index > 0) {
+
+ let aboveDoc = this.props.allListElements[this.props.index - 1];
+ let aboveDocGuid = StrCast(aboveDoc.presentId);
+ let aboveDocSelectedButtons: boolean[] = await this.getSelectedButtonsOfDoc(aboveDoc);
+
+
+ if (this.selectedButtons[buttonIndex.Group]) {
+ if (aboveDocSelectedButtons[buttonIndex.Group]) {
+ let aboveGroupArray = this.props.groupMappings.get(aboveDocGuid)!;
+ let propsDocPresId = StrCast(this.props.document.presentId);
+
+ this.halveGroupArray(aboveDoc, aboveGroupArray, droppedDoc, propsDocPresId);
+
+ } else {
+ let belowPresentId = StrCast(this.props.document.presentId);
+ let belowGroup = this.props.groupMappings.get(belowPresentId)!;
+ belowGroup.splice(belowGroup.indexOf(aboveDoc), 1);
+ belowGroup.unshift(droppedDoc);
+ droppedDoc.presentId = belowPresentId;
+ aboveDoc.presentId = Utils.GenerateGuid();
+ }
+
+
+ }
+ } else {
+ let propsPresId = StrCast(this.props.document.presentId);
+ if (this.selectedButtons[buttonIndex.Group]) {
+ let propsArray = this.props.groupMappings.get(propsPresId)!;
+ propsArray.unshift(droppedDoc);
+ droppedDoc.presentId = propsPresId;
+ }
+ }
+ } else {
+ if (this.props.index < this.props.allListElements.length - 1) {
+ let belowDoc = this.props.allListElements[this.props.index + 1];
+ let belowDocGuid = StrCast(belowDoc.presentId);
+ let belowDocSelectedButtons: boolean[] = await this.getSelectedButtonsOfDoc(belowDoc);
+
+ let propsDocGuid = StrCast(this.props.document.presentId);
+
+ if (belowDocSelectedButtons[buttonIndex.Group]) {
+ let belowGroupArray = this.props.groupMappings.get(belowDocGuid)!;
+ if (this.selectedButtons[buttonIndex.Group]) {
+
+ let propsGroupArray = this.props.groupMappings.get(propsDocGuid)!;
+
+ this.halveGroupArray(this.props.document, propsGroupArray, droppedDoc, belowDocGuid);
+
+ } else {
+ belowGroupArray.splice(belowGroupArray.indexOf(this.props.document), 1);
+ this.props.document.presentId = Utils.GenerateGuid();
+ belowGroupArray.unshift(droppedDoc);
+ droppedDoc.presentId = belowDocGuid;
+ }
+ }
+
+ }
+ }
+ }
+ this.autoSaveGroupChanges();
+
+ }
+
+ /**
+ * This method returns the selectedButtons boolean array of the passed in doc,
+ * retrieving it from the back-up.
+ */
+ getSelectedButtonsOfDoc = async (paramDoc: Doc) => {
+ let castedList = Cast(this.props.presButtonBackUp.selectedButtonDocs, listSpec(Doc));
+ let foundSelectedButtons: boolean[] = new Array(6);
+
+ //if this is the first time this doc mounts, push a doc for it to store
+ for (let doc of castedList!) {
+ let curDoc = await doc;
+ let curDocId = StrCast(curDoc.docId);
+ if (curDocId === paramDoc[Id]) {
+ let selectedButtonOfDoc = Cast(curDoc.selectedButtons, listSpec("boolean"), null);
+ if (selectedButtonOfDoc !== undefined) {
+ return selectedButtonOfDoc;
+ }
+ }
+ }
+
+ return foundSelectedButtons;
+
+ }
+
+ //This is used to add dragging as an event.
+ onPointerEnter = (e: React.PointerEvent): void => {
+ this.props.document.libraryBrush = true;
+ if (e.buttons === 1 && SelectionManager.GetIsDragging()) {
+ let selected = NumCast(this.props.mainDocument.selectedDoc, 0);
+
+ this.header!.className = "presentationView-item";
+
+
+ if (selected === this.props.index) {
+ //this doc is selected
+ this.header!.className = "presentationView-item presentationView-selected";
+ }
+ document.addEventListener("pointermove", this.onDragMove, true);
+ }
+ }
+
+ //This is used to remove the dragging when dropped.
+ onPointerLeave = (e: React.PointerEvent): void => {
+ this.props.document.libraryBrush = false;
+ //to get currently selected presentation doc
+ let selected = NumCast(this.props.mainDocument.selectedDoc, 0);
+
+ this.header!.className = "presentationView-item";
+
+
+ if (selected === this.props.index) {
+ //this doc is selected
+ this.header!.className = "presentationView-item presentationView-selected";
+
+ }
+ document.removeEventListener("pointermove", this.onDragMove, true);
+ }
+
+ /**
+ * This method is passed in to be used when dragging a document.
+ * It makes it possible to show dropping lines on drop targets.
+ */
+ onDragMove = (e: PointerEvent): void => {
+ this.props.document.libraryBrush = false;
+ let x = this.ScreenToLocalListTransform(e.clientX, e.clientY);
+ let rect = this.header!.getBoundingClientRect();
+ let bounds = this.ScreenToLocalListTransform(rect.left, rect.top + rect.height / 2);
+ let before = x[1] < bounds[1];
+ this.header!.className = "presentationView-item";
+ if (before) {
+ this.header!.className += " presentationView-item-above";
+ }
+ else if (!before) {
+ this.header!.className += " presentationView-item-below";
+ }
+ e.stopPropagation();
+ }
+
+ /**
+ * This method is passed in to on down event of presElement, so that drag and
+ * drop can be completed with DragManager functionality.
+ */
+ @action
+ move: DragManager.MoveFunction = (doc: Doc, target: Doc, addDoc) => {
+ return this.props.document !== target && this.props.removeDocByRef(doc) && addDoc(doc);
+ }
+
+ /**
+ * Helper method that gets called to divide a group array into two different groups
+ * including the targetDoc in first part.
+ * @param targetDoc document that is targeted as slicing point
+ * @param propsGroupArray the array that gets divided into 2
+ * @param droppedDoc the dropped document
+ * @param belowDocGuid presentId of the belowGroup
+ */
+ private halveGroupArray(targetDoc: Doc, propsGroupArray: Doc[], droppedDoc: Doc, belowDocGuid: string) {
+ let targetIndex = propsGroupArray.indexOf(targetDoc);
+ let firstPart = propsGroupArray.slice(0, targetIndex + 1);
+ let firstPartNewGuid = Utils.GenerateGuid();
+ firstPart.forEach((doc: Doc) => doc.presentId = firstPartNewGuid);
+ let secondPart = propsGroupArray.slice(targetIndex + 1);
+ secondPart.unshift(droppedDoc);
+ droppedDoc.presentId = belowDocGuid;
+ this.props.groupMappings.set(firstPartNewGuid, firstPart);
+ this.props.groupMappings.set(belowDocGuid, secondPart);
+ }
+
+ /**
+ * Helper method that creates a new group, pushing above document first,
+ * and dropped document second.
+ * @param aboveDoc the document above dropped document
+ * @param droppedDoc the dropped document itself
+ * @param aboveDocGuid above document's presentId
+ */
+ private createNewGroup(aboveDoc: Doc, droppedDoc: Doc, aboveDocGuid: string) {
+ let newGroup: Doc[] = [];
+ newGroup.push(aboveDoc);
+ newGroup.push(droppedDoc);
+ droppedDoc.presentId = aboveDocGuid;
+ this.props.groupMappings.set(aboveDocGuid, newGroup);
+ }
+
+ /**
+ * Helper method that finds the above document's group, and pushes the
+ * dropped document into that group, protecting the visual order of the
+ * presentation elements.
+ * @param aboveDoc the document above dropped document
+ * @param droppedDoc the dropped document itself
+ * @param aboveDocGuid above document's presentId
+ */
+ private protectOrderAndPush(aboveDocGuid: string, aboveDoc: Doc, droppedDoc: Doc) {
+ let groupArray = this.props.groupMappings.get(aboveDocGuid)!;
+ let tempStack: Doc[] = [];
+ while (groupArray[groupArray.length - 1] !== aboveDoc) {
+ tempStack.push(groupArray.pop()!);
+ }
+ groupArray.push(droppedDoc);
+ droppedDoc.presentId = aboveDocGuid;
+ while (tempStack.length !== 0) {
+ groupArray.push(tempStack.pop()!);
+ }
+ }
+
+
+
render() {
let p = this.props;
@@ -364,16 +778,18 @@ export default class PresentationElement extends React.Component<PresentationEle
//to get currently selected presentation doc
let selected = NumCast(p.mainDocument.selectedDoc, 0);
- let className = "presentationView-item";
+ let className = " presentationView-item";
if (selected === p.index) {
//this doc is selected
className += " presentationView-selected";
}
- let onEnter = (e: React.PointerEvent) => { p.document.libraryBrush = true; };
- let onLeave = (e: React.PointerEvent) => { p.document.libraryBrush = undefined; };
+ let dropAction = StrCast(this.props.document.dropAction) as dropActionType;
+ let onItemDown = SetupDrag(this.presElRef, () => p.document, this.move, dropAction, this.props.mainDocument[Id], true);
return (
<div className={className} key={p.document[Id] + p.index}
- onPointerEnter={onEnter} onPointerLeave={onLeave}
+ ref={this.presElRef}
+ onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}
+ onPointerDown={onItemDown}
style={{
outlineColor: "maroon",
outlineStyle: "dashed",
@@ -383,14 +799,14 @@ export default class PresentationElement extends React.Component<PresentationEle
<strong className="presentationView-name">
{`${p.index + 1}. ${title}`}
</strong>
- <button className="presentation-icon" onClick={e => { this.props.deleteDocument(p.index); e.stopPropagation(); }}>X</button>
+ <button className="presentation-icon" onPointerDown={(e) => e.stopPropagation()} onClick={e => { this.props.deleteDocument(p.index); e.stopPropagation(); }}>X</button>
<br></br>
- <button title="Zoom" className={this.selectedButtons[buttonIndex.Show] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onZoomDocumentClick}><FontAwesomeIcon icon={"search"} /></button>
- <button title="Navigate" className={this.selectedButtons[buttonIndex.Navigate] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onNavigateDocumentClick}><FontAwesomeIcon icon={"location-arrow"} /></button>
- <button title="Hide Document Till Presented" className={this.selectedButtons[buttonIndex.HideTillPressed] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onHideDocumentUntilPressClick}><FontAwesomeIcon icon={fileSolid} /></button>
- <button title="Fade Document After Presented" className={this.selectedButtons[buttonIndex.FadeAfter] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} color={"gray"} /></button>
- <button title="Hide Document After Presented" className={this.selectedButtons[buttonIndex.HideAfter] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
- <button title="Group With Up" className={this.selectedButtons[buttonIndex.Group] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={(e) => {
+ <button title="Zoom" className={this.selectedButtons[buttonIndex.Show] ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onZoomDocumentClick}><FontAwesomeIcon icon={"search"} /></button>
+ <button title="Navigate" className={this.selectedButtons[buttonIndex.Navigate] ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onNavigateDocumentClick}><FontAwesomeIcon icon={"location-arrow"} /></button>
+ <button title="Hide Document Till Presented" className={this.selectedButtons[buttonIndex.HideTillPressed] ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onHideDocumentUntilPressClick}><FontAwesomeIcon icon={fileSolid} /></button>
+ <button title="Fade Document After Presented" className={this.selectedButtons[buttonIndex.FadeAfter] ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} color={"gray"} /></button>
+ <button title="Hide Document After Presented" className={this.selectedButtons[buttonIndex.HideAfter] ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
+ <button title="Group With Up" className={this.selectedButtons[buttonIndex.Group] ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={(e) => {
e.stopPropagation();
this.changeGroupStatus();
this.onGroupClick(p.document, p.index, this.selectedButtons[buttonIndex.Group]);
diff --git a/src/client/views/presentationview/PresentationList.tsx b/src/client/views/presentationview/PresentationList.tsx
index 7abd3e366..2d63d41b5 100644
--- a/src/client/views/presentationview/PresentationList.tsx
+++ b/src/client/views/presentationview/PresentationList.tsx
@@ -7,6 +7,9 @@ import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc";
import { NumCast, StrCast } from "../../../new_fields/Types";
import { Id } from "../../../new_fields/FieldSymbols";
import PresentationElement, { buttonIndex } from "./PresentationElement";
+import { DragManager } from "../../util/DragManager";
+import { CollectionDockingView } from "../collections/CollectionDockingView";
+import "../../../new_fields/Doc";
@@ -16,11 +19,14 @@ interface PresListProps {
deleteDocument(index: number): void;
gotoDocument(index: number, fromDoc: number): Promise<void>;
groupMappings: Map<String, Doc[]>;
- presElementsMappings: Map<Doc, PresentationElement>;
+ PresElementsMappings: Map<Doc, PresentationElement>;
setChildrenDocs: (docList: Doc[]) => void;
presStatus: boolean;
presButtonBackUp: Doc;
presGroupBackUp: Doc;
+ removeDocByRef(doc: Doc): boolean;
+ clearElemMap(): void;
+
}
@@ -79,25 +85,31 @@ export default class PresentationViewList extends React.Component<PresListProps>
this.initializeGroupIds(children);
this.initializeScaleViews(children);
this.props.setChildrenDocs(children);
+ this.props.clearElemMap();
return (
-
- <div className="presentationView-listCont">
- {children.map((doc: Doc, index: number) =>
- <PresentationElement
- ref={(e) => { if (e) { this.props.presElementsMappings.set(doc, e); } }}
- key={doc[Id]}
- mainDocument={this.props.mainDocument}
- document={doc}
- index={index}
- deleteDocument={this.props.deleteDocument}
- gotoDocument={this.props.gotoDocument}
- groupMappings={this.props.groupMappings}
- allListElements={children}
- presStatus={this.props.presStatus}
- presButtonBackUp={this.props.presButtonBackUp}
- presGroupBackUp={this.props.presGroupBackUp}
- />
- )}
+ <div className="presentationView-listCont" >
+ {children.map((doc: Doc, index: number) =>
+ <PresentationElement
+ ref={(e) => {
+ if (e && e !== null) {
+ this.props.PresElementsMappings.set(doc, e);
+ }
+ }}
+ key={doc[Id]}
+ mainDocument={this.props.mainDocument}
+ document={doc}
+ index={index}
+ deleteDocument={this.props.deleteDocument}
+ gotoDocument={this.props.gotoDocument}
+ groupMappings={this.props.groupMappings}
+ allListElements={children}
+ presStatus={this.props.presStatus}
+ presButtonBackUp={this.props.presButtonBackUp}
+ presGroupBackUp={this.props.presGroupBackUp}
+ removeDocByRef={this.props.removeDocByRef}
+ PresElementsMappings={this.props.PresElementsMappings}
+ />
+ )}
</div>
);
}
diff --git a/src/client/views/presentationview/PresentationView.scss b/src/client/views/presentationview/PresentationView.scss
index a35a5849b..2bb0ec8c8 100644
--- a/src/client/views/presentationview/PresentationView.scss
+++ b/src/client/views/presentationview/PresentationView.scss
@@ -21,6 +21,14 @@
transition: all .1s;
}
+.presentationView-item-above {
+ border-top: black 2px solid;
+}
+
+.presentationView-item-below {
+ border-bottom: black 2px solid;
+}
+
.presentationView-listCont {
padding-left: 10px;
padding-right: 10px;
diff --git a/src/client/views/presentationview/PresentationView.tsx b/src/client/views/presentationview/PresentationView.tsx
index edbbeb8f9..f80840f96 100644
--- a/src/client/views/presentationview/PresentationView.tsx
+++ b/src/client/views/presentationview/PresentationView.tsx
@@ -1,6 +1,6 @@
import { observer } from "mobx-react";
import React = require("react");
-import { observable, action, runInAction, reaction } from "mobx";
+import { observable, action, runInAction, reaction, autorun } from "mobx";
import "./PresentationView.scss";
import { DocumentManager } from "../../util/DocumentManager";
import { Utils } from "../../../Utils";
@@ -231,6 +231,7 @@ export class PresentationView extends React.Component<PresViewProps> {
//checking if any of the group members had used zooming in
currentsArray.forEach((doc: Doc) => {
+ //let presElem: PresentationElement | undefined = this.presElementsMappings.get(doc);
if (this.presElementsMappings.get(doc)!.selected[buttonIndex.Show]) {
zoomOut = true;
return;
@@ -419,9 +420,33 @@ export class PresentationView extends React.Component<PresViewProps> {
}
//removing it from the backUp of selected Buttons
+ // let castedList = Cast(this.presButtonBackUp.selectedButtonDocs, listSpec(Doc));
+ // if (castedList) {
+ // castedList.forEach(async (doc, indexOfDoc) => {
+ // let curDoc = await doc;
+ // let curDocId = StrCast(curDoc.docId);
+ // if (curDocId === removedDoc[Id]) {
+ // if (castedList) {
+ // castedList.splice(indexOfDoc, 1);
+ // return;
+ // }
+ // }
+ // });
+
+ // }
+ //removing it from the backUp of selected Buttons
+
let castedList = Cast(this.presButtonBackUp.selectedButtonDocs, listSpec(Doc));
if (castedList) {
- castedList.splice(index, 1);
+ for (let doc of castedList) {
+ let curDoc = await doc;
+ let curDocId = StrCast(curDoc.docId);
+ if (curDocId === removedDoc[Id]) {
+ castedList.splice(castedList.indexOf(curDoc), 1);
+ break;
+
+ }
+ }
}
//removing it from the backup of groups
@@ -447,6 +472,19 @@ export class PresentationView extends React.Component<PresViewProps> {
}
}
+ public removeDocByRef = (doc: Doc) => {
+ let indexOfDoc = this.childrenDocs.indexOf(doc);
+ const value = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
+ if (value) {
+ value.splice(indexOfDoc, 1)[0];
+ }
+ //this.RemoveDoc(indexOfDoc, true);
+ if (indexOfDoc !== - 1) {
+ return true;
+ }
+ return false;
+ }
+
//The function that is called when a document is clicked or reached through next or back.
//it'll also execute the necessary actions if presentation is playing.
@action
@@ -752,6 +790,10 @@ export class PresentationView extends React.Component<PresViewProps> {
this.curPresentation.title = newTitle;
}
+ addPressElem = (keyDoc: Doc, elem: PresentationElement) => {
+ this.presElementsMappings.set(keyDoc, elem);
+ }
+
render() {
@@ -782,11 +824,13 @@ export class PresentationView extends React.Component<PresViewProps> {
deleteDocument={this.RemoveDoc}
gotoDocument={this.gotoDocument}
groupMappings={this.groupMappings}
- presElementsMappings={this.presElementsMappings}
+ PresElementsMappings={this.presElementsMappings}
setChildrenDocs={this.setChildrenDocs}
presStatus={this.presStatus}
presButtonBackUp={this.presButtonBackUp}
presGroupBackUp={this.presGroupBackUp}
+ removeDocByRef={this.removeDocByRef}
+ clearElemMap={() => this.presElementsMappings.clear()}
/>
</div>
);
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 16c44225a..2214ac8af 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -9,12 +9,12 @@ import { Docs } from '../../documents/Documents';
import { NumCast, Cast } from '../../../new_fields/Types';
import { Doc } from '../../../new_fields/Doc';
import { SearchItem } from './SearchItem';
-import { DocServer } from '../../DocServer';
import * as rp from 'request-promise';
import { Id } from '../../../new_fields/FieldSymbols';
import { SearchUtil } from '../../util/SearchUtil';
import { RouteStore } from '../../../server/RouteStore';
import { FilterBox } from './FilterBox';
+import { Utils } from '../../../Utils';
@observer
@@ -74,7 +74,7 @@ export class SearchBox extends React.Component {
public static async convertDataUri(imageUri: string, returnedFilename: string) {
try {
- let posting = DocServer.prepend(RouteStore.dataUriToImage);
+ let posting = Utils.prepend(RouteStore.dataUriToImage);
const returnedUri = await rp.post(posting, {
body: {
uri: imageUri,
@@ -154,7 +154,7 @@ export class SearchBox extends React.Component {
filteredDocs.forEach(doc => {
const index = this._resultsSet.get(doc);
const highlight = highlights[doc[Id]];
- const hlights = highlight ? Object.keys(highlight).map(key => key.substring(0, key.length - 2)) : []
+ const hlights = highlight ? Object.keys(highlight).map(key => key.substring(0, key.length - 2)) : [];
if (index === undefined) {
this._resultsSet.set(doc, this._results.length);
this._results.push([doc, hlights]);
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index a8f94b746..33a615cbf 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -11,6 +11,7 @@ import { listSpec } from '../new_fields/Schema';
import { List } from '../new_fields/List';
import { observer } from 'mobx-react';
import { observable } from 'mobx';
+import { Utils } from '../Utils';
@@ -57,7 +58,7 @@ class Uploader extends React.Component {
this.status = "getting user document";
- const res = await rp.get(DocServer.prepend(RouteStore.getUserDocumentId));
+ const res = await rp.get(Utils.prepend(RouteStore.getUserDocumentId));
if (!res) {
throw new Error("No user id returned");
}
@@ -104,6 +105,8 @@ class Uploader extends React.Component {
}
+DocServer.init(window.location.protocol, window.location.hostname, 4321, "image upload");
+
ReactDOM.render((
<Uploader />
),
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 1dd721396..5ae0753d8 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -10,14 +10,16 @@ import { RefField, FieldId } from "./RefField";
import { ToScriptString, SelfProxy, Parent, OnUpdate, Self, HandleUpdate, Update, Id } from "./FieldSymbols";
import { scriptingGlobal } from "../client/util/Scripting";
import { List } from "./List";
+import { DocumentType } from "../client/documents/Documents";
+import { ComputedField } from "./ScriptField";
export namespace Field {
export function toKeyValueString(doc: Doc, key: string): string {
const onDelegate = Object.keys(doc).includes(key);
- let field = FieldValue(doc[key]);
+ let field = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
if (Field.IsField(field)) {
- return (onDelegate ? "=" : "") + Field.toScriptString(field);
+ return (onDelegate ? "=" : "") + (field instanceof ComputedField ? `:=${field.script.originalScript}` : Field.toScriptString(field));
}
return "";
}
@@ -242,7 +244,7 @@ export namespace Doc {
let r = (doc === other);
let r2 = (doc.proto === other);
let r3 = (other.proto === doc);
- let r4 = (doc.proto === other.proto);
+ let r4 = (doc.proto === other.proto && other.proto !== undefined);
return r || r2 || r3 || r4;
}
@@ -296,7 +298,7 @@ export namespace Doc {
x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y),
r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b)
};
- }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE });
+ }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: -Number.MAX_VALUE, b: -Number.MAX_VALUE });
return bounds;
}
@@ -316,7 +318,7 @@ export namespace Doc {
if (extensionDoc === undefined) {
setTimeout(() => {
let docExtensionForField = new Doc(doc[Id] + fieldKey, true);
- docExtensionForField.title = "Extension of " + doc.title + "'s field:" + fieldKey;
+ docExtensionForField.title = doc.title + ":" + fieldKey + ".ext";
docExtensionForField.extendsDoc = doc;
let proto: Doc | undefined = doc;
while (proto && !Doc.IsPrototype(proto)) {
@@ -344,18 +346,23 @@ export namespace Doc {
// ... which means we change the layout to be an expanded view of the template layout.
// This allows the view override the template's properties and be referenceable as its own document.
- let expandedTemplateLayout = templateLayoutDoc["_expanded_" + dataDoc[Id]];
+ let expandedTemplateLayout = dataDoc[templateLayoutDoc[Id]];
+ if (expandedTemplateLayout instanceof Doc) {
+ return expandedTemplateLayout;
+ }
+ expandedTemplateLayout = dataDoc[templateLayoutDoc.title + templateLayoutDoc[Id]];
if (expandedTemplateLayout instanceof Doc) {
return expandedTemplateLayout;
}
if (expandedTemplateLayout === undefined && BoolCast(templateLayoutDoc.isTemplate)) {
setTimeout(() => {
- templateLayoutDoc["_expanded_" + dataDoc[Id]] = Doc.MakeDelegate(templateLayoutDoc);
- (templateLayoutDoc["_expanded_" + dataDoc[Id]] as Doc).title = templateLayoutDoc.title + " applied to " + dataDoc.title;
- (templateLayoutDoc["_expanded_" + dataDoc[Id]] as Doc).isExpandedTemplate = templateLayoutDoc;
+ let expandedDoc = Doc.MakeDelegate(templateLayoutDoc);
+ expandedDoc.title = templateLayoutDoc.title + "[" + StrCast(dataDoc.title).match(/\.\.\.[0-9]*/) + "]";
+ expandedDoc.isExpandedTemplate = templateLayoutDoc;
+ dataDoc[templateLayoutDoc.title + templateLayoutDoc[Id]] = expandedDoc;
}, 0);
}
- return templateLayoutDoc;
+ return templateLayoutDoc; // use the templateLayout when it's not a template or the expandedTemplate is pending.
}
export function MakeCopy(doc: Doc, copyProto: boolean = false): Doc {
@@ -385,12 +392,26 @@ export namespace Doc {
export function MakeDelegate(doc: Doc, id?: string): Doc;
export function MakeDelegate(doc: Opt<Doc>, id?: string): Opt<Doc>;
export function MakeDelegate(doc: Opt<Doc>, id?: string): Opt<Doc> {
- if (!doc) {
- return undefined;
+ if (doc) {
+ const delegate = new Doc(id, true);
+ delegate.proto = doc;
+ return delegate;
}
- const delegate = new Doc(id, true);
- delegate.proto = doc;
- return delegate;
+ return undefined;
+ }
+
+ let _applyCount: number = 0;
+ export function ApplyTemplate(templateDoc: Doc) {
+ if (!templateDoc) return undefined;
+ let otherdoc = new Doc();
+ otherdoc.width = templateDoc[WidthSym]();
+ otherdoc.height = templateDoc[HeightSym]();
+ otherdoc.title = templateDoc.title + "(..." + _applyCount++ + ")";
+ otherdoc.layout = Doc.MakeDelegate(templateDoc);
+ otherdoc.miniLayout = StrCast(templateDoc.miniLayout);
+ otherdoc.detailedLayout = otherdoc.layout;
+ otherdoc.type = DocumentType.TEMPLATE;
+ return otherdoc;
}
export function MakeTemplate(fieldTemplate: Doc, metaKey: string, proto: Doc) {
@@ -418,6 +439,13 @@ export namespace Doc {
fieldTemplate.nativeHeight = nh;
fieldTemplate.isTemplate = true;
fieldTemplate.showTitle = "title";
- fieldTemplate.proto = proto;
+ setTimeout(() => fieldTemplate.proto = proto);
+ }
+
+ export async function ToggleDetailLayout(d: Doc) {
+ let miniLayout = await PromiseValue(d.miniLayout);
+ let detailLayout = await PromiseValue(d.detailedLayout);
+ d.layout !== miniLayout ? miniLayout && (d.layout = d.miniLayout) : detailLayout && (d.layout = detailLayout);
+ if (d.layout === detailLayout) Doc.GetProto(d).nativeWidth = Doc.GetProto(d).nativeHeight = undefined;
}
} \ No newline at end of file
diff --git a/src/new_fields/InkField.ts b/src/new_fields/InkField.ts
index 4e3b7abe0..39c6c8ce3 100644
--- a/src/new_fields/InkField.ts
+++ b/src/new_fields/InkField.ts
@@ -2,7 +2,7 @@ import { Deserializable } from "../client/util/SerializationHelper";
import { serializable, custom, createSimpleSchema, list, object, map } from "serializr";
import { ObjectField } from "./ObjectField";
import { Copy, ToScriptString } from "./FieldSymbols";
-import { deepCopy } from "../Utils";
+import { DeepCopy } from "../Utils";
export enum InkTool {
None,
@@ -39,7 +39,7 @@ export class InkField extends ObjectField {
}
[Copy]() {
- return new InkField(deepCopy(this.inkData));
+ return new InkField(DeepCopy(this.inkData));
}
[ToScriptString]() {
diff --git a/src/new_fields/ScriptField.ts b/src/new_fields/ScriptField.ts
index e2994ed70..e5ec34f57 100644
--- a/src/new_fields/ScriptField.ts
+++ b/src/new_fields/ScriptField.ts
@@ -4,6 +4,8 @@ import { Copy, ToScriptString, Parent, SelfProxy } from "./FieldSymbols";
import { serializable, createSimpleSchema, map, primitive, object, deserialize, PropSchema, custom, SKIP } from "serializr";
import { Deserializable } from "../client/util/SerializationHelper";
import { Doc } from "../new_fields/Doc";
+import { Plugins } from "./util";
+import { computedFn } from "mobx-utils";
function optional(propSchema: PropSchema) {
return custom(value => {
@@ -86,11 +88,39 @@ export class ScriptField extends ObjectField {
@Deserializable("computed", deserializeScript)
export class ComputedField extends ScriptField {
//TODO maybe add an observable cache based on what is passed in for doc, considering there shouldn't really be that many possible values for doc
- value(doc: Doc) {
+ value = computedFn((doc: Doc) => {
const val = this.script.run({ this: doc });
if (val.success) {
return val.result;
}
return undefined;
+ });
+}
+
+export namespace ComputedField {
+ let useComputed = true;
+ export function DisableComputedFields() {
+ useComputed = false;
+ }
+
+ export function EnableComputedFields() {
+ useComputed = true;
+ }
+
+ export const undefined = "__undefined";
+
+ export function WithoutComputed<T>(fn: () => T) {
+ DisableComputedFields();
+ try {
+ return fn();
+ } finally {
+ EnableComputedFields();
+ }
}
+
+ Plugins.addGetterPlugin((doc, _, value) => {
+ if (useComputed && value instanceof ComputedField) {
+ return { value: value.value(doc), shouldReturn: true };
+ }
+ });
} \ No newline at end of file
diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts
index 47e467041..b59ec9b9a 100644
--- a/src/new_fields/util.ts
+++ b/src/new_fields/util.ts
@@ -1,5 +1,5 @@
import { UndoManager } from "../client/util/UndoManager";
-import { Doc, Field } from "./Doc";
+import { Doc, Field, FieldResult } from "./Doc";
import { SerializationHelper } from "../client/util/SerializationHelper";
import { ProxyField } from "./Proxy";
import { RefField } from "./RefField";
@@ -11,6 +11,20 @@ import { ComputedField } from "./ScriptField";
function _readOnlySetter(): never {
throw new Error("Documents can't be modified in read-only mode");
}
+
+export interface GetterResult {
+ value: FieldResult;
+ shouldReturn: boolean;
+}
+export type GetterPlugin = (receiver: any, prop: string | number, currentValue: any) => GetterResult | undefined;
+const getterPlugins: GetterPlugin[] = [];
+
+export namespace Plugins {
+ export function addGetterPlugin(plugin: GetterPlugin) {
+ getterPlugins.push(plugin);
+ }
+}
+
const _setterImpl = action(function (target: any, prop: string | symbol | number, value: any, receiver: any): boolean {
//console.log("-set " + target[SelfProxy].title + "(" + target[SelfProxy][prop] + ")." + prop.toString() + " = " + value);
if (SerializationHelper.IsSerializing()) {
@@ -85,12 +99,18 @@ export function getter(target: any, prop: string | symbol | number, receiver: an
function getFieldImpl(target: any, prop: string | number, receiver: any, ignoreProto: boolean = false): any {
receiver = receiver || target[SelfProxy];
- const field = target.__fields[prop];
+ let field = target.__fields[prop];
if (field instanceof ProxyField) {
return field.value();
}
- if (field instanceof ComputedField) {
- return field.value(receiver);
+ for (const plugin of getterPlugins) {
+ const res = plugin(receiver, prop, field);
+ if (res === undefined) continue;
+ if (res.shouldReturn) {
+ return res.value;
+ } else {
+ field = res.value;
+ }
}
if (field === undefined && !ignoreProto && prop !== "proto") {
const proto = getFieldImpl(target, "proto", receiver, true);//TODO tfs: instead of receiver we could use target[SelfProxy]... I don't which semantics we want or if it really matters
diff --git a/src/scraping/acm/chromedriver b/src/scraping/acm/chromedriver
index 9e9b16717..9e9b16717 100755..100644
--- a/src/scraping/acm/chromedriver
+++ b/src/scraping/acm/chromedriver
Binary files differ
diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts
index 5c13495ff..e30015e39 100644
--- a/src/server/RouteStore.ts
+++ b/src/server/RouteStore.ts
@@ -29,4 +29,7 @@ export enum RouteStore {
forgot = "/forgotpassword",
reset = "/reset/:token",
+ // APIS
+ cognitiveServices = "/cognitiveservices"
+
} \ No newline at end of file
diff --git a/src/server/authentication/controllers/user_controller.ts b/src/server/authentication/controllers/user_controller.ts
index 0e431f1e6..f5c6e1610 100644
--- a/src/server/authentication/controllers/user_controller.ts
+++ b/src/server/authentication/controllers/user_controller.ts
@@ -77,8 +77,9 @@ export let postSignup = (req: Request, res: Response, next: NextFunction) => {
let tryRedirectToTarget = (req: Request, res: Response) => {
if (req.session && req.session.target) {
- res.redirect(req.session.target);
+ let target = req.session.target;
req.session.target = undefined;
+ res.redirect(target);
} else {
res.redirect(RouteStore.home);
}
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index e796ccb43..1c52a3f11 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -12,6 +12,7 @@ import { List } from "../../../new_fields/List";
import { listSpec } from "../../../new_fields/Schema";
import { Cast, FieldValue, StrCast } from "../../../new_fields/Types";
import { RouteStore } from "../../RouteStore";
+import { Utils } from "../../../Utils";
export class CurrentUserUtils {
private static curr_email: string;
@@ -74,7 +75,7 @@ export class CurrentUserUtils {
}
public static loadCurrentUser() {
- return rp.get(DocServer.prepend(RouteStore.getCurrUser)).then(response => {
+ return rp.get(Utils.prepend(RouteStore.getCurrUser)).then(response => {
if (response) {
const result: { id: string, email: string } = JSON.parse(response);
return result;
@@ -87,7 +88,7 @@ export class CurrentUserUtils {
public static async loadUserDocument({ id, email }: { id: string, email: string }) {
this.curr_id = id;
this.curr_email = email;
- await rp.get(DocServer.prepend(RouteStore.getUserDocumentId)).then(id => {
+ await rp.get(Utils.prepend(RouteStore.getUserDocumentId)).then(id => {
if (id) {
return DocServer.GetRefField(id).then(async field => {
if (field instanceof Doc) {
diff --git a/src/server/index.ts b/src/server/index.ts
index 51fafbdc3..5b086a2cf 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -110,7 +110,7 @@ function addSecureRoute(method: Method,
if (req.user) {
handler(req.user, res, req);
} else {
- req.session!.target = `${req.headers.host}${req.originalUrl}`;
+ req.session!.target = req.originalUrl;
onRejection(res, req);
}
};
@@ -179,7 +179,6 @@ app.get("/whosOnline", (req, res) => {
res.send(users);
});
-
app.get("/thumbnail/:filename", (req, res) => {
let filename = req.params.filename;
let noExt = filename.substring(0, filename.length - ".png".length);
@@ -285,6 +284,20 @@ addSecureRoute(
RouteStore.getCurrUser
);
+addSecureRoute(Method.GET, (user, res, req) => {
+ let requested = req.params.requestedservice;
+ switch (requested) {
+ case "face":
+ res.send(process.env.FACE);
+ break;
+ case "vision":
+ res.send(process.env.VISION);
+ break;
+ default:
+ res.send(undefined);
+ }
+}, undefined, `${RouteStore.cognitiveServices}/:requestedservice`);
+
class NodeCanvasFactory {
create = (width: number, height: number) => {
var canvas = createCanvas(width, height);
@@ -345,38 +358,6 @@ app.post(
});
isImage = true;
}
- else if (pdfTypes.includes(ext)) {
- // Pdfjs.getDocument(uploadDir + file).promise
- // .then((pdf: Pdfjs.PDFDocumentProxy) => {
- // let numPages = pdf.numPages;
- // let factory = new NodeCanvasFactory();
- // for (let pageNum = 0; pageNum < numPages; pageNum++) {
- // console.log(pageNum);
- // pdf.getPage(pageNum + 1).then((page: Pdfjs.PDFPageProxy) => {
- // console.log("reading " + pageNum);
- // let viewport = page.getViewport(1);
- // let canvasAndContext = factory.create(viewport.width, viewport.height);
- // let renderContext = {
- // canvasContext: canvasAndContext.context,
- // viewport: viewport,
- // canvasFactory: factory
- // }
- // console.log("read " + pageNum);
-
- // page.render(renderContext).promise
- // .then(() => {
- // console.log("saving " + pageNum);
- // let stream = canvasAndContext.canvas.createPNGStream();
- // let out = fs.createWriteStream(uploadDir + file.substring(0, file.length - ext.length) + `-${pageNum + 1}.PNG`);
- // stream.pipe(out);
- // out.on("finish", () => console.log(`Success! Saved to ${uploadDir + file.substring(0, file.length - ext.length) + `-${pageNum + 1}.PNG`}`));
- // }, (reason: string) => {
- // console.error(reason + ` ${pageNum}`);
- // });
- // });
- // }
- // });
- }
if (isImage) {
resizers.forEach(resizer => {
fs.createReadStream(uploadDir + file).pipe(resizer.resizer).pipe(fs.createWriteStream(uploadDir + file.substring(0, file.length - ext.length) + resizer.suffix + ext));
@@ -450,7 +431,7 @@ app.get(RouteStore.reset, getReset);
app.post(RouteStore.reset, postReset);
app.use(RouteStore.corsProxy, (req, res) =>
- req.pipe(request(req.url.substring(1))).pipe(res));
+ req.pipe(request(decodeURIComponent(req.url.substring(1)))).pipe(res));
app.get(RouteStore.delete, (req, res) => {
if (release) {