aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorab <abdullah_ahmed@brown.edu>2019-07-23 11:38:10 -0400
committerab <abdullah_ahmed@brown.edu>2019-07-23 11:38:10 -0400
commit13c016d7f7765acda7f6ce2d69c14597469f55d7 (patch)
tree6fcf409ee4f0035443aa4fb67a05d24aae09689d /src
parentbd841fe56540e1a9177d2872310b10fefcb4acd1 (diff)
parentd880e4b2fcb4e7bab3ee25d63209b173efcf37c0 (diff)
merged
Diffstat (limited to 'src')
-rw-r--r--src/.DS_Storebin6148 -> 6148 bytes
-rw-r--r--src/Utils.ts29
-rw-r--r--src/client/DocServer.ts327
-rw-r--r--src/client/cognitive_services/CognitiveServices.ts135
-rw-r--r--src/client/documents/Documents.ts808
-rw-r--r--src/client/goldenLayout.js8
-rw-r--r--src/client/util/DocumentManager.ts16
-rw-r--r--src/client/util/DragManager.ts44
-rw-r--r--src/client/util/History.ts136
-rw-r--r--src/client/util/Import & Export/DirectoryImportBox.tsx18
-rw-r--r--src/client/util/LinkManager.ts11
-rw-r--r--src/client/util/RichTextSchema.tsx17
-rw-r--r--src/client/util/Scripting.ts72
-rw-r--r--src/client/util/SearchUtil.ts41
-rw-r--r--src/client/util/SerializationHelper.ts10
-rw-r--r--src/client/util/TooltipTextMenu.tsx32
-rw-r--r--src/client/util/type_decls.d2
-rw-r--r--src/client/views/DocumentDecorations.scss48
-rw-r--r--src/client/views/DocumentDecorations.tsx74
-rw-r--r--src/client/views/EditableView.scss1
-rw-r--r--src/client/views/EditableView.tsx1
-rw-r--r--src/client/views/GlobalKeyHandler.ts39
-rw-r--r--src/client/views/InkingCanvas.tsx9
-rw-r--r--src/client/views/Main.scss9
-rw-r--r--src/client/views/Main.tsx31
-rw-r--r--src/client/views/MainOverlayTextBox.scss12
-rw-r--r--src/client/views/MainOverlayTextBox.tsx9
-rw-r--r--src/client/views/MainView.tsx99
-rw-r--r--src/client/views/MetadataEntryMenu.scss66
-rw-r--r--src/client/views/MetadataEntryMenu.tsx171
-rw-r--r--src/client/views/OverlayView.scss42
-rw-r--r--src/client/views/OverlayView.tsx111
-rw-r--r--src/client/views/PreviewCursor.tsx13
-rw-r--r--src/client/views/ScriptingRepl.scss50
-rw-r--r--src/client/views/ScriptingRepl.tsx256
-rw-r--r--src/client/views/SearchBox.tsx183
-rw-r--r--src/client/views/collections/CollectionBaseView.scss3
-rw-r--r--src/client/views/collections/CollectionBaseView.tsx26
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx114
-rw-r--r--src/client/views/collections/CollectionPDFView.tsx16
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx14
-rw-r--r--src/client/views/collections/CollectionStackingView.scss16
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx248
-rw-r--r--src/client/views/collections/CollectionSubView.tsx34
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx61
-rw-r--r--src/client/views/collections/CollectionVideoView.tsx105
-rw-r--r--src/client/views/collections/CollectionView.tsx32
-rw-r--r--src/client/views/collections/ParentDocumentSelector.tsx4
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss44
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx114
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx18
-rw-r--r--src/client/views/document_templates/caption_toggle/DetailedCaptionToggle.tsx72
-rw-r--r--src/client/views/document_templates/image_card/ImageCard.tsx18
-rw-r--r--src/client/views/globalCssVariables.scss13
-rw-r--r--src/client/views/nodes/AudioBox.scss4
-rw-r--r--src/client/views/nodes/AudioBox.tsx8
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx29
-rw-r--r--src/client/views/nodes/DocumentView.tsx142
-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/FieldView.tsx9
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx87
-rw-r--r--src/client/views/nodes/ImageBox.scss18
-rw-r--r--src/client/views/nodes/ImageBox.tsx137
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx68
-rw-r--r--src/client/views/nodes/KeyValuePair.tsx37
-rw-r--r--src/client/views/nodes/LinkEditor.scss36
-rw-r--r--src/client/views/nodes/LinkEditor.tsx126
-rw-r--r--src/client/views/nodes/LinkMenu.tsx2
-rw-r--r--src/client/views/nodes/LinkMenuGroup.tsx40
-rw-r--r--src/client/views/nodes/LinkMenuItem.tsx19
-rw-r--r--src/client/views/nodes/PDFBox.tsx12
-rw-r--r--src/client/views/nodes/VideoBox.scss15
-rw-r--r--src/client/views/nodes/VideoBox.tsx231
-rw-r--r--src/client/views/nodes/WebBox.tsx19
-rw-r--r--src/client/views/pdf/Annotation.tsx6
-rw-r--r--src/client/views/pdf/PDFMenu.scss4
-rw-r--r--src/client/views/pdf/PDFMenu.tsx19
-rw-r--r--src/client/views/pdf/PDFViewer.tsx116
-rw-r--r--src/client/views/pdf/Page.tsx44
-rw-r--r--src/client/views/presentationview/PresentationElement.tsx466
-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.tsx52
-rw-r--r--src/client/views/search/FieldFilters.tsx2
-rw-r--r--src/client/views/search/FilterBox.tsx27
-rw-r--r--src/client/views/search/IconBar.tsx4
-rw-r--r--src/client/views/search/IconButton.tsx42
-rw-r--r--src/client/views/search/Pager.scss47
-rw-r--r--src/client/views/search/SearchBox.scss6
-rw-r--r--src/client/views/search/SearchBox.tsx225
-rw-r--r--src/client/views/search/SearchItem.scss302
-rw-r--r--src/client/views/search/SearchItem.tsx58
-rw-r--r--src/debug/Repl.tsx6
-rw-r--r--src/debug/Viewer.tsx15
-rw-r--r--src/mobile/ImageUpload.tsx9
-rw-r--r--src/new_fields/DateField.ts6
-rw-r--r--src/new_fields/Doc.ts101
-rw-r--r--src/new_fields/InkField.ts4
-rw-r--r--src/new_fields/Schema.ts2
-rw-r--r--src/new_fields/ScriptField.ts32
-rw-r--r--src/new_fields/Types.ts4
-rw-r--r--src/new_fields/util.ts29
-rw-r--r--src/scraping/acm/.gitignore2
-rw-r--r--[-rwxr-xr-x]src/scraping/acm/chromedriverbin10256192 -> 10256192 bytes
-rw-r--r--src/scraping/acm/debug.log38
-rw-r--r--src/scraping/acm/index.js2
-rw-r--r--src/server/GarbageCollector.ts78
-rw-r--r--src/server/RouteStore.ts3
-rw-r--r--src/server/Search.ts12
-rw-r--r--src/server/authentication/controllers/user_controller.ts22
-rw-r--r--src/server/authentication/models/current_user_utils.ts40
-rw-r--r--src/server/authentication/models/user_model.ts4
-rw-r--r--src/server/database.ts11
-rw-r--r--src/server/index.ts110
-rw-r--r--src/server/updateProtos.ts15
116 files changed, 5143 insertions, 1876 deletions
diff --git a/src/.DS_Store b/src/.DS_Store
index fc746835f..071dafa1e 100644
--- a/src/.DS_Store
+++ b/src/.DS_Store
Binary files differ
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 443f70bb1..8c64d2b2f 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -1,58 +1,135 @@
import * as OpenSocket from 'socket.io-client';
-import { MessageStore } from "./../server/Message";
+import { MessageStore, Diff } from "./../server/Message";
import { Opt } from '../new_fields/Doc';
import { Utils, emptyFunction } from '../Utils';
import { SerializationHelper } from './util/SerializationHelper';
import { RefField } from '../new_fields/RefField';
import { Id, HandleUpdate } from '../new_fields/FieldSymbols';
+import { CurrentUserUtils } from '../server/authentication/models/current_user_utils';
+/**
+ * This class encapsulates the transfer and cross-client synchronization of
+ * data stored only in documents (RefFields). In the process, it also
+ * creates and maintains a cache of documents so that they can be accessed
+ * more efficiently. Currently, there is no cache eviction scheme in place.
+ *
+ * NOTE: while this class is technically abstracted to work with any [RefField], because
+ * [Doc] instances are the only [RefField] we need / have implemented at the moment, the documentation
+ * will treat all data used here as [Doc]s
+ *
+ * Any time we want to write a new field to the database (via the server)
+ * or update ourselves based on the server's update message, that occurs here
+ */
export namespace DocServer {
let _cache: { [id: string]: RefField | Promise<Opt<RefField>> } = {};
- const _socket = OpenSocket(`${window.location.protocol}//${window.location.hostname}:4321`);
- const GUID: string = Utils.GenerateGuid();
-
- let _isReadOnly = false;
- export function makeReadOnly() {
- if (_isReadOnly) return;
- _isReadOnly = true;
- _CreateField = field => {
- _cache[field[Id]] = field;
- };
- _UpdateField = emptyFunction;
- _respondToUpdate = emptyFunction;
- }
+ let _socket: SocketIOClient.Socket;
+ // this client's distinct GUID created at initialization
+ let GUID: string;
+ // indicates whether or not a document is currently being udpated, and, if so, its id
+ let updatingId: string | undefined;
+
+ export function init(protocol: string, hostname: string, port: number, identifier: string) {
+ _cache = {};
+ GUID = identifier;
+ _socket = OpenSocket(`${protocol}//${hostname}:${port}`);
+
+ _GetRefField = _GetRefFieldImpl;
+ _GetRefFields = _GetRefFieldsImpl;
+ _CreateField = _CreateFieldImpl;
+ _UpdateField = _UpdateFieldImpl;
- export function makeEditable() {
- if (!_isReadOnly) return;
- location.reload();
- // _isReadOnly = false;
- // _CreateField = _CreateFieldImpl;
- // _UpdateField = _UpdateFieldImpl;
- // _respondToUpdate = _respondToUpdateImpl;
- // _cache = {};
+ /**
+ * Whenever the server sends us its handshake message on our
+ * websocket, we use the above function to return the handshake.
+ */
+ Utils.AddServerHandler(_socket, MessageStore.Foo, onConnection);
+ Utils.AddServerHandler(_socket, MessageStore.UpdateField, respondToUpdate);
+ Utils.AddServerHandler(_socket, MessageStore.DeleteField, respondToDelete);
+ Utils.AddServerHandler(_socket, MessageStore.DeleteFields, respondToDelete);
}
- 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");
}
- export function DeleteDatabase() {
- Utils.Emit(_socket, MessageStore.DeleteAll, {});
+ export namespace Control {
+
+ let _isReadOnly = false;
+ export function makeReadOnly() {
+ if (_isReadOnly) return;
+ _isReadOnly = true;
+ _CreateField = field => {
+ _cache[field[Id]] = field;
+ };
+ _UpdateField = emptyFunction;
+ _RespondToUpdate = emptyFunction;
+ }
+
+ export function makeEditable() {
+ if (!_isReadOnly) return;
+ location.reload();
+ // _isReadOnly = false;
+ // _CreateField = _CreateFieldImpl;
+ // _UpdateField = _UpdateFieldImpl;
+ // _respondToUpdate = _respondToUpdateImpl;
+ // _cache = {};
+ }
+
+ export function isReadOnly() { return _isReadOnly; }
+
}
- export function DeleteDocument(id: string) {
- Utils.Emit(_socket, MessageStore.DeleteField, id);
+ /**
+ * This function emits a message (with this client's
+ * unique GUID) to the server
+ * indicating that this client has connected
+ */
+ function onConnection() {
+ _socket.emit(MessageStore.Bar.Message, GUID);
}
- export function DeleteDocuments(ids: string[]) {
- Utils.Emit(_socket, MessageStore.DeleteFields, ids);
+ export namespace Util {
+
+ /**
+ * Emits a message to the server that wipes
+ * all documents in the database.
+ */
+ export function deleteDatabase() {
+ Utils.Emit(_socket, MessageStore.DeleteAll, {});
+ }
+
}
- export async function GetRefField(id: string): Promise<Opt<RefField>> {
+ // RETRIEVE DOCS FROM SERVER
+
+ /**
+ * Given a single Doc GUID, this utility function will asynchronously attempt to fetch the id's associated
+ * field, first looking in the RefField cache and then communicating with
+ * the server if the document has not been cached.
+ * @param id the id of the requested document
+ */
+ const _GetRefFieldImpl = (id: string): Promise<Opt<RefField>> => {
+ // an initial pass through the cache to determine whether the document needs to be fetched,
+ // is already in the process of being fetched or already exists in the
+ // cache
let cached = _cache[id];
if (cached === undefined) {
- const prom = Utils.EmitCallback(_socket, MessageStore.GetRefField, id).then(async fieldJson => {
+ // NOT CACHED => we'll have to send a request to the server
+
+ // synchronously, we emit a single callback to the server requesting the serialized (i.e. represented by a string)
+ // field for the given ids. This returns a promise, which, when resolved, indicates the the JSON serialized version of
+ // the field has been returned from the server
+ const getSerializedField = Utils.EmitCallback(_socket, MessageStore.GetRefField, id);
+
+ // when the serialized RefField has been received, go head and begin deserializing it into an object.
+ // Here, once deserialized, we also invoke .proto to 'load' the document's prototype, which ensures that all
+ // future .proto calls on the Doc won't have to go farther than the cache to get their actual value.
+ const deserializeField = getSerializedField.then(async fieldJson => {
+ // deserialize
const field = SerializationHelper.Deserialize(fieldJson);
+ // either way, overwrite or delete any promises cached at this id (that we inserted as flags
+ // to indicate that the field was in the process of being fetched). Now everything
+ // should be an actual value within or entirely absent from the cache.
if (field !== undefined) {
await field.proto;
_cache[id] = field;
@@ -61,45 +138,104 @@ export namespace DocServer {
}
return field;
});
- _cache[id] = prom;
- return prom;
+ // here, indicate that the document associated with this id is currently
+ // being retrieved and cached
+ _cache[id] = deserializeField;
+ return deserializeField;
} else if (cached instanceof Promise) {
+ // BEING RETRIEVED AND CACHED => some other caller previously (likely recently) called GetRefField(s),
+ // and requested the document I'm looking for. Shouldn't fetch again, just
+ // return this promise which will resolve to the field itself (see 7)
return cached;
} else {
- return cached;
+ // CACHED => great, let's just return the cached field we have
+ return Promise.resolve(cached);
}
+ };
+
+ let _GetRefField: (id: string) => Promise<Opt<RefField>> = errorFunc;
+
+ export function GetRefField(id: string): Promise<Opt<RefField>> {
+ return _GetRefField(id);
}
- export async function GetRefFields(ids: string[]): Promise<{ [id: string]: Opt<RefField> }> {
+ /**
+ * Given a list of Doc GUIDs, this utility function will asynchronously attempt to each id's associated
+ * field, first looking in the RefField cache and then communicating with
+ * the server if the document has not been cached.
+ * @param ids the ids that map to the reqested documents
+ */
+ const _GetRefFieldsImpl = async (ids: string[]): Promise<{ [id: string]: Opt<RefField> }> => {
const requestedIds: string[] = [];
const waitingIds: string[] = [];
const promises: Promise<Opt<RefField>>[] = [];
const map: { [id: string]: Opt<RefField> } = {};
+
+ // 1) an initial pass through the cache to determine
+ // i) which documents need to be fetched
+ // ii) which are already in the process of being fetched
+ // iii) which already exist in the cache
for (const id of ids) {
const cached = _cache[id];
if (cached === undefined) {
+ // NOT CACHED => we'll have to send a request to the server
requestedIds.push(id);
} else if (cached instanceof Promise) {
+ // BEING RETRIEVED AND CACHED => some other caller previously (likely recently) called GetRefField(s),
+ // and requested one of the documents I'm looking for. Shouldn't fetch again, just
+ // wait until this promise is resolved (see 7)
promises.push(cached);
waitingIds.push(id);
} else {
+ // CACHED => great, let's just add it to the field map
map[id] = cached;
}
}
- const prom = Utils.EmitCallback(_socket, MessageStore.GetRefFields, requestedIds).then(fields => {
+
+ // 2) synchronously, we emit a single callback to the server requesting the serialized (i.e. represented by a string)
+ // fields for the given ids. This returns a promise, which, when resolved, indicates that all the JSON serialized versions of
+ // the fields have been returned from the server
+ const getSerializedFields: Promise<any> = Utils.EmitCallback(_socket, MessageStore.GetRefFields, requestedIds);
+
+ // 3) when the serialized RefFields have been received, go head and begin deserializing them into objects.
+ // Here, once deserialized, we also invoke .proto to 'load' the documents' prototypes, which ensures that all
+ // future .proto calls on the Doc won't have to go farther than the cache to get their actual value.
+ const deserializeFields = getSerializedFields.then(async fields => {
const fieldMap: { [id: string]: RefField } = {};
+ const protosToLoad: any = [];
for (const field of fields) {
if (field !== undefined) {
- fieldMap[field.id] = SerializationHelper.Deserialize(field);
+ // deserialize
+ let deserialized: any = SerializationHelper.Deserialize(field);
+ fieldMap[field.id] = deserialized;
+ // adds to a list of promises that will be awaited asynchronously
+ protosToLoad.push(deserialized.proto);
}
}
-
+ // this actually handles the loading of prototypes
+ await Promise.all(protosToLoad);
return fieldMap;
});
- requestedIds.forEach(id => _cache[id] = prom.then(fields => fields[id]));
- const fields = await prom;
+
+ // 4) here, for each of the documents we've requested *ourselves* (i.e. weren't promises or found in the cache)
+ // we set the value at the field's id to a promise that will resolve to the field.
+ // When we find that promises exist at keys in the cache, THIS is where they were set, just by some other caller (method).
+ // The mapping in the .then call ensures that when other callers await these promises, they'll
+ // get the resolved field
+ requestedIds.forEach(id => _cache[id] = deserializeFields.then(fields => fields[id]));
+
+ // 5) at this point, all fields have a) been returned from the server and b) been deserialized into actual Field objects whose
+ // prototype documents, if any, have also been fetched and cached.
+ const fields = await deserializeFields;
+
+ // 6) with this confidence, we can now go through and update the cache at the ids of the fields that
+ // we explicitly had to fetch. To finish it off, we add whatever value we've come up with for a given
+ // id to the soon-to-be-returned field mapping.
requestedIds.forEach(id => {
const field = fields[id];
+ // either way, overwrite or delete any promises (that we inserted as flags
+ // to indicate that the field was in the process of being fetched). Now everything
+ // should be an actual value within or entirely absent from the cache.
if (field !== undefined) {
_cache[id] = field;
} else {
@@ -107,28 +243,42 @@ export namespace DocServer {
}
map[id] = field;
});
- await Promise.all(requestedIds.map(async id => {
- const field = fields[id];
- if (field) {
- await (field as any).proto;
- }
- }));
- const otherFields = await Promise.all(promises);
- waitingIds.forEach((id, index) => map[id] = otherFields[index]);
+
+ // 7) those promises we encountered in the else if of 1), which represent
+ // other callers having already submitted a request to the server for (a) document(s)
+ // in which we're interested, must still be awaited so that we can return the proper
+ // values for those as well.
+ //
+ // fortunately, those other callers will also hit their own version of 6) and clean up
+ // the shared cache when these promises resolve, so all we have to do is...
+ const otherCallersFetching = await Promise.all(promises);
+ // ...extract the RefFields returned from the resolution of those promises and add them to our
+ // own map.
+ waitingIds.forEach((id, index) => map[id] = otherCallersFetching[index]);
+
+ // now, we return our completed mapping from all of the ids that were passed into the method
+ // to their actual RefField | undefined values. This return value either becomes the input
+ // argument to the caller's promise (i.e. GetRefFields(["_id1_", "_id2_", "_id3_"]).then(map => //do something with map...))
+ // or it is the direct return result if the promise is awaited (i.e. let fields = await GetRefFields(["_id1_", "_id2_", "_id3_"])).
return map;
- }
+ };
- function _UpdateFieldImpl(id: string, diff: any) {
- if (id === updatingId) {
- return;
- }
- Utils.Emit(_socket, MessageStore.UpdateField, { id, diff });
+ let _GetRefFields: (ids: string[]) => Promise<{ [id: string]: Opt<RefField> }> = errorFunc;
+
+ export function GetRefFields(ids: string[]) {
+ return _GetRefFields(ids);
}
- let _UpdateField = _UpdateFieldImpl;
+ // WRITE A NEW DOCUMENT TO THE SERVER
- export function UpdateField(id: string, diff: any) {
- _UpdateField(id, diff);
+ /**
+ * A wrapper around the function local variable _createField.
+ * This allows us to swap in different executions while comfortably
+ * calling the same function throughout the code base (such as in Util.makeReadonly())
+ * @param field the [RefField] to be serialized and sent to the server to be stored in the database
+ */
+ export function CreateField(field: RefField) {
+ _CreateField(field);
}
function _CreateFieldImpl(field: RefField) {
@@ -137,37 +287,75 @@ export namespace DocServer {
Utils.Emit(_socket, MessageStore.CreateField, initialState);
}
- let _CreateField = _CreateFieldImpl;
+ let _CreateField: (field: RefField) => void = errorFunc;
- export function CreateField(field: RefField) {
- _CreateField(field);
+ // NOTIFY THE SERVER OF AN UPDATE TO A DOC'S STATE
+
+ /**
+ * A wrapper around the function local variable _emitFieldUpdate.
+ * This allows us to swap in different executions while comfortably
+ * calling the same function throughout the code base (such as in Util.makeReadonly())
+ * @param id the id of the [Doc] whose state has been updated in our client
+ * @param updatedState the new value of the document. At some point, this
+ * should actually be a proper diff, to improve efficiency
+ */
+ export function UpdateField(id: string, updatedState: any) {
+ _UpdateField(id, updatedState);
}
- let updatingId: string | undefined;
+ function _UpdateFieldImpl(id: string, diff: any) {
+ if (id === updatingId) {
+ return;
+ }
+ Utils.Emit(_socket, MessageStore.UpdateField, { id, diff });
+ }
+
+ let _UpdateField: (id: string, diff: any) => void = errorFunc;
+
function _respondToUpdateImpl(diff: any) {
const id = diff.id;
+ // to be valid, the Diff object must reference
+ // a document's id
if (id === undefined) {
return;
}
- const field = _cache[id];
const update = (f: Opt<RefField>) => {
+ // if the RefField is absent from the cache or
+ // its promise in the cache resolves to undefined, there
+ // can't be anything to update
if (f === undefined) {
return;
}
+ // extract this Doc's update handler
const handler = f[HandleUpdate];
if (handler) {
+ // set the 'I'm currently updating this Doc' flag
updatingId = id;
handler.call(f, diff.diff);
+ // reset to indicate no ongoing updates
updatingId = undefined;
}
};
+ // check the cache for the field
+ const field = _cache[id];
if (field instanceof Promise) {
+ // if the field is still being retrieved, update when the promise is resolved
field.then(update);
} else {
+ // otherwise, just execute the update
update(field);
}
}
+ export function DeleteDocument(id: string) {
+ Utils.Emit(_socket, MessageStore.DeleteField, id);
+ }
+
+ export function DeleteDocuments(ids: string[]) {
+ Utils.Emit(_socket, MessageStore.DeleteFields, ids);
+ }
+
+
function _respondToDeleteImpl(ids: string | string[]) {
function deleteId(id: string) {
delete _cache[id];
@@ -179,23 +367,14 @@ export namespace DocServer {
}
}
- let _respondToUpdate = _respondToUpdateImpl;
+ let _RespondToUpdate = _respondToUpdateImpl;
let _respondToDelete = _respondToDeleteImpl;
function respondToUpdate(diff: any) {
- _respondToUpdate(diff);
+ _RespondToUpdate(diff);
}
function respondToDelete(ids: string | string[]) {
_respondToDelete(ids);
}
-
- function connected() {
- _socket.emit(MessageStore.Bar.Message, GUID);
- }
-
- Utils.AddServerHandler(_socket, MessageStore.Foo, connected);
- Utils.AddServerHandler(_socket, MessageStore.UpdateField, respondToUpdate);
- Utils.AddServerHandler(_socket, MessageStore.DeleteField, respondToDelete);
- Utils.AddServerHandler(_socket, MessageStore.DeleteFields, respondToDelete);
} \ No newline at end of file
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 d1c9feb32..7563fda20 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -21,19 +21,19 @@ 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";
-import { Cast, NumCast, StrCast } from "../../new_fields/Types";
+import { Cast, NumCast, StrCast, ToConstructor, InterfaceValue, FieldValue } from "../../new_fields/Types";
import { IconField } from "../../new_fields/IconField";
import { listSpec } from "../../new_fields/Schema";
import { DocServer } from "../DocServer";
-import { InkField } from "../../new_fields/InkField";
import { dropActionType } from "../util/DragManager";
import { DateField } from "../../new_fields/DateField";
import { UndoManager } from "../util/UndoManager";
import { RouteStore } from "../../server/RouteStore";
+import { CollectionDockingView } from "../views/collections/CollectionDockingView";
import { LinkManager } from "../util/LinkManager";
import { DocumentManager } from "../util/DocumentManager";
import DirectoryImportBox from "../util/Import & Export/DirectoryImportBox";
@@ -41,20 +41,22 @@ import { Scripting } from "../util/Scripting";
var requestImageSize = require('../util/request-image-size');
var path = require('path');
-export enum DocTypes {
+export enum DocumentType {
NONE = "none",
- IMG = "image",
- HIST = "histogram",
- ICON = "icon",
TEXT = "text",
- PDF = "pdf",
+ HIST = "histogram",
+ IMG = "image",
WEB = "web",
COL = "collection",
KVP = "kvp",
VID = "video",
AUDIO = "audio",
+ PDF = "pdf",
+ ICON = "icon",
+ IMPORT = "import",
LINK = "link",
- IMPORT = "import"
+ LINKDOC = "linkdoc",
+ TEMPLATE = "template"
}
export interface DocumentOptions {
@@ -84,362 +86,516 @@ export interface DocumentOptions {
dbDoc?: Doc;
// [key: string]: Opt<Field>;
}
-const delegateKeys = ["x", "y", "width", "height", "panX", "panY"];
-
-export namespace DocUtils {
- export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", tags: string = "Default") {
- if (LinkManager.Instance.doesLinkExist(source, target)) return;
- let sv = DocumentManager.Instance.getDocumentView(source);
- if (sv && sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document === target) return;
- if (target === CurrentUserUtils.UserDocument) return;
-
- UndoManager.RunInBatch(() => {
- let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: "100%" });
- linkDoc.type = DocTypes.LINK;
- let linkDocProto = Doc.GetProto(linkDoc);
-
- linkDocProto.context = targetContext;
- linkDocProto.title = title === "" ? source.title + " to " + target.title : title;
- linkDocProto.linkDescription = description;
- linkDocProto.linkTags = tags;
- linkDocProto.type = DocTypes.LINK;
-
- linkDocProto.anchor1 = source;
- linkDocProto.anchor1Page = source.curPage;
- linkDocProto.anchor1Groups = new List<Doc>([]);
- linkDocProto.anchor2 = target;
- linkDocProto.anchor2Page = target.curPage;
- linkDocProto.anchor2Groups = new List<Doc>([]);
-
- LinkManager.Instance.addLink(linkDoc);
- return linkDoc;
- }, "make link");
+class EmptyBox {
+ public static LayoutString() {
+ return "";
}
}
export namespace Docs {
- let textProto: Doc;
- let histoProto: Doc;
- let imageProto: Doc;
- let webProto: Doc;
- let collProto: Doc;
- let kvpProto: Doc;
- let videoProto: Doc;
- let audioProto: Doc;
- let pdfProto: Doc;
- let iconProto: Doc;
- let importProto: Doc;
- // let linkProto: Doc;
- const textProtoId = "textProto";
- const histoProtoId = "histoProto";
- const pdfProtoId = "pdfProto";
- const imageProtoId = "imageProto";
- const webProtoId = "webProto";
- const collProtoId = "collectionProto";
- const kvpProtoId = "kvpProto";
- const videoProtoId = "videoProto";
- const audioProtoId = "audioProto";
- const iconProtoId = "iconProto";
- const importProtoId = "importProto";
- // const linkProtoId = "linkProto";
-
- export function initProtos(): Promise<void> {
- return DocServer.GetRefFields([textProtoId, histoProtoId, collProtoId, imageProtoId, webProtoId, kvpProtoId, videoProtoId, audioProtoId, pdfProtoId, iconProtoId]).then(fields => {
- textProto = fields[textProtoId] as Doc || CreateTextPrototype();
- histoProto = fields[histoProtoId] as Doc || CreateHistogramPrototype();
- collProto = fields[collProtoId] as Doc || CreateCollectionPrototype();
- imageProto = fields[imageProtoId] as Doc || CreateImagePrototype();
- webProto = fields[webProtoId] as Doc || CreateWebPrototype();
- kvpProto = fields[kvpProtoId] as Doc || CreateKVPPrototype();
- videoProto = fields[videoProtoId] as Doc || CreateVideoPrototype();
- audioProto = fields[audioProtoId] as Doc || CreateAudioPrototype();
- pdfProto = fields[pdfProtoId] as Doc || CreatePdfPrototype();
- iconProto = fields[iconProtoId] as Doc || CreateIconPrototype();
- importProto = fields[importProtoId] as Doc || CreateImportPrototype();
- });
- }
- function setupPrototypeOptions(protoId: string, title: string, layout: string, options: DocumentOptions): Doc {
- return Doc.assign(new Doc(protoId, true), { ...options, title: title, layout: layout });
- }
- function SetInstanceOptions<U extends Field>(doc: Doc, options: DocumentOptions, value: U) {
- const deleg = Doc.MakeDelegate(doc);
- deleg.data = value;
- return Doc.assign(deleg, options);
- }
- function SetDelegateOptions(doc: Doc, options: DocumentOptions, id?: string) {
- const deleg = Doc.MakeDelegate(doc, id);
- return Doc.assign(deleg, options);
- }
+ export namespace Prototypes {
- function CreateImagePrototype(): Doc {
- let imageProto = setupPrototypeOptions(imageProtoId, "IMAGE_PROTO", CollectionView.LayoutString("annotations"),
- { x: 0, y: 0, nativeWidth: 600, width: 300, backgroundLayout: ImageBox.LayoutString(), curPage: 0, type: DocTypes.IMG });
- return imageProto;
- }
+ type LayoutSource = { LayoutString: () => string };
+ type CollectionLayoutSource = { LayoutString: (fieldStr: string, fieldExt?: string) => string };
+ type CollectionViewType = [CollectionLayoutSource, string, string?];
+ type PrototypeTemplate = {
+ layout: {
+ view: LayoutSource,
+ collectionView?: CollectionViewType
+ },
+ options?: Partial<DocumentOptions>
+ };
+ type TemplateMap = Map<DocumentType, PrototypeTemplate>;
+ type PrototypeMap = Map<DocumentType, Doc>;
+ const data = "data";
+ const anno = "annotations";
- function CreateImportPrototype(): Doc {
- let importProto = setupPrototypeOptions(importProtoId, "IMPORT_PROTO", DirectoryImportBox.LayoutString(), { x: 0, y: 0, width: 600, height: 600, type: DocTypes.IMPORT });
- return importProto;
- }
+ const TemplateMap: TemplateMap = new Map([
+ [DocumentType.TEXT, {
+ layout: { view: FormattedTextBox },
+ options: { height: 150, backgroundColor: "#f1efeb" }
+ }],
+ [DocumentType.HIST, {
+ layout: { view: HistogramBox, collectionView: [CollectionView, data] as CollectionViewType },
+ options: { height: 300, backgroundColor: "black" }
+ }],
+ [DocumentType.IMG, {
+ layout: { view: ImageBox, collectionView: [CollectionView, data, anno] as CollectionViewType },
+ options: { nativeWidth: 600, curPage: 0 }
+ }],
+ [DocumentType.WEB, {
+ layout: { view: WebBox, collectionView: [CollectionView, data, anno] as CollectionViewType },
+ options: { height: 300 }
+ }],
+ [DocumentType.COL, {
+ layout: { view: CollectionView },
+ options: { panX: 0, panY: 0, scale: 1, width: 500, height: 500 }
+ }],
+ [DocumentType.KVP, {
+ layout: { view: KeyValueBox },
+ options: { height: 150 }
+ }],
+ [DocumentType.VID, {
+ layout: { view: VideoBox, collectionView: [CollectionVideoView, data, anno] as CollectionViewType },
+ options: { nativeWidth: 600, curPage: 0 },
+ }],
+ [DocumentType.AUDIO, {
+ layout: { view: AudioBox },
+ options: { height: 32 }
+ }],
+ [DocumentType.PDF, {
+ layout: { view: PDFBox, collectionView: [CollectionPDFView, data, anno] as CollectionViewType },
+ options: { nativeWidth: 1200, curPage: 1 }
+ }],
+ [DocumentType.ICON, {
+ layout: { view: IconBox },
+ options: { width: Number(MINIMIZED_ICON_SIZE), height: Number(MINIMIZED_ICON_SIZE) },
+ }],
+ [DocumentType.IMPORT, {
+ layout: { view: DirectoryImportBox },
+ options: { height: 150 }
+ }],
+ [DocumentType.LINKDOC, {
+ data: new List<Doc>(),
+ layout: { view: EmptyBox },
+ options: {}
+ }]
+ ]);
- function CreateHistogramPrototype(): Doc {
- let histoProto = setupPrototypeOptions(histoProtoId, "HISTO PROTO", CollectionView.LayoutString("annotations"),
- { x: 0, y: 0, width: 300, height: 300, backgroundColor: "black", backgroundLayout: HistogramBox.LayoutString(), type: DocTypes.HIST });
- return histoProto;
- }
- function CreateIconPrototype(): Doc {
- let iconProto = setupPrototypeOptions(iconProtoId, "ICON_PROTO", IconBox.LayoutString(),
- { x: 0, y: 0, width: Number(MINIMIZED_ICON_SIZE), height: Number(MINIMIZED_ICON_SIZE), type: DocTypes.ICON });
- return iconProto;
- }
- function CreateTextPrototype(): Doc {
- let textProto = setupPrototypeOptions(textProtoId, "TEXT_PROTO", FormattedTextBox.LayoutString(),
- { x: 0, y: 0, width: 300, backgroundColor: "#f1efeb", type: DocTypes.TEXT });
- return textProto;
- }
- function CreatePdfPrototype(): Doc {
- let pdfProto = setupPrototypeOptions(pdfProtoId, "PDF_PROTO", CollectionPDFView.LayoutString("annotations"),
- { x: 0, y: 0, width: 300, height: 300, backgroundLayout: PDFBox.LayoutString(), curPage: 1, type: DocTypes.PDF });
- return pdfProto;
- }
- function CreateWebPrototype(): Doc {
- let webProto = setupPrototypeOptions(webProtoId, "WEB_PROTO", WebBox.LayoutString(),
- { x: 0, y: 0, width: 300, height: 300, type: DocTypes.WEB });
- return webProto;
- }
- function CreateCollectionPrototype(): Doc {
- let collProto = setupPrototypeOptions(collProtoId, "COLLECTION_PROTO", CollectionView.LayoutString("data"),
- { panX: 0, panY: 0, scale: 1, width: 500, height: 500, type: DocTypes.COL });
- return collProto;
- }
+ // All document prototypes are initialized with at least these values
+ const defaultOptions: DocumentOptions = { x: 0, y: 0, width: 300 };
+ const suffix = "Proto";
- function CreateKVPPrototype(): Doc {
- let kvpProto = setupPrototypeOptions(kvpProtoId, "KVP_PROTO", KeyValueBox.LayoutString(),
- { x: 0, y: 0, width: 300, height: 150, type: DocTypes.KVP });
- return kvpProto;
- }
- function CreateVideoPrototype(): Doc {
- let videoProto = setupPrototypeOptions(videoProtoId, "VIDEO_PROTO", CollectionVideoView.LayoutString("annotations"),
- { x: 0, y: 0, nativeWidth: 600, width: 300, backgroundLayout: VideoBox.LayoutString(), curPage: 0, type: DocTypes.VID });
- return videoProto;
- }
- function CreateAudioPrototype(): Doc {
- let audioProto = setupPrototypeOptions(audioProtoId, "AUDIO_PROTO", AudioBox.LayoutString(),
- { x: 0, y: 0, width: 300, height: 150, type: DocTypes.AUDIO });
- return audioProto;
- }
+ /**
+ * This function loads or initializes the prototype for each docment type.
+ *
+ * This is an asynchronous function because it has to attempt
+ * to fetch the prototype documents from the server.
+ *
+ * Once we have this object that maps the prototype ids to a potentially
+ * undefined document, we either initialize our private prototype
+ * variables with the document returned from the server or, if prototypes
+ * haven't been initialized, the newly initialized prototype document.
+ */
+ export async function initialize(): Promise<void> {
+ // non-guid string ids for each document prototype
+ let prototypeIds = Object.values(DocumentType).filter(type => type !== DocumentType.NONE).map(type => type + suffix);
+ // fetch the actual prototype documents from the server
+ let actualProtos = await DocServer.GetRefFields(prototypeIds);
- export function CreateInstance(proto: Doc, data: Field, options: DocumentOptions, delegId?: string) {
- const { omit: protoProps, extract: delegateProps } = OmitKeys(options, delegateKeys);
- if (!("author" in protoProps)) {
- protoProps.author = CurrentUserUtils.email;
+ // update this object to include any default values: DocumentOptions for all prototypes
+ prototypeIds.map(id => {
+ let existing = actualProtos[id] as Doc;
+ let type = id.replace(suffix, "") as DocumentType;
+ // get or create prototype of the specified type...
+ let target = existing || buildPrototype(type, id);
+ // ...and set it if not undefined (can be undefined only if TemplateMap does not contain
+ // an entry dedicated to the given DocumentType)
+ target && PrototypeMap.set(type, target);
+ });
}
- if (!("creationDate" in protoProps)) {
- protoProps.creationDate = new DateField;
+
+ /**
+ * Retrieves the prototype for the given document type, or
+ * undefined if that type's proto doesn't have a configuration
+ * in the template map.
+ * @param type
+ */
+ const PrototypeMap: PrototypeMap = new Map();
+ export function get(type: DocumentType): Doc {
+ return PrototypeMap.get(type)!;
}
- protoProps.isPrototype = true;
- return SetDelegateOptions(SetInstanceOptions(proto, protoProps, data), delegateProps, delegId);
- }
+ export function MainLinkDocument() {
+ return Prototypes.get(DocumentType.LINKDOC);
+ }
- export function ImageDocument(url: string, options: DocumentOptions = {}) {
- let inst = CreateInstance(imageProto, new ImageField(new URL(url)), { title: path.basename(url), ...options });
- requestImageSize(window.origin + RouteStore.corsProxy + "/" + url)
- .then((size: any) => {
- let aspect = size.height / size.width;
- if (!inst.proto!.nativeWidth) {
- inst.proto!.nativeWidth = size.width;
- }
- inst.proto!.nativeHeight = Number(inst.proto!.nativeWidth!) * aspect;
- inst.height = NumCast(inst.width) * aspect;
- })
- .catch((err: any) => console.log(err));
- return inst;
- // let doc = SetInstanceOptions(GetImagePrototype(), { ...options, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] },
- // [new URL(url), ImageField]);
- // doc.SetText(KeyStore.Caption, "my caption...");
- // doc.SetText(KeyStore.BackgroundLayout, EmbeddedCaption());
- // doc.SetText(KeyStore.OverlayLayout, FixedCaption());
- // return doc;
- }
- export function VideoDocument(url: string, options: DocumentOptions = {}) {
- return CreateInstance(videoProto, new VideoField(new URL(url)), options);
- }
- export function AudioDocument(url: string, options: DocumentOptions = {}) {
- return CreateInstance(audioProto, new AudioField(new URL(url)), options);
- }
+ /**
+ * This is a convenience method that is used to initialize
+ * prototype documents for the first time.
+ *
+ * @param protoId the id of the prototype, indicating the specific prototype
+ * to initialize (see the *protoId list at the top of the namespace)
+ * @param title the prototype document's title, follows *-PROTO
+ * @param layout the layout key for this prototype and thus the
+ * layout key that all delegates will inherit
+ * @param options any value specified in the DocumentOptions object likewise
+ * becomes the default value for that key for all delegates
+ */
+ function buildPrototype(type: DocumentType, prototypeId: string): Opt<Doc> {
+ // load template from type
+ let template = TemplateMap.get(type);
+ if (!template) {
+ return undefined;
+ }
+ let layout = template.layout;
+ // create title
+ let upper = suffix.toUpperCase();
+ let title = prototypeId.toUpperCase().replace(upper, `_${upper}`);
+ // synthesize the default options, the type and title from computed values and
+ // whatever options pertain to this specific prototype
+ let options = { title: title, type: type, baseProto: true, ...defaultOptions, ...(template.options || {}) };
+ let primary = layout.view.LayoutString();
+ let collectionView = layout.collectionView;
+ if (collectionView) {
+ options.layout = collectionView[0].LayoutString(collectionView[1], collectionView[2]);
+ options.backgroundLayout = primary;
+ } else {
+ options.layout = primary;
+ }
+ return Doc.assign(new Doc(prototypeId, true), { ...options, baseLayout: primary });
+ }
- export function DirectoryImportDocument(options: DocumentOptions = {}) {
- return CreateInstance(importProto, new List<Doc>(), options);
}
- export function HistogramDocument(histoOp: HistogramOperation, options: DocumentOptions = {}) {
- return CreateInstance(histoProto, new HistogramField(histoOp), options);
- }
- export function TextDocument(options: DocumentOptions = {}) {
- return CreateInstance(textProto, "", options);
- }
- export function IconDocument(icon: string, options: DocumentOptions = {}) {
- return CreateInstance(iconProto, new IconField(icon), options);
- }
+ /**
+ * Encapsulates the factory used to create new document instances
+ * delegated from top-level prototypes
+ */
+ export namespace Create {
- export function PdfDocument(url: string, options: DocumentOptions = {}) {
- return CreateInstance(pdfProto, new PdfField(new URL(url)), options);
- }
+ const delegateKeys = ["x", "y", "width", "height", "panX", "panY"];
+
+ /**
+ * This function receives the relevant document prototype and uses
+ * it to create a new of that base-level prototype, or the
+ * underlying data document, which it then delegates again
+ * to create the view document.
+ *
+ * It also takes the opportunity to register the user
+ * that created the document and the time of creation.
+ *
+ * @param proto the specific document prototype off of which to model
+ * this new instance (textProto, imageProto, etc.)
+ * @param data the Field to store at this new instance's data key
+ * @param options any initial values to provide for this new instance
+ * @param delegId if applicable, an existing document id. If undefined, Doc's
+ * constructor just generates a new GUID. This is currently used
+ * only when creating a DockDocument from the current user's already existing
+ * main document.
+ */
+ export function InstanceFromProto(proto: Doc, data: Field, options: DocumentOptions, delegId?: string) {
+ const { omit: protoProps, extract: delegateProps } = OmitKeys(options, delegateKeys);
- export async function DBDocument(url: string, options: DocumentOptions = {}, columnOptions: DocumentOptions = {}) {
- let schemaName = options.title ? options.title : "-no schema-";
- let ctlog = await Gateway.Instance.GetSchema(url, schemaName);
- if (ctlog && ctlog.schemas) {
- let schema = ctlog.schemas[0];
- let schemaDoc = Docs.TreeDocument([], { ...options, nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: schema.displayName! });
- let schemaDocuments = Cast(schemaDoc.data, listSpec(Doc), []);
- if (!schemaDocuments) {
- return;
+ if (!("author" in protoProps)) {
+ protoProps.author = CurrentUserUtils.email;
}
- CurrentUserUtils.AddNorthstarSchema(schema, schemaDoc);
- const docs = schemaDocuments;
- CurrentUserUtils.GetAllNorthstarColumnAttributes(schema).map(attr => {
- DocServer.GetRefField(attr.displayName! + ".alias").then(action((field: Opt<Field>) => {
- if (field instanceof Doc) {
- docs.push(field);
- } else {
- var atmod = new ColumnAttributeModel(attr);
- let histoOp = new HistogramOperation(schema.displayName!,
- new AttributeTransformationModel(atmod, AggregateFunction.None),
- new AttributeTransformationModel(atmod, AggregateFunction.Count),
- new AttributeTransformationModel(atmod, AggregateFunction.Count));
- docs.push(Docs.HistogramDocument(histoOp, { ...columnOptions, width: 200, height: 200, title: attr.displayName! }));
+
+ if (!("creationDate" in protoProps)) {
+ protoProps.creationDate = new DateField;
+ }
+
+ protoProps.isPrototype = true;
+
+ let dataDoc = MakeDataDelegate(proto, protoProps, data);
+ let viewDoc = Doc.MakeDelegate(dataDoc, delegId);
+
+ return Doc.assign(viewDoc, delegateProps);
+ }
+
+ /**
+ * This function receives the relevant top level document prototype
+ * and models a new instance by delegating from it.
+ *
+ * Note that it stores the data it recieves at the delegate's data key,
+ * and applies any document options to this new delegate / instance.
+ * @param proto the prototype from which to model this new delegate
+ * @param options initial values to apply to this new delegate
+ * @param value the data to store in this new delegate
+ */
+ function MakeDataDelegate<D extends Field>(proto: Doc, options: DocumentOptions, value: D) {
+ const deleg = Doc.MakeDelegate(proto);
+ deleg.data = value;
+ return Doc.assign(deleg, options);
+ }
+
+ export function ImageDocument(url: string, options: DocumentOptions = {}) {
+ 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) {
+ inst.proto!.nativeWidth = size.width;
}
- }));
- });
- return schemaDoc;
+ inst.proto!.nativeHeight = Number(inst.proto!.nativeWidth!) * aspect;
+ inst.proto!.height = NumCast(inst.proto!.width) * aspect;
+ })
+ .catch((err: any) => console.log(err));
+ return inst;
}
- return Docs.TreeDocument([], { width: 50, height: 100, title: schemaName });
- }
- export function WebDocument(url: string, options: DocumentOptions = {}) {
- return CreateInstance(webProto, new WebField(new URL(url)), options);
- }
- export function HtmlDocument(html: string, options: DocumentOptions = {}) {
- return CreateInstance(webProto, new HtmlField(html), options);
- }
- export function KVPDocument(document: Doc, options: DocumentOptions = {}) {
- return CreateInstance(kvpProto, document, { title: document.title + ".kvp", ...options });
- }
- export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, makePrototype: boolean = true) {
- if (!makePrototype) {
- return SetInstanceOptions(collProto, { ...options, viewType: CollectionViewType.Freeform }, new List(documents));
+
+ export function VideoDocument(url: string, options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.VID), new VideoField(new URL(url)), options);
}
- return CreateInstance(collProto, new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Freeform });
- }
- export function SchemaDocument(schemaColumns: string[], documents: Array<Doc>, options: DocumentOptions) {
- return CreateInstance(collProto, new List(documents), { schemaColumns: new List(schemaColumns), ...options, viewType: CollectionViewType.Schema });
- }
- export function TreeDocument(documents: Array<Doc>, options: DocumentOptions) {
- return CreateInstance(collProto, new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Tree });
- }
- export function StackingDocument(documents: Array<Doc>, options: DocumentOptions) {
- return CreateInstance(collProto, new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Stacking });
- }
- export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) {
- return CreateInstance(collProto, new List(documents), { ...options, viewType: CollectionViewType.Docking, dockingConfig: config }, id);
- }
- export async function getDocumentFromType(type: string, path: string, options: DocumentOptions): Promise<Opt<Doc>> {
- let ctor: ((path: string, options: DocumentOptions) => (Doc | Promise<Doc | undefined>)) | undefined = undefined;
- if (type.indexOf("image") !== -1) {
- ctor = Docs.ImageDocument;
+ export function AudioDocument(url: string, options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(new URL(url)), options);
}
- if (type.indexOf("video") !== -1) {
- ctor = Docs.VideoDocument;
+
+ export function HistogramDocument(histoOp: HistogramOperation, options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.HIST), new HistogramField(histoOp), options);
}
- if (type.indexOf("audio") !== -1) {
- ctor = Docs.AudioDocument;
+
+ export function TextDocument(options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.TEXT), "", options);
}
- if (type.indexOf("pdf") !== -1) {
- ctor = Docs.PdfDocument;
- options.nativeWidth = 1200;
+
+ export function IconDocument(icon: string, options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.ICON), new IconField(icon), options);
}
- if (type.indexOf("excel") !== -1) {
- ctor = Docs.DBDocument;
- options.dropAction = "copy";
+
+ export function PdfDocument(url: string, options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.PDF), new PdfField(new URL(url)), options);
}
- if (type.indexOf("html") !== -1) {
- if (path.includes(window.location.hostname)) {
- let s = path.split('/');
- let id = s[s.length - 1];
- return DocServer.GetRefField(id).then(field => {
- if (field instanceof Doc) {
- let alias = Doc.MakeAlias(field);
- alias.x = options.x || 0;
- alias.y = options.y || 0;
- alias.width = options.width || 300;
- alias.height = options.height || options.width || 300;
- return alias;
- }
- return undefined;
+
+ export async function DBDocument(url: string, options: DocumentOptions = {}, columnOptions: DocumentOptions = {}) {
+ let schemaName = options.title ? options.title : "-no schema-";
+ let ctlog = await Gateway.Instance.GetSchema(url, schemaName);
+ if (ctlog && ctlog.schemas) {
+ let schema = ctlog.schemas[0];
+ let schemaDoc = Docs.Create.TreeDocument([], { ...options, nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: schema.displayName! });
+ let schemaDocuments = Cast(schemaDoc.data, listSpec(Doc), []);
+ if (!schemaDocuments) {
+ return;
+ }
+ CurrentUserUtils.AddNorthstarSchema(schema, schemaDoc);
+ const docs = schemaDocuments;
+ CurrentUserUtils.GetAllNorthstarColumnAttributes(schema).map(attr => {
+ DocServer.GetRefField(attr.displayName! + ".alias").then(action((field: Opt<Field>) => {
+ if (field instanceof Doc) {
+ docs.push(field);
+ } else {
+ var atmod = new ColumnAttributeModel(attr);
+ let histoOp = new HistogramOperation(schema.displayName!,
+ new AttributeTransformationModel(atmod, AggregateFunction.None),
+ new AttributeTransformationModel(atmod, AggregateFunction.Count),
+ new AttributeTransformationModel(atmod, AggregateFunction.Count));
+ docs.push(Docs.Create.HistogramDocument(histoOp, { ...columnOptions, width: 200, height: 200, title: attr.displayName! }));
+ }
+ }));
});
+ return schemaDoc;
}
- ctor = Docs.WebDocument;
- options = { height: options.width, ...options, title: path, nativeWidth: undefined };
+ return Docs.Create.TreeDocument([], { width: 50, height: 100, title: schemaName });
}
- return ctor ? ctor(path, options) : undefined;
- }
- export function CaptionDocument(doc: Doc) {
- const captionDoc = Doc.MakeAlias(doc);
- captionDoc.overlayLayout = FixedCaption();
- captionDoc.width = Cast(doc.width, "number", 0);
- captionDoc.height = Cast(doc.height, "number", 0);
- return captionDoc;
- }
+ export function WebDocument(url: string, options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.WEB), new WebField(new URL(url)), options);
+ }
- // example of custom display string for an image that shows a caption.
- function EmbeddedCaption() {
- return `<div style="height:100%">
- <div style="position:relative; margin:auto; height:85%; width:85%;" >`
- + ImageBox.LayoutString() +
- `</div>
- <div style="position:relative; height:15%; text-align:center; ">`
- + FormattedTextBox.LayoutString("caption") +
- `</div>
- </div>`;
- }
- export function FixedCaption(fieldName: string = "caption") {
- return `<div style="position:absolute; height:30px; bottom:0; width:100%">
- <div style="position:absolute; width:100%; height:100%; text-align:center;bottom:0;">`
- + FormattedTextBox.LayoutString(fieldName) +
- `</div>
- </div>`;
+ export function HtmlDocument(html: string, options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.WEB), new HtmlField(html), options);
+ }
+
+ export function KVPDocument(document: Doc, options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.KVP), document, { title: document.title + ".kvp", ...options });
+ }
+
+ export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions) {
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Freeform });
+ }
+
+ export function SchemaDocument(schemaColumns: string[], documents: Array<Doc>, options: DocumentOptions) {
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { schemaColumns: new List(schemaColumns), ...options, viewType: CollectionViewType.Schema });
+ }
+
+ export function TreeDocument(documents: Array<Doc>, options: DocumentOptions) {
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Tree });
+ }
+
+ export function StackingDocument(documents: Array<Doc>, options: DocumentOptions) {
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Stacking });
+ }
+
+ export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) {
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, viewType: CollectionViewType.Docking, dockingConfig: config }, id);
+ }
+
+ export function DirectoryImportDocument(options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.IMPORT), new List<Doc>(), options);
+ }
+
+ export type DocConfig = {
+ doc: Doc,
+ initialWidth?: number
+ };
+
+ export function StandardCollectionDockingDocument(configs: Array<DocConfig>, options: DocumentOptions, id?: string, type: string = "row") {
+ let layoutConfig = {
+ content: [
+ {
+ type: type,
+ content: [
+ ...configs.map(config => CollectionDockingView.makeDocumentConfig(config.doc, undefined, config.initialWidth))
+ ]
+ }
+ ]
+ };
+ return DockDocument(configs.map(c => c.doc), JSON.stringify(layoutConfig), options, id);
+ }
}
- function OuterCaption() {
- return (`
-<div>
- <div style="margin:auto; height:calc(100%); width:100%;">
- {layout}
- </div>
- <div style="height:(100% + 25px); width:100%; position:absolute">
- <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"caption"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} renderDepth={renderDepth}/>
- </div>
-</div>
- `);
+ 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) {
+ ctor = Docs.Create.ImageDocument;
+ }
+ if (type.indexOf("video") !== -1) {
+ ctor = Docs.Create.VideoDocument;
+ }
+ if (type.indexOf("audio") !== -1) {
+ ctor = Docs.Create.AudioDocument;
+ }
+ if (type.indexOf("pdf") !== -1) {
+ ctor = Docs.Create.PdfDocument;
+ options.nativeWidth = 1200;
+ }
+ if (type.indexOf("excel") !== -1) {
+ ctor = Docs.Create.DBDocument;
+ options.dropAction = "copy";
+ }
+ if (type.indexOf("html") !== -1) {
+ if (path.includes(window.location.hostname)) {
+ let s = path.split('/');
+ let id = s[s.length - 1];
+ return DocServer.GetRefField(id).then(field => {
+ if (field instanceof Doc) {
+ let alias = Doc.MakeAlias(field);
+ alias.x = options.x || 0;
+ alias.y = options.y || 0;
+ alias.width = options.width || 300;
+ alias.height = options.height || options.width || 300;
+ return alias;
+ }
+ return undefined;
+ });
+ }
+ ctor = Docs.Create.WebDocument;
+ options = { height: options.width, ...options, title: path, nativeWidth: undefined };
+ }
+ return ctor ? ctor(path, options) : undefined;
+ }
}
- function InnerCaption() {
- return (`
- <div>
- <div style="margin:auto; height:calc(100% - 25px); width:100%;">
- {layout}
- </div>
- <div style="height:25px; width:100%; position:absolute">
- <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"caption"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} renderDepth={renderDepth}/>
- </div>
- </div>
- `);
+}
+
+export namespace DocUtils {
+
+ export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", tags: string = "Default", sourceContext?: Doc) {
+ if (LinkManager.Instance.doesLinkExist(source, target)) return;
+ let sv = DocumentManager.Instance.getDocumentView(source);
+ if (sv && sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document === target) return;
+ if (target === CurrentUserUtils.UserDocument) return;
+
+ let linkDoc;
+ UndoManager.RunInBatch(() => {
+ linkDoc = Docs.Create.TextDocument({ width: 100, height: 30, borderRounding: "100%" });
+ linkDoc.type = DocumentType.LINK;
+ let linkDocProto = Doc.GetProto(linkDoc);
+
+ linkDocProto.targetContext = targetContext;
+ linkDocProto.sourceContext = sourceContext;
+ linkDocProto.title = title === "" ? source.title + " to " + target.title : title;
+ linkDocProto.linkDescription = description;
+ linkDocProto.linkTags = tags;
+ linkDocProto.type = DocumentType.LINK;
+
+ linkDocProto.anchor1 = source;
+ linkDocProto.anchor1Page = source.curPage;
+ linkDocProto.anchor1Groups = new List<Doc>([]);
+ linkDocProto.anchor2 = target;
+ linkDocProto.anchor2Page = target.curPage;
+ linkDocProto.anchor2Groups = new List<Doc>([]);
+
+ LinkManager.Instance.addLink(linkDoc);
+
+ }, "make link");
+ return linkDoc;
}
}
-Scripting.addGlobal("Docs", Docs); \ No newline at end of file
+Scripting.addGlobal("Docs", Docs);
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/DocumentManager.ts b/src/client/util/DocumentManager.ts
index acd8dcef7..262194a40 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -3,7 +3,7 @@ import { DocumentView } from '../views/nodes/DocumentView';
import { Doc, DocListCast, Opt } from '../../new_fields/Doc';
import { FieldValue, Cast, NumCast, BoolCast, StrCast } from '../../new_fields/Types';
import { listSpec } from '../../new_fields/Schema';
-import { undoBatch } from './UndoManager';
+import { undoBatch, UndoManager } from './UndoManager';
import { CollectionDockingView } from '../views/collections/CollectionDockingView';
import { CollectionView } from '../views/collections/CollectionView';
import { CollectionPDFView } from '../views/collections/CollectionPDFView';
@@ -106,14 +106,16 @@ export class DocumentManager {
@computed
public get LinkedDocumentViews() {
- let pairs = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false)).reduce((pairs, dv) => {
+ let pairs = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush)).reduce((pairs, dv) => {
let linksList = LinkManager.Instance.getAllRelatedLinks(dv.props.Document);
pairs.push(...linksList.reduce((pairs, link) => {
if (link) {
let linkToDoc = LinkManager.Instance.getOppositeAnchor(link, dv.props.Document);
- DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => {
- pairs.push({ a: dv, b: docView1, l: link });
- });
+ if (linkToDoc) {
+ DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => {
+ pairs.push({ a: dv, b: docView1, l: link });
+ });
+ }
}
return pairs;
}, [] as { a: DocumentView, b: DocumentView, l: Doc }[]));
@@ -140,7 +142,9 @@ export class DocumentManager {
if (!forceDockFunc && (docView = DocumentManager.Instance.getDocumentView(doc))) {
docView.props.Document.libraryBrush = true;
if (linkPage !== undefined) docView.props.Document.curPage = linkPage;
- docView.props.focus(docView.props.Document, willZoom);
+ UndoManager.RunInBatch(() => {
+ docView!.props.focus(docView!.props.Document, willZoom);
+ }, "focus");
} else {
if (!contextDoc) {
if (docContext) {
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index cb71db2c5..323908302 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -58,19 +58,27 @@ export function SetupDrag(
return onItemDown;
}
+function moveLinkedDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean {
+ const document = SelectionManager.SelectedDocuments()[0];
+ document.props.removeDocument && document.props.removeDocument(doc);
+ addDocument(doc);
+ return true;
+}
+
export async function DragLinkAsDocument(dragEle: HTMLElement, x: number, y: number, linkDoc: Doc, sourceDoc: Doc) {
let draggeddoc = LinkManager.Instance.getOppositeAnchor(linkDoc, sourceDoc);
-
- let moddrag = await Cast(draggeddoc.annotationOn, Doc);
- let dragdocs = moddrag ? [moddrag] : [draggeddoc];
- let dragData = new DragManager.DocumentDragData(dragdocs, dragdocs);
- dragData.dropAction = "alias" as dropActionType;
- DragManager.StartLinkedDocumentDrag([dragEle], sourceDoc, dragData, x, y, {
- handlers: {
- dragComplete: action(emptyFunction),
- },
- hideSource: false
- });
+ if (draggeddoc) {
+ let moddrag = await Cast(draggeddoc.annotationOn, Doc);
+ let dragdocs = moddrag ? [moddrag] : [draggeddoc];
+ let dragData = new DragManager.DocumentDragData(dragdocs, dragdocs);
+ dragData.moveDocument = moveLinkedDocument;
+ DragManager.StartLinkedDocumentDrag([dragEle], dragData, x, y, {
+ handlers: {
+ dragComplete: action(emptyFunction),
+ },
+ hideSource: false
+ });
+ }
}
export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: number, sourceDoc: Doc) {
@@ -81,8 +89,9 @@ export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: n
let linkDocs = LinkManager.Instance.getAllRelatedLinks(srcTarg);
if (linkDocs) {
draggedDocs = linkDocs.map(link => {
- return LinkManager.Instance.getOppositeAnchor(link, sourceDoc);
- });
+ let opp = LinkManager.Instance.getOppositeAnchor(link, sourceDoc);
+ if (opp) return opp;
+ }) as Doc[];
}
}
if (draggedDocs.length) {
@@ -93,7 +102,8 @@ export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: n
}
let dragdocs = moddrag.length ? moddrag : draggedDocs;
let dragData = new DragManager.DocumentDragData(dragdocs, dragdocs);
- DragManager.StartLinkedDocumentDrag([dragEle], sourceDoc, dragData, x, y, {
+ dragData.moveDocument = moveLinkedDocument;
+ DragManager.StartLinkedDocumentDrag([dragEle], dragData, x, y, {
handlers: {
dragComplete: action(emptyFunction),
},
@@ -102,6 +112,7 @@ export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: n
}
}
+
export namespace DragManager {
export function Root() {
const root = document.getElementById("root");
@@ -227,15 +238,14 @@ export namespace DragManager {
});
}
- export function StartLinkedDocumentDrag(eles: HTMLElement[], sourceDoc: Doc, dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
+ export function StartLinkedDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
+ dragData.moveDocument = moveLinkedDocument;
runInAction(() => StartDragFunctions.map(func => func()));
StartDrag(eles, dragData, downX, downY, options,
(dropData: { [id: string]: any }) => {
- // dropData.droppedDocuments =
let droppedDocuments: Doc[] = dragData.draggedDocuments.reduce((droppedDocs: Doc[], d) => {
let dvs = DocumentManager.Instance.getDocumentViews(d);
-
if (dvs.length) {
let inContext = dvs.filter(dv => dv.props.ContainingCollectionView === SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView);
if (inContext.length) {
diff --git a/src/client/util/History.ts b/src/client/util/History.ts
index 545ea8629..e9ff21b22 100644
--- a/src/client/util/History.ts
+++ b/src/client/util/History.ts
@@ -2,6 +2,8 @@ import { Doc, Opt, Field } from "../../new_fields/Doc";
import { DocServer } from "../DocServer";
import { RouteStore } from "../../server/RouteStore";
import { MainView } from "../views/MainView";
+import * as qs from 'query-string';
+import { Utils, OmitKeys } from "../../Utils";
export namespace HistoryUtil {
export interface DocInitializerList {
@@ -11,9 +13,11 @@ export namespace HistoryUtil {
export interface DocUrl {
type: "doc";
docId: string;
- initializers: {
+ initializers?: {
[docId: string]: DocInitializerList;
};
+ readonly?: boolean;
+ nro?: boolean;
}
export type ParsedUrl = DocUrl;
@@ -21,7 +25,7 @@ export namespace HistoryUtil {
// const handlers: ((state: ParsedUrl | null) => void)[] = [];
function onHistory(e: PopStateEvent) {
if (window.location.pathname !== RouteStore.home) {
- const url = e.state as ParsedUrl || parseUrl(window.location.pathname);
+ const url = e.state as ParsedUrl || parseUrl(window.location);
if (url) {
switch (url.type) {
case "doc":
@@ -62,42 +66,111 @@ export namespace HistoryUtil {
// }
// }
- export function parseUrl(pathname: string): ParsedUrl | undefined {
- let pathnameSplit = pathname.split("/");
- if (pathnameSplit.length !== 2) {
- return undefined;
+ const parsers: { [type: string]: (pathname: string[], opts: qs.ParsedQuery) => ParsedUrl | undefined } = {};
+ const stringifiers: { [type: string]: (state: ParsedUrl) => string } = {};
+
+ type ParserValue = true | "none" | "json" | ((value: string) => any);
+
+ type Parser = {
+ [key: string]: ParserValue
+ };
+
+ function addParser(type: string, requiredFields: Parser, optionalFields: Parser, customParser?: (pathname: string[], opts: qs.ParsedQuery, current: ParsedUrl) => ParsedUrl | null | undefined) {
+ function parse(parser: ParserValue, value: string | string[] | null | undefined) {
+ if (value === undefined || value === null) {
+ return value;
+ }
+ if (Array.isArray(value)) {
+ } else if (parser === true || parser === "json") {
+ value = JSON.parse(value);
+ } else if (parser === "none") {
+ } else {
+ value = parser(value);
+ }
+ return value;
}
- const type = pathnameSplit[0];
- const data = pathnameSplit[1];
+ parsers[type] = (pathname, opts) => {
+ const current: any = { type };
+ for (const required in requiredFields) {
+ if (!(required in opts)) {
+ return undefined;
+ }
+ const parser = requiredFields[required];
+ let value = opts[required];
+ value = parse(parser, value);
+ if (value !== null && value !== undefined) {
+ current[required] = value;
+ }
+ }
+ for (const opt in optionalFields) {
+ if (!(opt in opts)) {
+ continue;
+ }
+ const parser = optionalFields[opt];
+ let value = opts[opt];
+ value = parse(parser, value);
+ if (value !== undefined) {
+ current[opt] = value;
+ }
+ }
+ if (customParser) {
+ const val = customParser(pathname, opts, current);
+ if (val === null) {
+ return undefined;
+ } else if (val === undefined) {
+ return current;
+ } else {
+ return val;
+ }
+ }
+ return current;
+ };
+ }
- if (type === "doc") {
- const s = data.split("?");
- if (s.length < 1 || s.length > 2) {
- return undefined;
+ function addStringifier(type: string, keys: string[], customStringifier?: (state: ParsedUrl, current: string) => string) {
+ stringifiers[type] = state => {
+ let path = Utils.prepend(`/${type}`);
+ if (customStringifier) {
+ path = customStringifier(state, path);
}
- const docId = s[0];
- const initializers = s.length === 2 ? JSON.parse(decodeURIComponent(s[1])) : {};
- return {
- type: "doc",
- docId,
- initializers
- };
+ const queryObj = OmitKeys(state, keys).extract;
+ const query: any = {};
+ Object.keys(queryObj).forEach(key => query[key] = queryObj[key] === null ? null : JSON.stringify(queryObj[key]));
+ const queryString = qs.stringify(query);
+ return path + (queryString ? `?${queryString}` : "");
+ };
+ }
+
+ addParser("doc", {}, { readonly: true, initializers: true, nro: true }, (pathname, opts, current) => {
+ if (pathname.length !== 2) return undefined;
+
+ current.initializers = current.initializers || {};
+ const docId = pathname[1];
+ current.docId = docId;
+ });
+ addStringifier("doc", ["initializers", "readonly", "nro"], (state, current) => {
+ return `${current}/${state.docId}`;
+ });
+
+
+ export function parseUrl(location: Location | URL): ParsedUrl | undefined {
+ const pathname = location.pathname.substring(1);
+ const search = location.search;
+ const opts = qs.parse(search, { sort: false });
+ let pathnameSplit = pathname.split("/");
+
+ const type = pathnameSplit[0];
+
+ if (type in parsers) {
+ return parsers[type](pathnameSplit, opts);
}
return undefined;
}
export function createUrl(params: ParsedUrl): string {
- let baseUrl = DocServer.prepend(`/${params.type}`);
- switch (params.type) {
- case "doc":
- const initializers = encodeURIComponent(JSON.stringify(params.initializers));
- const id = params.docId;
- let url = baseUrl + `/${id}`;
- if (Object.keys(params.initializers).length) {
- url += `?${initializers}`;
- }
- return url;
+ if (params.type in stringifiers) {
+ return stringifiers[params.type](params);
}
return "";
}
@@ -112,7 +185,10 @@ export namespace HistoryUtil {
async function onDocUrl(url: DocUrl) {
const field = await DocServer.GetRefField(url.docId);
- await Promise.all(Object.keys(url.initializers).map(id => initDoc(id, url.initializers[id])));
+ const init = url.initializers;
+ if (init) {
+ await Promise.all(Object.keys(init).map(id => initDoc(id, init[id])));
+ }
if (field instanceof Doc) {
MainView.Instance.openWorkspace(field, true);
}
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index ce95ba90e..75b0b52a7 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx
@@ -8,7 +8,7 @@ import { FieldViewProps, FieldView } from "../../views/nodes/FieldView";
import Measure, { ContentRect } from "react-measure";
import { library } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faArrowUp, faTag, faPlus } from '@fortawesome/free-solid-svg-icons';
+import { faTag, faPlus, faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons';
import { Docs, DocumentOptions } from "../../documents/Documents";
import { observer } from "mobx-react";
import ImportMetadataEntry, { keyPlaceholder, valuePlaceholder } from "./ImportMetadataEntry";
@@ -40,7 +40,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
constructor(props: FieldViewProps) {
super(props);
- library.add(faArrowUp, faTag, faPlus);
+ library.add(faTag, faPlus);
let doc = this.props.Document;
this.editingMetadata = this.editingMetadata || false;
this.persistent = this.persistent || false;
@@ -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.getDocumentFromType(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--);
});
@@ -136,7 +136,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
};
let parent = this.props.ContainingCollectionView;
if (parent) {
- let importContainer = Docs.StackingDocument(docs, options);
+ let importContainer = Docs.Create.StackingDocument(docs, options);
importContainer.singleColumn = false;
Doc.AddDocToList(Doc.GetProto(parent.props.Document), "data", importContainer);
!this.persistent && this.props.removeDocument && this.props.removeDocument(doc);
@@ -237,12 +237,12 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
}} />
<div style={{
position: "absolute",
- left: this.left + 12.6,
- top: this.top + 11,
+ left: this.left + 8,
+ top: this.top + 10,
opacity: uploading ? 0 : 1,
transition: "0.4s opacity ease"
}}>
- <FontAwesomeIcon icon={faArrowUp} color="#FFFFFF" size={"2x"} />
+ <FontAwesomeIcon icon={faCloudUploadAlt} color="#FFFFFF" size={"2x"} />
</div>
<img
style={{
@@ -318,7 +318,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
opacity: uploading ? 0 : 1,
transition: "0.4s opacity ease"
}}
- icon={isEditing ? faArrowUp : faTag}
+ icon={isEditing ? faCloudUploadAlt : faTag}
color="#FFFFFF"
size={"1x"}
/>
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 944bc532f..a647f22c1 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -5,6 +5,7 @@ import { listSpec } from "../../new_fields/Schema";
import { List } from "../../new_fields/List";
import { Id } from "../../new_fields/FieldSymbols";
import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
+import { Docs } from "../documents/Documents";
/*
@@ -35,7 +36,9 @@ export class LinkManager {
// the linkmanagerdoc stores a list of docs representing all linkdocs in 'allLinks' and a list of strings representing all group types in 'allGroupTypes'
// lists of strings representing the metadata keys for each group type is stored under a key that is the same as the group type
public get LinkManagerDoc(): Doc | undefined {
- return FieldValue(Cast(CurrentUserUtils.UserDocument.linkManagerDoc, Doc));
+ // return FieldValue(Cast(CurrentUserUtils.UserDocument.linkManagerDoc, Doc));
+
+ return Docs.Prototypes.MainLinkDocument();
}
public getAllLinks(): Doc[] {
@@ -232,11 +235,11 @@ export class LinkManager {
// finds the opposite anchor of a given anchor in a link
//TODO This should probably return undefined if there isn't an opposite anchor
//TODO This should also await the return value of the anchor so we don't filter out promises
- public getOppositeAnchor(linkDoc: Doc, anchor: Doc): Doc {
+ public getOppositeAnchor(linkDoc: Doc, anchor: Doc): Doc | undefined {
if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, null))) {
- return Cast(linkDoc.anchor2, Doc, null)!;
+ return Cast(linkDoc.anchor2, Doc, null);
} else {
- return Cast(linkDoc.anchor1, Doc, null)!;
+ return Cast(linkDoc.anchor1, Doc, null);
}
}
} \ No newline at end of file
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index a2842ca42..ce9e29b26 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -215,12 +215,13 @@ export const marks: { [index: string]: MarkSpec } = {
link: {
attrs: {
href: {},
+ location: { default: null },
title: { default: null }
},
inclusive: false,
parseDOM: [{
tag: "a[href]", getAttrs(dom: any) {
- return { href: dom.getAttribute("href"), title: dom.getAttribute("title") };
+ return { href: dom.getAttribute("href"), location: dom.getAttribute("location"), title: dom.getAttribute("title") };
}
}],
toDOM(node: any) { return ["a", node.attrs, 0]; }
@@ -409,6 +410,20 @@ export const marks: { [index: string]: MarkSpec } = {
}]
},
+ p18: {
+ parseDOM: [{ style: 'font-size: 18px;' }],
+ toDOM: () => ['span', {
+ style: 'font-size: 18px;'
+ }]
+ },
+
+ p20: {
+ parseDOM: [{ style: 'font-size: 20px;' }],
+ toDOM: () => ['span', {
+ style: 'font-size: 20px;'
+ }]
+ },
+
p24: {
parseDOM: [{ style: 'font-size: 24px;' }],
toDOM: () => ['span', {
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index 3156c4f43..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
@@ -33,6 +35,8 @@ export interface CompileError {
errors: any[];
}
+export type CompileResult = CompiledScript | CompileError;
+
export namespace Scripting {
export function addGlobal(global: { name: string }): void;
export function addGlobal(name: string, global: any): void;
@@ -53,15 +57,36 @@ 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;
-export type CompileResult = CompiledScript | CompileError;
function Run(script: string | undefined, customParams: string[], diagnostics: any[], originalScript: string, options: ScriptOptions): CompileResult {
const errors = diagnostics.some(diag => diag.category === ts.DiagnosticCategory.Error);
if ((options.typecheck !== false && errors) || !script) {
@@ -161,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;
@@ -168,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) {
@@ -191,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();
@@ -202,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 338628960..ee5a83710 100644
--- a/src/client/util/SearchUtil.ts
+++ b/src/client/util/SearchUtil.ts
@@ -2,31 +2,44 @@ 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[] } };
+
export interface IdSearchResult {
ids: string[];
numFound: number;
+ highlighting: HighlightingResult | undefined;
}
export interface DocSearchResult {
docs: Doc[];
numFound: number;
+ highlighting: HighlightingResult | undefined;
}
- export function Search(query: string, returnDocs: true): Promise<DocSearchResult>;
- export function Search(query: string, returnDocs: false): Promise<IdSearchResult>;
- export async function Search(query: string, returnDocs: boolean) {
- const result: IdSearchResult = JSON.parse(await rp.get(DocServer.prepend("/search"), {
- qs: { query }
+ export interface SearchParams {
+ hl?: boolean;
+ "hl.fl"?: string;
+ start?: number;
+ rows?: number;
+ fq?: string;
+ }
+ export function Search(query: string, returnDocs: true, options?: SearchParams): Promise<DocSearchResult>;
+ 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(Utils.prepend("/search"), {
+ qs: { ...options, q: query },
}));
if (!returnDocs) {
return result;
}
- const { ids, numFound } = result;
+ const { ids, numFound, highlighting } = result;
const docMap = await DocServer.GetRefFields(ids);
const docs = ids.map((id: string) => docMap[id]).filter((doc: any) => doc instanceof Doc);
- return { docs, numFound };
+ return { docs, numFound, highlighting };
}
export async function GetAliasesOfDocument(doc: Doc): Promise<Doc[]>;
@@ -35,31 +48,31 @@ export namespace SearchUtil {
const proto = Doc.GetProto(doc);
const protoId = proto[Id];
if (returnDocs) {
- return (await Search(`proto_i:"${protoId}"`, returnDocs)).docs;
+ return (await Search("", returnDocs, { fq: `proto_i:"${protoId}"` })).docs;
} else {
- return (await Search(`proto_i:"${protoId}"`, returnDocs)).ids;
+ return (await Search("", returnDocs, { fq: `proto_i:"${protoId}"` })).ids;
}
// return Search(`{!join from=id to=proto_i}id:${protoId}`, true);
}
export async function GetViewsOfDocument(doc: Doc): Promise<Doc[]> {
- const results = await Search(`proto_i:"${doc[Id]}"`, true);
+ const results = await Search("", true, { fq: `proto_i:"${doc[Id]}"` });
return results.docs;
}
export async function GetContextsOfDocument(doc: Doc): Promise<{ contexts: Doc[], aliasContexts: Doc[] }> {
- const docContexts = (await Search(`data_l:"${doc[Id]}"`, true)).docs;
+ const docContexts = (await Search("", true, { fq: `data_l:"${doc[Id]}"` })).docs;
const aliases = await GetAliasesOfDocument(doc, false);
- const aliasContexts = (await Promise.all(aliases.map(doc => Search(`data_l:"${doc}"`, true))));
+ const aliasContexts = (await Promise.all(aliases.map(doc => Search("", true, { fq: `data_l:"${doc}"` }))));
const contexts = { contexts: docContexts, aliasContexts: [] as Doc[] };
aliasContexts.forEach(result => contexts.aliasContexts.push(...result.docs));
return contexts;
}
export async function GetContextIdsOfDocument(doc: Doc): Promise<{ contexts: string[], aliasContexts: string[] }> {
- const docContexts = (await Search(`data_l:"${doc[Id]}"`, false)).ids;
+ const docContexts = (await Search("", false, { fq: `data_l:"${doc[Id]}"` })).ids;
const aliases = await GetAliasesOfDocument(doc, false);
- const aliasContexts = (await Promise.all(aliases.map(doc => Search(`data_l:"${doc}"`, false))));
+ const aliasContexts = (await Promise.all(aliases.map(doc => Search("", false, { fq: `data_l:"${doc}"` }))));
const contexts = { contexts: docContexts, aliasContexts: [] as string[] };
aliasContexts.forEach(result => contexts.aliasContexts.push(...result.ids));
return contexts;
diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts
index a7246d7c4..dca539f3b 100644
--- a/src/client/util/SerializationHelper.ts
+++ b/src/client/util/SerializationHelper.ts
@@ -1,5 +1,6 @@
import { PropSchema, serialize, deserialize, custom, setDefaultModelSchema, getDefaultModelSchema, primitive, SKIP } from "serializr";
import { Field } from "../../new_fields/Doc";
+import { ClientUtils } from "./ClientUtils";
export namespace SerializationHelper {
let serializing: number = 0;
@@ -9,7 +10,7 @@ export namespace SerializationHelper {
export function Serialize(obj: Field): any {
if (obj === undefined || obj === null) {
- return undefined;
+ return null;
}
if (typeof obj !== 'object') {
@@ -38,7 +39,12 @@ export namespace SerializationHelper {
serializing += 1;
if (!obj.__type) {
- throw Error("No property 'type' found in JSON.");
+ if (ClientUtils.RELEASE) {
+ console.warn("No property 'type' found in JSON.");
+ return undefined;
+ } else {
+ throw Error("No property 'type' found in JSON.");
+ }
}
if (!(obj.__type in serializationTypes)) {
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 103157bbc..b3b7848e8 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -24,6 +24,7 @@ import { FormattedTextBoxProps, FormattedTextBox } from "../views/nodes/Formatte
import { typeAlias } from "babel-types";
import React from "react";
import ReactDOM from "react-dom";
+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 {
@@ -66,6 +67,7 @@ export class TooltipTextMenu {
this.tooltip.className = "tooltipMenu";
this.dragElement(this.tooltip);
+ this.dragElement(this.tooltip);
// this.createCollapse();
// if (this._collapseBtn) {
// this.tooltip.appendChild(this._collapseBtn.render(this.view).dom);
@@ -137,6 +139,8 @@ export class TooltipTextMenu {
this.fontSizeToNum.set(schema.marks.p12, 12);
this.fontSizeToNum.set(schema.marks.p14, 14);
this.fontSizeToNum.set(schema.marks.p16, 16);
+ this.fontSizeToNum.set(schema.marks.p18, 18);
+ this.fontSizeToNum.set(schema.marks.p20, 20);
this.fontSizeToNum.set(schema.marks.p24, 24);
this.fontSizeToNum.set(schema.marks.p32, 32);
this.fontSizeToNum.set(schema.marks.p48, 48);
@@ -152,6 +156,7 @@ export class TooltipTextMenu {
this.listTypes = Array.from(this.listTypeToIcon.keys());
//custom tools
+ // this.tooltip.appendChild(this.createLink().render(this.view).dom);
this._brushdom = this.createBrush().render(this.view).dom;
this.tooltip.appendChild(this._brushdom);
@@ -283,8 +288,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)) {
@@ -310,12 +315,21 @@ export class TooltipTextMenu {
this.linkDrag.onpointerdown = (e: PointerEvent) => {
let dragData = new DragManager.LinkDragData(this.editorProps.Document);
dragData.dontClearTextBox = true;
+ // hack to get source context -sy
+ let docView = DocumentManager.Instance.getDocumentView(this.editorProps.Document);
+ e.stopPropagation();
+ let ctrlKey = e.ctrlKey;
DragManager.StartLinkDrag(this.linkDrag!, dragData, e.clientX, e.clientY,
{
handlers: {
dragComplete: action(() => {
- let m = dragData.droppedDocuments;
- this.makeLink(DocServer.prepend("/doc/" + m[0][Id]));
+ // let m = dragData.droppedDocuments;
+ let linkDoc = dragData.linkDocument;
+ let proto = Doc.GetProto(linkDoc);
+ if (docView && docView.props.ContainingCollectionView) {
+ proto.sourceContext = docView.props.ContainingCollectionView.props.Document;
+ }
+ linkDoc instanceof Doc && this.makeLink(Utils.prepend("/doc/" + linkDoc[Id]), ctrlKey ? "onRight" : "inTab");
}),
},
hideSource: false
@@ -335,17 +349,17 @@ export class TooltipTextMenu {
this.linkText.onkeydown = (e: KeyboardEvent) => {
if (e.key === "Enter") {
- this.makeLink(this.linkText!.textContent!);
+ // this.makeLink(this.linkText!.textContent!);
e.stopPropagation();
e.preventDefault();
}
};
- this.tooltip.appendChild(this.linkEditor);
+ // this.tooltip.appendChild(this.linkEditor);
}
- makeLink = (target: string) => {
+ makeLink = (target: string, location: string) => {
let node = this.view.state.selection.$from.nodeAfter;
- let link = this.view.state.schema.mark(this.view.state.schema.marks.link, { href: target });
+ let link = this.view.state.schema.mark(this.view.state.schema.marks.link, { href: target, location: location });
this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link));
this.view.dispatch(this.view.state.tr.addMark(this.view.state.selection.from, this.view.state.selection.to, link));
node = this.view.state.selection.$from.nodeAfter;
@@ -586,7 +600,7 @@ export class TooltipTextMenu {
let node = state.doc.nodeAt(from);
node && node.marks.map(m => {
m.type === markType && (curLink = m.attrs.href);
- })
+ });
//toggleMark(markType)(state, dispatch);
//return true;
}
diff --git a/src/client/util/type_decls.d b/src/client/util/type_decls.d
index 2cbe1dd40..1f95af00c 100644
--- a/src/client/util/type_decls.d
+++ b/src/client/util/type_decls.d
@@ -204,3 +204,5 @@ declare const Docs: {
TreeDocument(documents: Doc[], options?: DocumentOptions): Doc;
StackingDocument(documents: Doc[], options?: DocumentOptions): Doc;
};
+
+declare function d(...args:any[]):any;
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 2d430512b..0b7411fca 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -1,6 +1,7 @@
@import "globalCssVariables";
$linkGap : 3px;
+
.documentDecorations {
position: absolute;
}
@@ -52,15 +53,16 @@ $linkGap : 3px;
grid-column-start: 5;
grid-column-end: 7;
}
-
- #documentDecorations-borderRadius{
+
+ #documentDecorations-borderRadius {
grid-column-start: 5;
grid-column-end: 7;
border-radius: 100%;
- .borderRadiusTooltip{
- width:10px;
- height:10px;
- position:absolute;
+
+ .borderRadiusTooltip {
+ width: 10px;
+ height: 10px;
+ position: absolute;
}
}
@@ -68,8 +70,9 @@ $linkGap : 3px;
#documentDecorations-bottomRightResizer {
cursor: nwse-resize;
}
+
#documentDecorations-bottomRightResizer {
- grid-row:4;
+ grid-row: 4;
}
#documentDecorations-topRightResizer,
@@ -86,7 +89,8 @@ $linkGap : 3px;
#documentDecorations-rightResizer {
cursor: ew-resize;
}
- .title{
+
+ .title {
background: $alt-accent;
grid-column-start: 3;
grid-column-end: 4;
@@ -129,7 +133,6 @@ $linkGap : 3px;
.linkFlyout {
grid-column: 2/4;
- margin-top: $linkGap;
}
.linkButton-empty:hover {
@@ -145,6 +148,7 @@ $linkGap : 3px;
}
.link-button-container {
+ margin-top: $linkGap;
grid-column: 1/4;
width: auto;
height: auto;
@@ -152,9 +156,13 @@ $linkGap : 3px;
flex-direction: row;
}
+.linkButtonWrapper {
+ pointer-events: auto;
+ padding-right: 5px;
+ width: 25px;
+}
+
.linkButton-linker {
- margin-left: 5px;
- margin-top: $linkGap;
height: 20px;
width: 20px;
text-align: center;
@@ -169,7 +177,8 @@ $linkGap : 3px;
transform: scale(1.05);
}
-.linkButton-empty, .linkButton-nonempty {
+.linkButton-empty,
+.linkButton-nonempty {
height: 20px;
width: 20px;
border-radius: 50%;
@@ -195,8 +204,6 @@ $linkGap : 3px;
.templating-menu {
position: absolute;
- bottom: 0;
- left: 50px;
pointer-events: auto;
text-transform: uppercase;
letter-spacing: 2px;
@@ -208,15 +215,17 @@ $linkGap : 3px;
align-items: center;
}
-.fa-icon-link {
+.documentdecorations-icon {
margin-top: 3px;
}
-.templating-button {
+
+.templating-button,
+.docDecs-tagButton {
width: 20px;
height: 20px;
border-radius: 50%;
opacity: 0.9;
- font-size:14;
+ font-size: 14;
background-color: $dark-color;
color: $light-color;
text-align: center;
@@ -230,14 +239,15 @@ $linkGap : 3px;
#template-list {
position: absolute;
- top: 0;
- left: 30px;
+ top: 25px;
+ left: 0px;
width: max-content;
font-family: $sans-serif;
font-size: 12px;
background-color: $light-color-secondary;
padding: 2px 12px;
list-style: none;
+
.templateToggle {
text-align: left;
}
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index d6cf793ab..2f7bea365 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,5 +1,5 @@
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faLink } from '@fortawesome/free-solid-svg-icons';
+import { faLink, faTag } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { action, computed, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
@@ -27,11 +27,14 @@ import React = require("react");
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;
library.add(faLink);
+library.add(faTag);
@observer
export class DocumentDecorations extends React.Component<{}, { value: string }> {
@@ -83,27 +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);
-
- // move data doc fields to layout doc as needed (nativeWidth/nativeHeight, data, ??)
- let backgroundLayout = StrCast(fieldTemplate.backgroundLayout);
- let layout = StrCast(fieldTemplate.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
- if (backgroundLayout) {
- layout = StrCast(fieldTemplate.layout).replace(/fieldKey={"annotations"}/, `fieldKey={"${metaKey}"} fieldExt={"annotations"}`);
- backgroundLayout = backgroundLayout.replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
+ 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);
}
- let nw = Cast(fieldTemplate.nativeWidth, "number");
- let nh = Cast(fieldTemplate.nativeHeight, "number");
-
- fieldTemplate.title = metaKey;
- fieldTemplate.layout = layout;
- fieldTemplate.backgroundLayout = backgroundLayout;
- fieldTemplate.nativeWidth = nw;
- fieldTemplate.nativeHeight = nh;
- fieldTemplate.embed = true;
- fieldTemplate.isTemplate = true;
- fieldTemplate.templates = new List<string>([Templates.TitleBar(metaKey)]);
- fieldTemplate.proto = Doc.GetProto(docTemplate);
}
else {
if (SelectionManager.SelectedDocuments().length > 0) {
@@ -305,7 +294,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
@undoBatch
@action createIcon = (selected: DocumentView[], layoutString: string): Doc => {
let doc = selected[0].props.Document;
- let iconDoc = Docs.IconDocument(layoutString);
+ let iconDoc = Docs.Create.IconDocument(layoutString);
iconDoc.isButton = true;
iconDoc.proto!.title = selected.length > 1 ? "-multiple-.icon" : StrCast(doc.title) + ".icon";
iconDoc.labelField = selected.length > 1 ? undefined : this._fieldKey;
@@ -470,7 +459,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
if (this._linkButton.current !== null && (e.movementX > 1 || e.movementY > 1)) {
document.removeEventListener("pointermove", this.onLinkButtonMoved);
document.removeEventListener("pointerup", this.onLinkButtonUp);
-
DragLinksAsDocuments(this._linkButton.current, e.x, e.y, SelectionManager.SelectedDocuments()[0].props.Document);
}
e.stopPropagation();
@@ -552,7 +540,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
doc.width = actualdW;
if (fixedAspect) doc.height = nheight / nwidth * doc.width;
else doc.height = actualdH;
- Doc.SetInPlace(element.props.Document, "nativeHeight", (doc.height || 0) / doc.width * (doc.nativeWidth || 0), true);
}
else {
if (!fixedAspect) {
@@ -561,7 +548,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
doc.height = actualdH;
if (fixedAspect) doc.width = nwidth / nheight * doc.height;
else doc.width = actualdW;
- Doc.SetInPlace(element.props.Document, "nativeWidth", (doc.width || 0) / doc.height * (doc.nativeHeight || 0), true);
}
} else {
dW && (doc.width = actualdW);
@@ -615,8 +601,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
if (!canEmbed) return (null);
return (
<div className="linkButtonWrapper">
- <div style={{ paddingTop: 3, marginLeft: 30 }} title="Drag Embed" className="linkButton-linker" ref={this._embedButton} onPointerDown={this.onEmbedButtonDown}>
- <FontAwesomeIcon className="fa-image" icon="image" size="sm" />
+ <div title="Drag Embed" className="linkButton-linker" ref={this._embedButton} onPointerDown={this.onEmbedButtonDown}>
+ <FontAwesomeIcon className="documentdecorations-icon" icon="image" size="sm" />
</div>
</div>
);
@@ -629,10 +615,9 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
this._textDoc = thisDoc;
return (
<div className="tooltipwrapper">
- <div style={{ paddingTop: 3, marginLeft: 30 }} title="Hide Tooltip" className="linkButton-linker" ref={this._tooltipoff} onPointerDown={this.onTooltipOff}>
+ <div title="Hide Tooltip" className="linkButton-linker" ref={this._tooltipoff} onPointerDown={this.onTooltipOff}>
{/* <FontAwesomeIcon className="fa-image" icon="image" size="sm" /> */}
- T
- </div>
+ </div>
</div>
);
@@ -653,6 +638,17 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
}
}
+ get metadataMenu() {
+ return (
+ <div className="linkButtonWrapper">
+ <Flyout anchorPoint={anchorPoints.TOP_LEFT}
+ content={<MetadataEntryMenu docs={() => SelectionManager.SelectedDocuments().map(dv => dv.props.Document)} suggestWithFunction />}>{/* tfs: @bcz This might need to be the data document? */}
+ <div className="docDecs-tagButton" title="Add fields"><FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="sm" /></div>
+ </Flyout>
+ </div>
+ );
+ }
+
render() {
var bounds = this.Bounds;
let seldoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined;
@@ -703,6 +699,17 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
templates.set(template, checked);
});
+ bounds.x = Math.max(0, bounds.x - this._resizeBorderWidth / 2) + this._resizeBorderWidth / 2;
+ bounds.y = Math.max(0, bounds.y - this._resizeBorderWidth / 2 - this._titleHeight) + this._resizeBorderWidth / 2 + this._titleHeight;
+ const borderRadiusDraggerWidth = 15;
+ bounds.r = Math.min(window.innerWidth, bounds.r + borderRadiusDraggerWidth + this._resizeBorderWidth / 2) - this._resizeBorderWidth / 2 - borderRadiusDraggerWidth;
+ bounds.b = Math.min(window.innerHeight, bounds.b + this._resizeBorderWidth / 2 + this._linkBoxHeight) - this._resizeBorderWidth / 2 - this._linkBoxHeight;
+ if (bounds.x > bounds.r) {
+ bounds.x = bounds.r - this._resizeBorderWidth;
+ }
+ if (bounds.y > bounds.b) {
+ bounds.y = bounds.b - (this._resizeBorderWidth + this._linkBoxHeight + this._titleHeight);
+ }
return (<div className="documentDecorations">
<div className="documentDecorations-background" style={{
width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px",
@@ -742,10 +749,13 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
</div>
<div className="linkButtonWrapper">
<div title="Drag Link" className="linkButton-linker" ref={this._linkerButton} onPointerDown={this.onLinkerButtonDown}>
- <FontAwesomeIcon className="fa-icon-link" icon="link" size="sm" />
+ <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" />
</div>
</div>
- <TemplateMenu docs={SelectionManager.ViewsSortedVertically()} templates={templates} />
+ <div className="linkButtonWrapper">
+ <TemplateMenu docs={SelectionManager.ViewsSortedVertically()} templates={templates} />
+ </div>
+ {this.metadataMenu}
{this.considerEmbed()}
{/* {this.considerTooltip()} */}
</div>
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 989fb1be9..f2cdffd38 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -67,6 +67,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;
}
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 0d95bb96c..e8a588e58 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -4,9 +4,10 @@ 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) => KeyControlInfo;
+type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo;
type KeyControlInfo = {
preventDefault: boolean,
stopPropagation: boolean
@@ -42,7 +43,7 @@ export default class KeyManager {
return;
}
- let control = handleConstrained(keyname);
+ let control = handleConstrained(keyname, e);
control.stopPropagation && e.stopPropagation();
control.preventDefault && e.preventDefault();
@@ -53,7 +54,7 @@ export default class KeyManager {
}
});
- private unmodified = action((keyname: string) => {
+ private unmodified = action((keyname: string, e: KeyboardEvent) => {
switch (keyname) {
case "escape":
if (MainView.Instance.isPointerDown) {
@@ -69,11 +70,21 @@ export default class KeyManager {
break;
case "delete":
case "backspace":
- SelectionManager.SelectedDocuments().map(docView => {
- let doc = docView.props.Document;
- let remove = docView.props.removeDocument;
- remove && remove(doc);
- });
+ if (document.activeElement) {
+ if (document.activeElement.tagName === "INPUT" || document.activeElement.tagName === "TEXTAREA") {
+ return { stopPropagation: false, preventDefault: false };
+ }
+ }
+ UndoManager.RunInBatch(() => {
+ SelectionManager.SelectedDocuments().map(docView => {
+ let doc = docView.props.Document;
+ let remove = docView.props.removeDocument;
+ remove && remove(doc);
+ });
+ }, "delete");
+ break;
+ case "enter":
+ SelectionManager.SelectedDocuments().map(selected => Doc.ToggleDetailLayout(selected.props.Document));
break;
}
@@ -100,15 +111,25 @@ export default class KeyManager {
};
});
- private ctrl = action((keyname: string) => {
+ private ctrl = action((keyname: string, e: KeyboardEvent) => {
let stopPropagation = true;
let preventDefault = true;
switch (keyname) {
case "arrowright":
+ if (document.activeElement) {
+ if (document.activeElement.tagName === "INPUT" || document.activeElement.tagName === "TEXTAREA") {
+ return { stopPropagation: false, preventDefault: false };
+ }
+ }
MainView.Instance.mainFreeform && CollectionDockingView.Instance.AddRightSplit(MainView.Instance.mainFreeform, undefined);
break;
case "arrowleft":
+ if (document.activeElement) {
+ if (document.activeElement.tagName === "INPUT" || document.activeElement.tagName === "TEXTAREA") {
+ return { stopPropagation: false, preventDefault: false };
+ }
+ }
MainView.Instance.mainFreeform && CollectionDockingView.Instance.CloseRightSplit(MainView.Instance.mainFreeform);
break;
case "f":
diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx
index fd7e5b07d..3e0d7b476 100644
--- a/src/client/views/InkingCanvas.tsx
+++ b/src/client/views/InkingCanvas.tsx
@@ -13,6 +13,7 @@ import { Cast, PromiseValue, NumCast } from "../../new_fields/Types";
interface InkCanvasProps {
getScreenTransform: () => Transform;
+ AnnotationDocument: Doc;
Document: Doc;
inkFieldKey: string;
children: () => JSX.Element[];
@@ -41,7 +42,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
}
componentDidMount() {
- PromiseValue(Cast(this.props.Document[this.props.inkFieldKey], InkField)).then(ink => runInAction(() => {
+ PromiseValue(Cast(this.props.AnnotationDocument[this.props.inkFieldKey], InkField)).then(ink => runInAction(() => {
if (ink) {
let bounds = Array.from(ink.inkData).reduce(([mix, max, miy, may], [id, strokeData]) =>
strokeData.pathData.reduce(([mix, max, miy, may], p) =>
@@ -56,12 +57,12 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
@computed
get inkData(): Map<string, StrokeData> {
- let map = Cast(this.props.Document[this.props.inkFieldKey], InkField);
+ let map = Cast(this.props.AnnotationDocument[this.props.inkFieldKey], InkField);
return !map ? new Map : new Map(map.inkData);
}
set inkData(value: Map<string, StrokeData>) {
- Doc.GetProto(this.props.Document)[this.props.inkFieldKey] = new InkField(value);
+ this.props.AnnotationDocument[this.props.inkFieldKey] = new InkField(value);
}
@action
@@ -151,7 +152,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
get drawnPaths() {
let curPage = NumCast(this.props.Document.curPage, -1);
let paths = Array.from(this.inkData).reduce((paths, [id, strokeData]) => {
- if (strokeData.page === -1 || Math.round(strokeData.page) === Math.round(curPage)) {
+ if (strokeData.page === -1 || (Math.abs(Math.round(strokeData.page) - Math.round(curPage)) < 3)) {
paths.push(<InkingStroke key={id} id={id}
line={strokeData.pathData}
count={strokeData.pathData.length}
diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss
index f52e3b658..a16123476 100644
--- a/src/client/views/Main.scss
+++ b/src/client/views/Main.scss
@@ -235,16 +235,17 @@ ul#add-options-list {
}
.mainView-libraryHandle {
- opacity: 0.6;
width: 20px;
height: 40px;
top: 50%;
- border-radius: 20px;
+ border: 1px solid black;
+ border-radius: 5px;
position: absolute;
z-index: 1;
- background: gray;
}
-
+.svg-inline--fa {
+ vertical-align: unset;
+}
.mainView-workspace {
height:200px;
position:relative;
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 281d9159b..86578af3e 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -3,9 +3,36 @@ import { Docs } from "../documents/Documents";
import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
import * as ReactDOM from 'react-dom';
import * as React from 'react';
+import { Cast } from "../../new_fields/Types";
+import { Doc, DocListCastAsync } from "../../new_fields/Doc";
+import { List } from "../../new_fields/List";
+import { DocServer } from "../DocServer";
+
+let swapDocs = async () => {
+ let oldDoc = await Cast(CurrentUserUtils.UserDocument.linkManagerDoc, Doc);
+ // Docs.Prototypes.MainLinkDocument().allLinks = new List<Doc>();
+ if (oldDoc) {
+ let links = await DocListCastAsync(oldDoc.allLinks);
+ // if (links && DocListCast(links)) {
+ if (links && links.length) {
+ let data = await DocListCastAsync(Docs.Prototypes.MainLinkDocument().allLinks);
+ if (data) {
+ data.push(...links.filter(i => data!.indexOf(i) === -1));
+ Docs.Prototypes.MainLinkDocument().allLinks = new List<Doc>(data.filter((i, idx) => data!.indexOf(i) === idx));
+ }
+ else {
+ Docs.Prototypes.MainLinkDocument().allLinks = new List<Doc>(links);
+ }
+ }
+ CurrentUserUtils.UserDocument.linkManagerDoc = undefined;
+ }
+};
(async () => {
- await Docs.initProtos();
- await CurrentUserUtils.loadCurrentUser();
+ const info = await CurrentUserUtils.loadCurrentUser();
+ DocServer.init(window.location.protocol, window.location.hostname, 4321, info.email);
+ await Docs.Prototypes.initialize();
+ await CurrentUserUtils.loadUserDocument(info);
+ await swapDocs();
ReactDOM.render(<MainView />, document.getElementById('root'));
})(); \ No newline at end of file
diff --git a/src/client/views/MainOverlayTextBox.scss b/src/client/views/MainOverlayTextBox.scss
index f636ca070..c9d44e194 100644
--- a/src/client/views/MainOverlayTextBox.scss
+++ b/src/client/views/MainOverlayTextBox.scss
@@ -1,24 +1,28 @@
@import "globalCssVariables";
+
.mainOverlayTextBox-textInput {
background-color: rgba(248, 6, 6, 0.001);
width: 400px;
height: 200px;
- position:absolute;
+ position: absolute;
overflow: visible;
top: 0;
left: 0;
- pointer-events: none;
+ pointer-events: none;
z-index: $mainTextInput-zindex;
+
.formattedTextBox-cont {
background-color: rgba(248, 6, 6, 0.001);
width: 100%;
height: 100%;
- position:absolute;
+ position: absolute;
top: 0;
left: 0;
}
}
-.mainOverlayTextBox-.unscaled_div{
+
+.mainOverlayTextBox-unscaled_div {
+ // width: 0px;
z-index: 10000;
position: absolute;
pointer-events: none;
diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx
index 789b9bc25..b2fb11afe 100644
--- a/src/client/views/MainOverlayTextBox.tsx
+++ b/src/client/views/MainOverlayTextBox.tsx
@@ -1,4 +1,4 @@
-import { action, observable, reaction } from 'mobx';
+import { action, observable, reaction, trace } from 'mobx';
import { observer } from 'mobx-react';
import "normalize.css";
import * as React from 'react';
@@ -52,8 +52,11 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
if (box) {
this.TextDoc = box.props.Document;
this.TextDataDoc = box.props.DataDoc;
- let sxf = Utils.GetScreenTransform(box ? box.CurrentDiv : undefined);
- let xf = () => { box.props.ScreenToLocalTransform(); return new Transform(-sxf.translateX, -sxf.translateY, 1 / sxf.scale); };
+ let xf = () => {
+ box.props.ScreenToLocalTransform();
+ let sxf = Utils.GetScreenTransform(box ? box.CurrentDiv : undefined);
+ return new Transform(-sxf.translateX, -sxf.translateY, 1 / sxf.scale);
+ };
this.setTextDoc(box.props.fieldKey, box.CurrentDiv, xf, BoolCast(box.props.Document.autoHeight, false) || box.props.height === "min-content");
}
else {
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 0fe834352..beb038f5b 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,5 +1,5 @@
import { IconName, library } from '@fortawesome/fontawesome-svg-core';
-import { faArrowDown, faArrowUp, faCheck, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faImage, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faThumbtack, faTree, faUndoAlt } from '@fortawesome/free-solid-svg-icons';
+import { faArrowDown, faCloudUploadAlt, faArrowUp, faClone, faCheck, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faPortrait, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faThumbtack, faTree, faUndoAlt, faCat } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, configure, observable, runInAction, reaction, trace } from 'mobx';
import { observer } from 'mobx-react';
@@ -13,10 +13,10 @@ import { Id } from '../../new_fields/FieldSymbols';
import { InkTool } from '../../new_fields/InkField';
import { List } from '../../new_fields/List';
import { listSpec } from '../../new_fields/Schema';
-import { Cast, FieldValue, NumCast, BoolCast } from '../../new_fields/Types';
+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';
@@ -38,6 +38,7 @@ import { PresentationView } from './presentationview/PresentationView';
import { PreviewCursor } from './PreviewCursor';
import { FilterBox } from './search/FilterBox';
import { CollectionTreeView } from './collections/CollectionTreeView';
+import { ClientUtils } from '../util/ClientUtils';
@observer
export class MainView extends React.Component {
@@ -57,13 +58,18 @@ export class MainView extends React.Component {
private set mainContainer(doc: Opt<Doc>) {
if (doc) {
if (!("presentationView" in doc)) {
- doc.presentationView = new List<Doc>([Docs.TreeDocument([], { title: "Presentation" })]);
+ doc.presentationView = new List<Doc>([Docs.Create.TreeDocument([], { title: "Presentation" })]);
}
CurrentUserUtils.UserDocument.activeWorkspace = doc;
}
}
componentWillMount() {
+ var tag = document.createElement('script');
+
+ tag.src = "https://www.youtube.com/iframe_api";
+ var firstScriptTag = document.getElementsByTagName('script')[0];
+ firstScriptTag.parentNode!.insertBefore(tag, firstScriptTag);
window.removeEventListener("keydown", KeyManager.Instance.handle);
window.addEventListener("keydown", KeyManager.Instance.handle);
@@ -93,15 +99,6 @@ export class MainView extends React.Component {
MainView.Instance = this;
// causes errors to be generated when modifying an observable outside of an action
configure({ enforceActions: "observed" });
- if (window.location.search.includes("readonly")) {
- DocServer.makeReadOnly();
- }
- if (window.location.search.includes("safe")) {
- if (!window.location.search.includes("nro")) {
- DocServer.makeReadOnly();
- }
- CollectionBaseView.SetSafeMode(true);
- }
if (window.location.pathname !== RouteStore.home) {
let pathname = window.location.pathname.substr(1).split("/");
if (pathname.length > 1) {
@@ -114,7 +111,8 @@ export class MainView extends React.Component {
library.add(faFont);
library.add(faExclamation);
- library.add(faImage);
+ library.add(faPortrait);
+ library.add(faCat);
library.add(faFilePdf);
library.add(faObjectGroup);
library.add(faTable);
@@ -125,12 +123,14 @@ export class MainView extends React.Component {
library.add(faFilm);
library.add(faMusic);
library.add(faTree);
+ library.add(faClone);
library.add(faCut);
library.add(faCommentAlt);
library.add(faThumbtack);
library.add(faCheck);
library.add(faArrowDown);
library.add(faArrowUp);
+ library.add(faCloudUploadAlt);
this.initEventListeners();
this.initAuthenticationRouters();
}
@@ -172,9 +172,9 @@ export class MainView extends React.Component {
if (!(workspaces instanceof Doc)) return;
const list = Cast((CurrentUserUtils.UserDocument.workspaces as Doc).data, listSpec(Doc));
if (list) {
- let freeformDoc = Docs.FreeformDocument([], { x: 0, y: 400, width: this.pwidth * .7, height: this.pheight, title: `WS collection ${list.length + 1}` });
+ let freeformDoc = Docs.Create.FreeformDocument([], { x: 0, y: 400, width: this.pwidth * .7, height: this.pheight, title: `WS collection ${list.length + 1}` });
var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(freeformDoc, freeformDoc, 600)] }] };
- let mainDoc = Docs.DockDocument([CurrentUserUtils.UserDocument, freeformDoc], JSON.stringify(dockingLayout), { title: `Workspace ${list.length + 1}` }, id);
+ let mainDoc = Docs.Create.DockDocument([CurrentUserUtils.UserDocument, freeformDoc], JSON.stringify(dockingLayout), { title: `Workspace ${list.length + 1}` }, id);
if (!CurrentUserUtils.UserDocument.linkManagerDoc) {
let linkManagerDoc = new Doc();
linkManagerDoc.allLinks = new List<Doc>([]);
@@ -194,12 +194,21 @@ export class MainView extends React.Component {
openWorkspace = async (doc: Doc, fromHistory = false) => {
CurrentUserUtils.MainDocId = doc[Id];
this.mainContainer = doc;
- if (BoolCast(doc.readOnly)) {
- DocServer.makeReadOnly();
+ const state = HistoryUtil.parseUrl(window.location) || {} as any;
+ fromHistory || HistoryUtil.pushState({ type: "doc", docId: doc[Id], readonly: state.readonly, nro: state.nro });
+ if (state.readonly === true || state.readonly === null) {
+ DocServer.Control.makeReadOnly();
+ } else if (state.safe) {
+ if (!state.nro) {
+ DocServer.Control.makeReadOnly();
+ }
+ CollectionBaseView.SetSafeMode(true);
+ } else if (state.nro || state.nro === null || state.readonly === false) {
+ } else if (BoolCast(doc.readOnly)) {
+ DocServer.Control.makeReadOnly();
} else {
- DocServer.makeEditable();
+ DocServer.Control.makeEditable();
}
- fromHistory || HistoryUtil.pushState({ type: "doc", docId: doc[Id], initializers: {} });
const col = await Cast(CurrentUserUtils.UserDocument.optionalRightCollection, Doc);
// if there is a pending doc, and it has new data, show it (syip: we use a timeout to prevent collection docking view from being uninitialized)
setTimeout(async () => {
@@ -324,13 +333,15 @@ export class MainView extends React.Component {
}
@computed
get mainContent() {
+ let sidebar = CurrentUserUtils.UserDocument.sidebar;
+ if (!(sidebar instanceof Doc)) return (null);
return <div>
<div className="mainView-libraryHandle"
- style={{ left: `${this.flyoutWidth - 10}px` }}
+ style={{ cursor: "ew-resize", left: `${this.flyoutWidth - 10}px`, backgroundColor: `${StrCast(sidebar.backgroundColor, "lightGray")}` }}
onPointerDown={this.onPointerDown}>
<span title="library View Dragger" style={{ width: "100%", height: "100%", position: "absolute" }} />
</div>
- <div className="mainView-libraryFlyout" style={{ width: `${this.flyoutWidth}px` }}>
+ <div className="mainView-libraryFlyout" style={{ width: `${this.flyoutWidth}px`, zIndex: 1 }}>
{this.flyout}
</div>
{this.dockingContent}
@@ -360,43 +371,47 @@ export class MainView extends React.Component {
let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg";
- let addColNode = action(() => Docs.FreeformDocument([], { width: this.pwidth * .7, height: this.pheight, title: "a freeform collection" }));
+ // let addDockingNode = action(() => Docs.Create.StandardCollectionDockingDocument([{ doc: addColNode(), initialWidth: 200 }], { width: 200, height: 200, title: "a nested docking freeform collection" }));
+ let addSchemaNode = action(() => Docs.Create.SchemaDocument(["title"], [], { width: 200, height: 200, title: "a schema collection" }));
+ //let addTreeNode = action(() => Docs.TreeDocument([CurrentUserUtils.UserDocument], { width: 250, height: 400, title: "Library:" + CurrentUserUtils.email, dropAction: "alias" }));
+ // let addTreeNode = action(() => Docs.TreeDocument(this._northstarSchemas, { width: 250, height: 400, title: "northstar schemas", dropAction: "copy" }));
+ let addColNode = action(() => Docs.Create.FreeformDocument([], { width: this.pwidth * .7, height: this.pheight, title: "a freeform collection" }));
let addTreeNode = action(() => CurrentUserUtils.UserDocument);
- let addImageNode = action(() => Docs.ImageDocument(imgurl, { width: 200, title: "an image of a cat" }));
- let addImportCollectionNode = action(() => Docs.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 }));
+ let addImageNode = action(() => Docs.Create.ImageDocument(imgurl, { width: 200, title: "an image of a cat" }));
+ let addImportCollectionNode = action(() => Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 }));
let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc][] = [
- [React.createRef<HTMLDivElement>(), "image", "Add Image", addImageNode],
[React.createRef<HTMLDivElement>(), "object-group", "Add Collection", addColNode],
- [React.createRef<HTMLDivElement>(), "tree", "Add Tree", addTreeNode],
- [React.createRef<HTMLDivElement>(), "arrow-up", "Import Directory", addImportCollectionNode],
+ // [React.createRef<HTMLDivElement>(), "clone", "Add Docking Frame", addDockingNode],
+ [React.createRef<HTMLDivElement>(), "cloud-upload-alt", "Import Directory", addImportCollectionNode],
];
+ if (!ClientUtils.RELEASE) btns.unshift([React.createRef<HTMLDivElement>(), "cat", "Add Cat Image", addImageNode]);
return < div id="add-nodes-menu" style={{ left: this.flyoutWidth + 5 }} >
<input type="checkbox" id="add-menu-toggle" ref={this.addMenuToggle} />
- <label htmlFor="add-menu-toggle" title="Add Node"><p>+</p></label>
+ <label htmlFor="add-menu-toggle" style={{ marginTop: 2 }} title="Add Node"><p>+</p></label>
<div id="add-options-content">
<ul id="add-options-list">
<li key="search"><button className="add-button round-button" title="Search" onClick={this.toggleSearch}><FontAwesomeIcon icon="search" size="sm" /></button></li>
- <li key="undo"><button className="add-button round-button" title="Undo" onClick={() => UndoManager.Undo()}><FontAwesomeIcon icon="undo-alt" size="sm" /></button></li>
- <li key="redo"><button className="add-button round-button" title="Redo" onClick={() => UndoManager.Redo()}><FontAwesomeIcon icon="redo-alt" size="sm" /></button></li>
- <li key="color"><button className="add-button round-button" title="Select Color" onClick={() => this.toggleColorPicker()}><div className="toolbar-color-button" style={{ backgroundColor: InkingControl.Instance.selectedColor }} >
- <div className="toolbar-color-picker" onClick={this.onColorClick} style={this._colorPickerDisplay ? { color: "black", display: "block" } : { color: "black", display: "none" }}>
- <SketchPicker color={InkingControl.Instance.selectedColor} onChange={InkingControl.Instance.switchColor} />
- </div>
- </div></button></li>
+ <li key="undo"><button className="add-button round-button" title="Undo" style={{ opacity: UndoManager.CanUndo() ? 1 : 0.5, transition: "0.4s ease all" }} onClick={() => UndoManager.Undo()}><FontAwesomeIcon icon="undo-alt" size="sm" /></button></li>
+ <li key="redo"><button className="add-button round-button" title="Redo" style={{ opacity: UndoManager.CanRedo() ? 1 : 0.5, transition: "0.4s ease all" }} onClick={() => UndoManager.Redo()}><FontAwesomeIcon icon="redo-alt" size="sm" /></button></li>
{btns.map(btn =>
<li key={btn[1]} ><div ref={btn[0]}>
<button className="round-button add-button" title={btn[2]} onPointerDown={SetupDrag(btn[0], btn[3])}>
<FontAwesomeIcon icon={btn[1]} size="sm" />
</button>
</div></li>)}
- <li key="undoTest"><button className="add-button round-button" onClick={() => UndoManager.PrintBatches()}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li>
+ <li key="undoTest"><button className="add-button round-button" title="Click if undo isn't working" onClick={() => UndoManager.TraceOpenBatches()}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li>
+ <li key="color"><button className="add-button round-button" title="Select Color" onClick={() => this.toggleColorPicker()}><div className="toolbar-color-button" style={{ backgroundColor: InkingControl.Instance.selectedColor }} >
+ <div className="toolbar-color-picker" onClick={this.onColorClick} style={this._colorPickerDisplay ? { color: "black", display: "block" } : { color: "black", display: "none" }}>
+ <SketchPicker color={InkingControl.Instance.selectedColor} onChange={InkingControl.Instance.switchColor} />
+ </div>
+ </div></button></li>
<li key="ink" style={{ paddingRight: "6px" }}><button className="toolbar-button round-button" title="Ink" onClick={() => InkingControl.Instance.toggleDisplay()}><FontAwesomeIcon icon="pen-nib" size="sm" /> </button></li>
- <li key="pen"><button onClick={() => InkingControl.Instance.switchTool(InkTool.Pen)} style={this.selected(InkTool.Pen)}><FontAwesomeIcon icon="pen" size="lg" title="Pen" /></button></li>
- <li key="marker"><button onClick={() => InkingControl.Instance.switchTool(InkTool.Highlighter)} style={this.selected(InkTool.Highlighter)}><FontAwesomeIcon icon="highlighter" size="lg" title="Pen" /></button></li>
- <li key="eraser"><button onClick={() => InkingControl.Instance.switchTool(InkTool.Eraser)} style={this.selected(InkTool.Eraser)}><FontAwesomeIcon icon="eraser" size="lg" title="Pen" /></button></li>
+ <li key="pen"><button onClick={() => InkingControl.Instance.switchTool(InkTool.Pen)} title="Pen" style={this.selected(InkTool.Pen)}><FontAwesomeIcon icon="pen" size="lg" /></button></li>
+ <li key="marker"><button onClick={() => InkingControl.Instance.switchTool(InkTool.Highlighter)} title="Highlighter" style={this.selected(InkTool.Highlighter)}><FontAwesomeIcon icon="highlighter" size="lg" /></button></li>
+ <li key="eraser"><button onClick={() => InkingControl.Instance.switchTool(InkTool.Eraser)} title="Eraser" style={this.selected(InkTool.Eraser)}><FontAwesomeIcon icon="eraser" size="lg" /></button></li>
<li key="inkControls"><InkingControl /></li>
</ul>
</div>
@@ -418,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>
];
}
@@ -443,7 +458,7 @@ export class MainView extends React.Component {
<PDFMenu />
<MainOverlayTextBox firstinstance={true} />
<OverlayView />
- </div>
+ </div >
);
}
}
diff --git a/src/client/views/MetadataEntryMenu.scss b/src/client/views/MetadataEntryMenu.scss
new file mode 100644
index 000000000..bcfc9a82d
--- /dev/null
+++ b/src/client/views/MetadataEntryMenu.scss
@@ -0,0 +1,66 @@
+.metadataEntry-outerDiv {
+ display: flex;
+ width: 300px;
+}
+
+.react-autosuggest__container {
+ position: relative;
+}
+
+.react-autosuggest__container,
+.metadataEntry-input {
+ width: 100%;
+ margin-left: 5px;
+ margin-right: 5px;
+}
+
+.metadataEntry-input,
+.react-autosuggest__input {
+ border: 1px solid #aaa;
+ border-radius: 4px;
+ width: 100%;
+}
+
+.react-autosuggest__input--focused {
+ outline: none;
+}
+
+.react-autosuggest__input--open {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+.react-autosuggest__suggestions-container {
+ display: none;
+}
+
+.react-autosuggest__suggestions-container--open {
+ display: block;
+ position: fixed;
+ overflow-y: auto;
+ max-height: 400px;
+ width: 180px;
+ border: 1px solid #aaa;
+ background-color: #fff;
+ font-family: Helvetica, sans-serif;
+ font-weight: 300;
+ font-size: 16px;
+ border-bottom-left-radius: 4px;
+ border-bottom-right-radius: 4px;
+ z-index: 2;
+}
+
+.react-autosuggest__suggestions-list {
+ margin: 0;
+ padding: 0;
+ list-style-type: none;
+}
+
+.react-autosuggest__suggestion {
+ cursor: pointer;
+ padding: 10px 20px;
+}
+
+.react-autosuggest__suggestion--highlighted {
+ background-color: #ddd;
+} \ No newline at end of file
diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx
new file mode 100644
index 000000000..bd5a307b3
--- /dev/null
+++ b/src/client/views/MetadataEntryMenu.tsx
@@ -0,0 +1,171 @@
+import * as React from 'react';
+import "./MetadataEntryMenu.scss";
+import { observer } from 'mobx-react';
+import { observable, action, runInAction, trace } from 'mobx';
+import { KeyValueBox } from './nodes/KeyValueBox';
+import { Doc, Field } from '../../new_fields/Doc';
+import * as Autosuggest from 'react-autosuggest';
+
+export type DocLike = Doc | Doc[] | Promise<Doc> | Promise<Doc[]>;
+export interface MetadataEntryProps {
+ docs: DocLike | (() => DocLike);
+ onError?: () => boolean;
+ suggestWithFunction?: boolean;
+}
+
+@observer
+export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
+ @observable private _currentKey: string = "";
+ @observable private _currentValue: string = "";
+ @observable private suggestions: string[] = [];
+ private userModified = false;
+
+ private autosuggestRef = React.createRef<Autosuggest>();
+
+ @action
+ onKeyChange = (e: React.ChangeEvent, { newValue }: { newValue: string }) => {
+ this._currentKey = newValue;
+ if (!this.userModified) {
+ this.previewValue();
+ }
+ }
+
+ previewValue = async () => {
+ let field: Field | undefined | null = null;
+ let onProto: boolean = false;
+ let value: string | undefined = undefined;
+ let docs = this.props.docs;
+ if (typeof docs === "function") {
+ if (this.props.suggestWithFunction) {
+ docs = docs();
+ } else {
+ return;
+ }
+ }
+ docs = await docs;
+ if (docs instanceof Doc) {
+ await docs[this._currentKey];
+ value = Field.toKeyValueString(docs, this._currentKey);
+ } else {
+ for (const doc of docs) {
+ const v = await doc[this._currentKey];
+ onProto = onProto || !Object.keys(doc).includes(this._currentKey);
+ if (field === null) {
+ field = v;
+ } else if (v !== field) {
+ value = "multiple values";
+ }
+ }
+ }
+ if (value === undefined) {
+ if (field !== null && field !== undefined) {
+ value = (onProto ? "" : "= ") + Field.toScriptString(field);
+ } else {
+ value = "";
+ }
+ }
+ const s = value;
+ runInAction(() => this._currentValue = s);
+ }
+
+ @action
+ onValueChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this._currentValue = e.target.value;
+ this.userModified = e.target.value.trim() !== "";
+ }
+
+ onValueKeyDown = async (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ const script = KeyValueBox.CompileKVPScript(this._currentValue);
+ if (!script) return;
+ let doc = this.props.docs;
+ if (typeof doc === "function") {
+ doc = doc();
+ }
+ doc = await doc;
+ let success: boolean;
+ if (doc instanceof Doc) {
+ success = KeyValueBox.ApplyKVPScript(doc, this._currentKey, script);
+ } else {
+ success = doc.every(d => KeyValueBox.ApplyKVPScript(d, this._currentKey, script));
+ }
+ if (!success) {
+ if (this.props.onError) {
+ if (this.props.onError()) {
+ this.clearInputs();
+ }
+ } else {
+ this.clearInputs();
+ }
+ } else {
+ this.clearInputs();
+ }
+ }
+ }
+
+ @action
+ clearInputs = () => {
+ this._currentKey = "";
+ this._currentValue = "";
+ this.userModified = false;
+ if (this.autosuggestRef.current) {
+ const input: HTMLInputElement = (this.autosuggestRef.current as any).input;
+ input && input.focus();
+ }
+ }
+
+ getKeySuggestions = async (value: string): Promise<string[]> => {
+ value = value.toLowerCase();
+ let docs = this.props.docs;
+ if (typeof docs === "function") {
+ if (this.props.suggestWithFunction) {
+ docs = docs();
+ } else {
+ return [];
+ }
+ }
+ docs = await docs;
+ if (docs instanceof Doc) {
+ return Object.keys(docs).filter(key => key.toLowerCase().startsWith(value));
+ } else {
+ const keys = new Set<string>();
+ docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key)));
+ return Array.from(keys).filter(key => key.toLowerCase().startsWith(value));
+ }
+ }
+ getSuggestionValue = (suggestion: string) => suggestion;
+
+ renderSuggestion = (suggestion: string) => {
+ return <p>{suggestion}</p>;
+ }
+
+ onSuggestionFetch = async ({ value }: { value: string }) => {
+ const sugg = await this.getKeySuggestions(value);
+ runInAction(() => {
+ this.suggestions = sugg;
+ });
+ }
+
+ @action
+ onSuggestionClear = () => {
+ this.suggestions = [];
+ }
+
+ render() {
+ return (
+ <div className="metadataEntry-outerDiv">
+ Key:
+ <Autosuggest inputProps={{ value: this._currentKey, onChange: this.onKeyChange }}
+ getSuggestionValue={this.getSuggestionValue}
+ suggestions={this.suggestions}
+ alwaysRenderSuggestions
+ renderSuggestion={this.renderSuggestion}
+ onSuggestionsFetchRequested={this.onSuggestionFetch}
+ onSuggestionsClearRequested={this.onSuggestionClear}
+ ref={this.autosuggestRef} />
+ Value:
+ <input className="metadataEntry-input" value={this._currentValue} onChange={this.onValueChange} onKeyDown={this.onValueKeyDown} />
+ </div>
+ );
+ }
+} \ No newline at end of file
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/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index 7c1d00eb0..e7a5475ed 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -33,11 +33,16 @@ export class PreviewCursor extends React.Component<{}> {
onKeyPress = (e: KeyboardEvent) => {
// Mixing events between React and Native is finicky. In FormattedTextBox, we set the
// DASHFormattedTextBoxHandled flag when a text box consumes a key press so that we can ignore
- // the keyPress here.
+ // the keyPress here. 112-
//if not these keys, make a textbox if preview cursor is active!
- if (e.key.startsWith("F") && !e.key.endsWith("F")) {
- } else if (e.key !== "Escape" && e.key !== "Alt" && e.key !== "Shift" && e.key !== "Meta" && e.key !== "Control" && !e.defaultPrevented && !(e as any).DASHFormattedTextBoxHandled) {
- if ((!e.ctrlKey && !e.metaKey) || (e.key >= "a" && e.key <= "z")) {
+ if (e.key !== "Escape" && e.key !== "Backspace" && e.key !== "Delete" && e.key !== "CapsLock" &&
+ e.key !== "Alt" && e.key !== "Shift" && e.key !== "Meta" && e.key !== "Control" &&
+ e.key !== "Insert" && e.key !== "Home" && e.key !== "End" && e.key !== "PageUp" && e.key !== "PageDown" &&
+ e.key !== "NumLock" &&
+ (e.keyCode < 112 || e.keyCode > 123) && // F1 thru F12 keys
+ !e.key.startsWith("Arrow") &&
+ !e.defaultPrevented && !(e as any).DASHFormattedTextBoxHandled) {
+ if (!e.ctrlKey && !e.metaKey) {// /^[a-zA-Z0-9$*^%#@+-=_|}{[]"':;?/><.,}]$/.test(e.key)) {
PreviewCursor.Visible && PreviewCursor._onKeyPress && PreviewCursor._onKeyPress(e);
PreviewCursor.Visible = false;
}
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
new file mode 100644
index 000000000..33cb63df5
--- /dev/null
+++ b/src/client/views/SearchBox.tsx
@@ -0,0 +1,183 @@
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faObjectGroup, faSearch } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import * as rp from 'request-promise';
+import { Doc } from '../../new_fields/Doc';
+import { Id } from '../../new_fields/FieldSymbols';
+import { NumCast } from '../../new_fields/Types';
+import { DocServer } from '../DocServer';
+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);
+
+@observer
+export class SearchBox extends React.Component {
+ @observable
+ searchString: string = "";
+
+ @observable private _open: boolean = false;
+ @observable private _resultsOpen: boolean = false;
+
+ @observable
+ private _results: Doc[] = [];
+
+ @action.bound
+ onChange(e: React.ChangeEvent<HTMLInputElement>) {
+ this.searchString = e.target.value;
+ }
+
+ @action
+ submitSearch = async () => {
+ let query = this.searchString;
+ //gets json result into a list of documents that can be used
+ const results = await this.getResults(query);
+
+ runInAction(() => {
+ this._resultsOpen = true;
+ this._results = results;
+ });
+ }
+
+ @action
+ getResults = async (query: string) => {
+ let response = await rp.get(Utils.prepend('/search'), {
+ qs: {
+ query
+ }
+ });
+ let res: string[] = JSON.parse(response);
+ const fields = await DocServer.GetRefFields(res);
+ const docs: Doc[] = [];
+ for (const id of res) {
+ const field = fields[id];
+ if (field instanceof Doc) {
+ docs.push(field);
+ }
+ }
+ return docs;
+ }
+
+ @action
+ handleClickFilter = (e: Event): void => {
+ var className = (e.target as any).className;
+ var id = (e.target as any).id;
+ if (className !== "filter-button" && className !== "filter-form") {
+ this._open = false;
+ }
+
+ }
+
+ @action
+ handleClickResults = (e: Event): void => {
+ var className = (e.target as any).className;
+ var id = (e.target as any).id;
+ if (id !== "result") {
+ this._resultsOpen = false;
+ this._results = [];
+ }
+
+ }
+
+ componentWillMount() {
+ document.addEventListener('mousedown', this.handleClickFilter, false);
+ document.addEventListener('mousedown', this.handleClickResults, false);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('mousedown', this.handleClickFilter, false);
+ document.removeEventListener('mousedown', this.handleClickResults, false);
+ }
+
+ @action
+ toggleFilterDisplay = () => {
+ this._open = !this._open;
+ }
+
+ enter = (e: React.KeyboardEvent<HTMLInputElement>) => {
+ if (e.key === "Enter") {
+ this.submitSearch();
+ }
+ }
+
+ collectionRef = React.createRef<HTMLSpanElement>();
+ startDragCollection = async () => {
+ const results = await this.getResults(this.searchString);
+ const docs = results.map(doc => {
+ const isProto = Doc.GetT(doc, "isPrototype", "boolean", true);
+ if (isProto) {
+ return Doc.MakeDelegate(doc);
+ } else {
+ return Doc.MakeAlias(doc);
+ }
+ });
+ let x = 0;
+ let y = 0;
+ for (const doc of docs) {
+ doc.x = x;
+ doc.y = y;
+ const size = 200;
+ const aspect = NumCast(doc.nativeHeight) / NumCast(doc.nativeWidth, 1);
+ if (aspect > 1) {
+ doc.height = size;
+ doc.width = size / aspect;
+ } else if (aspect > 0) {
+ doc.width = size;
+ doc.height = size * aspect;
+ } else {
+ doc.width = size;
+ doc.height = size;
+ }
+ doc.zoomBasis = 1;
+ x += 250;
+ if (x > 1000) {
+ x = 0;
+ y += 300;
+ }
+ }
+ return Docs.Create.FreeformDocument(docs, { width: 400, height: 400, panX: 175, panY: 175, backgroundColor: "grey", title: `Search Docs: "${this.searchString}"` });
+ }
+
+ // Useful queries:
+ // Delegates of a document: {!join from=id to=proto_i}id:{protoId}
+ // Documents in a collection: {!join from=data_l to=id}id:{collectionProtoId}
+ render() {
+ return (
+ <div>
+ <div className="searchBox-container">
+ <div className="searchBox-bar">
+ <span onPointerDown={SetupDrag(this.collectionRef, this.startDragCollection)} ref={this.collectionRef}>
+ <FontAwesomeIcon icon="object-group" className="searchBox-barChild" size="lg" />
+ </span>
+ <input value={this.searchString} onChange={this.onChange} type="text" placeholder="Search..."
+ className="searchBox-barChild searchBox-input" onKeyPress={this.enter}
+ style={{ width: this._resultsOpen ? "500px" : undefined }} />
+ {/* <button className="searchBox-barChild searchBox-filter" onClick={this.toggleFilterDisplay}>Filter</button> */}
+ {/* <FontAwesomeIcon icon="search" size="lg" className="searchBox-barChild searchBox-submit" /> */}
+ </div>
+ {this._resultsOpen ? (
+ <div className="searchBox-results">
+ {this._results.map(result => <SearchItem doc={result} key={result[Id]} highlighting={[]} />)}
+ </div>
+ ) : null}
+ </div>
+ {this._open ? (
+ <div className="filter-form" id="filter" style={this._open ? { display: "flex" } : { display: "none" }}>
+ <div className="filter-form" id="header">Filter Search Results</div>
+ <div className="filter-form" id="option">
+ filter by collection, key, type of node
+ </div>
+
+ </div>
+ ) : null}
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionBaseView.scss b/src/client/views/collections/CollectionBaseView.scss
index 1f5acb96a..34bcb705e 100644
--- a/src/client/views/collections/CollectionBaseView.scss
+++ b/src/client/views/collections/CollectionBaseView.scss
@@ -1,11 +1,12 @@
@import "../globalCssVariables";
#collectionBaseView {
border-width: 0;
- box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw;
border-color: $light-color-secondary;
border-style: solid;
border-radius: 0 0 $border-radius $border-radius;
box-sizing: border-box;
border-radius: inherit;
pointer-events: all;
+ width:100%;
+ height:100%;
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
index e4f9b5058..72faf52c4 100644
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -5,7 +5,7 @@ import { Doc } from '../../../new_fields/Doc';
import { Id } from '../../../new_fields/FieldSymbols';
import { List } from '../../../new_fields/List';
import { listSpec } from '../../../new_fields/Schema';
-import { BoolCast, Cast, NumCast, PromiseValue } from '../../../new_fields/Types';
+import { BoolCast, Cast, NumCast, PromiseValue, StrCast } from '../../../new_fields/Types';
import { DocumentManager } from '../../util/DocumentManager';
import { SelectionManager } from '../../util/SelectionManager';
import { ContextMenu } from '../ContextMenu';
@@ -18,7 +18,8 @@ export enum CollectionViewType {
Schema,
Docking,
Tree,
- Stacking
+ Stacking,
+ Masonry
}
export interface CollectionRenderProps {
@@ -74,6 +75,8 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
this.props.whenActiveChanged(isActive);
}
+ @computed get extensionDoc() { return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt); }
+
@action.bound
addDocument(doc: Doc, allowDuplicates: boolean = false): boolean {
var curPage = NumCast(this.props.Document.curPage, -1);
@@ -82,13 +85,15 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
Doc.GetProto(doc).annotationOn = this.props.Document;
}
allowDuplicates = true;
- const value = Cast(this.dataDoc[this.dataField], listSpec(Doc));
+ let targetDataDoc = this.props.fieldExt || this.props.Document.isTemplate ? this.extensionDoc : this.props.Document;
+ let targetField = (this.props.fieldExt || this.props.Document.isTemplate) && this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey;
+ const value = Cast(targetDataDoc[targetField], listSpec(Doc));
if (value !== undefined) {
if (allowDuplicates || !value.some(v => v instanceof Doc && v[Id] === doc[Id])) {
value.push(doc);
}
} else {
- Doc.GetProto(this.dataDoc)[this.dataField] = new List([doc]);
+ Doc.GetProto(targetDataDoc)[targetField] = new List([doc]);
}
return true;
}
@@ -98,7 +103,9 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
let docView = DocumentManager.Instance.getDocumentView(doc, this.props.ContainingCollectionView);
docView && SelectionManager.DeselectDoc(docView);
//TODO This won't create the field if it doesn't already exist
- const value = Cast(this.dataDoc[this.dataField], listSpec(Doc), []);
+ let targetDataDoc = this.props.fieldExt || this.props.Document.isTemplate ? this.extensionDoc : this.props.Document;
+ let targetField = (this.props.fieldExt || this.props.Document.isTemplate) && this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey;
+ let value = Cast(targetDataDoc[targetField], listSpec(Doc), []);
let index = value.reduce((p, v, i) => (v instanceof Doc && v[Id] === doc[Id]) ? i : p, -1);
PromiseValue(Cast(doc.annotationOn, Doc)).then(annotationOn =>
annotationOn === this.dataDoc.Document && (doc.annotationOn = undefined)
@@ -116,7 +123,10 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
@action.bound
moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean {
- if (Doc.AreProtosEqual(this.dataDoc, targetCollection)) {
+ let self = this;
+ let targetDataDoc = this.props.fieldExt || this.props.Document.isTemplate ? this.extensionDoc : this.props.Document;
+ if (Doc.AreProtosEqual(targetDataDoc, targetCollection)) {
+ //if (Doc.AreProtosEqual(this.extensionDoc, targetCollection)) {
return true;
}
if (this.removeDocument(doc)) {
@@ -135,7 +145,9 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
};
const viewtype = this.collectionViewType;
return (
- <div id="collectionBaseView" className={this.props.className || "collectionView-cont"}
+ <div id="collectionBaseView"
+ 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)}
</div>
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index d477f96f0..ba7903419 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -1,6 +1,6 @@
import 'golden-layout/src/css/goldenlayout-base.css';
import 'golden-layout/src/css/goldenlayout-dark-theme.css';
-import { action, Lambda, observable, reaction } from "mobx";
+import { action, Lambda, observable, reaction, trace, computed } from "mobx";
import { observer } from "mobx-react";
import * as ReactDOM from 'react-dom';
import Measure from "react-measure";
@@ -26,8 +26,9 @@ import React = require("react");
import { MainView } from '../MainView';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faFile } from '@fortawesome/free-solid-svg-icons';
+import { faFile, faUnlockAlt } from '@fortawesome/free-solid-svg-icons';
import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
+import { Docs } from '../../documents/Documents';
library.add(faFile);
@observer
@@ -259,6 +260,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
// Because this is in a set timeout, if this component unmounts right after mounting,
// we will leak a GoldenLayout, because we try to destroy it before we ever create it
setTimeout(() => this.setupGoldenLayout(), 1);
+ DocListCast((CurrentUserUtils.UserDocument.workspaces as Doc).data).map(d => d.workspaceBrush = false);
this.props.Document.workspaceBrush = true;
}
this._ignoreStateChange = "";
@@ -297,7 +299,10 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
onPointerUp = (e: React.PointerEvent): void => {
if (this._flush) {
this._flush = false;
- setTimeout(() => this.stateChanged(), 10);
+ setTimeout(() => {
+ CollectionDockingView.Instance._ignoreStateChange = JSON.stringify(CollectionDockingView.Instance._goldenLayout.toConfig());
+ this.stateChanged();
+ }, 10);
}
}
@action
@@ -341,6 +346,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
}
itemDropped = () => {
+ CollectionDockingView.Instance._ignoreStateChange = JSON.stringify(CollectionDockingView.Instance._goldenLayout.toConfig());
this.stateChanged();
}
@@ -356,6 +362,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
if (tab.contentItem.config.fixed) {
tab.contentItem.parent.config.fixed = true;
}
+
let doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId) as Doc;
let dataDoc = await DocServer.GetRefField(tab.contentItem.config.props.dataDocumentId) as Doc;
if (doc instanceof Doc) {
@@ -406,12 +413,16 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
if (doc instanceof Doc) {
let theDoc = doc;
CollectionDockingView.Instance._removedDocs.push(theDoc);
- if (CurrentUserUtils.UserDocument.recentlyClosed instanceof Doc) {
- Doc.AddDocToList(CurrentUserUtils.UserDocument.recentlyClosed, "data", doc, undefined, true, true);
+
+ const recent = await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc);
+ if (recent) {
+ Doc.AddDocToList(recent, "data", doc, undefined, true, true);
}
SelectionManager.DeselectAll();
}
+ CollectionDockingView.Instance._ignoreStateChange = JSON.stringify(CollectionDockingView.Instance._goldenLayout.toConfig());
tab.contentItem.remove();
+ CollectionDockingView.Instance._ignoreStateChange = JSON.stringify(CollectionDockingView.Instance._goldenLayout.toConfig());
});
}
@@ -426,14 +437,24 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
stackCreated = (stack: any) => {
//stack.header.controlsContainer.find('.lm_popout').hide();
+ stack.header.element[0].style.backgroundColor = DocServer.Control.isReadOnly() ? "#228540" : undefined;
+ stack.header.element.on('mousedown', (e: any) => {
+ if (e.target === stack.header.element[0] && e.button === 1) {
+ this.AddTab(stack, Docs.Create.FreeformDocument([], { width: this.props.PanelWidth(), height: this.props.PanelHeight(), title: "Untitled Collection" }), undefined);
+ }
+ });
stack.header.controlsContainer.find('.lm_close') //get the close icon
.off('click') //unbind the current click handler
- .click(action(function () {
+ .click(action(async function () {
//if (confirm('really close this?')) {
+ const recent = await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc);
stack.remove();
- stack.contentItems.map(async (contentItem: any) => {
+ stack.contentItems.forEach(async (contentItem: any) => {
let doc = await DocServer.GetRefField(contentItem.config.props.documentId);
if (doc instanceof Doc) {
+ if (recent) {
+ Doc.AddDocToList(recent, "data", doc, undefined, true, true);
+ }
let theDoc = doc;
CollectionDockingView.Instance._removedDocs.push(theDoc);
}
@@ -444,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");
}));
}
@@ -470,6 +491,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
interface DockedFrameProps {
documentId: FieldId;
dataDocumentId: FieldId;
+ glContainer: any;
//collectionDockingView: CollectionDockingView
}
@observer
@@ -479,6 +501,9 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
@observable private _panelHeight = 0;
@observable private _document: Opt<Doc>;
@observable private _dataDoc: Opt<Doc>;
+
+ @observable private _isActive: boolean = false;
+
get _stack(): any {
let parent = (this.props as any).glContainer.parent.parent;
if (this._document && this._document.excludeFromLibrary && parent.parent && parent.parent.contentItems.length > 1) {
@@ -496,6 +521,25 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
}));
}
+ componentDidMount() {
+ this.props.glContainer.layoutManager.on("activeContentItemChanged", this.onActiveContentItemChanged);
+ this.props.glContainer.on("tab", this.onActiveContentItemChanged);
+ this.onActiveContentItemChanged();
+ }
+
+ componentWillUnmount() {
+ this.props.glContainer.layoutManager.off("activeContentItemChanged", this.onActiveContentItemChanged);
+ this.props.glContainer.off("tab", this.onActiveContentItemChanged);
+ }
+
+ @action.bound
+ private onActiveContentItemChanged() {
+ if (this.props.glContainer.tab) {
+ this._isActive = this.props.glContainer.tab.isActive;
+ }
+ }
+
+
nativeWidth = () => NumCast(this._document!.nativeWidth, this._panelWidth);
nativeHeight = () => {
let nh = NumCast(this._document!.nativeHeight, this._panelHeight);
@@ -539,41 +583,51 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
CollectionDockingView.Instance.AddTab(this._stack, doc, dataDoc);
}
}
- get content() {
+ @computed get docView() {
if (!this._document) {
return (null);
}
let resolvedDataDoc = this._document.layout instanceof Doc ? this._document : this._dataDoc;
+ return <DocumentView key={this._document[Id]}
+ Document={this._document}
+ DataDoc={resolvedDataDoc}
+ bringToFront={emptyFunction}
+ addDocument={undefined}
+ removeDocument={undefined}
+ ContentScaling={this.contentScaling}
+ PanelWidth={this.nativeWidth}
+ PanelHeight={this.nativeHeight}
+ ScreenToLocalTransform={this.ScreenToLocalTransform}
+ renderDepth={0}
+ selectOnLoad={false}
+ parentActive={returnTrue}
+ whenActiveChanged={emptyFunction}
+ focus={emptyFunction}
+ addDocTab={this.addDocTab}
+ ContainingCollectionView={undefined}
+ zoomToScale={emptyFunction}
+ getScale={returnOne} />;
+ }
+
+ @computed get content() {
+ if (!this._document) {
+ return (null);
+ }
return (
<div className="collectionDockingView-content" ref={this._mainCont}
style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px) scale(${this.scaleToFitMultiplier})` }}>
- <DocumentView key={this._document[Id]}
- Document={this._document}
- DataDoc={resolvedDataDoc}
- bringToFront={emptyFunction}
- addDocument={undefined}
- removeDocument={undefined}
- ContentScaling={this.contentScaling}
- PanelWidth={this.nativeWidth}
- PanelHeight={this.nativeHeight}
- ScreenToLocalTransform={this.ScreenToLocalTransform}
- renderDepth={0}
- selectOnLoad={false}
- parentActive={returnTrue}
- whenActiveChanged={emptyFunction}
- focus={emptyFunction}
- addDocTab={this.addDocTab}
- ContainingCollectionView={undefined}
- zoomToScale={emptyFunction}
- getScale={returnOne} />
+ {this.docView}
</div >);
}
render() {
+ if (!this._isActive) return null;
let theContent = this.content;
return !this._document ? (null) :
<Measure offset onResize={action((r: any) => { this._panelWidth = r.offset.width; this._panelHeight = r.offset.height; })}>
- {({ measureRef }) => <div ref={measureRef}> {theContent} </div>}
+ {({ measureRef }) => <div ref={measureRef}>
+ {theContent}
+ </div>}
</Measure>;
}
-}
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx
index 31a73ab36..9074854d6 100644
--- a/src/client/views/collections/CollectionPDFView.tsx
+++ b/src/client/views/collections/CollectionPDFView.tsx
@@ -1,6 +1,6 @@
import { action, IReactionDisposer, observable, reaction } from "mobx";
import { observer } from "mobx-react";
-import { WidthSym } from "../../../new_fields/Doc";
+import { WidthSym, HeightSym } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
import { NumCast } from "../../../new_fields/Types";
import { emptyFunction } from "../../../Utils";
@@ -29,15 +29,7 @@ export class CollectionPDFView extends React.Component<FieldViewProps> {
this._reactionDisposer = reaction(
() => NumCast(this.props.Document.scrollY),
() => {
- // let transform = this.props.ScreenToLocalTransform();
- if (this._buttonTray.current) {
- // console.log(this._buttonTray.current.offsetHeight);
- // console.log(NumCast(this.props.Document.scrollY));
- let scale = this.nativeWidth() / this.props.Document[WidthSym]();
- this.props.Document.panY = NumCast(this.props.Document.scrollY);
- // console.log(scale);
- }
- // console.log(this.props.Document[HeightSym]());
+ this.props.Document.panY = NumCast(this.props.Document.scrollY);
},
{ fireImmediately: true }
);
@@ -47,8 +39,8 @@ export class CollectionPDFView extends React.Component<FieldViewProps> {
this._reactionDisposer && this._reactionDisposer();
}
- public static LayoutString(fieldKey: string = "data") {
- return FieldView.LayoutString(CollectionPDFView, fieldKey);
+ public static LayoutString(fieldKey: string = "data", fieldExt: string = "annotations") {
+ return FieldView.LayoutString(CollectionPDFView, fieldKey, fieldExt);
}
@observable _inThumb = false;
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index b54e8aff0..2cf50e551 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -31,6 +31,8 @@ import { CollectionVideoView } from "./CollectionVideoView";
import { CollectionView } from "./CollectionView";
import { undoBatch } from "../../util/UndoManager";
import { timesSeries } from "async";
+import { ImageBox } from "../nodes/ImageBox";
+import { ComputedField } from "../../../new_fields/ScriptField";
library.add(faCog);
@@ -263,7 +265,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
let dbName = StrCast(this.props.Document.title);
let res = await Gateway.Instance.PostSchema(csv, dbName);
if (self.props.CollectionView.props.addDocument) {
- let schemaDoc = await Docs.DBDocument("https://www.cs.brown.edu/" + dbName, { title: dbName }, { dbDoc: self.props.Document });
+ let schemaDoc = await Docs.Create.DBDocument("https://www.cs.brown.edu/" + dbName, { title: dbName }, { dbDoc: self.props.Document });
if (schemaDoc) {
//self.props.CollectionView.props.addDocument(schemaDoc, false);
self.props.Document.schemaDoc = schemaDoc;
@@ -401,10 +403,11 @@ interface CollectionSchemaPreviewProps {
Document?: Doc;
DataDocument?: Doc;
childDocs?: Doc[];
- fitToBox?: () => number[];
renderDepth: number;
+ fitToBox?: boolean;
width: () => number;
height: () => number;
+ showOverlays?: (doc: Doc) => { title?: string, caption?: string };
CollectionView?: CollectionView | CollectionPDFView | CollectionVideoView;
getTransform: () => Transform;
addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
@@ -445,8 +448,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();
}
@@ -488,6 +495,7 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre
fitToBox={this.props.fitToBox}
renderDepth={this.props.renderDepth + 1}
selectOnLoad={false}
+ showOverlays={this.props.showOverlays}
addDocument={this.props.addDocument}
removeDocument={this.props.removeDocument}
moveDocument={this.props.moveDocument}
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index bc733f152..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%;
@@ -56,6 +58,9 @@
margin-left: -5;
}
+ .collectionStackingView-columnDoc{
+ display: inline-block;
+ }
.collectionStackingView-columnDoc,
.collectionStackingView-masonryDoc {
@@ -68,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 aea74321e..0e5f9a321 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -1,24 +1,28 @@
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 { BoolCast, NumCast, Cast, StrCast } from "../../../new_fields/Types";
import { emptyFunction, Utils } from "../../../Utils";
-import { ContextMenu } from "../ContextMenu";
import { CollectionSchemaPreview } from "./CollectionSchemaView";
import "./CollectionStackingView.scss";
import { CollectionSubView } from "./CollectionSubView";
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); }
@@ -26,119 +30,119 @@ 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); }
- singleColDocHeight(d: Doc) {
- let nw = NumCast(d.nativeWidth);
- let nh = NumCast(d.nativeHeight);
- let aspect = nw && nh ? nh / nw : 1;
- let wid = Math.min(d[WidthSym](), this.columnWidth);
- return (nw && nh) ? wid * aspect : d[HeightSym]();
+ @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) =>
- height + this.singleColDocHeight(d) + (i === this.filteredChildren.length - 1 ? this.yMargin : this.gridGap), this.yMargin))
+ (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
moveDocument = (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean): boolean => {
return this.props.removeDocument(doc) && addDocument(doc);
}
- getSingleDocTransform(doc: Doc, ind: number, width: number) {
- let localY = this.filteredChildren.reduce((height, d, i) =>
- height + (i < ind ? this.singleColDocHeight(d) + this.gridGap : 0), this.yMargin);
- let translate = this.props.ScreenToLocalTransform().inverse().transformPoint((this.props.PanelWidth() - width) / 2, localY);
- let outerXf = Utils.GetScreenTransform(this._masonryGridRef!);
- let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translate[0], outerXf.translateY - translate[1]);
- return this.props.ScreenToLocalTransform().translate(offset[0], offset[1]).scale(NumCast(doc.width, 1) / this.columnWidth);
- }
createRef = (ele: HTMLDivElement | null) => {
this._masonryGridRef = ele;
this.createDropTarget(ele!);
}
- @computed
- get singleColumnChildren() {
- return this.filteredChildren.map((d, i) => {
- let layoutDoc = Doc.expandTemplateLayout(d, this.props.DataDoc);
- let width = () => d.nativeWidth ? Math.min(d[WidthSym](), this.columnWidth) : this.columnWidth;
- let height = () => this.singleColDocHeight(layoutDoc);
- let dxf = () => this.getSingleDocTransform(layoutDoc, i, width()).scale(this.columnWidth / d[WidthSym]());
- let gap = i === 0 ? 0 : this.gridGap;
- return <div className="collectionStackingView-columnDoc"
- key={d[Id]}
- style={{ width: width(), display: "inline-block", marginTop: gap, height: `${height() / (this.props.Document[HeightSym]() - 2 * this.yMargin) * 100}%` }} >
- <CollectionSchemaPreview
- Document={layoutDoc}
- DataDocument={d !== this.props.DataDoc ? this.props.DataDoc : undefined}
- renderDepth={this.props.renderDepth}
- width={width}
- height={height}
- getTransform={dxf}
- CollectionView={this.props.CollectionView}
- addDocument={this.props.addDocument}
- moveDocument={this.props.moveDocument}
- removeDocument={this.props.removeDocument}
- active={this.props.active}
- whenActiveChanged={this.props.whenActiveChanged}
- addDocTab={this.props.addDocTab}
- setPreviewScript={emptyFunction}
- previewScript={undefined}>
- </CollectionSchemaPreview>
- </div>;
- });
+ overlays = (doc: Doc) => {
+ return doc.type === DocumentType.IMG || doc.type === DocumentType.VID ? { title: "title", caption: "caption" } : {};
}
- getDocTransform(doc: Doc, dref: HTMLDivElement) {
- let { scale, translateX, translateY } = Utils.GetScreenTransform(dref);
+
+ getDisplayDoc(layoutDoc: Doc, d: Doc, dxf: () => Transform) {
+ let resolvedDataDoc = !this.props.Document.isTemplate && this.props.DataDoc !== this.props.Document ? this.props.DataDoc : undefined;
+ let width = () => d.nativeWidth ? Math.min(layoutDoc[WidthSym](), this.columnWidth) : this.columnWidth;
+ let height = () => this.getDocHeight(layoutDoc);
+ let finalDxf = () => dxf().scale(this.columnWidth / layoutDoc[WidthSym]());
+ return <CollectionSchemaPreview
+ Document={layoutDoc}
+ DataDocument={resolvedDataDoc}
+ showOverlays={this.overlays}
+ renderDepth={this.props.renderDepth}
+ width={width}
+ height={height}
+ getTransform={finalDxf}
+ CollectionView={this.props.CollectionView}
+ addDocument={this.props.addDocument}
+ moveDocument={this.props.moveDocument}
+ removeDocument={this.props.removeDocument}
+ active={this.props.active}
+ whenActiveChanged={this.props.whenActiveChanged}
+ addDocTab={this.props.addDocTab}
+ setPreviewScript={emptyFunction}
+ previewScript={undefined}>
+ </CollectionSchemaPreview>;
+ }
+ getDocHeight(d: Doc) {
+ let nw = NumCast(d.nativeWidth);
+ let nh = NumCast(d.nativeHeight);
+ let aspect = nw && nh ? nh / nw : 1;
+ let wid = Math.min(d[WidthSym](), this.columnWidth);
+ 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);
return this.props.ScreenToLocalTransform().translate(offset[0], offset[1]).scale(NumCast(doc.width, 1) / this.columnWidth);
}
- docXfs: any[] = []
- @computed
- get children() {
- this.docXfs.length = 0;
- return this.filteredChildren.map((d, i) => {
- let aspect = d.nativeHeight ? NumCast(d.nativeWidth) / NumCast(d.nativeHeight) : undefined;
- let dref = React.createRef<HTMLDivElement>();
- let dxf = () => this.getDocTransform(d, dref.current!).scale(this.columnWidth / d[WidthSym]());
- let width = () => d.nativeWidth ? Math.min(d[WidthSym](), this.columnWidth) : this.columnWidth;
- let height = () => aspect ? width() / aspect : d[HeightSym]();
- let rowSpan = Math.ceil((height() + this.gridGap) / (this._gridSize + 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}` }} >
- <CollectionSchemaPreview
- Document={d}
- DataDocument={this.props.Document.layout instanceof Doc ? this.props.Document : this.props.DataDoc}
- renderDepth={this.props.renderDepth}
- CollectionView={this.props.CollectionView}
- addDocument={this.props.addDocument}
- moveDocument={this.props.moveDocument}
- removeDocument={this.props.removeDocument}
- getTransform={dxf}
- width={width}
- height={height}
- active={this.props.active}
- addDocTab={this.props.addDocTab}
- whenActiveChanged={this.props.whenActiveChanged}
- setPreviewScript={emptyFunction}
- previewScript={undefined}>
- </CollectionSchemaPreview>
- </div>);
+ getDocTransform(doc: Doc, dref: HTMLDivElement) {
+ 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);
+ let translate = this.props.ScreenToLocalTransform().inverse().transformPoint((this.props.PanelWidth() - width) / 2, localY);
+ return this.offsetTransform(doc, translate[0], translate[1]);
+ }
+
+ children(docs: Doc[]) {
+ this._docXfs.length = 0;
+ 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._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}` }} >
+ {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.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)}
+ </div>;
+ }
});
}
- _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];
@@ -148,29 +152,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
@@ -178,13 +174,13 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
let targInd = -1;
let where = [de.x, de.y];
if (de.data instanceof DragManager.DocumentDragData) {
- this.docXfs.map((cd, i) => {
+ this._docXfs.map((cd, i) => {
let pos = cd.dxf().inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap);
let pos1 = cd.dxf().inverse().transformPoint(cd.width(), cd.height());
if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && where[1] < pos1[1]) {
targInd = i;
}
- })
+ });
}
if (super.drop(e, de)) {
let newDoc = de.data.droppedDocuments[0];
@@ -204,13 +200,13 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
onDrop = (e: React.DragEvent): void => {
let where = [e.clientX, e.clientY];
let targInd = -1;
- this.docXfs.map((cd, i) => {
+ this._docXfs.map((cd, i) => {
let pos = cd.dxf().inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap);
let pos1 = cd.dxf().inverse().transformPoint(cd.width(), cd.height());
if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && where[1] < pos1[1]) {
targInd = i;
}
- })
+ });
super.onDrop(e, {}, () => {
if (targInd !== -1) {
let newDoc = this.childDocs[this.childDocs.length - 1];
@@ -222,28 +218,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.singleColumn ? this.singleColumnChildren : 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 873fb518c..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;
@@ -48,16 +49,17 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
@computed get extensionDoc() { return Doc.resolvedFieldDataDoc(BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt); }
+
get childDocs() {
let self = this;
//TODO tfs: This might not be what we want?
//This linter error can't be fixed because of how js arguments work, so don't switch this to filter(FieldValue)
- return DocListCast((BoolCast(this.props.Document.isTemplate) ? this.extensionDoc : this.props.Document)[this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey]);
+ return DocListCast(this.extensionDoc[this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey]);
}
get childDocList() {
//TODO tfs: This might not be what we want?
//This linter error can't be fixed because of how js arguments work, so don't switch this to filter(FieldValue)
- return Cast((BoolCast(this.props.Document.isTemplate) ? this.extensionDoc : this.props.Document)[this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey], listSpec(Doc));
+ return Cast(this.extensionDoc[this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey], listSpec(Doc));
}
@action
@@ -73,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);
@@ -102,7 +104,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
} else if (de.data.moveDocument) {
let movedDocs = de.data.options === this.props.Document[Id] ? de.data.draggedDocuments : de.data.droppedDocuments;
added = movedDocs.reduce((added: boolean, d) =>
- de.data.moveDocument(d, this.props.Document, this.props.addDocument) || added, false);
+ de.data.moveDocument(d, /*this.props.DataDoc ? this.props.DataDoc :*/ this.props.Document, this.props.addDocument) || added, false);
} else {
added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false);
}
@@ -144,10 +146,10 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
}
});
} else {
- this.props.addDocument && this.props.addDocument(Docs.WebDocument(href, options));
+ this.props.addDocument && this.props.addDocument(Docs.Create.WebDocument(href, options));
}
} else if (text) {
- this.props.addDocument && this.props.addDocument(Docs.TextDocument({ ...options, width: 100, height: 25, documentText: "@@@" + text }), false);
+ this.props.addDocument && this.props.addDocument(Docs.Create.TextDocument({ ...options, width: 100, height: 25, documentText: "@@@" + text }), false);
}
return;
}
@@ -157,13 +159,13 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
let img = tags[0].startsWith("img") ? tags[0] : tags.length > 1 && tags[1].startsWith("img") ? tags[1] : "";
if (img) {
let split = img.split("src=\"")[1].split("\"")[0];
- let doc = Docs.ImageDocument(split, { ...options, width: 300 });
+ let doc = Docs.Create.ImageDocument(split, { ...options, width: 300 });
this.props.addDocument(doc, false);
return;
} 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
@@ -171,7 +173,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
}
});
} else {
- let htmlDoc = Docs.HtmlDocument(html, { ...options, width: 300, height: 300, documentText: text });
+ let htmlDoc = Docs.Create.HtmlDocument(html, { ...options, width: 300, height: 300, documentText: text });
this.props.addDocument(htmlDoc, false);
}
return;
@@ -179,7 +181,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
}
if (text && text.indexOf("www.youtube.com/watch") !== -1) {
const url = text.replace("youtube.com/watch?v=", "youtube.com/embed/");
- this.props.addDocument(Docs.WebDocument(url, { ...options, width: 300, height: 300 }));
+ this.props.addDocument(Docs.Create.VideoDocument(url, { ...options, title: url, width: 400, height: 315, nativeWidth: 600, nativeHeight: 472.5 }));
return;
}
@@ -192,11 +194,11 @@ 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) {
- Docs.getDocumentFromType(type, str, { ...options, width: 300, nativeWidth: 300 })
+ Docs.Get.DocumentFromType(type, str, { ...options, width: 300, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300 })
.then(doc => doc && this.props.addDocument(doc, false));
}
});
@@ -217,9 +219,9 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
body: formData
}).then(async (res: Response) => {
(await res.json()).map(action((file: any) => {
- let full = { ...options, nativeWidth: 300, width: 300, title: dropFileName };
- let path = DocServer.prepend(file);
- Docs.getDocumentFromType(type, path, full).then(doc => doc && this.props.addDocument(doc));
+ let full = { ...options, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300, width: 300, title: dropFileName };
+ let path = Utils.prepend(file);
+ Docs.Get.DocumentFromType(type, path, full).then(doc => doc && this.props.addDocument(doc));
}));
});
promises.push(prom);
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index d7725f444..d05cc375e 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -1,7 +1,7 @@
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 } from "mobx";
+import { action, computed, observable, trace, untracked } from "mobx";
import { observer } from "mobx-react";
import { Doc, DocListCast, HeightSym, WidthSym, Opt } from '../../../new_fields/Doc';
import { Id } from '../../../new_fields/FieldSymbols';
@@ -9,7 +9,7 @@ import { List } from '../../../new_fields/List';
import { Document, listSpec } from '../../../new_fields/Schema';
import { BoolCast, Cast, NumCast, StrCast } from '../../../new_fields/Types';
import { emptyFunction, Utils } from '../../../Utils';
-import { Docs, DocUtils, DocTypes } from '../../documents/Documents';
+import { Docs, DocUtils, DocumentType } from '../../documents/Documents';
import { DocumentManager } from '../../util/DocumentManager';
import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager";
import { SelectionManager } from '../../util/SelectionManager';
@@ -58,6 +58,7 @@ library.add(faCaretDown);
library.add(faCaretRight);
library.add(faCaretSquareDown);
library.add(faCaretSquareRight);
+library.add(faArrowsAltH);
@observer
/**
@@ -73,9 +74,10 @@ class TreeView extends React.Component<TreeViewProps> {
@observable _collapsed: boolean = true;
@computed get fieldKey() {
- let keys = Array.from(Object.keys(this.resolvedDataDoc));
+ 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) {
- keys.push(...Array.from(Object.keys(this.resolvedDataDoc.proto)));
+ let arr = Array.from(Object.keys(this.resolvedDataDoc.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[] = [];
@@ -113,12 +115,12 @@ class TreeView extends React.Component<TreeViewProps> {
}
}
onPointerLeave = (e: React.PointerEvent): void => {
- this.props.document.libraryBrush = false;
+ this.props.document.libraryBrush = undefined;
this._header!.current!.className = "treeViewItem-header";
document.removeEventListener("pointermove", this.onDragMove, true);
}
onDragMove = (e: PointerEvent): void => {
- this.props.document.libraryBrush = false;
+ this.props.document.libraryBrush = undefined;
let x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY);
let rect = this._header!.current!.getBoundingClientRect();
let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2);
@@ -152,7 +154,8 @@ class TreeView extends React.Component<TreeViewProps> {
let docList = Cast(this.resolvedDataDoc[this.fieldKey], listSpec(Doc));
let doc = Cast(this.resolvedDataDoc[this.fieldKey], Doc);
let isDoc = doc instanceof Doc || docList;
- return <div className="bullet" onClick={action(() => this._collapsed = !this._collapsed)}>
+ let c;
+ return <div className="bullet" onClick={action(() => this._collapsed = !this._collapsed)} style={{ color: StrCast(this.props.document.color, "black"), opacity: 0.4 }}>
{<FontAwesomeIcon icon={this._collapsed ? (isDoc ? "caret-square-right" : "caret-right") : (isDoc ? "caret-square-down" : "caret-down")} />}
</div>;
}
@@ -170,7 +173,7 @@ class TreeView extends React.Component<TreeViewProps> {
SetValue={(value: string) => (Doc.GetProto(this.resolvedDataDoc)[key] = value) ? true : true}
OnFillDown={(value: string) => {
Doc.GetProto(this.resolvedDataDoc)[key] = value;
- let doc = Docs.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
+ let 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);
}}
@@ -203,7 +206,7 @@ class TreeView extends React.Component<TreeViewProps> {
let onItemDown = SetupDrag(reference, () => this.resolvedDataDoc, this.move, this.props.dropAction, this.props.treeViewId, true);
let headerElements = (
- <span className="collectionTreeView-keyHeader" key={this._chosenKey}
+ <span className="collectionTreeView-keyHeader" key={this._chosenKey + "chosen"}
onPointerDown={action(() => {
let ind = this.keyList.indexOf(this._chosenKey);
ind = (ind + 1) % this.keyList.length;
@@ -219,8 +222,8 @@ class TreeView extends React.Component<TreeViewProps> {
return <>
<div className="docContainer" id={`docContainer-${this.props.parentKey}`} ref={reference} onPointerDown={onItemDown}
style={{
- background: BoolCast(this.props.document.libraryBrush, false) ? "#06121212" : "0",
- outline: BoolCast(this.props.document.workspaceBrush, false) ? "dashed 1px #06123232" : undefined,
+ background: BoolCast(this.props.document.libraryBrush) ? "#06121212" : "0",
+ outline: BoolCast(this.props.document.workspaceBrush) ? "dashed 1px #06123232" : undefined,
pointerEvents: this.props.active() || SelectionManager.GetIsDragging() ? "all" : "none"
}}
>
@@ -246,7 +249,7 @@ class TreeView extends React.Component<TreeViewProps> {
ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => MainView.Instance.openWorkspace(this.resolvedDataDoc)), 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.KVPDocument(this.props.document, { width: 300, height: 300 }); this.props.addDocTab(kvp, this.props.dataDoc ? this.props.dataDoc : kvp, "onRight"); }, icon: "layer-group" });
+ ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { let kvp = Docs.Create.KVPDocument(this.props.document, { width: 300, height: 300 }); this.props.addDocTab(kvp, this.props.dataDoc ? this.props.dataDoc : kvp, "onRight"); }, icon: "layer-group" });
ContextMenu.Instance.displayMenu(e.pageX > 156 ? e.pageX - 156 : 0, e.pageY - 15);
e.stopPropagation();
e.preventDefault();
@@ -300,7 +303,14 @@ class TreeView extends React.Component<TreeViewProps> {
let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.props.document, this._chosenKey, doc, addBefore, before);
let groups = LinkManager.Instance.getRelatedGroupedLinks(this.props.document);
groups.forEach((groupLinkDocs, groupType) => {
- let destLinks = groupLinkDocs.map(d => LinkManager.Instance.getOppositeAnchor(d, this.props.document));
+ // let destLinks = groupLinkDocs.map(d => LinkManager.Instance.getOppositeAnchor(d, this.props.document));
+ let destLinks: Doc[] = [];
+ groupLinkDocs.forEach((doc) => {
+ let opp = LinkManager.Instance.getOppositeAnchor(doc, this.props.document);
+ if (opp) {
+ destLinks.push(opp);
+ }
+ });
ele.push(
<div key={"treeviewlink-" + groupType + "subtitle"}>
<div className="collectionTreeView-subtitle">{groupType}:</div>
@@ -314,10 +324,10 @@ class TreeView extends React.Component<TreeViewProps> {
return ele;
}
- @computed get docBounds() {
- if (StrCast(this.props.document.type).indexOf(DocTypes.COL) === -1) return undefined;
+ @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);
- return Doc.ComputeContentBounds(layoutDoc);
+ return Doc.ComputeContentBounds(DocListCast(layoutDoc.data));
}
docWidth = () => {
let aspect = NumCast(this.props.document.nativeHeight) / NumCast(this.props.document.nativeWidth);
@@ -325,7 +335,7 @@ class TreeView extends React.Component<TreeViewProps> {
return NumCast(this.props.document.nativeWidth) ? Math.min(this.props.document[WidthSym](), this.props.panelWidth() - 5) : this.props.panelWidth() - 5;
}
docHeight = () => {
- let bounds = this.docBounds;
+ let bounds = this.boundsOfCollectionDocument;
return Math.min(this.MAX_EMBED_HEIGHT, (() => {
let aspect = NumCast(this.props.document.nativeHeight) / NumCast(this.props.document.nativeWidth);
if (aspect) return this.docWidth() * aspect;
@@ -333,10 +343,6 @@ class TreeView extends React.Component<TreeViewProps> {
return NumCast(this.props.document.height) ? NumCast(this.props.document.height) : 50;
})());
}
- fitToBox = () => {
- let bounds = this.docBounds!;
- return [(bounds.x + bounds.r) / 2, (bounds.y + bounds.b) / 2, Math.min(this.docHeight() / (bounds.b - bounds.y), this.docWidth() / (bounds.r - bounds.x))];
- }
render() {
let contentElement: (JSX.Element | null) = null;
@@ -354,12 +360,12 @@ class TreeView extends React.Component<TreeViewProps> {
</ul >;
} else {
let layoutDoc = Doc.expandTemplateLayout(this.props.document, this.props.dataDoc);
- contentElement = <div ref={this._dref} style={{ display: "inline-block", height: this.docHeight() }} key={this.props.document[Id]}>
+ 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}
- fitToBox={this.docBounds && !NumCast(this.props.document.nativeWidth) ? this.fitToBox : undefined}
+ fitToBox={this.boundsOfCollectionDocument !== undefined}
width={this.docWidth}
height={this.docHeight}
getTransform={this.docTransform}
@@ -427,7 +433,7 @@ class TreeView extends React.Component<TreeViewProps> {
dataDoc={dataDoc}
containingCollection={containingCollection}
treeViewId={treeViewId}
- key={child[Id]}
+ key={child[Id] + "child " + i}
indentDocument={indent}
renderDepth={renderDepth}
deleteDoc={remove}
@@ -526,10 +532,9 @@ export class CollectionTreeView extends CollectionSubView(Document) {
let dropAction = StrCast(this.props.Document.dropAction) as dropActionType;
let addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before);
let moveDoc = (d: Doc, target: Doc, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc);
-
return !this.childDocs ? (null) : (
<div id="body" className="collectionTreeView-dropTarget"
- style={{ overflow: "auto" }}
+ style={{ overflow: "auto", background: StrCast(this.props.Document.backgroundColor, "lightgray") }}
onContextMenu={this.onContextMenu}
onWheel={(e: React.WheelEvent) => (e.target as any).scrollHeight > (e.target as any).clientHeight && e.stopPropagation()}
onDrop={this.onTreeDrop}
@@ -542,7 +547,7 @@ 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.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
+ let 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 1984965ba..31a8a93e0 100644
--- a/src/client/views/collections/CollectionVideoView.tsx
+++ b/src/client/views/collections/CollectionVideoView.tsx
@@ -1,49 +1,44 @@
-import { action, observable, trace } from "mobx";
-import * as htmlToImage from "html-to-image";
+import { action } from "mobx";
import { observer } from "mobx-react";
-import { ContextMenu } from "../ContextMenu";
-import { CollectionViewType, CollectionBaseView, CollectionRenderProps } from "./CollectionBaseView";
-import React = require("react");
-import "./CollectionVideoView.scss";
-import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";
+import { NumCast } from "../../../new_fields/Types";
import { FieldView, FieldViewProps } from "../nodes/FieldView";
-import { emptyFunction, Utils } from "../../../Utils";
-import { Id } from "../../../new_fields/FieldSymbols";
import { VideoBox } from "../nodes/VideoBox";
-import { NumCast, Cast, StrCast } from "../../../new_fields/Types";
-import { VideoField } from "../../../new_fields/URLField";
-import { SearchBox } from "../search/SearchBox";
-import { DocServer } from "../../DocServer";
-import { Docs, DocUtils } from "../../documents/Documents";
+import { CollectionBaseView, CollectionRenderProps, CollectionViewType } from "./CollectionBaseView";
+import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";
+import "./CollectionVideoView.scss";
+import React = require("react");
+import { InkingControl } from "../InkingControl";
+import { InkTool } from "../../../new_fields/InkField";
@observer
export class CollectionVideoView extends React.Component<FieldViewProps> {
private _videoBox?: VideoBox;
- public static LayoutString(fieldKey: string = "data") {
- return FieldView.LayoutString(CollectionVideoView, fieldKey);
+ public static LayoutString(fieldKey: string = "data", fieldExt: string = "annotations") {
+ return FieldView.LayoutString(CollectionVideoView, fieldKey, fieldExt);
}
private get uIButtons() {
let scaling = Math.min(1.8, this.props.ScreenToLocalTransform().Scale);
let curTime = NumCast(this.props.Document.curPage);
- return ([
- <div className="collectionVideoView-time" key="time" onPointerDown={this.onResetDown} style={{ transform: `scale(${scaling}, ${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
onPlayDown = () => {
- if (this._videoBox && this._videoBox.player) {
+ if (this._videoBox) {
if (this._videoBox.Playing) {
this._videoBox.Pause();
} else {
@@ -61,56 +56,34 @@ 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);
}
}
- 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
- }
-
- let field = Cast(this.props.Document[this.props.fieldKey], VideoField);
- if (field) {
- let url = field.url.href;
- ContextMenu.Instance.addItem({
- description: "Copy path", event: () => { Utils.CopyText(url); }, icon: "expand-arrows-alt"
- });
+ @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));
}
- let width = NumCast(this.props.Document.width);
- let height = NumCast(this.props.Document.height);
- ContextMenu.Instance.addItem({
- description: "Take Snapshot", event: async () => {
- var canvas = document.createElement('canvas');
- canvas.width = 640;
- canvas.height = 640 * NumCast(this.props.Document.nativeHeight) / NumCast(this.props.Document.nativeWidth);
- var ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions
- ctx && ctx.drawImage(this._videoBox!.player!, 0, 0, canvas.width, canvas.height);
-
- //convert to desired file format
- var dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
- // if you want to preview the captured image,
-
- let filename = encodeURIComponent("snapshot" + this.props.Document.title + "_" + this.props.Document.curPage).replace(/\./g, "");
- SearchBox.convertDataUri(dataUrl, filename).then((returnedFilename) => {
- if (returnedFilename) {
- let url = DocServer.prepend(returnedFilename);
- let imageSummary = Docs.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-"
- });
- this.props.addDocument && this.props.addDocument(imageSummary, false);
- DocUtils.MakeLink(imageSummary, this.props.Document);
- }
- });
- },
- icon: "expand-arrows-alt"
- });
+ e.stopImmediatePropagation();
+ }
+ @action
+ onPointerUp = (e: PointerEvent) => {
+ document.removeEventListener("pointermove", this.onPointerMove, true);
+ document.removeEventListener("pointerup", this.onPointerUp, true);
+ InkingControl.Instance.switchTool(InkTool.None);
+ this._isclick < 10 && (this.props.Document.curPage = 0);
}
-
setVideoBox = (videoBox: VideoBox) => { this._videoBox = videoBox; };
private subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => {
@@ -123,7 +96,7 @@ export class CollectionVideoView extends React.Component<FieldViewProps> {
render() {
return (
- <CollectionBaseView {...this.props} className="collectionVideoView-cont" onContextMenu={this.onContextMenu}>
+ <CollectionBaseView {...this.props} className="collectionVideoView-cont" >
{this.subView}
</CollectionBaseView>);
}
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index e500e5c70..045c8531e 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,10 +25,13 @@ 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> {
- public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(CollectionView, fieldStr); }
+ public static LayoutString(fieldStr: string = "data", fieldExt: string = "") { return FieldView.LayoutString(CollectionView, fieldStr, fieldExt); }
private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => {
let props = { ...this.props, ...renderProps };
@@ -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} />);
@@ -43,8 +48,9 @@ export class CollectionView extends React.Component<FieldViewProps> {
return (null);
}
- get isAnnotationOverlay() { return this.props.fieldKey === "annotations" || this.props.fieldExt === "annotations"; }
+ get isAnnotationOverlay() { return this.props.fieldExt ? true : false; }
+ static _applyCount: number = 0;
onContextMenu = (e: React.MouseEvent): void => {
if (!this.isAnnotationOverlay && !e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
let subItems: ContextMenuProps[] = [];
@@ -54,15 +60,19 @@ 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);
+ otherdoc.width = this.props.Document[WidthSym]();
+ otherdoc.height = this.props.Document[HeightSym]();
+ otherdoc.title = this.props.Document.title + "(..." + CollectionView._applyCount++ + ")"; // previously "applied"
+ otherdoc.layout = Doc.MakeDelegate(this.props.Document);
+ otherdoc.miniLayout = StrCast(this.props.Document.miniLayout);
+ otherdoc.detailedLayout = otherdoc.layout;
+ otherdoc.type = DocumentType.TEMPLATE;
this.props.addDocTab && this.props.addDocTab(otherdoc, undefined, "onRight");
}), icon: "project-diagram"
});
diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx
index c0f489cd8..c3e55d825 100644
--- a/src/client/views/collections/ParentDocumentSelector.tsx
+++ b/src/client/views/collections/ParentDocumentSelector.tsx
@@ -23,9 +23,9 @@ export class SelectorContextMenu extends React.Component<SelectorProps> {
async fetchDocuments() {
let aliases = (await SearchUtil.GetAliasesOfDocument(this.props.Document)).filter(doc => doc !== this.props.Document);
- const { docs } = await SearchUtil.Search(`data_l:"${this.props.Document[Id]}"`, true);
+ const { docs } = await SearchUtil.Search("", true, { fq: `data_l:"${this.props.Document[Id]}"` });
const map: Map<Doc, Doc> = new Map;
- const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search(`data_l:"${doc[Id]}"`, true).then(result => result.docs)));
+ const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search("", true, { fq: `data_l:"${doc[Id]}"` }).then(result => result.docs)));
allDocs.forEach((docs, index) => docs.forEach(doc => map.set(doc, aliases[index])));
docs.forEach(doc => map.delete(doc));
runInAction(() => {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index ccf261c95..00407d39a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -36,9 +36,9 @@
// linear-gradient(to bottom, $light-color-secondary 1px, transparent 1px);
// background-size: 30px 30px;
// }
- box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw;
+ opacity: 0.99;
border: 0px solid $light-color-secondary;
- border-radius: $border-radius;
+ border-radius: inherit;
box-sizing: border-box;
position: absolute;
@@ -52,46 +52,6 @@
height: 100%;
}
-
-.collectionfreeformview-overlay {
- .collectionfreeformview>.jsx-parser {
- position: inherit;
- height: 100%;
- }
-
- >.jsx-parser {
- position: absolute;
- z-index: 0;
- }
-
- .formattedTextBox-cont {
- background: $light-color-secondary;
- overflow: visible;
- }
-
- opacity: 0.99;
- border: 0px solid transparent;
- border-radius: $border-radius;
- box-sizing: border-box;
- position:absolute;
- z-index: -1;
-
- .marqueeView {
- overflow: hidden;
- }
-
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
-
- .collectionfreeformview {
- .formattedTextBox-cont {
- background: yellow;
- }
- }
-}
-
// selection border...?
.border {
border-style: solid;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index cb55a0d5d..703873681 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -4,14 +4,14 @@ 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";
import { HistoryUtil } from "../../../util/History";
import { SelectionManager } from "../../../util/SelectionManager";
import { Transform } from "../../../util/Transform";
-import { undoBatch } from "../../../util/UndoManager";
+import { undoBatch, UndoManager } from "../../../util/UndoManager";
import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss";
import { ContextMenu } from "../../ContextMenu";
import { InkingCanvas } from "../../InkingCanvas";
@@ -52,13 +52,22 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
private get _pwidth() { return this.props.PanelWidth(); }
private get _pheight() { return this.props.PanelHeight(); }
+ @computed get contentBounds() {
+ let bounds = this.props.fitToBox && !NumCast(this.nativeWidth) ? Doc.ComputeContentBounds(DocListCast(this.props.Document.data)) : undefined;
+ return {
+ panX: bounds ? (bounds.x + bounds.r) / 2 : this.Document.panX || 0,
+ panY: bounds ? (bounds.y + bounds.b) / 2 : this.Document.panY || 0,
+ scale: bounds ? Math.min(this.props.PanelHeight() / (bounds.b - bounds.y), this.props.PanelWidth() / (bounds.r - bounds.x)) : this.Document.scale || 1
+ };
+ }
+
@computed get nativeWidth() { return this.Document.nativeWidth || 0; }
@computed get nativeHeight() { return this.Document.nativeHeight || 0; }
- public get isAnnotationOverlay() { return this.props.fieldKey === "annotations" || this.props.fieldExt === "annotations"; }
+ public get isAnnotationOverlay() { return this.props.fieldExt ? true : false; } // fieldExt will be "" or "annotation". should maybe generalize this, or make it more specific (ie, 'annotation' instead of 'fieldExt')
private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; }
- private panX = () => this.props.fitToBox ? this.props.fitToBox()[0] : this.Document.panX || 0;
- private panY = () => this.props.fitToBox ? this.props.fitToBox()[1] : this.Document.panY || 0;
- private zoomScaling = () => this.props.fitToBox ? this.props.fitToBox()[2] : this.Document.scale || 1;
+ private panX = () => this.contentBounds.panX;
+ private panY = () => this.contentBounds.panY;
+ private zoomScaling = () => this.contentBounds.scale;
private centeringShiftX = () => !this.nativeWidth ? this._pwidth / 2 : 0; // shift so pan position is at center of window for non-overlay collections
private centeringShiftY = () => !this.nativeHeight ? this._pheight / 2 : 0;// shift so pan position is at center of window for non-overlay collections
private getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth + 1, -this.borderWidth + 1).translate(-this.centeringShiftX(), -this.centeringShiftY()).transform(this.getLocalTransform());
@@ -86,6 +95,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
});
}
+ @computed get fieldExtensionDoc() {
+ return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, "true");
+ }
+
+
@undoBatch
@action
drop = (e: Event, de: DragManager.DropEvent) => {
@@ -93,10 +107,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
if (de.data instanceof DragManager.DocumentDragData) {
if (de.data.droppedDocuments.length) {
let dragDoc = de.data.droppedDocuments[0];
- let zoom = NumCast(dragDoc.zoomBasis, 1);
let [xp, yp] = this.getTransform().transformPoint(de.x, de.y);
- let x = xp - de.data.xOffset / zoom;
- let y = yp - de.data.yOffset / zoom;
+ let x = xp - de.data.xOffset;
+ let y = yp - de.data.yOffset;
let dropX = NumCast(de.data.droppedDocuments[0].x);
let dropY = NumCast(de.data.droppedDocuments[0].y);
de.data.droppedDocuments.forEach(d => {
@@ -117,10 +130,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
else if (de.data instanceof DragManager.AnnotationDragData) {
if (de.data.dropDocument) {
let dragDoc = de.data.dropDocument;
- let zoom = NumCast(dragDoc.zoomBasis, 1);
let [xp, yp] = this.getTransform().transformPoint(de.x, de.y);
- let x = xp - de.data.xOffset / zoom;
- let y = yp - de.data.yOffset / zoom;
+ let x = xp - de.data.xOffset;
+ let y = yp - de.data.yOffset;
let dropX = NumCast(de.data.dropDocument.x);
let dropY = NumCast(de.data.dropDocument.y);
dragDoc.x = x + NumCast(dragDoc.x) - dropX;
@@ -159,18 +171,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
if (!this.isAnnotationOverlay) {
PDFMenu.Instance.fadeOut(true);
let minx = docs.length ? NumCast(docs[0].x) : 0;
- let maxx = docs.length ? NumCast(docs[0].width) / NumCast(docs[0].zoomBasis, 1) + minx : minx;
+ let maxx = docs.length ? NumCast(docs[0].width) + minx : minx;
let miny = docs.length ? NumCast(docs[0].y) : 0;
- let maxy = docs.length ? NumCast(docs[0].height) / NumCast(docs[0].zoomBasis, 1) + miny : miny;
+ let maxy = docs.length ? NumCast(docs[0].height) + miny : miny;
let ranges = docs.filter(doc => doc).reduce((range, doc) => {
let x = NumCast(doc.x);
- let xe = x + NumCast(doc.width) / NumCast(doc.zoomBasis, 1);
+ let xe = x + NumCast(doc.width);
let y = NumCast(doc.y);
- let ye = y + NumCast(doc.height) / NumCast(doc.zoomBasis, 1);
+ let ye = y + NumCast(doc.height);
return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]],
[range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]];
}, [[minx, maxx], [miny, maxy]]);
- let ink = Cast(this.extensionDoc.ink, InkField);
+ let ink = Cast(this.fieldExtensionDoc.ink, InkField);
if (ink && ink.inkData) {
ink.inkData.forEach((value: StrokeData, key: string) => {
let bounds = InkingCanvas.StrokeRect(value);
@@ -251,11 +263,11 @@ 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) {
- this.props.Document.scrollY = panY;
+ this.props.Document.scrollY = panY - scale * this.props.Document[HeightSym]();
}
}
@@ -283,6 +295,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
const panY = this.Document.panY;
const id = this.Document[Id];
const state = HistoryUtil.getState();
+ state.initializers = state.initializers || {};
// TODO This technically isn't correct if type !== "doc", as
// currently nothing is done, but we should probably push a new state
if (state.type === "doc" && panX !== undefined && panY !== undefined) {
@@ -299,10 +312,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
}
SelectionManager.DeselectAll();
- const newPanX = NumCast(doc.x) + NumCast(doc.width) / NumCast(doc.zoomBasis, 1) / 2;
- const newPanY = NumCast(doc.y) + NumCast(doc.height) / NumCast(doc.zoomBasis, 1) / 2;
+ const newPanX = NumCast(doc.x) + NumCast(doc.width) / 2;
+ const newPanY = NumCast(doc.y) + NumCast(doc.height) / 2;
const newState = HistoryUtil.getState();
- newState.initializers[id] = { panX: newPanX, panY: newPanY };
+ (newState.initializers || (newState.initializers = {}))[id] = { panX: newPanX, panY: newPanY };
HistoryUtil.pushState(newState);
this.setPan(newPanX, newPanY);
@@ -343,7 +356,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
getChildDocumentViewProps(childDocLayout: Doc): DocumentViewProps {
- let resolvedDataDoc = this.props.DataDoc !== this.props.Document ? this.props.DataDoc : undefined;
+ let self = this;
+ let resolvedDataDoc = !this.props.Document.isTemplate && this.props.DataDoc !== this.props.Document ? this.props.DataDoc : undefined;
let layoutDoc = Doc.expandTemplateLayout(childDocLayout, resolvedDataDoc);
return {
DataDoc: resolvedDataDoc !== layoutDoc && resolvedDataDoc ? resolvedDataDoc : undefined,
@@ -415,7 +429,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
let docviews = docs.reduce((prev, doc) => {
if (!(doc instanceof Doc)) return prev;
var page = NumCast(doc.page, -1);
- if (Math.round(page) === Math.round(curPage) || page === -1) {
+ if ((Math.abs(Math.round(page) - Math.round(curPage)) < 3) || page === -1) {
let minim = BoolCast(doc.isMinimized, false);
if (minim === undefined || !minim) {
const pos = script ? this.getCalculatedPositions(script, { doc, index: prev.length, collection: this.Document, docs, state }) : {};
@@ -441,34 +455,37 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
description: "Arrange contents in grid",
event: async () => {
const docs = await DocListCastAsync(this.Document[this.props.fieldKey]);
- if (docs) {
- let startX = this.Document.panX || 0;
- let x = startX;
- let y = this.Document.panY || 0;
- let i = 0;
- const width = Math.max(...docs.map(doc => NumCast(doc.width)));
- const height = Math.max(...docs.map(doc => NumCast(doc.height)));
- for (const doc of docs) {
- doc.x = x;
- doc.y = y;
- x += width + 20;
- if (++i === 6) {
- i = 0;
- x = startX;
- y += height + 20;
+ UndoManager.RunInBatch(() => {
+ if (docs) {
+ let startX = this.Document.panX || 0;
+ let x = startX;
+ let y = this.Document.panY || 0;
+ let i = 0;
+ const width = Math.max(...docs.map(doc => NumCast(doc.width)));
+ const height = Math.max(...docs.map(doc => NumCast(doc.height)));
+ for (const doc of docs) {
+ doc.x = x;
+ doc.y = y;
+ x += width + 20;
+ if (++i === 6) {
+ i = 0;
+ x = startX;
+ y += height + 20;
+ }
}
}
- }
+ }, "arrange contents");
}
});
ContextMenu.Instance.addItem({
description: "Add freeform arrangement",
event: () => {
let addOverlay = (key: "arrangeScript" | "arrangeInit", options: OverlayElementOptions, params?: Record<string, string>, requiredType?: string) => {
- let overlayDisposer: () => void;
+ let overlayDisposer: () => void = emptyFunction;
const script = this.Document[key];
let originalText: string | undefined = undefined;
if (script) originalText = script.script.originalScript;
+ // tslint:disable-next-line: no-unnecessary-callback-wrapper
let scriptingBox = <ScriptBox initialText={originalText} onCancel={() => overlayDisposer()} onSave={(text, onError) => {
const script = CompileScript(text, {
params,
@@ -485,10 +502,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}");
}
});
}
@@ -498,22 +515,19 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
...this.views
]
render() {
- const containerName = `collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`;
const easing = () => this.props.Document.panTransformType === "Ease";
- if (this.props.fieldExt) Doc.UpdateDocumentExtensionForField(this.extensionDoc, this.props.fieldKey);
+ Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey);
return (
- <div className={containerName} ref={this.createDropTarget} onWheel={this.onPointerWheel}
- style={{ borderRadius: "inherit" }}
+ <div className={"collectionfreeformview-container"} ref={this.createDropTarget} onWheel={this.onPointerWheel}
onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} onContextMenu={this.onContextMenu}>
<MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} isSelected={this.props.isSelected}
addDocument={this.addDocument} removeDocument={this.props.removeDocument} addLiveTextDocument={this.addLiveTextBox}
getContainerTransform={this.getContainerTransform} getTransform={this.getTransform}>
<CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY}
easing={easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
-
<CollectionFreeFormLinksView {...this.props} key="freeformLinks">
- <InkingCanvas getScreenTransform={this.getTransform} Document={this.extensionDoc} inkFieldKey={this.props.fieldExt ? "ink" : this.props.fieldKey + "_ink"} >
+ <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} AnnotationDocument={this.fieldExtensionDoc} inkFieldKey={"ink"} >
{this.childViews}
</InkingCanvas>
</CollectionFreeFormLinksView>
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 8a619bfae..b765517a2 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -76,7 +76,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
}
ns.map(line => {
let indent = line.search(/\S|$/);
- let newBox = Docs.TextDocument({ width: 200, height: 35, x: x + indent / 3 * 10, y: y, documentText: "@@@" + line, title: line });
+ let newBox = Docs.Create.TextDocument({ width: 200, height: 35, x: x + indent / 3 * 10, y: y, documentText: "@@@" + line, title: line });
this.props.addDocument(newBox, false);
y += 40 * this.props.getTransform().Scale;
});
@@ -86,13 +86,13 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
navigator.clipboard.readText().then(text => {
let ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== "");
if (ns.length === 1 && text.startsWith("http")) {
- this.props.addDocument(Docs.ImageDocument(text, { nativeWidth: 300, width: 300, x: x, y: y }), false);// paste an image from its URL in the paste buffer
+ this.props.addDocument(Docs.Create.ImageDocument(text, { nativeWidth: 300, width: 300, x: x, y: y }), false);// paste an image from its URL in the paste buffer
} else {
this.pasteTable(ns, x, y);
}
});
} else if (!e.ctrlKey) {
- let newBox = Docs.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" });
+ let newBox = Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" });
newBox.proto!.autoHeight = true;
this.props.addLiveTextDocument(newBox);
}
@@ -134,7 +134,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
doc.width = 200;
docList.push(doc);
}
- let newCol = Docs.SchemaDocument([...(groupAttr ? ["_group"] : []), ...columns.filter(c => c)], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 });
+ let newCol = Docs.Create.SchemaDocument([...(groupAttr ? ["_group"] : []), ...columns.filter(c => c)], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 });
this.props.addDocument(newCol, false);
}
@@ -147,7 +147,6 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
PreviewCursor.Visible = false;
this.cleanupInteractions(true);
if (e.button === 2 || (e.button === 0 && e.altKey)) {
- if (!this.props.container.props.active()) this.props.selectDocuments([this.props.container.props.Document]);
document.addEventListener("pointermove", this.onPointerMove, true);
document.addEventListener("pointerup", this.onPointerUp, true);
document.addEventListener("keydown", this.marqueeCommand, true);
@@ -181,6 +180,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
@action
onPointerUp = (e: PointerEvent): void => {
+ if (!this.props.container.props.active()) this.props.selectDocuments([this.props.container.props.Document]);
// console.log("pointer up!");
if (this._visible) {
// console.log("visible");
@@ -271,7 +271,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
});
}
let inkData = this.ink ? this.ink.inkData : undefined;
- let newCollection = Docs.FreeformDocument(selected, {
+ let newCollection = Docs.Create.FreeformDocument(selected, {
x: bounds.left,
y: bounds.top,
panX: 0,
@@ -292,14 +292,14 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
d.page = -1;
return d;
});
- let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
+ let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
newCollection.proto!.summaryDoc = summary;
selected = [newCollection];
newCollection.x = bounds.left + bounds.width;
summary.proto!.subBulletDocs = new List<Doc>(selected);
//summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight"
summary.templates = new List<string>([Templates.Bullet.Layout]);
- let container = Docs.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, title: "-summary-" });
+ let container = Docs.Create.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, title: "-summary-" });
container.viewType = CollectionViewType.Stacking;
this.props.addLiveTextDocument(container);
// });
@@ -311,7 +311,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
d.page = -1;
return d;
});
- let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
+ let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
newCollection.proto!.summaryDoc = summary;
selected = [newCollection];
newCollection.x = bounds.left + bounds.width;
diff --git a/src/client/views/document_templates/caption_toggle/DetailedCaptionToggle.tsx b/src/client/views/document_templates/caption_toggle/DetailedCaptionToggle.tsx
new file mode 100644
index 000000000..f8104cef3
--- /dev/null
+++ b/src/client/views/document_templates/caption_toggle/DetailedCaptionToggle.tsx
@@ -0,0 +1,72 @@
+import * as React from 'react';
+import { FontWeightProperty, FontStyleProperty, FontSizeProperty, ColorProperty } from 'csstype';
+import { observer } from 'mobx-react';
+import { observable, action, runInAction } from 'mobx';
+import { FormattedTextBox, FormattedTextBoxProps } from '../../nodes/FormattedTextBox';
+import { FieldViewProps } from '../../nodes/FieldView';
+
+interface DetailedCaptionDataProps {
+ captionFieldKey?: string;
+ detailsFieldKey?: string;
+}
+
+interface DetailedCaptionStylingProps {
+ sharedFontColor?: ColorProperty;
+ captionFontStyle?: FontStyleProperty;
+ detailsFontStyle?: FontStyleProperty;
+ toggleSize?: number;
+}
+
+@observer
+export default class DetailedCaptionToggle extends React.Component<DetailedCaptionDataProps & DetailedCaptionStylingProps & FieldViewProps> {
+ @observable loaded: boolean = false;
+ @observable detailsExpanded: boolean = false;
+
+ @action toggleDetails = (e: React.MouseEvent<HTMLDivElement>) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.detailsExpanded = !this.detailsExpanded;
+ }
+
+ componentDidMount() {
+ runInAction(() => this.loaded = true);
+ }
+
+ render() {
+ let size = this.props.toggleSize || 20;
+ return (
+ <div style={{
+ transition: "0.5s opacity ease",
+ opacity: this.loaded ? 1 : 0,
+ bottom: 0,
+ fontSize: 14,
+ width: "100%",
+ position: "absolute"
+ }}>
+ {/* caption */}
+ <div style={{ opacity: this.detailsExpanded ? 0 : 1, transition: "opacity 0.3s ease" }}>
+ <FormattedTextBox {...this.props} fieldKey={this.props.captionFieldKey || "caption"} />
+ </div>
+ {/* details */}
+ <div style={{ opacity: this.detailsExpanded ? 1 : 0, transition: "opacity 0.3s ease" }}>
+ <FormattedTextBox {...this.props} fieldKey={this.props.detailsFieldKey || "captiondetails"} />
+ </div>
+ {/* toggle */}
+ <div
+ style={{
+ width: size,
+ height: size,
+ borderRadius: "50%",
+ backgroundColor: "red",
+ zIndex: 3,
+ cursor: "pointer"
+ }}
+ onClick={this.toggleDetails}
+ >
+ <span style={{ color: "white" }}></span>
+ </div>
+ </div>
+ );
+ }
+
+}
diff --git a/src/client/views/document_templates/image_card/ImageCard.tsx b/src/client/views/document_templates/image_card/ImageCard.tsx
new file mode 100644
index 000000000..9931515f3
--- /dev/null
+++ b/src/client/views/document_templates/image_card/ImageCard.tsx
@@ -0,0 +1,18 @@
+import * as React from 'react';
+import { DocComponent } from '../../DocComponent';
+import { FieldViewProps } from '../../nodes/FieldView';
+import { createSchema, makeInterface } from '../../../../new_fields/Schema';
+import { createInterface } from 'readline';
+import { ImageBox } from '../../nodes/ImageBox';
+
+export default class ImageCard extends React.Component<FieldViewProps> {
+
+ render() {
+ return (
+ <div style={{ padding: 30, borderRadius: 15 }}>
+ <ImageBox {...this.props} />
+ </div>
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/globalCssVariables.scss b/src/client/views/globalCssVariables.scss
index fec105516..6dffee586 100644
--- a/src/client/views/globalCssVariables.scss
+++ b/src/client/views/globalCssVariables.scss
@@ -13,23 +13,26 @@ $darker-alt-accent: rgb(178, 206, 248);
$intermediate-color: #9c9396;
$dark-color: #121721;
// fonts
-$sans-serif: "Noto Sans", sans-serif;
+$sans-serif: "Noto Sans",
+sans-serif;
// $sans-serif: "Roboto Slab", sans-serif;
-$serif: "Crimson Text", serif;
+$serif: "Crimson Text",
+serif;
// misc values
$border-radius: 0.3em;
//
$search-thumnail-size: 175;
- // dragged items
-$contextMenu-zindex: 1000; // context menu shows up over everything
+// dragged items
+$contextMenu-zindex: 100000; // context menu shows up over everything
$mainTextInput-zindex: 999; // then text input overlay so that it's context menu will appear over decorations, etc
$docDecorations-zindex: 998; // then doc decorations appear over everything else
$remoteCursors-zindex: 997; // ... not sure what level the remote cursors should go -- is this right?
$COLLECTION_BORDER_WIDTH: 1;
$MINIMIZED_ICON_SIZE:25;
$MAX_ROW_HEIGHT: 44px;
-:export {
+
+:export {
contextMenuZindex: $contextMenu-zindex;
COLLECTION_BORDER_WIDTH: $COLLECTION_BORDER_WIDTH;
MINIMIZED_ICON_SIZE: $MINIMIZED_ICON_SIZE;
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss
index 704cdc31c..972966204 100644
--- a/src/client/views/nodes/AudioBox.scss
+++ b/src/client/views/nodes/AudioBox.scss
@@ -1,4 +1,6 @@
.audiobox-cont{
- height: 100%;
+ top:0;
+ max-height: 32px;
+ position: absolute;
width: 100%;
} \ No newline at end of file
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index be12dced3..be6ae630f 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -16,12 +16,10 @@ export class AudioBox extends React.Component<FieldViewProps> {
let path = field.url.href;
return (
- <div>
- <audio controls className="audiobox-cont">
- <source src={path} type="audio/mpeg" />
- Not supported.
+ <audio controls className="audiobox-cont" style={{ pointerEvents: "all" }}>
+ <source src={path} type="audio/mpeg" />
+ Not supported.
</audio>
- </div>
);
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index eb786d537..ed6b224a7 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -89,34 +89,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
return new List<string>();
}
@computed get finalLayout() {
- const baseLayout = this.props.layoutKey === "overlayLayout" ? "<div/>" : this.layout;
- let base = baseLayout;
- let layout = baseLayout;
-
- // bcz: templates are intended only for a document's primary layout or overlay (not background). However,
- // a DocumentContentsView is used to render annotation overlays, so we detect that here
- // by checking the layoutKey. This should probably be moved into
- // a prop so that the overlay can explicitly turn off templates.
- if ((this.props.layoutKey === "overlayLayout" && StrCast(this.props.Document.layout).indexOf("CollectionView") !== -1) ||
- (this.props.layoutKey === "layout" && StrCast(this.props.Document.layout).indexOf("CollectionView") === -1) ||
- (this.props.layoutKey === "layout" && NumCast(this.props.Document.viewType)) !== CollectionViewType.Freeform) {
- this.templates.forEach(template => {
- let self = this;
- // this scales constants in the markup by the scaling applied to the document, but caps the constants to be smaller
- // than the width/height of the containing document
- function convertConstantsToNative(match: string, offset: number, x: string) {
- let px = Number(match.replace("px", ""));
- return `${Math.min(NumCast(self.props.Document.height, 0),
- Math.min(NumCast(self.props.Document.width, 0),
- px * self.props.ScreenToLocalTransform().Scale))}px`;
- }
- // let nativizedTemplate = template.replace(/([0-9]+)px/g, convertConstantsToNative);
- // layout = nativizedTemplate.replace("{layout}", base);
- layout = template.replace("{layout}", base);
- base = layout;
- });
- }
- return layout;
+ return this.props.layoutKey === "overlayLayout" ? "<div/>" : this.layout;
}
render() {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 430409ee3..907ba3713 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -8,9 +8,9 @@ import { ObjectField } from "../../../new_fields/ObjectField";
import { createSchema, makeInterface, listSpec } from "../../../new_fields/Schema";
import { BoolCast, Cast, FieldValue, StrCast, NumCast, PromiseValue } from "../../../new_fields/Types";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
-import { emptyFunction, Utils } from "../../../Utils";
+import { emptyFunction, Utils, returnFalse, returnTrue } from "../../../Utils";
import { DocServer } from "../../DocServer";
-import { Docs, DocUtils } from "../../documents/Documents";
+import { Docs, DocUtils, DocumentType } from "../../documents/Documents";
import { DocumentManager } from "../../util/DocumentManager";
import { DragManager, dropActionType } from "../../util/DragManager";
import { SearchUtil } from "../../util/SearchUtil";
@@ -34,6 +34,10 @@ import { ContextMenuProps } from '../ContextMenuItem';
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);
@@ -72,12 +76,13 @@ export interface DocumentViewProps {
ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
Document: Doc;
DataDoc?: Doc;
- fitToBox?: () => number[];
+ fitToBox?: boolean;
addDocument?: (doc: Doc, allowDuplicates?: boolean) => boolean;
removeDocument?: (doc: Doc) => boolean;
moveDocument?: (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
ScreenToLocalTransform: () => Transform;
renderDepth: number;
+ showOverlays?: (doc: Doc) => { title?: string, caption?: string };
ContentScaling: () => number;
PanelWidth: () => number;
PanelHeight: () => number;
@@ -283,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;
@@ -291,7 +297,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
fullScreenAlias.templates = new List<string>();
this.props.addDocTab(fullScreenAlias, this.dataDoc, "inTab");
SelectionManager.DeselectAll();
- this.props.Document.libraryBrush = false;
+ this.props.Document.libraryBrush = undefined;
}
else if (CurrentUserUtils.MainDocId !== this.props.Document[Id] &&
(Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD &&
@@ -342,9 +348,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
let first = linkedDocs.filter(d => Doc.AreProtosEqual(d.anchor1 as Doc, this.props.Document));
let linkedFwdDocs = first.length ? [first[0].anchor2 as Doc, first[0].anchor1 as Doc] : [expandedDocs[0], expandedDocs[0]];
- let linkedFwdContextDocs = [first.length ? await (first[0].context) as Doc : undefined, undefined];
+ // @TODO: shouldn't always follow target context
+ let linkedFwdContextDocs = [first.length ? await (first[0].targetContext) as Doc : undefined, undefined];
- let linkedFwdPage = [first.length ? NumCast(first[0].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");
@@ -373,9 +380,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
onPointerMove = (e: PointerEvent): void => {
if (!e.cancelBubble && this.active) {
if (!this.props.Document.excludeFromLibrary && (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3)) {
- document.removeEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
if (!e.altKey && !this.topMost && e.buttons === 1 && !BoolCast(this.props.Document.lockedPosition)) {
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined, this._hitExpander);
}
}
@@ -394,7 +401,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
deleteClicked = (): void => { SelectionManager.DeselectAll(); this.props.removeDocument && this.props.removeDocument(this.props.Document); }
@undoBatch
- fieldsClicked = (): void => { let kvp = Docs.KVPDocument(this.props.Document, { width: 300, height: 300 }); this.props.addDocTab(kvp, this.dataDoc, "onRight"); }
+ fieldsClicked = (): void => { let kvp = Docs.Create.KVPDocument(this.props.Document, { width: 300, height: 300 }); this.props.addDocTab(kvp, this.dataDoc, "onRight"); }
@undoBatch
makeBtnClicked = (): void => {
@@ -419,6 +426,22 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@undoBatch
@action
drop = async (e: Event, de: DragManager.DropEvent) => {
+ if (de.data instanceof DragManager.AnnotationDragData) {
+ e.stopPropagation();
+ let annotationDoc = de.data.annotationDocument;
+ annotationDoc.linkedToDoc = true;
+ let targetDoc = this.props.Document;
+ let annotations = await DocListCastAsync(annotationDoc.annotations);
+ if (annotations) {
+ annotations.forEach(anno => {
+ anno.target = targetDoc;
+ });
+ }
+ let pdfDoc = await Cast(annotationDoc.pdfDoc, Doc);
+ if (pdfDoc) {
+ DocUtils.MakeLink(annotationDoc, targetDoc, undefined, `Annotation from ${StrCast(pdfDoc.title)}`, "", StrCast(pdfDoc.title));
+ }
+ }
if (de.data instanceof DragManager.LinkDragData) {
let sourceDoc = de.data.linkSourceDocument;
let destDoc = this.props.Document;
@@ -436,8 +459,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
else {
// const docs = await SearchUtil.Search(`data_l:"${destDoc[Id]}"`, true);
// const views = docs.map(d => DocumentManager.Instance.getDocumentView(d)).filter(d => d).map(d => d as DocumentView);
- DocUtils.MakeLink(sourceDoc, destDoc, this.props.ContainingCollectionView ? this.props.ContainingCollectionView.props.Document : undefined);
+ let linkDoc = DocUtils.MakeLink(sourceDoc, destDoc, this.props.ContainingCollectionView ? this.props.ContainingCollectionView.props.Document : undefined);
de.data.droppedDocuments.push(destDoc);
+ de.data.linkDocument = linkDoc;
}
}
}
@@ -520,45 +544,56 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
cm.addItem({ description: this.props.Document.isButton ? "Remove Button" : "Make Button", event: this.makeBtnClicked, icon: "concierge-bell" });
cm.addItem({
description: "Make Portal", event: () => {
- let portal = Docs.FreeformDocument([], { width: this.props.Document[WidthSym]() + 10, height: this.props.Document[HeightSym](), title: this.props.Document.title + ".portal" });
+ let portal = Docs.Create.FreeformDocument([], { width: this.props.Document[WidthSym]() + 10, height: this.props.Document[HeightSym](), title: this.props.Document.title + ".portal" });
Doc.GetProto(this.props.Document).subBulletDocs = new List<Doc>([portal]);
//summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight"
Doc.GetProto(this.props.Document).templates = new List<string>([Templates.Bullet.Layout]);
- let coll = Docs.StackingDocument([this.props.Document, portal], { x: NumCast(this.props.Document.x), y: NumCast(this.props.Document.y), width: this.props.Document[WidthSym]() + 10, height: this.props.Document[HeightSym](), title: this.props.Document.title + ".cont" });
+ let coll = Docs.Create.StackingDocument([this.props.Document, portal], { x: NumCast(this.props.Document.x), y: NumCast(this.props.Document.y), width: this.props.Document[WidthSym]() + 10, height: this.props.Document[HeightSym](), title: this.props.Document.title + ".cont" });
this.props.addDocument && this.props.addDocument(coll);
this.props.removeDocument && this.props.removeDocument(this.props.Document);
}, icon: "window-restore"
- })
+ });
cm.addItem({
description: "Find aliases", event: async () => {
const aliases = await SearchUtil.GetAliasesOfDocument(this.props.Document);
- this.props.addDocTab && this.props.addDocTab(Docs.SchemaDocument(["title"], aliases, {}), undefined, "onRight"); // bcz: dataDoc?
+ 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: "Copy URL", event: () => Utils.CopyText(DocServer.prepend("/doc/" + this.props.Document[Id])), icon: "link" });
+ cm.addItem({ description: "Zoom to Document", event: () => this.props.focus(this.props.Document, true), icon: "search" });
+ 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 };
- const users: User[] = JSON.parse(await rp.get(DocServer.prepend(RouteStore.getUsers)));
- let usersMenu: ContextMenuProps[] = users.filter(({ email }) => email !== CurrentUserUtils.email).map(({ email, userDocumentId }) => ({
- description: email, event: async () => {
- const userDocument = await Cast(DocServer.GetRefField(userDocumentId), Doc);
- if (!userDocument) {
- throw new Error(`Couldn't get user document of user ${email}`);
- }
- const notifDoc = await Cast(userDocument.optionalRightCollection, Doc);
- if (notifDoc instanceof Doc) {
- const data = await Cast(notifDoc.data, listSpec(Doc));
- const sharedDoc = Doc.MakeAlias(this.props.Document);
- if (data) {
- data.push(sharedDoc);
- } else {
- notifDoc.data = new List([sharedDoc]);
+ let usersMenu: ContextMenuProps[] = [];
+ try {
+ 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 () => {
+ const userDocument = await Cast(DocServer.GetRefField(userDocumentId), Doc);
+ if (!userDocument) {
+ throw new Error(`Couldn't get user document of user ${email}`);
+ }
+ const notifDoc = await Cast(userDocument.optionalRightCollection, Doc);
+ if (notifDoc instanceof Doc) {
+ const data = await Cast(notifDoc.data, listSpec(Doc));
+ const sharedDoc = Doc.MakeAlias(this.props.Document);
+ if (data) {
+ data.push(sharedDoc);
+ } else {
+ notifDoc.data = new List([sharedDoc]);
+ }
}
}
- }
- }));
+ }));
+ } catch {
+
+ }
runInAction(() => {
cm.addItem({ description: "Share...", subitems: usersMenu, icon: "share" });
if (!this.topMost) {
@@ -573,7 +608,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
onPointerEnter = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = true; };
- onPointerLeave = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = false; };
+ onPointerLeave = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = undefined; };
isSelected = () => SelectionManager.IsSelected(this);
@action select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); };
@@ -593,6 +628,17 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
let foregroundColor = StrCast(this.props.Document.layout instanceof Doc ? this.props.Document.layout.color : this.props.Document.color);
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 templates = Cast(this.props.Document.templates, listSpec("string"));
+ if (templates instanceof List) {
+ templates.map(str => {
+ if (str.indexOf("{props.Document.title}") !== -1) showTitle = "title";
+ if (str.indexOf("fieldKey={\"caption\"}") !== -1) showCaption = "caption";
+ });
+ }
+ let showTextTitle = showTitle && StrCast(this.props.Document.layout).startsWith("<FormattedTextBox") || (this.props.Document.layout instanceof Doc && StrCast(this.props.Document.layout.layout).startsWith("<FormattedTextBox")) ? showTitle : undefined;
return (
<div className={`documentView-node${this.topMost ? "-topmost" : ""}`}
ref={this._mainCont}
@@ -614,7 +660,35 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}
>
- {this.contents}
+ {!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% - 25px)" : "100%", display: "inline-block", position: "absolute", top: showTextTitle ? "25px" : undefined }}>
+ {this.contents}
+ </div>
+ {!showTitle ? (null) :
+ <div style={{
+ position: showTextTitle ? "relative" : "absolute", top: 0, 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()})`
+ }}>
+ <EditableView
+ contents={this.props.Document[showTitle]}
+ display={"block"}
+ height={72}
+ GetValue={() => StrCast(this.props.Document[showTitle])}
+ SetValue={(value: string) => (Doc.GetProto(this.props.Document)[showTitle] = value) ? true : true}
+ />
+ </div>
+ }
+ {!showCaption ? (null) :
+ <div style={{ position: "absolute", bottom: 0, transformOrigin: "bottom left", width: `${100 * this.props.ContentScaling()}%`, transform: `scale(${1 / this.props.ContentScaling()})` }}>
+ <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>
+ }
</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/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index c5fc6c65a..ea6730cd0 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -18,9 +18,6 @@ import { ImageBox } from "./ImageBox";
import { PDFBox } from "./PDFBox";
import { VideoBox } from "./VideoBox";
import { Id } from "../../../new_fields/FieldSymbols";
-import { BoolCast, Cast } from "../../../new_fields/Types";
-import { DarpaDatasetDoc } from "../../northstar/model/idea/idea";
-
//
// these properties get assigned through the render() method of the DocumentView when it creates this node.
@@ -31,7 +28,7 @@ export interface FieldViewProps {
fieldKey: string;
fieldExt: string;
leaveNativeSize?: boolean;
- fitToBox?: () => number[];
+ fitToBox?: boolean;
ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
Document: Doc;
DataDoc?: Doc;
@@ -55,8 +52,8 @@ export interface FieldViewProps {
@observer
export class FieldView extends React.Component<FieldViewProps> {
- public static LayoutString(fieldType: { name: string }, fieldStr: string = "data") {
- return `<${fieldType.name} {...props} fieldKey={"${fieldStr}"} />`;
+ public static LayoutString(fieldType: { name: string }, fieldStr: string = "data", fieldExt: string = "") {
+ return `<${fieldType.name} {...props} fieldKey={"${fieldStr}"} fieldExt={"${fieldExt}"} />`;
}
@computed
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 8db7a9327..524500f35 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -1,6 +1,6 @@
import { library } from '@fortawesome/fontawesome-svg-core';
import { faEdit, faSmile } from '@fortawesome/free-solid-svg-icons';
-import { action, IReactionDisposer, observable, reaction, runInAction, computed, Lambda } from "mobx";
+import { action, IReactionDisposer, observable, reaction, runInAction, computed, Lambda, trace } from "mobx";
import { observer } from "mobx-react";
import { baseKeymap } from "prosemirror-commands";
import { history } from "prosemirror-history";
@@ -10,11 +10,11 @@ import { Node as ProsNode } from "prosemirror-model";
import { EditorState, Plugin, Transaction } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { Doc, Opt } from "../../../new_fields/Doc";
-import { Id } from '../../../new_fields/FieldSymbols';
+import { Id, Copy } from '../../../new_fields/FieldSymbols';
import { List } from '../../../new_fields/List';
import { RichTextField } from "../../../new_fields/RichTextField";
import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema";
-import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { BoolCast, Cast, NumCast, StrCast, DateCast } from "../../../new_fields/Types";
import { DocServer } from "../../DocServer";
import { Docs } from '../../documents/Documents';
import { DocumentManager } from '../../util/DocumentManager';
@@ -35,6 +35,9 @@ import { FieldView, FieldViewProps } from "./FieldView";
import "./FormattedTextBox.scss";
import React = require("react");
import { For } from 'babel-types';
+import { DateField } from '../../../new_fields/DateField';
+import { thisExpression } from 'babel-types';
+import { Utils } from '../../../Utils';
library.add(faEdit);
library.add(faSmile);
@@ -73,6 +76,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
private _linkClicked = "";
private _reactionDisposer: Opt<IReactionDisposer>;
private _searchReactionDisposer?: Lambda;
+ private _textReactionDisposer: Opt<IReactionDisposer>;
private _proxyReactionDisposer: Opt<IReactionDisposer>;
private dropDisposer?: DragManager.DragDropDisposer;
public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
@@ -133,7 +137,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
- @computed get dataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; }
+ @computed get extensionDoc() { return Doc.resolvedFieldDataDoc(this.dataDoc, this.props.fieldKey, "dummy"); }
+
+ @computed get dataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
dispatchTransaction = (tx: Transaction) => {
if (this._editorView) {
@@ -149,13 +155,15 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON()));
Doc.GetProto(this.dataDoc)[this.props.fieldKey + "_text"] = state.doc.textBetween(0, state.doc.content.size, "\n\n");
}
+ if (this.extensionDoc) this.extensionDoc.text = state.doc.textBetween(0, state.doc.content.size, "\n\n");
+ if (this.extensionDoc) this.extensionDoc.lastModified = new DateField(new Date(Date.now()));
+ this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON()));
this._applyingChange = false;
let title = StrCast(this.dataDoc.title);
if (title && title.startsWith("-") && this._editorView) {
let str = this._editorView.state.doc.textContent;
let titlestr = str.substr(0, Math.min(40, str.length));
- let target = this.dataDoc.proto ? this.dataDoc.proto : this.dataDoc;
- target.title = "-" + titlestr + (str.length > 40 ? "..." : "");
+ this.dataDoc.title = "-" + titlestr + (str.length > 40 ? "..." : "");
}
}
}
@@ -289,10 +297,22 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
const field = this.dataDoc ? Cast(this.dataDoc[this.props.fieldKey], RichTextField) : undefined;
return field ? field.Data : `{"doc":{"type":"doc","content":[]},"selection":{"type":"text","anchor":0,"head":0}}`;
},
- field => this._editorView && !this._applyingChange &&
- this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field)))
+ field2 => {
+ this._editorView && !this._applyingChange &&
+ this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field2)));
+ }
);
+ this._textReactionDisposer = reaction(
+ () => this.extensionDoc,
+ () => {
+ if (this.dataDoc.text || this.dataDoc.lastModified) {
+ this.extensionDoc.text = this.dataDoc.text;
+ this.extensionDoc.lastModified = DateCast(this.dataDoc.lastModified)[Copy]();
+ this.dataDoc.text = undefined;
+ this.dataDoc.lastModified = undefined;
+ }
+ }, { fireImmediately: true });
this.setupEditor(config, this.dataDoc, this.props.fieldKey);
this._searchReactionDisposer = reaction(() => {
@@ -347,15 +367,10 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
componentWillUnmount() {
- if (this._editorView) {
- this._editorView.destroy();
- }
- if (this._reactionDisposer) {
- this._reactionDisposer();
- }
- if (this._proxyReactionDisposer) {
- this._proxyReactionDisposer();
- }
+ this._editorView && this._editorView.destroy();
+ this._reactionDisposer && this._reactionDisposer();
+ this._proxyReactionDisposer && this._proxyReactionDisposer();
+ this._textReactionDisposer && this._textReactionDisposer();
}
onPointerDown = (e: React.PointerEvent): void => {
@@ -368,21 +383,39 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
let ctrlKey = e.ctrlKey;
if (e.button === 0 && ((!this.props.isSelected() && !e.ctrlKey) || (this.props.isSelected() && e.ctrlKey)) && !e.metaKey && e.target) {
let href = (e.target as any).href;
+ let location: string;
+ if ((e.target as any).attributes.location) {
+ location = (e.target as any).attributes.location.value;
+ }
for (let parent = (e.target as any).parentNode; !href && parent; parent = parent.parentNode) {
href = parent.childNodes[0].href ? parent.childNodes[0].href : parent.href;
}
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(f => {
- (f instanceof Doc) && DocumentManager.Instance.jumpToDocument(f, ctrlKey, false, document => this.props.addDocTab(document, undefined, "inTab"));
+ DocServer.GetRefField(this._linkClicked).then(async linkDoc => {
+ if (linkDoc instanceof Doc) {
+ let proto = Doc.GetProto(linkDoc);
+ let targetContext = await Cast(proto.targetContext, Doc);
+ let jumpToDoc = await Cast(linkDoc.anchor2, Doc);
+ if (jumpToDoc) {
+ if (DocumentManager.Instance.getDocumentView(jumpToDoc)) {
+
+ DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, undefined, undefined, NumCast((jumpToDoc === linkDoc.anchor2 ? linkDoc.anchor2Page : linkDoc.anchor1Page)));
+ return;
+ }
+ }
+ if (targetContext) {
+ DocumentManager.Instance.jumpToDocument(targetContext, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab"));
+ }
+ }
});
e.stopPropagation();
e.preventDefault();
}
} else {
- let webDoc = Docs.WebDocument(href, { x: NumCast(this.props.Document.x, 0) + NumCast(this.props.Document.width, 0), y: NumCast(this.props.Document.y) });
+ let webDoc = Docs.Create.WebDocument(href, { x: NumCast(this.props.Document.x, 0) + NumCast(this.props.Document.width, 0), y: NumCast(this.props.Document.y) });
this.props.addDocument && this.props.addDocument(webDoc);
this._linkClicked = webDoc[Id];
}
@@ -415,7 +448,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
onPointerWheel = (e: React.WheelEvent): void => {
- if (this.props.isSelected()) {
+ // if a text note is not selected and scrollable, this prevents us from being able to scroll and zoom out at the same time
+ if (this.props.isSelected() || e.currentTarget.scrollHeight > e.currentTarget.clientHeight) {
e.stopPropagation();
}
}
@@ -472,8 +506,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
if (StrCast(this.dataDoc.title).startsWith("-") && this._editorView) {
let str = this._editorView.state.doc.textContent;
let titlestr = str.substr(0, Math.min(40, str.length));
- let target = this.dataDoc.proto ? this.dataDoc.proto : this.dataDoc;
- target.title = "-" + titlestr + (str.length > 40 ? "..." : "");
+ this.dataDoc.title = "-" + titlestr + (str.length > 40 ? "..." : "");
}
if (!this._undoTyping) {
this._undoTyping = UndoManager.StartBatch("undoTyping");
@@ -484,13 +517,14 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
@action
tryUpdateHeight() {
if (this.props.isOverlay && this.props.Document.autoHeight) {
+ let self = this;
let xf = this._ref.current!.getBoundingClientRect();
let scrBounds = this.props.ScreenToLocalTransform().transformBounds(0, 0, xf.width, xf.height);
let nh = NumCast(this.dataDoc.nativeHeight, 0);
let dh = NumCast(this.props.Document.height, 0);
let sh = scrBounds.height;
this.props.Document.height = nh ? dh / nh * sh : sh;
- this.dataDoc.proto!.nativeHeight = nh ? sh : undefined;
+ this.dataDoc.nativeHeight = nh ? sh : undefined;
}
}
@@ -516,6 +550,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
let style = this.props.isOverlay ? "scroll" : "hidden";
let rounded = StrCast(this.props.Document.borderRounding) === "100%" ? "-rounded" : "";
let interactive = InkingControl.Instance.selectedTool ? "" : "interactive";
+ Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
return (
<div className={`formattedTextBox-cont-${style}`} ref={this._ref}
style={{
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 f0363d0b8..0f60bd0fb 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,6 +1,6 @@
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faImage } from '@fortawesome/free-solid-svg-icons';
-import { action, observable, computed } from 'mobx';
+import { faImage, faFileAudio } from '@fortawesome/free-solid-svg-icons';
+import { action, observable, computed, runInAction } from 'mobx';
import { observer } from "mobx-react";
import Lightbox from 'react-image-lightbox';
import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
@@ -8,7 +8,7 @@ import { Doc, HeightSym, WidthSym, DocListCast } from '../../../new_fields/Doc';
import { List } from '../../../new_fields/List';
import { createSchema, listSpec, makeInterface } from '../../../new_fields/Schema';
import { Cast, FieldValue, NumCast, StrCast, BoolCast } from '../../../new_fields/Types';
-import { ImageField } from '../../../new_fields/URLField';
+import { ImageField, AudioField } from '../../../new_fields/URLField';
import { Utils } from '../../../Utils';
import { DragManager } from '../../util/DragManager';
import { undoBatch } from '../../util/UndoManager';
@@ -21,17 +21,34 @@ import { FieldView, FieldViewProps } from './FieldView';
import "./ImageBox.scss";
import React = require("react");
import { RouteStore } from '../../../server/RouteStore';
+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');
library.add(faImage);
+library.add(faFileAudio);
export const pageSchema = createSchema({
curPage: "number",
});
+interface Window {
+ MediaRecorder: MediaRecorder;
+}
+
+declare class MediaRecorder {
+ // whatever MediaRecorder has
+ constructor(e: any);
+}
+
type ImageDocument = makeInterface<[typeof pageSchema, typeof positionSchema]>;
const ImageDocument = makeInterface(pageSchema, positionSchema);
@@ -86,6 +103,8 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
e.stopPropagation();
}
}
+ } else if (!this.props.isSelected()) {
+ e.stopPropagation();
}
}));
// de.data.removeDocument() bcz: need to implement
@@ -136,13 +155,52 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
}
}
+ recordAudioAnnotation = () => {
+ let gumStream: any;
+ let recorder: any;
+ let self = this;
+ navigator.mediaDevices.getUserMedia({
+ audio: true
+ }).then(function (stream) {
+ gumStream = stream;
+ recorder = new MediaRecorder(stream);
+ recorder.ondataavailable = async function (e: any) {
+ const formData = new FormData();
+ formData.append("file", e.data);
+ const res = await fetch(Utils.prepend(RouteStore.upload), {
+ method: 'POST',
+ body: formData
+ });
+ const files = await res.json();
+ const url = Utils.prepend(files[0]);
+ // 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;
+ let audioAnnos = Cast(self.extensionDoc.audioAnnotations, listSpec(Doc));
+ if (audioAnnos === undefined) {
+ self.extensionDoc.audioAnnotations = new List([audioDoc]);
+ } else {
+ audioAnnos.push(audioDoc);
+ }
+ };
+ runInAction(() => self._audioState = 2);
+ recorder.start();
+ setTimeout(() => {
+ recorder.stop();
+ runInAction(() => self._audioState = 0);
+ gumStream.getAudioTracks()[0].stop();
+ }, 5000);
+ });
+ }
+
specificContextMenu = (e: React.MouseEvent): void => {
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({
+ 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;
@@ -156,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 });
}
}
@@ -203,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;
@@ -221,6 +286,48 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
});
}
+ @observable _audioState = 0;
+
+ @action
+ onPointerEnter = () => {
+ let self = this;
+ let audioAnnos = DocListCast(this.extensionDoc.audioAnnotations);
+ if (audioAnnos.length && this._audioState === 0) {
+ let anno = audioAnnos[Math.floor(Math.random() * audioAnnos.length)];
+ anno.data instanceof AudioField && new Howl({
+ src: [anno.data.url.href],
+ format: ["mp3"],
+ autoplay: true,
+ loop: false,
+ volume: 0.5,
+ onend: function () {
+ runInAction(() => self._audioState = 0);
+ }
+ });
+ this._audioState = 1;
+ }
+ // else {
+ // if (this._audioState === 0) {
+ // this._audioState = 1;
+ // new Howl({
+ // src: ["https://www.kozco.com/tech/piano2-CoolEdit.mp3"],
+ // autoplay: true,
+ // loop: false,
+ // volume: 0.5,
+ // onend: function () {
+ // runInAction(() => self._audioState = 0);
+ // }
+ // });
+ // }
+ // }
+ }
+
+ @action
+ audioDown = () => {
+ this.recordAudioAnnotation();
+ }
+
+
render() {
// let transform = this.props.ScreenToLocalTransform().inverse();
let pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50;
@@ -231,10 +338,10 @@ 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[] = ["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.extensionDoc, this.props.fieldKey);
+ Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
let alts = DocListCast(this.extensionDoc.Alternates);
let altpaths: string[] = alts.filter(doc => doc.data instanceof ImageField).map(doc => this.choosePath((doc.data as ImageField).url));
let field = this.dataDoc[this.props.fieldKey];
@@ -260,12 +367,20 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
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 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 2f5a0f963..9fc0f2080 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -2,7 +2,7 @@
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
-import { CompileScript, ScriptOptions } from "../../util/Scripting";
+import { CompileScript, ScriptOptions, CompiledScript } from "../../util/Scripting";
import { FieldView, FieldViewProps } from './FieldView';
import "./KeyValueBox.scss";
import { KeyValuePair } from "./KeyValuePair";
@@ -21,6 +21,12 @@ import { ImageField } from "../../../new_fields/URLField";
import { SelectionManager } from "../../util/SelectionManager";
import { listSpec } from "../../../new_fields/Schema";
+export type KVPScript = {
+ script: CompiledScript;
+ type: "computed" | "script" | false;
+ onDelegate: boolean;
+};
+
@observer
export class KeyValueBox extends React.Component<FieldViewProps> {
private _mainCont = React.createRef<HTMLDivElement>();
@@ -48,22 +54,27 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
}
}
}
- public static SetField(doc: Doc, key: string, value: string) {
+ public static CompileKVPScript(value: string): KVPScript | undefined {
let eq = value.startsWith("=");
- let target = eq ? doc : Doc.GetProto(doc);
value = eq ? value.substr(1) : value;
- let dubEq = value.startsWith(":=") ? 1 : value.startsWith(";=") ? 2 : 0;
+ const dubEq = value.startsWith(":=") ? "computed" : value.startsWith(";=") ? "script" : false;
value = dubEq ? value.substr(2) : value;
let options: ScriptOptions = { addReturn: true, params: { this: "Doc" } };
if (dubEq) options.typecheck = false;
let script = CompileScript(value, options);
if (!script.compiled) {
- return false;
+ return undefined;
}
+ return { script, type: dubEq, onDelegate: eq };
+ }
+
+ public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript): boolean {
+ const { script, type, onDelegate } = kvpScript;
+ const target = onDelegate ? doc : Doc.GetProto(doc);
let field: Field;
- if (dubEq === 1) {
+ if (type === "computed") {
field = new ComputedField(script);
- } else if (dubEq === 2) {
+ } else if (type === "script") {
field = new ScriptField(script);
} else {
let res = script.run({ this: target });
@@ -77,6 +88,12 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
return false;
}
+ public static SetField(doc: Doc, key: string, value: string) {
+ const script = this.CompileKVPScript(value);
+ if (!script) return false;
+ return this.ApplyKVPScript(doc, key, script);
+ }
+
onPointerDown = (e: React.PointerEvent): void => {
if (e.buttons === 1 && this.props.isSelected()) {
e.stopPropagation();
@@ -97,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;
}
});
@@ -159,7 +176,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
}
getTemplate = async () => {
- let parent = Docs.StackingDocument([], { width: 800, height: 800, title: "Template" });
+ let parent = Docs.Create.StackingDocument([], { width: 800, height: 800, title: "Template" });
parent.singleColumn = false;
parent.columnWidth = 100;
for (let row of this.rows.filter(row => row.isChecked)) {
@@ -178,26 +195,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
let fieldTemplate = await this.inferType(sourceDoc[metaKey], metaKey);
let previousViewType = fieldTemplate.viewType;
-
- // move data doc fields to layout doc as needed (nativeWidth/nativeHeight, data, ??)
- let backgroundLayout = StrCast(fieldTemplate.backgroundLayout);
- let layout = StrCast(fieldTemplate.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
- if (backgroundLayout) {
- layout = StrCast(fieldTemplate.layout).replace(/fieldKey={"annotations"}/, `fieldKey={"${metaKey}"} fieldExt={"annotations"}`);
- backgroundLayout = backgroundLayout.replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
- }
- let nw = NumCast(fieldTemplate.nativeWidth);
- let nh = NumCast(fieldTemplate.nativeHeight);
-
- fieldTemplate.title = metaKey;
- fieldTemplate.layout = layout;
- fieldTemplate.backgroundLayout = backgroundLayout;
- fieldTemplate.nativeWidth = nw;
- fieldTemplate.nativeHeight = nh;
- fieldTemplate.embed = true;
- fieldTemplate.isTemplate = true;
- fieldTemplate.templates = new List<string>([Templates.TitleBar(metaKey)]);
- fieldTemplate.proto = Doc.GetProto(parentStackingDoc);
+ Doc.MakeTemplate(fieldTemplate, metaKey, Doc.GetProto(parentStackingDoc));
previousViewType && (fieldTemplate.viewType = previousViewType);
Cast(parentStackingDoc.data, listSpec(Doc))!.push(fieldTemplate);
@@ -206,23 +204,23 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
inferType = async (data: FieldResult, metaKey: string) => {
let options = { width: 300, height: 300, title: metaKey };
if (data instanceof RichTextField || typeof data === "string" || typeof data === "number") {
- return Docs.TextDocument(options);
+ return Docs.Create.TextDocument(options);
} else if (data instanceof List) {
if (data.length === 0) {
- return Docs.StackingDocument([], options);
+ return Docs.Create.StackingDocument([], options);
}
let first = await Cast(data[0], Doc);
if (!first) {
- return Docs.StackingDocument([], options);
+ return Docs.Create.StackingDocument([], options);
}
switch (first.type) {
case "image":
- return Docs.StackingDocument([], options);
+ return Docs.Create.StackingDocument([], options);
case "text":
- return Docs.TreeDocument([], options);
+ return Docs.Create.TreeDocument([], options);
}
} else if (data instanceof ImageField) {
- return Docs.ImageDocument("https://image.flaticon.com/icons/png/512/23/23765.png", options);
+ return Docs.Create.ImageDocument("https://image.flaticon.com/icons/png/512/23/23765.png", options);
}
return new Doc;
}
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index b5db52ac7..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,22 +106,16 @@ 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}
height={36}
GetValue={() => {
- const onDelegate = Object.keys(props.Document).includes(props.fieldKey);
-
- let field = FieldValue(props.Document[props.fieldKey]);
- if (Field.IsField(field)) {
- return (onDelegate ? "=" : "") + Field.toScriptString(field);
- }
- return "";
+ return Field.toKeyValueString(props.Document, props.fieldKey);
}}
SetValue={(value: string) =>
KeyValueBox.SetField(props.Document, props.fieldKey, value)}>
diff --git a/src/client/views/nodes/LinkEditor.scss b/src/client/views/nodes/LinkEditor.scss
index 3c49c2212..fc5f2410c 100644
--- a/src/client/views/nodes/LinkEditor.scss
+++ b/src/client/views/nodes/LinkEditor.scss
@@ -47,7 +47,7 @@
border-radius: 3px;
.linkEditor-group-row {
- // display: flex;
+ display: flex;
margin-bottom: 3px;
.linkEditor-group-row-label {
@@ -108,6 +108,28 @@
&:hover {
background-color: lightgray;
}
+
+ &.onDown {
+ background-color: gray;
+ }
+ }
+}
+
+.linkEditor-typeButton {
+ background-color: transparent;
+ color: $dark-color;
+ width: 100%;
+ height: 20px;
+ padding: 0 3px;
+ padding-bottom: 2px;
+ text-align: left;
+ text-transform: none;
+ letter-spacing: normal;
+ font-size: 12px;
+ font-weight: bold;
+
+ &:hover {
+ background-color: $light-color;
}
}
@@ -115,19 +137,9 @@
height: 20px;
display: flex;
justify-content: flex-end;
+ margin-top: 5px;
.linkEditor-button {
margin-left: 6px;
}
-
- // .linkEditor-groupOpts {
- // width: calc(20% - 3px);
- // height: 20px;
- // padding: 0;
- // font-size: 10px;
-
- // &:disabled {
- // background-color: gray;
- // }
- // }
} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index a97ec8831..afde85b69 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -22,11 +22,9 @@ interface GroupTypesDropdownProps {
// this dropdown could be generalized
@observer
class GroupTypesDropdown extends React.Component<GroupTypesDropdownProps> {
- @observable private _searchTerm: string = "";
+ @observable private _searchTerm: string = this.props.groupType;
@observable private _groupType: string = this.props.groupType;
-
- @action setSearchTerm = (value: string): void => { this._searchTerm = value; };
- @action setGroupType = (value: string): void => { this._groupType = value; };
+ @observable private _isEditing: boolean = false;
@action
createGroup = (groupType: string): void => {
@@ -34,9 +32,52 @@ class GroupTypesDropdown extends React.Component<GroupTypesDropdownProps> {
LinkManager.Instance.addGroupType(groupType);
}
+ @action
onChange = (val: string): void => {
- this.setSearchTerm(val);
- this.setGroupType(val);
+ this._searchTerm = val;
+ this._groupType = val;
+ this._isEditing = true;
+ }
+
+ @action
+ onKeyDown = (e: React.KeyboardEvent): void => {
+ if (e.key === "Enter") {
+ let allGroupTypes = Array.from(LinkManager.Instance.getAllGroupTypes());
+ let groupOptions = allGroupTypes.filter(groupType => groupType.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1);
+ let exactFound = groupOptions.findIndex(groupType => groupType.toUpperCase() === this._searchTerm.toUpperCase());
+
+ if (exactFound > -1) {
+ let groupType = groupOptions[exactFound];
+ this.props.setGroupType(groupType);
+ this._groupType = groupType;
+ } else {
+ this.createGroup(this._searchTerm);
+ this._groupType = this._searchTerm;
+ }
+
+ this._searchTerm = this._groupType;
+ this._isEditing = false;
+ }
+ }
+
+ @action
+ onOptionClick = (value: string, createNew: boolean): void => {
+ if (createNew) {
+ this.createGroup(this._searchTerm);
+ this._groupType = this._searchTerm;
+
+ } else {
+ this.props.setGroupType(value);
+ this._groupType = value;
+
+ }
+ this._searchTerm = this._groupType;
+ this._isEditing = false;
+ }
+
+ @action
+ onButtonPointerDown = (): void => {
+ this._isEditing = true;
}
renderOptions = (): JSX.Element[] | JSX.Element => {
@@ -47,29 +88,35 @@ class GroupTypesDropdown extends React.Component<GroupTypesDropdownProps> {
let exactFound = groupOptions.findIndex(groupType => groupType.toUpperCase() === this._searchTerm.toUpperCase()) > -1;
let options = groupOptions.map(groupType => {
- return <div key={groupType} className="linkEditor-option"
- onClick={() => { this.props.setGroupType(groupType); this.setGroupType(groupType); this.setSearchTerm(""); }}>{groupType}</div>;
+ let ref = React.createRef<HTMLDivElement>();
+ return <div key={groupType} ref={ref} className="linkEditor-option"
+ onClick={() => this.onOptionClick(groupType, false)}>{groupType}</div>;
});
// if search term does not already exist as a group type, give option to create new group type
if (!exactFound && this._searchTerm !== "") {
- options.push(<div key={""} className="linkEditor-option"
- onClick={() => { this.createGroup(this._searchTerm); this.setGroupType(this._searchTerm); this.setSearchTerm(""); }}>Define new "{this._searchTerm}" relationship</div>);
+ let ref = React.createRef<HTMLDivElement>();
+ options.push(<div key={""} ref={ref} className="linkEditor-option"
+ onClick={() => this.onOptionClick(this._searchTerm, true)}>Define new "{this._searchTerm}" relationship</div>);
}
return options;
}
render() {
- return (
- <div className="linkEditor-dropdown">
- <input type="text" value={this._groupType} placeholder="Search for or create a new group"
- onChange={e => this.onChange(e.target.value)}></input>
- <div className="linkEditor-options-wrapper">
- {this.renderOptions()}
- </div>
- </div >
- );
+ if (this._isEditing || this._groupType === "") {
+ return (
+ <div className="linkEditor-dropdown">
+ <input type="text" value={this._groupType} placeholder="Search for or create a new group"
+ onChange={e => this.onChange(e.target.value)} onKeyDown={this.onKeyDown} autoFocus></input>
+ <div className="linkEditor-options-wrapper">
+ {this.renderOptions()}
+ </div>
+ </div >
+ );
+ } else {
+ return <button className="linkEditor-typeButton" onClick={() => this.onButtonPointerDown()}>{this._groupType}</button>;
+ }
}
}
@@ -200,7 +247,9 @@ export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
destGroupDoc.type = groupType;
destGroupDoc.metadata = destMdDoc;
- LinkManager.Instance.addGroupToAnchor(this.props.linkDoc, destDoc, destGroupDoc, true);
+ if (destDoc) {
+ LinkManager.Instance.addGroupToAnchor(this.props.linkDoc, destDoc, destGroupDoc, true);
+ }
}
@action
@@ -242,7 +291,7 @@ export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
if (index > -1) keys.splice(index, 1);
let cols = ["anchor1", "anchor2", ...[...keys]];
let docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType);
- let createTable = action(() => Docs.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" }));
+ let createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" }));
let ref = React.createRef<HTMLDivElement>();
return <div ref={ref}><button className="linkEditor-button" onPointerDown={SetupDrag(ref, createTable)} title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button></div>;
}
@@ -274,7 +323,7 @@ export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
}
return (
<div className="linkEditor-group">
- <div className="linkEditor-group-row">
+ <div className="linkEditor-group-row ">
<p className="linkEditor-group-row-label">type:</p>
<GroupTypesDropdown groupType={groupType} setGroupType={this.setGroupType} />
</div>
@@ -308,7 +357,10 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
// create new metadata document for group
let mdDoc = new Doc();
mdDoc.anchor1 = this.props.sourceDoc.title;
- mdDoc.anchor2 = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc).title;
+ let opp = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
+ if (opp) {
+ mdDoc.anchor2 = opp.title;
+ }
// create new group document
let groupDoc = new Doc();
@@ -326,20 +378,22 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
return <LinkGroupEditor key={"gred-" + StrCast(groupDoc.type)} linkDoc={this.props.linkDoc} sourceDoc={this.props.sourceDoc} groupDoc={groupDoc} />;
});
- return (
- <div className="linkEditor">
- <button className="linkEditor-back" onPointerDown={() => this.props.showLinks()}><FontAwesomeIcon icon="arrow-left" size="sm" /></button>
- <div className="linkEditor-info">
- <p className="linkEditor-linkedTo">editing link to: <b>{destination.proto!.title}</b></p>
- <button className="linkEditor-button" onPointerDown={() => this.deleteLink()} title="Delete link"><FontAwesomeIcon icon="trash" size="sm" /></button>
+ if (destination) {
+ return (
+ <div className="linkEditor">
+ <button className="linkEditor-back" onPointerDown={() => this.props.showLinks()}><FontAwesomeIcon icon="arrow-left" size="sm" /></button>
+ <div className="linkEditor-info">
+ <p className="linkEditor-linkedTo">editing link to: <b>{destination.proto!.title}</b></p>
+ <button className="linkEditor-button" onPointerDown={() => this.deleteLink()} title="Delete link"><FontAwesomeIcon icon="trash" size="sm" /></button>
+ </div>
+ <div className="linkEditor-groupsLabel">
+ <b>Relationships:</b>
+ <button className="linkEditor-button" onClick={() => this.addGroup()} title=" Add Group"><FontAwesomeIcon icon="plus" size="sm" /></button>
+ </div>
+ {groups.length > 0 ? groups : <div className="linkEditor-group">There are currently no relationships associated with this link.</div>}
</div>
- <div className="linkEditor-groupsLabel">
- <b>Relationships:</b>
- <button className="linkEditor-button" onClick={() => this.addGroup()} title=" Add Group"><FontAwesomeIcon icon="plus" size="sm" /></button>
- </div>
- {groups.length > 0 ? groups : <div className="linkEditor-group">There are currently no relationships associated with this link.</div>}
- </div>
- );
+ );
+ }
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx
index cccf3c329..1eda7d1fb 100644
--- a/src/client/views/nodes/LinkMenu.tsx
+++ b/src/client/views/nodes/LinkMenu.tsx
@@ -14,7 +14,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
library.add(faTrash);
import { Cast, FieldValue, StrCast } from "../../../new_fields/Types";
import { Id } from "../../../new_fields/FieldSymbols";
-import { DocTypes } from "../../documents/Documents";
+import { DocumentType } from "../../documents/Documents";
interface Props {
docView: DocumentView;
diff --git a/src/client/views/nodes/LinkMenuGroup.tsx b/src/client/views/nodes/LinkMenuGroup.tsx
index e4cf56d20..ae97bed2f 100644
--- a/src/client/views/nodes/LinkMenuGroup.tsx
+++ b/src/client/views/nodes/LinkMenuGroup.tsx
@@ -12,6 +12,8 @@ import { DragLinksAsDocuments, DragManager, SetupDrag } from "../../util/DragMan
import { emptyFunction } from "../../../Utils";
import { Docs } from "../../documents/Documents";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { UndoManager } from "../../util/UndoManager";
+import { StrCast } from "../../../new_fields/Types";
interface LinkMenuGroupProps {
sourceDoc: Doc;
@@ -40,21 +42,27 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
e.stopPropagation();
}
+
onLinkButtonMoved = async (e: PointerEvent) => {
- if (this._drag.current !== null && (e.movementX > 1 || e.movementY > 1)) {
- document.removeEventListener("pointermove", this.onLinkButtonMoved);
- document.removeEventListener("pointerup", this.onLinkButtonUp);
+ UndoManager.RunInBatch(() => {
+ if (this._drag.current !== null && (e.movementX > 1 || e.movementY > 1)) {
+ document.removeEventListener("pointermove", this.onLinkButtonMoved);
+ document.removeEventListener("pointerup", this.onLinkButtonUp);
- let draggedDocs = this.props.group.map(linkDoc => LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc));
- let dragData = new DragManager.DocumentDragData(draggedDocs, draggedDocs.map(d => undefined));
+ let draggedDocs = this.props.group.map(linkDoc => {
+ let opp = LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc);
+ if (opp) return opp;
+ }) as Doc[];
+ let dragData = new DragManager.DocumentDragData(draggedDocs, draggedDocs.map(d => undefined));
- DragManager.StartLinkedDocumentDrag([this._drag.current], this.props.sourceDoc, dragData, e.x, e.y, {
- handlers: {
- dragComplete: action(emptyFunction),
- },
- hideSource: false
- });
- }
+ DragManager.StartLinkedDocumentDrag([this._drag.current], dragData, e.x, e.y, {
+ handlers: {
+ dragComplete: action(emptyFunction),
+ },
+ hideSource: false
+ });
+ }
+ }, "drag links");
e.stopPropagation();
}
@@ -64,7 +72,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
if (index > -1) keys.splice(index, 1);
let cols = ["anchor1", "anchor2", ...[...keys]];
let docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType);
- let createTable = action(() => Docs.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" }));
+ let createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" }));
let ref = React.createRef<HTMLDivElement>();
return <div ref={ref}><button className="linkEditor-button linkEditor-tableButton" onPointerDown={SetupDrag(ref, createTable)} title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button></div>;
}
@@ -72,8 +80,10 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
render() {
let groupItems = this.props.group.map(linkDoc => {
let destination = LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc);
- return <LinkMenuItem key={destination[Id] + this.props.sourceDoc[Id]} groupType={this.props.groupType}
- linkDoc={linkDoc} sourceDoc={this.props.sourceDoc} destinationDoc={destination} showEditor={this.props.showEditor} />;
+ if (destination && this.props.sourceDoc) {
+ return <LinkMenuItem key={destination[Id] + this.props.sourceDoc[Id]} groupType={this.props.groupType}
+ linkDoc={linkDoc} sourceDoc={this.props.sourceDoc} destinationDoc={destination} showEditor={this.props.showEditor} />;
+ }
});
return (
diff --git a/src/client/views/nodes/LinkMenuItem.tsx b/src/client/views/nodes/LinkMenuItem.tsx
index 9728671c0..a0c37a719 100644
--- a/src/client/views/nodes/LinkMenuItem.tsx
+++ b/src/client/views/nodes/LinkMenuItem.tsx
@@ -7,7 +7,7 @@ import { undoBatch } from "../../util/UndoManager";
import './LinkMenu.scss';
import React = require("react");
import { Doc } from '../../../new_fields/Doc';
-import { StrCast, Cast, BoolCast, FieldValue } from '../../../new_fields/Types';
+import { StrCast, Cast, BoolCast, FieldValue, NumCast } from '../../../new_fields/Types';
import { observable, action } from 'mobx';
import { LinkManager } from '../../util/LinkManager';
import { DragLinkAsDocument } from '../../util/DragManager';
@@ -32,15 +32,28 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
@undoBatch
onFollowLink = async (e: React.PointerEvent): Promise<void> => {
e.stopPropagation();
+ e.persist();
let jumpToDoc = this.props.destinationDoc;
let pdfDoc = FieldValue(Cast(this.props.destinationDoc, Doc));
if (pdfDoc) {
jumpToDoc = pdfDoc;
}
+ let proto = Doc.GetProto(this.props.linkDoc);
+ let targetContext = await Cast(proto.targetContext, Doc);
+ let sourceContext = await Cast(proto.sourceContext, Doc);
+ let self = this;
if (DocumentManager.Instance.getDocumentView(jumpToDoc)) {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey);
+ DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, undefined, undefined, NumCast((this.props.destinationDoc === self.props.linkDoc.anchor2 ? self.props.linkDoc.anchor2Page : self.props.linkDoc.anchor1Page)));
+ }
+ else if (!((this.props.destinationDoc === self.props.linkDoc.anchor2 && targetContext) || (this.props.destinationDoc === self.props.linkDoc.anchor1 && sourceContext))) {
+ DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, document => CollectionDockingView.Instance.AddRightSplit(document, undefined));
} else {
- CollectionDockingView.Instance.AddRightSplit(jumpToDoc, undefined);
+ if (this.props.destinationDoc === self.props.linkDoc.anchor2 && targetContext) {
+ DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, document => CollectionDockingView.Instance.AddRightSplit(targetContext!, undefined));
+ }
+ else if (this.props.destinationDoc === self.props.linkDoc.anchor1 && sourceContext) {
+ DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, document => CollectionDockingView.Instance.AddRightSplit(sourceContext!, undefined));
+ }
}
}
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index cc02bb282..5a5e6e6dd 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -23,9 +23,11 @@ import { CompileScript } from '../../util/Scripting';
import { Flyout, anchorPoints } from '../DocumentDecorations';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { ScriptField } from '../../../new_fields/ScriptField';
+import { KeyCodes } from '../../northstar/utils/KeyCodes';
type PdfDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>;
const PdfDocument = makeInterface(positionSchema, pageSchema);
+export const handleBackspace = (e: React.KeyboardEvent) => { if (e.keyCode === KeyCodes.BACKSPACE) e.stopPropagation(); };
@observer
export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocument) {
@@ -175,13 +177,13 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
Annotation View Settings
</div>
<div className="pdfBox-settingsFlyout-kvpInput">
- <input placeholder="Key" className="pdfBox-settingsFlyout-input" onChange={this.newKeyChange}
+ <input placeholder="Key" className="pdfBox-settingsFlyout-input" onKeyDown={handleBackspace} onChange={this.newKeyChange}
style={{ gridColumn: 1 }} ref={this._keyRef} />
- <input placeholder="Value" className="pdfBox-settingsFlyout-input" onChange={this.newValueChange}
+ <input placeholder="Value" className="pdfBox-settingsFlyout-input" onKeyDown={handleBackspace} onChange={this.newValueChange}
style={{ gridColumn: 3 }} ref={this._valueRef} />
</div>
<div className="pdfBox-settingsFlyout-kvpInput">
- <input placeholder="Custom Script" onChange={this.newScriptChange} style={{ gridColumn: "1 / 4" }} ref={this._scriptRef} />
+ <input placeholder="Custom Script" onChange={this.newScriptChange} onKeyDown={handleBackspace} style={{ gridColumn: "1 / 4" }} ref={this._scriptRef} />
</div>
<div className="pdfBox-settingsFlyout-kvpInput">
<button style={{ gridColumn: 1 }} onClick={this.resetFilters}>
@@ -228,6 +230,10 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
}
}
+
+ @computed get fieldExtensionDoc() {
+ return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, "true");
+ }
render() {
// uses mozilla pdf as default
const pdfUrl = Cast(this.props.Document.data, PdfField);
diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss
index 35db64cf4..d651a8621 100644
--- a/src/client/views/nodes/VideoBox.scss
+++ b/src/client/views/nodes/VideoBox.scss
@@ -1,8 +1,17 @@
-.videoBox-cont, .videoBox-cont-fullScreen{
- width: 100%;
+.videoBox-content-YouTube, .videoBox-content-YouTube-fullScreen,
+.videoBox-content, .videoBox-content-interactive, .videoBox-cont-fullScreen {
+ width: 100%;
+}
+
+.videoBox-content, .videoBox-content-interactive, .videoBox-content-fullScreen {
height: Auto;
}
-.videoBox-cont-fullScreen {
+.videoBox-content-YouTube, .videoBox-content-YouTube-fullScreen {
+ height: 100%;
+}
+
+.videoBox-content-interactive, .videoBox-content-fullScreen,
+.videoBox-content-YouTube-fullScreen {
pointer-events: all;
} \ No newline at end of file
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 264d3c1f7..5624d41a9 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -1,13 +1,20 @@
import React = require("react");
-import { action, IReactionDisposer, observable, reaction } from "mobx";
+import { action, computed, IReactionDisposer, observable, reaction, runInAction, untracked, trace } from "mobx";
import { observer } from "mobx-react";
-import * as rp from "request-promise";
+import * as rp from 'request-promise';
+import { InkTool } from "../../../new_fields/InkField";
import { makeInterface } from "../../../new_fields/Schema";
-import { Cast, FieldValue } from "../../../new_fields/Types";
+import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
import { VideoField } from "../../../new_fields/URLField";
import { RouteStore } from "../../../server/RouteStore";
+import { Utils } from "../../../Utils";
import { DocServer } from "../../DocServer";
+import { Docs, DocUtils } from "../../documents/Documents";
+import { ContextMenu } from "../ContextMenu";
+import { ContextMenuProps } from "../ContextMenuItem";
import { DocComponent } from "../DocComponent";
+import { DocumentDecorations } from "../DocumentDecorations";
+import { InkingControl } from "../InkingControl";
import { positionSchema } from "./DocumentView";
import { FieldView, FieldViewProps } from './FieldView';
import { pageSchema } from "./ImageBox";
@@ -19,7 +26,14 @@ const VideoDocument = makeInterface(positionSchema, pageSchema);
@observer
export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoDocument) {
private _reactionDisposer?: IReactionDisposer;
+ private _youtubeReactionDisposer?: IReactionDisposer;
+ private _youtubePlayer: any = undefined;
private _videoRef: HTMLVideoElement | null = null;
+ private _youtubeIframeId: number = -1;
+ private _youtubeContentCreated = false;
+ static _youtubeIframeCounter: number = 0;
+ @observable _forceCreateYouTubeIFrame = false;
+ @observable static _showControls: boolean;
@observable _playTimer?: NodeJS.Timeout = undefined;
@observable _fullScreen = false;
@observable public Playing: boolean = false;
@@ -38,39 +52,62 @@ 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() {
+ @action public Play = (update: boolean = true) => {
this.Playing = true;
- if (this.player) this.player.play();
- if (!this._playTimer) this._playTimer = setInterval(this.updateTimecode, 500);
+ update && this.player && this.player.play();
+ update && this._youtubePlayer && this._youtubePlayer.playVideo();
+ !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 5));
+ this.updateTimecode();
}
- @action public Pause() {
+ @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;
- if (this.player) this.player.pause();
- if (this._playTimer) {
- clearInterval(this._playTimer);
- this._playTimer = undefined;
- }
+ update && this.player && this.player.pause();
+ update && this._youtubePlayer && this._youtubePlayer.pauseVideo();
+ this._playTimer && clearInterval(this._playTimer);
+ this._playTimer = undefined;
+ this.updateTimecode();
}
@action public FullScreen() {
this._fullScreen = true;
this.player && this.player.requestFullscreen();
+ this._youtubePlayer && this.props.addDocTab(this.props.Document, this.props.DataDoc, "inTab");
}
@action
updateTimecode = () => {
this.player && (this.props.Document.curPage = this.player.currentTime);
+ this._youtubePlayer && (this.props.Document.curPage = this._youtubePlayer.getCurrentTime());
}
componentDidMount() {
if (this.props.setVideoBox) this.props.setVideoBox(this);
+
+ if (this.youtubeVideoId) {
+ let youtubeaspect = 400 / 315;
+ var nativeWidth = FieldValue(this.Document.nativeWidth, 0);
+ var nativeHeight = FieldValue(this.Document.nativeHeight, 0);
+ if (!nativeWidth || !nativeHeight) {
+ if (!this.Document.nativeWidth) this.Document.nativeWidth = 600;
+ this.Document.nativeHeight = this.Document.nativeWidth / youtubeaspect;
+ this.Document.height = FieldValue(this.Document.width, 0) / youtubeaspect;
+ }
+ }
}
+
componentWillUnmount() {
this.Pause();
- if (this._reactionDisposer) this._reactionDisposer();
+ this._reactionDisposer && this._reactionDisposer();
+ this._youtubeReactionDisposer && this._youtubeReactionDisposer();
}
@action
@@ -85,58 +122,136 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
}
}
- getMp4ForVideo(videoId: string = "JN5beCVArMs") {
- return new Promise(async (resolve, reject) => {
- const videoInfoRequestConfig = {
- headers: {
- connection: 'keep-alive',
- "user-agent": 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:43.0) Gecko/20100101 Firefox/46.0',
+ public static async convertDataUri(imageUri: string, returnedFilename: string) {
+ try {
+ let posting = Utils.prepend(RouteStore.dataUriToImage);
+ const returnedUri = await rp.post(posting, {
+ body: {
+ uri: imageUri,
+ name: returnedFilename
},
- };
- try {
- let responseSchema: any = {};
- const videoInfoResponse = await rp.get(DocServer.prepend(RouteStore.corsProxy + "/" + `https://www.youtube.com/watch?v=${videoId}`), videoInfoRequestConfig);
- const dataHtml = videoInfoResponse;
- const start = dataHtml.indexOf('ytplayer.config = ') + 18;
- const end = dataHtml.indexOf(';ytplayer.load');
- const subString = dataHtml.substring(start, end);
- const subJson = JSON.parse(subString);
- const stringSub = subJson.args.player_response;
- const stringSubJson = JSON.parse(stringSub);
- const adaptiveFormats = stringSubJson.streamingData.adaptiveFormats;
- const videoDetails = stringSubJson.videoDetails;
- responseSchema.adaptiveFormats = adaptiveFormats;
- responseSchema.videoDetails = videoDetails;
- resolve(responseSchema);
- }
- catch (err) {
- console.log(`
- --- Youtube ---
- Function: getMp4ForVideo
- Error: `, err);
- reject(err);
- }
- });
- }
- onPointerDown = (e: React.PointerEvent) => {
- e.stopPropagation();
- }
+ json: true,
+ });
+ return returnedUri;
- render() {
+ } catch (e) {
+ console.log(e);
+ }
+ }
+ specificContextMenu = (e: React.MouseEvent): void => {
let field = Cast(this.Document[this.props.fieldKey], VideoField);
+ 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: "Toggle Show Controls", event: action(() => VideoBox._showControls = !VideoBox._showControls), icon: "expand-arrows-alt" });
+ let width = NumCast(this.props.Document.width);
+ let height = NumCast(this.props.Document.height);
+ subitems.push({
+ description: "Take Snapshot", event: async () => {
+ var canvas = document.createElement('canvas');
+ canvas.width = 640;
+ canvas.height = 640 * NumCast(this.props.Document.nativeHeight) / NumCast(this.props.Document.nativeWidth);
+ var ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions
+ if (ctx) {
+ ctx.rect(0, 0, canvas.width, canvas.height);
+ ctx.fillStyle = "blue";
+ ctx.fill();
+ this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height);
+ }
- // this.getMp4ForVideo().then((mp4) => {
- // console.log(mp4);
- // }).catch(e => {
- // console.log("")
- // });
- // //
+ //convert to desired file format
+ var dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
+ // if you want to preview the captured image,
+ let filename = encodeURIComponent("snapshot" + this.props.Document.title + "_" + this.props.Document.curPage).replace(/\./g, "");
+ VideoBox.convertDataUri(dataUrl, filename).then(returnedFilename => {
+ if (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-"
+ });
+ this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.addDocument && this.props.ContainingCollectionView.props.addDocument(imageSummary, false);
+ DocUtils.MakeLink(imageSummary, this.props.Document);
+ }
+ });
+ },
+ icon: "expand-arrows-alt"
+ });
+ ContextMenu.Instance.addItem({ description: "Video Funcs...", subitems: subitems });
+ }
+ }
- let style = "videoBox-cont" + (this._fullScreen ? "-fullScreen" : "");
+ @computed get content() {
+ let field = Cast(this.Document[this.props.fieldKey], VideoField);
+ let interactive = InkingControl.Instance.selectedTool || !this.props.isSelected() ? "" : "-interactive";
+ let style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
return !field ? <div>Loading</div> :
- <video className={`${style}`} ref={this.setVideoRef} onCanPlay={this.videoLoad} onPointerDown={this.onPointerDown}>
+ <video className={`${style}`} ref={this.setVideoRef} onCanPlay={this.videoLoad} controls={VideoBox._showControls}
+ onPlay={() => this.Play()} onSeeked={this.updateTimecode} onPause={() => this.Pause()} onClick={e => e.preventDefault()}>
<source src={field.url.href} type="video/mp4" />
Not supported.
</video>;
}
-} \ No newline at end of file
+
+ @computed get youtubeVideoId() {
+ let field = Cast(this.Document[this.props.fieldKey], VideoField);
+ return field && field.url.href.indexOf("youtube") !== -1 ? ((arr: string[]) => arr[arr.length - 1])(field.url.href.split("/")) : "";
+ }
+
+ @action youtubeIframeLoaded = (e: any) => {
+ if (!this._youtubeContentCreated) {
+ this._forceCreateYouTubeIFrame = !this._forceCreateYouTubeIFrame;
+ return;
+ }
+ else this._youtubeContentCreated = false;
+
+ let iframe = e.target;
+ let started = true;
+ let onYoutubePlayerStateChange = (event: any) => runInAction(() => {
+ if (started && event.data === YT.PlayerState.PLAYING) {
+ started = false;
+ this._youtubePlayer.unMute();
+ this.Pause();
+ return;
+ }
+ if (event.data === YT.PlayerState.PLAYING && !this.Playing) this.Play(false);
+ if (event.data === YT.PlayerState.PAUSED && this.Playing) this.Pause(false);
+ });
+ let onYoutubePlayerReady = (event: any) => {
+ this._reactionDisposer && this._reactionDisposer();
+ this._youtubeReactionDisposer && this._youtubeReactionDisposer();
+ this._reactionDisposer = reaction(() => this.props.Document.curPage, () => !this.Playing && this.Seek(this.Document.curPage || 0));
+ this._youtubeReactionDisposer = reaction(() => [this.props.isSelected(), DocumentDecorations.Instance.Interacting, InkingControl.Instance.selectedTool], () => {
+ let interactive = InkingControl.Instance.selectedTool === InkTool.None && this.props.isSelected() && !DocumentDecorations.Instance.Interacting;
+ iframe.style.pointerEvents = interactive ? "all" : "none";
+ }, { fireImmediately: true });
+ };
+ this._youtubePlayer = new YT.Player(`${this.youtubeVideoId + this._youtubeIframeId}-player`, {
+ events: {
+ 'onReady': onYoutubePlayerReady,
+ 'onStateChange': onYoutubePlayerStateChange,
+ }
+ });
+
+ }
+
+ @computed get youtubeContent() {
+ this._youtubeIframeId = VideoBox._youtubeIframeCounter++;
+ this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true;
+ let style = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : "");
+ let start = untracked(() => Math.round(NumCast(this.props.Document.curPage)));
+ return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`}
+ onLoad={this.youtubeIframeLoaded} className={`${style}`} width={NumCast(this.props.Document.nativeWidth, 640)} height={NumCast(this.props.Document.nativeHeight, 390)}
+ src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=1&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._showControls ? 1 : 0}`}
+ ></iframe>;
+ }
+
+ render() {
+ return <div style={{ pointerEvents: "all", width: "100%", height: "100%" }} onContextMenu={this.specificContextMenu}>
+ {this.youtubeVideoId ? this.youtubeContent : this.content}
+ </div>;
+ }
+}
+
+VideoBox._showControls = true; \ No newline at end of file
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 98c57fc75..162ac1d98 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -6,12 +6,29 @@ import { InkingControl } from "../InkingControl";
import { FieldView, FieldViewProps } from './FieldView';
import "./WebBox.scss";
import React = require("react");
+import { InkTool } from "../../../new_fields/InkField";
+import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
@observer
export class WebBox extends React.Component<FieldViewProps> {
public static LayoutString() { return FieldView.LayoutString(WebBox); }
+ componentWillMount() {
+
+ let field = Cast(this.props.Document[this.props.fieldKey], WebField);
+ if (field && field.url.href.indexOf("youtube") !== -1) {
+ let youtubeaspect = 400 / 315;
+ var nativeWidth = NumCast(this.props.Document.nativeWidth, 0);
+ var nativeHeight = NumCast(this.props.Document.nativeHeight, 0);
+ if (!nativeWidth || !nativeHeight || Math.abs(nativeWidth / nativeHeight - youtubeaspect) > 0.05) {
+ if (!nativeWidth) this.props.Document.nativeWidth = 600;
+ this.props.Document.nativeHeight = NumCast(this.props.Document.nativeWidth) / youtubeaspect;
+ this.props.Document.height = NumCast(this.props.Document.width) / youtubeaspect;
+ }
+ }
+ }
+
_ignore = 0;
onPreWheel = (e: React.WheelEvent) => {
this._ignore = e.timeStamp;
@@ -46,7 +63,7 @@ export class WebBox extends React.Component<FieldViewProps> {
let frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting;
- let classname = "webBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
+ let classname = "webBox-cont" + (this.props.isSelected() && InkingControl.Instance.selectedTool === InkTool.None && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
return (
<>
<div className={classname} >
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index 0a1661a1a..ed7081b1d 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -75,7 +75,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
() => this.props.parent.Index,
() => {
if (this.props.parent.Index === this.props.index) {
- this.props.parent.scrollTo(this.props.y * scale - (NumCast(this.props.parent.props.parent.Document.pdfHeight) / 2));
+ this.props.parent.scrollTo(this.props.y * scale);
}
}
);
@@ -87,11 +87,11 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
}
deleteAnnotation = () => {
- let annotation = DocListCast(this.props.parent.props.parent.Document.annotations);
+ let annotation = DocListCast(this.props.parent.props.parent.fieldExtensionDoc.annotations);
let group = FieldValue(Cast(this.props.document.group, Doc));
if (group && annotation.indexOf(group) !== -1) {
let newAnnotations = annotation.filter(a => a !== FieldValue(Cast(this.props.document.group, Doc)));
- this.props.parent.props.parent.Document.annotations = new List<Doc>(newAnnotations);
+ this.props.parent.props.parent.fieldExtensionDoc.annotations = new List<Doc>(newAnnotations);
}
if (group) {
diff --git a/src/client/views/pdf/PDFMenu.scss b/src/client/views/pdf/PDFMenu.scss
index a4624b1f6..b06d19c53 100644
--- a/src/client/views/pdf/PDFMenu.scss
+++ b/src/client/views/pdf/PDFMenu.scss
@@ -21,6 +21,10 @@
.pdfMenu-dragger {
height: 100%;
transition: width .2s;
+ background-image: url("https://logodix.com/logo/1020374.png");
+ background-size: 90% 100%;
+ background-repeat: no-repeat;
+ background-position: left center;
}
.pdfMenu-addTag {
diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx
index d6970e7f4..27c2a8f1a 100644
--- a/src/client/views/pdf/PDFMenu.tsx
+++ b/src/client/views/pdf/PDFMenu.tsx
@@ -5,6 +5,7 @@ import { observer } from "mobx-react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { emptyFunction, returnFalse } from "../../../Utils";
import { Doc } from "../../../new_fields/Doc";
+import { handleBackspace } from "../nodes/PDFBox";
@observer
export default class PDFMenu extends React.Component {
@@ -17,7 +18,7 @@ export default class PDFMenu extends React.Component {
@observable private _transitionDelay: string = "";
- StartDrag: (e: PointerEvent, ele: HTMLDivElement) => void = emptyFunction;
+ StartDrag: (e: PointerEvent, ele: HTMLElement) => void = emptyFunction;
Highlight: (d: Doc | undefined, color: string | undefined) => void = emptyFunction;
Delete: () => void = emptyFunction;
Snippet: (marquee: { left: number, top: number, width: number, height: number }) => void = emptyFunction;
@@ -33,7 +34,7 @@ export default class PDFMenu extends React.Component {
private _offsetY: number = 0;
private _offsetX: number = 0;
private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
- private _commentCont: React.RefObject<HTMLDivElement> = React.createRef();
+ private _commentCont = React.createRef<HTMLButtonElement>();
private _snippetButton: React.RefObject<HTMLButtonElement> = React.createRef();
private _dragging: boolean = false;
@observable private _keyValue: string = "";
@@ -237,24 +238,24 @@ export default class PDFMenu extends React.Component {
render() {
let buttons = this.Status === "pdf" || this.Status === "snippet" ? [
- <button className="pdfMenu-button" title="Click to Highlight" onClick={this.highlightClicked} key="1"
+ <button key="1" className="pdfMenu-button" title="Click to Highlight" onClick={this.highlightClicked}
style={this.Highlighting ? { backgroundColor: "#121212" } : {}}>
<FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} />
</button>,
<button className="pdfMenu-button" title="Drag to Annotate" ref={this._commentCont} onPointerDown={this.pointerDown}><FontAwesomeIcon icon="comment-alt" size="lg" key="2" /></button>,
this.Status === "snippet" ? <button className="pdfMenu-button" title="Drag to Snippetize Selection" onPointerDown={this.snippetStart} ref={this._snippetButton}><FontAwesomeIcon icon="cut" size="lg" /></button> : undefined,
- <button className="pdfMenu-button" title="Pin Menu" onClick={this.togglePin} key="3"
+ <button key="3" className="pdfMenu-button" title="Pin Menu" onClick={this.togglePin}
style={this.Pinned ? { backgroundColor: "#121212" } : {}}>
<FontAwesomeIcon icon="thumbtack" size="lg" style={{ transition: "transform 0.1s", transform: this.Pinned ? "rotate(45deg)" : "" }} />
</button>
] : [
- <button className="pdfMenu-button" title="Delete Anchor" onPointerDown={this.deleteClicked}><FontAwesomeIcon icon="trash-alt" size="lg" key="1" /></button>,
- <button className="pdfMenu-button" title="Pin to Presentation" onPointerDown={this.PinToPres}><FontAwesomeIcon icon="map-pin" size="lg" key="2" /></button>,
+ <button key="4" className="pdfMenu-button" title="Delete Anchor" onPointerDown={this.deleteClicked}><FontAwesomeIcon icon="trash-alt" size="lg" key="1" /></button>,
+ <button key="5" className="pdfMenu-button" title="Pin to Presentation" onPointerDown={this.PinToPres}><FontAwesomeIcon icon="map-pin" size="lg" key="2" /></button>,
<div className="pdfMenu-addTag" key="3">
- <input onChange={this.keyChanged} placeholder="Key" style={{ gridColumn: 1 }} />
- <input onChange={this.valueChanged} placeholder="Value" style={{ gridColumn: 3 }} />
+ <input onKeyDown={handleBackspace} onChange={this.keyChanged} placeholder="Key" style={{ gridColumn: 1 }} />
+ <input onKeyDown={handleBackspace} onChange={this.valueChanged} placeholder="Value" style={{ gridColumn: 3 }} />
</div>,
- <button className="pdfMenu-button" title={`Add tag: ${this._keyValue} with value: ${this._valueValue}`} onPointerDown={this.addTag}><FontAwesomeIcon style={{ transition: "all .2s" }} color={this._added ? "#42f560" : "white"} icon="check" size="lg" key="4" /></button>,
+ <button key="6" className="pdfMenu-button" title={`Add tag: ${this._keyValue} with value: ${this._valueValue}`} onPointerDown={this.addTag}><FontAwesomeIcon style={{ transition: "all .2s" }} color={this._added ? "#42f560" : "white"} icon="check" size="lg" key="4" /></button>,
];
return (
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 8af29110f..b7ded7e06 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -4,26 +4,22 @@ 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 } 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";
+import { KeyCodes } from "../../northstar/utils/KeyCodes";
const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer");
export const scale = 2;
@@ -89,15 +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);
@@ -125,28 +118,14 @@ export class Viewer extends React.Component<IViewerProps> {
}, { fireImmediately: true });
this._annotationReactionDisposer = reaction(
- () => this.props.parent.Document && DocListCast(this.props.parent.Document.annotations),
- (annotations: Doc[]) =>
- annotations && annotations.length && this.renderAnnotations(annotations, true),
+ () => {
+ return this.props.parent && this.props.parent.fieldExtensionDoc && DocListCast(this.props.parent.fieldExtensionDoc.annotations);
+ },
+ (annotations: Doc[]) => {
+ annotations && annotations.length && this.renderAnnotations(annotations, true);
+ },
{ 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(
@@ -156,7 +135,9 @@ export class Viewer extends React.Component<IViewerProps> {
let scriptfield = Cast(this.props.parent.Document.filterScript, ScriptField);
this._script = scriptfield ? scriptfield.script : CompileScript("return true");
if (this.props.parent.props.ContainingCollectionView) {
- let ccvAnnos = DocListCast(this.props.parent.props.ContainingCollectionView.props.Document.annotations);
+ let fieldDoc = Doc.resolvedFieldDataDoc(this.props.parent.props.ContainingCollectionView.props.DataDoc ?
+ this.props.parent.props.ContainingCollectionView.props.DataDoc : this.props.parent.props.ContainingCollectionView.props.Document, this.props.parent.props.ContainingCollectionView.props.fieldKey, "true");
+ let ccvAnnos = DocListCast(fieldDoc.annotations);
ccvAnnos.forEach(d => {
if (this._script && this._script.compiled) {
let run = this._script.run(d);
@@ -185,7 +166,9 @@ export class Viewer extends React.Component<IViewerProps> {
}
scrollTo(y: number) {
- this.props.parent.scrollTo(y);
+ if (this.props.mainCont.current) {
+ this.props.parent.scrollTo(y - this.props.mainCont.current.clientHeight);
+ }
}
@action
@@ -229,12 +212,13 @@ export class Viewer extends React.Component<IViewerProps> {
}
}
+ @action
makeAnnotationDocument = (sourceDoc: Doc | undefined, s: number, color: string): Doc => {
let annoDocs: Doc[] = [];
- let mainAnnoDoc = Docs.CreateInstance(new Doc(), "", {});
+ let mainAnnoDoc = Docs.Create.InstanceFromProto(new Doc(), "", {});
mainAnnoDoc.title = "Annotation on " + StrCast(this.props.parent.Document.title);
- mainAnnoDoc.pdfDoc = this.props.parent.Document;
+ mainAnnoDoc.pdfDoc = this.props.parent.props.Document;
let minY = Number.MAX_VALUE;
this._savedAnnotations.forEach((key: number, value: HTMLDivElement[]) => {
for (let anno of value) {
@@ -270,13 +254,14 @@ export class Viewer extends React.Component<IViewerProps> {
if (de.data instanceof DragManager.LinkDragData) {
let sourceDoc = de.data.linkSourceDocument;
let destDoc = this.makeAnnotationDocument(sourceDoc, 1, "red");
- let targetAnnotations = DocListCast(this.props.parent.Document.annotations);
+ de.data.droppedDocuments.push(destDoc);
+ let targetAnnotations = DocListCast(this.props.parent.fieldExtensionDoc.annotations);
if (targetAnnotations) {
targetAnnotations.push(destDoc);
- this.props.parent.Document.annotations = new List<Doc>(targetAnnotations);
+ this.props.parent.fieldExtensionDoc.annotations = new List<Doc>(targetAnnotations);
}
else {
- this.props.parent.Document.annotations = new List<Doc>([destDoc]);
+ this.props.parent.fieldExtensionDoc.annotations = new List<Doc>([destDoc]);
}
e.stopPropagation();
}
@@ -335,12 +320,12 @@ 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` }} />);
} catch (e) {
-
+ console.log(e);
}
}
}
@@ -442,11 +427,7 @@ export class Viewer extends React.Component<IViewerProps> {
@action
search = (searchString: string) => {
- if (searchString.length === 0) {
- return;
- }
-
- if (this._rendered) {
+ if (this._pdfViewer._pageViewsReady) {
this._pdfFindController.executeCommand('find',
{
caseSensitive: false,
@@ -459,6 +440,17 @@ export class Viewer extends React.Component<IViewerProps> {
else {
let container = this._mainCont.current;
if (container) {
+ container.addEventListener("pagesloaded", () => {
+ console.log("rendered");
+ this._pdfFindController.executeCommand('find',
+ {
+ caseSensitive: false,
+ findPrevious: undefined,
+ highlightAll: true,
+ phraseSearch: true,
+ query: searchString
+ });
+ });
container.addEventListener("pagerendered", () => {
console.log("rendered");
this._pdfFindController.executeCommand('find',
@@ -469,7 +461,6 @@ export class Viewer extends React.Component<IViewerProps> {
phraseSearch: true,
query: searchString
});
- this._rendered = true;
});
}
}
@@ -533,23 +524,22 @@ export class Viewer extends React.Component<IViewerProps> {
if (!this._pdfFindController) {
if (container && viewer) {
let simpleLinkService = new SimpleLinkService();
- let pdfViewer = new PDFJSViewer.PDFViewer({
+ this._pdfViewer = new PDFJSViewer.PDFViewer({
container: container,
viewer: viewer,
linkService: simpleLinkService
});
simpleLinkService.setPdf(this.props.pdf);
container.addEventListener("pagesinit", () => {
- pdfViewer.currentScaleValue = 1;
+ this._pdfViewer.currentScaleValue = 1;
});
container.addEventListener("pagerendered", () => {
console.log("rendered");
- this._rendered = true;
});
- pdfViewer.setDocument(this.props.pdf);
- this._pdfFindController = new PDFJSViewer.PDFFindController(pdfViewer);
+ this._pdfViewer.setDocument(this.props.pdf);
+ this._pdfFindController = new PDFJSViewer.PDFFindController(this._pdfViewer);
// this._pdfFindController._linkService = pdfLinkService;
- pdfViewer.findController = this._pdfFindController;
+ this._pdfViewer.findController = this._pdfFindController;
}
}
}
@@ -588,7 +578,7 @@ export class Viewer extends React.Component<IViewerProps> {
}
return true;
});
- this.Index = Math.min(this.Index + 1, filtered.length - 1)
+ this.Index = Math.min(this.Index + 1, filtered.length - 1);
}
nextResult = () => {
@@ -648,7 +638,7 @@ export class Viewer extends React.Component<IViewerProps> {
<button className="pdfViewer-overlayButton" title="Open Search Bar"></button>
{/* <button title="Previous Result" onClick={() => this.search(this._searchString)}><FontAwesomeIcon icon="arrow-up" size="3x" color="white" /></button>
<button title="Next Result" onClick={this.nextResult}><FontAwesomeIcon icon="arrow-down" size="3x" color="white" /></button> */}
- <input placeholder="Search" className="pdfViewer-overlaySearchBar" onChange={this.searchStringChanged} />
+ <input onKeyDown={(e: React.KeyboardEvent) => e.keyCode === KeyCodes.ENTER ? this.search(this._searchString) : e.keyCode === KeyCodes.BACKSPACE ? e.stopPropagation() : true} placeholder="Search" className="pdfViewer-overlaySearchBar" onChange={this.searchStringChanged} />
<button title="Search" onClick={() => this.search(this._searchString)}><FontAwesomeIcon icon="search" size="3x" color="white" /></button>
</div>
<button className="pdfViewer-overlayButton" onClick={this.prevAnnotation} title="Previous Annotation"
@@ -684,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/pdf/Page.tsx b/src/client/views/pdf/Page.tsx
index 49eac71c4..c9d442fe5 100644
--- a/src/client/views/pdf/Page.tsx
+++ b/src/client/views/pdf/Page.tsx
@@ -2,7 +2,7 @@ import { observer } from "mobx-react";
import React = require("react");
import { observable, action, runInAction, IReactionDisposer, reaction } from "mobx";
import * as Pdfjs from "pdfjs-dist";
-import { Opt, Doc, FieldResult, Field, DocListCast, WidthSym, HeightSym } from "../../../new_fields/Doc";
+import { Opt, Doc, FieldResult, Field, DocListCast, WidthSym, HeightSym, DocListCastAsync } from "../../../new_fields/Doc";
import "./PDFViewer.scss";
import "pdfjs-dist/web/pdf_viewer.css";
import { PDFBox } from "../nodes/PDFBox";
@@ -10,7 +10,7 @@ import { DragManager } from "../../util/DragManager";
import { Docs, DocUtils } from "../../documents/Documents";
import { List } from "../../../new_fields/List";
import { emptyFunction } from "../../../Utils";
-import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { Cast, NumCast, StrCast, BoolCast } from "../../../new_fields/Types";
import { listSpec } from "../../../new_fields/Schema";
import { menuBar } from "prosemirror-menu";
import { AnnotationTypes, PDFViewer, scale } from "./PDFViewer";
@@ -55,6 +55,8 @@ export default class Page extends React.Component<IPageProps> {
// private _curly: React.RefObject<HTMLImageElement>;
private _marqueeing: boolean = false;
private _reactionDisposer?: IReactionDisposer;
+ private _startY: number = 0;
+ private _startX: number = 0;
constructor(props: IPageProps) {
super(props);
@@ -138,9 +140,9 @@ export default class Page extends React.Component<IPageProps> {
highlight = (targetDoc?: Doc, color: string = "red") => {
// creates annotation documents for current highlights
let annotationDoc = this.props.makeAnnotationDocuments(targetDoc, scale, color, false);
- let targetAnnotations = Cast(this.props.parent.Document.annotations, listSpec(Doc));
+ let targetAnnotations = Cast(this.props.parent.fieldExtensionDoc.annotations, listSpec(Doc));
if (targetAnnotations === undefined) {
- Doc.GetProto(this.props.parent.Document).annotations = new List([annotationDoc]);
+ Doc.GetProto(this.props.parent.fieldExtensionDoc).annotations = new List([annotationDoc]);
} else {
targetAnnotations.push(annotationDoc);
}
@@ -152,20 +154,34 @@ export default class Page extends React.Component<IPageProps> {
* start a drag event and create or put the necessary info into the drag event.
*/
@action
- startDrag = (e: PointerEvent, ele: HTMLDivElement): void => {
+ startDrag = (e: PointerEvent, ele: HTMLElement): void => {
e.preventDefault();
e.stopPropagation();
let thisDoc = this.props.parent.Document;
// document that this annotation is linked to
- let targetDoc = Docs.TextDocument({ width: 200, height: 200, title: "New Annotation" });
+ let targetDoc = Docs.Create.TextDocument({ width: 200, height: 200, title: "New Annotation" });
targetDoc.targetPage = this.props.page;
- let annotationDoc = this.highlight(targetDoc, "red");
+ let annotationDoc = this.highlight(undefined, "red");
+ annotationDoc.linkedToDoc = false;
// create dragData and star tdrag
let dragData = new DragManager.AnnotationDragData(thisDoc, annotationDoc, targetDoc);
if (this._textLayer.current) {
DragManager.StartAnnotationDrag([ele], dragData, e.pageX, e.pageY, {
handlers: {
- dragComplete: emptyFunction,
+ dragComplete: async () => {
+ if (!(await annotationDoc.linkedToDoc)) {
+ let annotations = await DocListCastAsync(annotationDoc.annotations);
+ if (annotations) {
+ annotations.forEach(anno => {
+ anno.target = targetDoc;
+ });
+ }
+ let pdfDoc = await Cast(annotationDoc.pdfDoc, Doc);
+ if (pdfDoc) {
+ DocUtils.MakeLink(annotationDoc, targetDoc, undefined, `Annotation from ${StrCast(pdfDoc.title)}`, "", StrCast(pdfDoc.title));
+ }
+ }
+ }
},
hideSource: false
});
@@ -220,8 +236,8 @@ export default class Page extends React.Component<IPageProps> {
let current = this._textLayer.current;
if (current) {
let boundingRect = current.getBoundingClientRect();
- this._marqueeX = (e.clientX - boundingRect.left) * (current.offsetWidth / boundingRect.width);
- this._marqueeY = (e.clientY - boundingRect.top) * (current.offsetHeight / boundingRect.height);
+ this._startX = this._marqueeX = (e.clientX - boundingRect.left) * (current.offsetWidth / boundingRect.width);
+ this._startY = this._marqueeY = (e.clientY - boundingRect.top) * (current.offsetHeight / boundingRect.height);
}
this._marqueeing = true;
if (this._marquee.current) this._marquee.current.style.opacity = "0.2";
@@ -244,8 +260,12 @@ export default class Page extends React.Component<IPageProps> {
if (current) {
// transform positions and find the width and height to set the marquee to
let boundingRect = current.getBoundingClientRect();
- this._marqueeWidth = (e.clientX - boundingRect.left) * (current.offsetWidth / boundingRect.width) - this._marqueeX;
- this._marqueeHeight = (e.clientY - boundingRect.top) * (current.offsetHeight / boundingRect.height) - this._marqueeY;
+ this._marqueeWidth = ((e.clientX - boundingRect.left) * (current.offsetWidth / boundingRect.width)) - this._startX;
+ this._marqueeHeight = ((e.clientY - boundingRect.top) * (current.offsetHeight / boundingRect.height)) - this._startY;
+ this._marqueeX = Math.min(this._startX, this._startX + this._marqueeWidth);
+ this._marqueeY = Math.min(this._startY, this._startY + this._marqueeHeight);
+ this._marqueeWidth = Math.abs(this._marqueeWidth);
+ this._marqueeHeight = Math.abs(this._marqueeHeight);
let { background, opacity, transform: transform } = this.getCurlyTransform();
if (this._marquee.current /*&& this._curly.current*/) {
this._marquee.current.style.background = background;
diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx
index 6896ee452..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,33 +778,35 @@ 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 = false; };
+ 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",
- outlineWidth: BoolCast(p.document.libraryBrush, false) ? `1px` : "0px",
+ outlineWidth: BoolCast(p.document.libraryBrush) ? `1px` : "0px",
}}
onClick={e => { p.gotoDocument(p.index, NumCast(this.props.mainDocument.selectedDoc)); e.stopPropagation(); }}>
<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 a3fa553b7..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
@@ -591,7 +629,7 @@ export class PresentationView extends React.Component<PresViewProps> {
@action
addNewPresentation = (presTitle: string) => {
//creating a new presentation doc
- let newPresentationDoc = Docs.TreeDocument([], { title: presTitle });
+ let newPresentationDoc = Docs.Create.TreeDocument([], { title: presTitle });
this.props.Documents.push(newPresentationDoc);
//setting that new doc as current
@@ -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/FieldFilters.tsx b/src/client/views/search/FieldFilters.tsx
index 648aac20a..7a33282d2 100644
--- a/src/client/views/search/FieldFilters.tsx
+++ b/src/client/views/search/FieldFilters.tsx
@@ -34,7 +34,7 @@ export class FieldFilters extends React.Component<FieldFilterProps> {
<div className="field-filters">
<CheckBox default={true} numCount={3} parent={this} originalStatus={this.props.titleFieldStatus} updateStatus={this.props.updateTitleStatus} title={Keys.TITLE} />
<CheckBox default={true} numCount={3} parent={this} originalStatus={this.props.authorFieldStatus} updateStatus={this.props.updateAuthorStatus} title={Keys.AUTHOR} />
- <CheckBox default={true} numCount={3} parent={this} originalStatus={this.props.dataFieldStatus} updateStatus={this.props.updateDataStatus} title={Keys.DATA} />
+ <CheckBox default={false} numCount={3} parent={this} originalStatus={this.props.dataFieldStatus} updateStatus={this.props.updateDataStatus} title={"Deleted Docs"} />
</div>
);
}
diff --git a/src/client/views/search/FilterBox.tsx b/src/client/views/search/FilterBox.tsx
index 6053db595..4b1887339 100644
--- a/src/client/views/search/FilterBox.tsx
+++ b/src/client/views/search/FilterBox.tsx
@@ -6,7 +6,7 @@ import { faTimes, faCheckCircle, faObjectGroup } from '@fortawesome/free-solid-s
import { library } from '@fortawesome/fontawesome-svg-core';
import { Doc } from '../../../new_fields/Doc';
import { Id } from '../../../new_fields/FieldSymbols';
-import { DocTypes } from '../../documents/Documents';
+import { DocumentType } from '../../documents/Documents';
import { Cast, StrCast } from '../../../new_fields/Types';
import * as _ from "lodash";
import { ToggleBar } from './ToggleBar';
@@ -35,7 +35,7 @@ export enum Keys {
export class FilterBox extends React.Component {
static Instance: FilterBox;
- public _allIcons: string[] = [DocTypes.AUDIO, DocTypes.COL, DocTypes.HIST, DocTypes.IMG, DocTypes.LINK, DocTypes.PDF, DocTypes.TEXT, DocTypes.VID, DocTypes.WEB];
+ public _allIcons: string[] = [DocumentType.AUDIO, DocumentType.COL, DocumentType.HIST, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.TEXT, DocumentType.VID, DocumentType.WEB];
//if true, any keywords can be used. if false, all keywords are required.
//this also serves as an indicator if the word status filter is applied
@@ -48,6 +48,7 @@ export class FilterBox extends React.Component {
@observable private _authorFieldStatus: boolean = true;
@observable private _dataFieldStatus: boolean = true;
//this also serves as an indicator if the collection status filter is applied
+ @observable public _deletedDocsStatus: boolean = false;
@observable private _collectionStatus = false;
@observable private _collectionSelfStatus = true;
@observable private _collectionParentStatus = true;
@@ -94,6 +95,9 @@ export class FilterBox extends React.Component {
}
});
+
+ let el = acc[i] as HTMLElement;
+ el.click();
}
});
}
@@ -168,13 +172,13 @@ export class FilterBox extends React.Component {
if (this._authorFieldStatus) {
finalQuery = finalQuery + this.basicFieldFilters(query, Keys.AUTHOR);
}
- if (this._dataFieldStatus) {
+ if (this._deletedDocsStatus) {
finalQuery = finalQuery + this.basicFieldFilters(query, Keys.DATA);
}
return finalQuery;
}
- get fieldFiltersApplied() { return !(this._dataFieldStatus && this._authorFieldStatus && this._titleFieldStatus); }
+ get fieldFiltersApplied() { return !(this._authorFieldStatus && this._titleFieldStatus); }
//TODO: basically all of this
//gets all of the collections of all the docviews that are selected
@@ -244,12 +248,19 @@ export class FilterBox extends React.Component {
return "+(" + finalColString + ")" + query;
}
+ get filterTypes() {
+ return this._icons.length === 9 ? undefined : this._icons;
+ }
+
@action
filterDocsByType(docs: Doc[]) {
+ if (this._icons.length === 9) {
+ return docs;
+ }
let finalDocs: Doc[] = [];
docs.forEach(doc => {
let layoutresult = Cast(doc.type, "string");
- if (!layoutresult || this._icons.includes(layoutresult)) {
+ if (layoutresult && this._icons.includes(layoutresult)) {
finalDocs.push(doc);
}
});
@@ -339,7 +350,7 @@ export class FilterBox extends React.Component {
updateAuthorStatus(newStat: boolean) { this._authorFieldStatus = newStat; }
@action.bound
- updateDataStatus(newStat: boolean) { this._dataFieldStatus = newStat; }
+ updateDataStatus(newStat: boolean) { this._deletedDocsStatus = newStat; }
@action.bound
updateCollectionStatus(newStat: boolean) { this._collectionStatus = newStat; }
@@ -355,7 +366,7 @@ export class FilterBox extends React.Component {
getParentCollectionStatus() { return this._collectionParentStatus; }
getTitleStatus() { return this._titleFieldStatus; }
getAuthorStatus() { return this._authorFieldStatus; }
- getDataStatus() { return this._dataFieldStatus; }
+ getDataStatus() { return this._deletedDocsStatus; }
getActiveFilters() {
console.log(this._authorFieldStatus, this._titleFieldStatus, this._dataFieldStatus);
@@ -433,7 +444,7 @@ export class FilterBox extends React.Component {
<div style={{ marginLeft: "auto" }}><NaviconButton onClick={this.toggleFieldOpen} /></div>
</div>
<div className="filter-panel"><FieldFilters
- titleFieldStatus={this._titleFieldStatus} dataFieldStatus={this._dataFieldStatus} authorFieldStatus={this._authorFieldStatus}
+ titleFieldStatus={this._titleFieldStatus} dataFieldStatus={this._deletedDocsStatus} authorFieldStatus={this._authorFieldStatus}
updateAuthorStatus={this.updateAuthorStatus} updateDataStatus={this.updateDataStatus} updateTitleStatus={this.updateTitleStatus} /> </div>
</div>
</div>
diff --git a/src/client/views/search/IconBar.tsx b/src/client/views/search/IconBar.tsx
index 744dd898a..4712b0abc 100644
--- a/src/client/views/search/IconBar.tsx
+++ b/src/client/views/search/IconBar.tsx
@@ -4,7 +4,7 @@ import { observable, action } from 'mobx';
// import "./SearchBox.scss";
import "./IconBar.scss";
import "./IconButton.scss";
-import { DocTypes } from '../../documents/Documents';
+import { DocumentType } from '../../documents/Documents';
import { faSearch, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faMusic, faLink, faChartBar, faGlobeAsia, faBan, faTimesCircle, faCheckCircle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { library } from '@fortawesome/fontawesome-svg-core';
@@ -63,7 +63,7 @@ export class IconBar extends React.Component {
<div className="type-outer">
<div className={"type-icon all"}
onClick={this.selectAll}>
- <FontAwesomeIcon className="fontawesome-icon" icon={faCheckCircle} />
+ <FontAwesomeIcon className="fontawesome-icon" icon={faCheckCircle} />
</div>
<div className="filter-description">Select All</div>
</div>
diff --git a/src/client/views/search/IconButton.tsx b/src/client/views/search/IconButton.tsx
index 23ab42de0..bfe2c7d0b 100644
--- a/src/client/views/search/IconButton.tsx
+++ b/src/client/views/search/IconButton.tsx
@@ -6,7 +6,7 @@ import "./IconButton.scss";
import { faSearch, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faMusic, faLink, faChartBar, faGlobeAsia, faBan, faVideo, faCaretDown } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { library, icon } from '@fortawesome/fontawesome-svg-core';
-import { DocTypes } from '../../documents/Documents';
+import { DocumentType } from '../../documents/Documents';
import '../globalCssVariables.scss';
import * as _ from "lodash";
import { IconBar } from './IconBar';
@@ -80,25 +80,25 @@ export class IconButton extends React.Component<IconButtonProps>{
@action.bound
getIcon() {
switch (this.props.type) {
- case (DocTypes.NONE):
+ case (DocumentType.NONE):
return faBan;
- case (DocTypes.AUDIO):
+ case (DocumentType.AUDIO):
return faMusic;
- case (DocTypes.COL):
+ case (DocumentType.COL):
return faObjectGroup;
- case (DocTypes.HIST):
+ case (DocumentType.HIST):
return faChartBar;
- case (DocTypes.IMG):
+ case (DocumentType.IMG):
return faImage;
- case (DocTypes.LINK):
+ case (DocumentType.LINK):
return faLink;
- case (DocTypes.PDF):
+ case (DocumentType.PDF):
return faFilePdf;
- case (DocTypes.TEXT):
+ case (DocumentType.TEXT):
return faStickyNote;
- case (DocTypes.VID):
+ case (DocumentType.VID):
return faVideo;
- case (DocTypes.WEB):
+ case (DocumentType.WEB):
return faGlobeAsia;
default:
return faCaretDown;
@@ -149,25 +149,25 @@ export class IconButton extends React.Component<IconButtonProps>{
getFA = () => {
switch (this.props.type) {
- case (DocTypes.NONE):
+ case (DocumentType.NONE):
return (<FontAwesomeIcon className="fontawesome-icon" icon={faBan} />);
- case (DocTypes.AUDIO):
+ case (DocumentType.AUDIO):
return (<FontAwesomeIcon className="fontawesome-icon" icon={faMusic} />);
- case (DocTypes.COL):
+ case (DocumentType.COL):
return (<FontAwesomeIcon className="fontawesome-icon" icon={faObjectGroup} />);
- case (DocTypes.HIST):
+ case (DocumentType.HIST):
return (<FontAwesomeIcon className="fontawesome-icon" icon={faChartBar} />);
- case (DocTypes.IMG):
+ case (DocumentType.IMG):
return (<FontAwesomeIcon className="fontawesome-icon" icon={faImage} />);
- case (DocTypes.LINK):
+ case (DocumentType.LINK):
return (<FontAwesomeIcon className="fontawesome-icon" icon={faLink} />);
- case (DocTypes.PDF):
+ case (DocumentType.PDF):
return (<FontAwesomeIcon className="fontawesome-icon" icon={faFilePdf} />);
- case (DocTypes.TEXT):
+ case (DocumentType.TEXT):
return (<FontAwesomeIcon className="fontawesome-icon" icon={faStickyNote} />);
- case (DocTypes.VID):
+ case (DocumentType.VID):
return (<FontAwesomeIcon className="fontawesome-icon" icon={faVideo} />);
- case (DocTypes.WEB):
+ case (DocumentType.WEB):
return (<FontAwesomeIcon className="fontawesome-icon" icon={faGlobeAsia} />);
default:
return (<FontAwesomeIcon className="fontawesome-icon" icon={faCaretDown} />);
diff --git a/src/client/views/search/Pager.scss b/src/client/views/search/Pager.scss
deleted file mode 100644
index 2b9c81b93..000000000
--- a/src/client/views/search/Pager.scss
+++ /dev/null
@@ -1,47 +0,0 @@
-@import "../globalCssVariables";
-
-.search-pager {
- background-color: $dark-color;
- border-radius: 10px;
- width: 500px;
- display: flex;
- justify-content: center;
- // margin-left: 27px;
- float: right;
- margin-right: 74px;
- margin-left: auto;
-
- // flex-direction: column;
-
- .search-arrows {
- display: flex;
- justify-content: center;
- margin: 10px;
- width: 50%;
-
- .arrow {
- -webkit-transition: all 0.2s ease-in-out;
- -moz-transition: all 0.2s ease-in-out;
- -o-transition: all 0.2s ease-in-out;
- transition: all 0.2s ease-in-out;
-
- .fontawesome-icon {
- color: $light-color;
- width: 20px;
- height: 20px;
- margin-right: 2px;
- margin-left: 2px;
- // opacity: .7;
- }
- }
-
- .pager-title {
- text-align: center;
- // font-size: 8px;
- // margin-bottom: 10px;
- color: $light-color;
- // padding: 2px;
- width: 40%;
- }
- }
-} \ No newline at end of file
diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss
index 7541b328a..fcdc79220 100644
--- a/src/client/views/search/SearchBox.scss
+++ b/src/client/views/search/SearchBox.scss
@@ -41,7 +41,7 @@
}
.searchBox-results {
- margin-left: 27px;
+ margin-right: 136px;
top: 300px;
display: flex;
flex-direction: column;
@@ -50,6 +50,9 @@
height: 100%;
// overflow: hidden;
// overflow-y: auto;
+ max-height: 560px;
+ overflow: hidden;
+ overflow-y: auto;
.no-result {
width: 500px;
@@ -61,5 +64,6 @@
text-transform: uppercase;
text-align: left;
font-weight: bold;
+ margin-left: 28px;
}
} \ No newline at end of file
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 393a5a924..2214ac8af 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -1,38 +1,49 @@
import * as React from 'react';
import { observer } from 'mobx-react';
-import { observable, action, runInAction } from 'mobx';
+import { observable, action, runInAction, flow, computed } from 'mobx';
import "./SearchBox.scss";
import "./FilterBox.scss";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { SetupDrag } from '../../util/DragManager';
import { Docs } from '../../documents/Documents';
-import { NumCast } from '../../../new_fields/Types';
+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
export class SearchBox extends React.Component {
@observable private _searchString: string = "";
@observable private _resultsOpen: boolean = false;
- @observable private _results: Doc[] = [];
+ @observable private _searchbarOpen: boolean = false;
+ @observable private _results: [Doc, string[]][] = [];
+ private _resultsSet = new Map<Doc, number>();
@observable private _openNoResults: boolean = false;
- @observable public _pageNum: number = 0;
- //temp
- @observable public _maxNum: number = 10;
+ @observable private _visibleElements: JSX.Element[] = [];
+
+ private resultsRef = React.createRef<HTMLDivElement>();
+
+ private _isSearch: ("search" | "placeholder" | undefined)[] = [];
+ private _numTotalResults = -1;
+ private _endIndex = -1;
static Instance: SearchBox;
+ private _maxSearchIndex: number = 0;
+ private _curRequest?: Promise<any> = undefined;
+
constructor(props: any) {
super(props);
SearchBox.Instance = this;
+ this.resultsScrolled = this.resultsScrolled.bind(this);
}
@action
@@ -49,19 +60,21 @@ export class SearchBox extends React.Component {
onChange(e: React.ChangeEvent<HTMLInputElement>) {
this._searchString = e.target.value;
- if (this._searchString === "") {
- this._results = [];
- this._openNoResults = false;
- }
+ this._openNoResults = false;
+ this._results = [];
+ this._resultsSet.clear();
+ this._visibleElements = [];
+ this._numTotalResults = -1;
+ this._endIndex = -1;
+ this._curRequest = undefined;
+ this._maxSearchIndex = 0;
}
- enter = (e: React.KeyboardEvent) => {
- if (e.key === "Enter") { this.submitSearch(); }
- }
+ enter = (e: React.KeyboardEvent) => { if (e.key === "Enter") { this.submitSearch(); } };
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,
@@ -78,38 +91,97 @@ export class SearchBox extends React.Component {
@action
submitSearch = async () => {
- let query = this._searchString; // searchbox gets query
- let results: Doc[];
-
+ let query = this._searchString;
query = FilterBox.Instance.getFinalQuery(query);
+ this._results = [];
+ this._resultsSet.clear();
+ this._isSearch = [];
+ this._visibleElements = [];
+ FilterBox.Instance.closeFilter();
//if there is no query there should be no result
if (query === "") {
- results = [];
+ return;
}
else {
- //gets json result into a list of documents that can be used
- //these are filtered by type
- results = await this.getResults(query);
+ this._endIndex = 12;
+ this._maxSearchIndex = 0;
+ this._numTotalResults = -1;
+ await this.getResults(query);
}
runInAction(() => {
this._resultsOpen = true;
- this._results = results;
+ this._searchbarOpen = true;
this._openNoResults = true;
+ this.resultsScrolled();
});
}
- @action
+ getAllResults = async (query: string) => {
+ return SearchUtil.Search(query, true, { fq: this.filterQuery, start: 0, rows: 10000000 });
+ }
+
+ private get filterQuery() {
+ const types = FilterBox.Instance.filterTypes;
+ const includeDeleted = FilterBox.Instance.getDataStatus();
+ return "NOT baseProto_b:true" + (includeDeleted ? "" : " AND NOT deleted:true") + (types ? ` AND (${types.map(type => `({!join from=id to=proto_i}type_t:"${type}" AND NOT type_t:*) OR type_t:"${type}"`).join(" ")})` : "");
+ }
+
+
+ private lockPromise?: Promise<void>;
getResults = async (query: string) => {
- const { docs } = await SearchUtil.Search(query, true);
- return FilterBox.Instance.filterDocsByType(docs);
+ if (this.lockPromise) {
+ await this.lockPromise;
+ }
+ this.lockPromise = new Promise(async res => {
+ while (this._results.length <= this._endIndex && (this._numTotalResults === -1 || this._maxSearchIndex < this._numTotalResults)) {
+ this._curRequest = SearchUtil.Search(query, true, { fq: this.filterQuery, start: this._maxSearchIndex, rows: 10, hl: true, "hl.fl": "*" }).then(action(async (res: SearchUtil.DocSearchResult) => {
+
+ // happens at the beginning
+ if (res.numFound !== this._numTotalResults && this._numTotalResults === -1) {
+ this._numTotalResults = res.numFound;
+ }
+
+ const highlighting = res.highlighting || {};
+ const highlightList = res.docs.map(doc => highlighting[doc[Id]]);
+ const docs = await Promise.all(res.docs.map(async doc => (await Cast(doc.extendsDoc, Doc)) || doc));
+ const highlights: typeof res.highlighting = {};
+ docs.forEach((doc, index) => highlights[doc[Id]] = highlightList[index]);
+ let filteredDocs = FilterBox.Instance.filterDocsByType(docs);
+ runInAction(() => {
+ // this._results.push(...filteredDocs);
+ 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)) : [];
+ if (index === undefined) {
+ this._resultsSet.set(doc, this._results.length);
+ this._results.push([doc, hlights]);
+ } else {
+ this._results[index][1].push(...hlights);
+ }
+ });
+ });
+
+ this._curRequest = undefined;
+ }));
+ this._maxSearchIndex += 10;
+
+ await this._curRequest;
+ }
+ this.resultsScrolled();
+ res();
+ });
+ return this.lockPromise;
}
collectionRef = React.createRef<HTMLSpanElement>();
startDragCollection = async () => {
- const results = await this.getResults(FilterBox.Instance.getFinalQuery(this._searchString));
- const docs = results.map(doc => {
+ let res = await this.getAllResults(FilterBox.Instance.getFinalQuery(this._searchString));
+ let filtered = FilterBox.Instance.filterDocsByType(res.docs);
+ // console.log(this._results)
+ const docs = filtered.map(doc => {
const isProto = Doc.GetT(doc, "isPrototype", "boolean", true);
if (isProto) {
return Doc.MakeDelegate(doc);
@@ -141,7 +213,8 @@ export class SearchBox extends React.Component {
y += 300;
}
}
- return Docs.FreeformDocument(docs, { width: 400, height: 400, panX: 175, panY: 175, backgroundColor: "grey", title: `Search Docs: "${this._searchString}"` });
+ return Docs.Create.FreeformDocument(docs, { width: 400, height: 400, panX: 175, panY: 175, backgroundColor: "grey", title: `Search Docs: "${this._searchString}"` });
+
}
@action.bound
@@ -150,6 +223,7 @@ export class SearchBox extends React.Component {
this._openNoResults = false;
FilterBox.Instance.closeFilter();
this._resultsOpen = true;
+ this._searchbarOpen = true;
FilterBox.Instance._pointerTime = e.timeStamp;
}
@@ -157,34 +231,107 @@ export class SearchBox extends React.Component {
closeSearch = () => {
FilterBox.Instance.closeFilter();
this.closeResults();
+ this._searchbarOpen = false;
}
@action.bound
closeResults() {
this._resultsOpen = false;
this._results = [];
+ this._resultsSet.clear();
+ this._visibleElements = [];
+ this._numTotalResults = -1;
+ this._endIndex = -1;
+ this._curRequest = undefined;
}
+ @action
+ resultsScrolled = (e?: React.UIEvent<HTMLDivElement>) => {
+ let scrollY = e ? e.currentTarget.scrollTop : this.resultsRef.current ? this.resultsRef.current.scrollTop : 0;
+ let buffer = 4;
+ let startIndex = Math.floor(Math.max(0, scrollY / 70 - buffer));
+ let endIndex = Math.ceil(Math.min(this._numTotalResults - 1, startIndex + (560 / 70) + buffer));
+
+ this._endIndex = endIndex === -1 ? 12 : endIndex;
+
+ if ((this._numTotalResults === 0 || this._results.length === 0) && this._openNoResults) {
+ this._visibleElements = [<div className="no-result">No Search Results</div>];
+ return;
+ }
+
+ if (this._numTotalResults <= this._maxSearchIndex) {
+ this._numTotalResults = this._results.length;
+ }
+
+ // only hit right at the beginning
+ // visibleElements is all of the elements (even the ones you can't see)
+ else if (this._visibleElements.length !== this._numTotalResults) {
+ // undefined until a searchitem is put in there
+ this._visibleElements = Array<JSX.Element>(this._numTotalResults === -1 ? 0 : this._numTotalResults);
+ // indicates if things are placeholders
+ this._isSearch = Array<undefined>(this._numTotalResults === -1 ? 0 : this._numTotalResults);
+ }
+
+ for (let i = 0; i < this._numTotalResults; i++) {
+ //if the index is out of the window then put a placeholder in
+ //should ones that have already been found get set to placeholders?
+ if (i < startIndex || i > endIndex) {
+ if (this._isSearch[i] !== "placeholder") {
+ this._isSearch[i] = "placeholder";
+ this._visibleElements[i] = <div className="searchBox-placeholder" key={`searchBox-placeholder-${i}`}>Loading...</div>;
+ }
+ }
+ else {
+ if (this._isSearch[i] !== "search") {
+ let result: [Doc, string[]] | undefined = undefined;
+ if (i >= this._results.length) {
+ this.getResults(this._searchString);
+ if (i < this._results.length) result = this._results[i];
+ if (result) {
+ this._visibleElements[i] = <SearchItem doc={result[0]} key={result[0][Id]} highlighting={result[1]} />;
+ this._isSearch[i] = "search";
+ }
+ }
+ else {
+ result = this._results[i];
+ if (result) {
+ this._visibleElements[i] = <SearchItem doc={result[0]} key={result[0][Id]} highlighting={result[1]} />;
+ this._isSearch[i] = "search";
+ }
+ }
+ }
+ }
+ }
+ if (this._maxSearchIndex >= this._numTotalResults) {
+ this._visibleElements.length = this._results.length;
+ this._isSearch.length = this._results.length;
+ }
+ }
+
+ @computed
+ get resFull() { return this._numTotalResults <= 8; }
+
+ @computed
+ get resultHeight() { return this._numTotalResults * 70; }
+
render() {
return (
<div className="searchBox-container">
<div className="searchBox-bar">
- <span className="searchBox-barChild searchBox-collection" onPointerDown={SetupDrag(this.collectionRef, this.startDragCollection)} ref={this.collectionRef}>
+ <span className="searchBox-barChild searchBox-collection" onPointerDown={SetupDrag(this.collectionRef, this.startDragCollection)} ref={this.collectionRef} title="Drag Results as Collection">
<FontAwesomeIcon icon="object-group" size="lg" />
</span>
<input value={this._searchString} onChange={this.onChange} type="text" placeholder="Search..."
className="searchBox-barChild searchBox-input" onPointerDown={this.openSearch} onKeyPress={this.enter}
- style={{ width: this._resultsOpen ? "500px" : "100px" }} />
+ style={{ width: this._searchbarOpen ? "500px" : "100px" }} />
+ <button className="searchBox-barChild searchBox-submit" onClick={this.submitSearch} onPointerDown={FilterBox.Instance.stopProp}>Submit</button>
<button className="searchBox-barChild searchBox-filter" onClick={FilterBox.Instance.openFilter} onPointerDown={FilterBox.Instance.stopProp}>Filter</button>
</div>
- <div className="searchBox-results" style={this._resultsOpen ? { display: "flex" } : { display: "none" }}>
- {(this._results.length !== 0) ? (
- this._results.map(result => <SearchItem doc={result} query={this._searchString} key={result[Id]} />)
- ) :
- this._openNoResults ? (<div className="no-result">No Search Results</div>) : null}
- </div>
- <div style={this._results.length !== 0 ? { display: "flex" } : { display: "none" }}>
- {/* <Pager /> */}
+ <div className="searchBox-results" onScroll={this.resultsScrolled} style={{
+ display: this._resultsOpen ? "flex" : "none",
+ height: this.resFull ? "560px" : this.resultHeight, overflow: this.resFull ? "auto" : "visible"
+ }} ref={this.resultsRef}>
+ {this._visibleElements}
</div>
</div>
);
diff --git a/src/client/views/search/SearchItem.scss b/src/client/views/search/SearchItem.scss
index fa4c9cb38..273d49349 100644
--- a/src/client/views/search/SearchItem.scss
+++ b/src/client/views/search/SearchItem.scss
@@ -6,20 +6,25 @@
justify-content: flex-end;
height: 70px;
z-index: 0;
+}
- .search-item {
- width: 500px;
- background: $light-color-secondary;
- border-color: $intermediate-color;
- border-bottom-style: solid;
- padding: 10px;
- height: 70px;
- z-index: 0;
- display: inline-block;
-
- .main-search-info {
- display: flex;
- flex-direction: row;
+.searchBox-placeholder,
+.search-overview .search-item {
+ width: 500px;
+ background: $light-color-secondary;
+ border-color: $intermediate-color;
+ border-bottom-style: solid;
+ padding: 10px;
+ height: 70px;
+ z-index: 0;
+ display: inline-block;
+
+ .main-search-info {
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+
+ .search-title-container {
width: 100%;
.search-title {
@@ -28,160 +33,183 @@
width: 100%;
font-weight: bold;
}
+ }
- .search-info {
+ .search-info {
+ display: flex;
+ justify-content: flex-end;
+
+ .link-container.item {
+ margin-left: auto;
+ margin-right: auto;
+ height: 26px;
+ width: 26px;
+ border-radius: 13px;
+ background: $dark-color;
+ color: $light-color-secondary;
display: flex;
- justify-content: flex-end;
-
- .link-container.item {
- margin-left: auto;
- margin-right: auto;
- height: 26px;
- width: 26px;
- border-radius: 13px;
- background: $dark-color;
- color: $light-color-secondary;
- display: flex;
- justify-content: center;
- align-items: center;
- -webkit-transition: all 0.2s ease-in-out;
- -moz-transition: all 0.2s ease-in-out;
- -o-transition: all 0.2s ease-in-out;
- transition: all 0.2s ease-in-out;
- transform-origin: top right;
- overflow: hidden;
+ justify-content: center;
+ align-items: center;
+ -webkit-transition: all 0.2s ease-in-out;
+ -moz-transition: all 0.2s ease-in-out;
+ -o-transition: all 0.2s ease-in-out;
+ transition: all 0.2s ease-in-out;
+ transform-origin: top right;
+ overflow: hidden;
+ position: relative;
+
+ .link-count {
+ opacity: 1;
+ position: absolute;
+ z-index: 1000;
+ text-align: center;
+ -webkit-transition: opacity 0.2s ease-in-out;
+ -moz-transition: opacity 0.2s ease-in-out;
+ -o-transition: opacity 0.2s ease-in-out;
+ transition: opacity 0.2s ease-in-out;
+ }
+
+ .link-extended {
+ // display: none;
+ visibility: hidden;
+ opacity: 0;
position: relative;
+ z-index: 500;
+ overflow: hidden;
+ -webkit-transition: opacity 0.2s ease-in-out .2s, visibility 0s linear 0s;
+ -moz-transition: opacity 0.2s ease-in-out .2s, visibility 0s linear 0s;
+ -o-transition: opacity 0.2s ease-in-out .2s, visibility 0s linear 0s;
+ transition: opacity 0.2s ease-in-out .2s, visibility 0s linear 0s;
+ // transition-delay: 1s;
+ }
- .link-count {
- opacity: 1;
- position: absolute;
- z-index: 1000;
- text-align: center;
- -webkit-transition: opacity 0.2s ease-in-out;
- -moz-transition: opacity 0.2s ease-in-out;
- -o-transition: opacity 0.2s ease-in-out;
- transition: opacity 0.2s ease-in-out;
- }
+ }
- .link-extended {
- opacity: 0;
- position: relative;
- z-index: 500;
- overflow: hidden;
- -webkit-transition: opacity 0.2s ease-in-out;
- -moz-transition: opacity 0.2s ease-in-out;
- -o-transition: opacity 0.2s ease-in-out;
- transition: opacity 0.2s ease-in-out;
- }
- }
+ .link-container.item:hover {
+ width: 70px;
+ }
- .link-container.item:hover {
- width: 70px;
- }
+ .link-container.item:hover .link-count {
+ opacity: 0;
+ }
- .link-container.item:hover .link-count {
- opacity: 0;
- }
+ .link-container.item:hover .link-extended {
+ opacity: 1;
+ visibility: visible;
+ // display: inline;
+ }
- .link-container.item:hover .link-extended {
- opacity: 1;
- }
-
- .icon-icons {
- width:50px
- }
- .icon-live {
- width:175px;
+ .icon-icons {
+ width: 50px
+ }
+
+ .icon-live {
+ width: 175px;
+ }
+
+ .icon-icons,
+ .icon-live {
+ height: 50px;
+ margin: auto;
+ overflow: hidden;
+
+ .search-type {
+ display: inline-block;
+ width: 100%;
+ position: absolute;
+ justify-content: center;
+ align-items: center;
+ position: relative;
+ margin-right: 5px;
}
- .icon-icons, .icon-live {
- height:50px;
- margin:auto;
+ .pdfBox-cont {
overflow: hidden;
- .search-type {
- display: inline-block;
- width:100%;
- position: absolute;
- justify-content: center;
- align-items: center;
- position: relative;
- margin-right: 5px;
- }
- .pdfBox-cont {
- overflow: hidden;
-
- img {
- width:100% !important;
- height:auto !important;
- }
- }
- .search-type:hover+.search-label {
- opacity: 1;
+ img {
+ width: 100% !important;
+ height: auto !important;
}
+ }
- .search-label {
- font-size: 10;
- position: relative;
- right: 0px;
- text-transform: capitalize;
- opacity: 0;
- -webkit-transition: opacity 0.2s ease-in-out;
- -moz-transition: opacity 0.2s ease-in-out;
- -o-transition: opacity 0.2s ease-in-out;
- transition: opacity 0.2s ease-in-out;
- }
+ .search-type:hover+.search-label {
+ opacity: 1;
}
- .icon-live:hover {
- height:175px;
- .pdfBox-cont {
- img {
- width:100% !important;
- }
- }
+ .search-label {
+ font-size: 10;
+ position: relative;
+ right: 0px;
+ text-transform: capitalize;
+ opacity: 0;
+ -webkit-transition: opacity 0.2s ease-in-out;
+ -moz-transition: opacity 0.2s ease-in-out;
+ -o-transition: opacity 0.2s ease-in-out;
+ transition: opacity 0.2s ease-in-out;
}
}
- .search-info:hover {
- width:60%;
+
+ .icon-live:hover {
+ height: 175px;
+
+ .pdfBox-cont {
+ img {
+ width: 100% !important;
+ }
+ }
}
}
- }
- .search-item:hover~.searchBox-instances,
- .searchBox-instances:hover,
- .searchBox-instances:active {
- opacity: 1;
- background: $lighter-alt-accent;
- -webkit-transform: scale(1);
- -ms-transform: scale(1);
- transform: scale(1);
+ .search-info:hover {
+ width: 60%;
+ }
}
+}
- .search-item:hover {
- transition: all 0.2s;
- background: $lighter-alt-accent;
- }
+.search-item:hover~.searchBox-instances,
+.searchBox-instances:hover,
+.searchBox-instances:active {
+ opacity: 1;
+ background: $lighter-alt-accent;
+ -webkit-transform: scale(1);
+ -ms-transform: scale(1);
+ transform: scale(1);
+}
- .searchBox-instances {
- float: left;
- opacity: 1;
- width: 150px;
- transition: all 0.2s ease;
- color: black;
- transform-origin: top right;
- -webkit-transform: scale(0);
- -ms-transform: scale(0);
- transform: scale(0);
- }
+.search-item:hover {
+ transition: all 0.2s;
+ background: $lighter-alt-accent;
+}
+.searchBox-instances {
+ float: left;
+ opacity: 1;
+ width: 150px;
+ transition: all 0.2s ease;
+ color: black;
+ transform-origin: top right;
+ -webkit-transform: scale(0);
+ -ms-transform: scale(0);
+ transform: scale(0);
}
+
+
.search-overview:hover {
z-index: 1;
}
+
+.searchBox-placeholder {
+ min-height: 70px;
+ margin-left: 150px;
+ text-transform: uppercase;
+ text-align: left;
+ font-weight: bold;
+}
+
.collection {
- display:flex;
+ display: flex;
}
+
.collection-item {
- width:35px;
+ width: 35px;
} \ No newline at end of file
diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx
index 2b8d3eda3..5b0e20348 100644
--- a/src/client/views/search/SearchItem.tsx
+++ b/src/client/views/search/SearchItem.tsx
@@ -8,7 +8,7 @@ import { Doc, DocListCast, HeightSym, WidthSym } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
import { emptyFunction, returnFalse, returnOne, Utils } from "../../../Utils";
-import { DocTypes } from "../../documents/Documents";
+import { DocumentType } from "../../documents/Documents";
import { DocumentManager } from "../../util/DocumentManager";
import { SetupDrag, DragManager } from "../../util/DragManager";
import { LinkManager } from "../../util/LinkManager";
@@ -33,6 +33,7 @@ import { DocServer } from "../../DocServer";
export interface SearchItemProps {
doc: Doc;
query?: string;
+ highlighting: string[];
}
library.add(faCaretUp);
@@ -58,9 +59,9 @@ export class SelectorContextMenu extends React.Component<SearchItemProps> {
async fetchDocuments() {
let aliases = (await SearchUtil.GetViewsOfDocument(this.props.doc)).filter(doc => doc !== this.props.doc);
- const { docs } = await SearchUtil.Search(`data_l:"${this.props.doc[Id]}"`, true);
+ const { docs } = await SearchUtil.Search("", true, { fq: `data_l:"${this.props.doc[Id]}"` });
const map: Map<Doc, Doc> = new Map;
- const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search(`data_l:"${doc[Id]}"`, true).then(result => result.docs)));
+ const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search("", true, { fq: `data_l:"${doc[Id]}"` }).then(result => result.docs)));
allDocs.forEach((docs, index) => docs.forEach(doc => map.set(doc, aliases[index])));
docs.forEach(doc => map.delete(doc));
runInAction(() => {
@@ -145,10 +146,12 @@ export class SearchItem extends React.Component<SearchItemProps> {
private _previewDoc?: Doc;
onClick = () => {
+ // I dont think this is the best functionality because clicking the name of the collection does that. Change it back if you'd like
DocumentManager.Instance.jumpToDocument(this.props.doc, false);
if (this.props.doc.data instanceof RichTextField) {
this.highlightTextBox(this.props.doc);
}
+ // CollectionDockingView.Instance.AddRightSplit(this.props.doc, undefined);
}
@observable _useIcons = true;
@observable _displayDim = 50;
@@ -167,7 +170,7 @@ export class SearchItem extends React.Component<SearchItemProps> {
}
fitToBox = () => {
- let bounds = Doc.ComputeContentBounds(this.props.doc);
+ let bounds = Doc.ComputeContentBounds([this.props.doc]);
return [(bounds.x + bounds.r) / 2, (bounds.y + bounds.b) / 2, Number(SEARCH_THUMBNAIL_SIZE) / Math.max((bounds.b - bounds.y), (bounds.r - bounds.x)), this._displayDim];
}
@@ -185,7 +188,7 @@ export class SearchItem extends React.Component<SearchItemProps> {
if (!this._useIcons) {
let renderDoc = this.props.doc;
//let box: number[] = [];
- if (layoutresult.indexOf(DocTypes.COL) !== -1) {
+ if (layoutresult.indexOf(DocumentType.COL) !== -1) {
renderDoc = Doc.MakeDelegate(renderDoc);
let bounds = DocListCast(renderDoc.data).reduce((bounds, doc) => {
var [sptX, sptY] = [NumCast(doc.x), NumCast(doc.y)];
@@ -210,8 +213,8 @@ export class SearchItem extends React.Component<SearchItemProps> {
onPointerEnter={action(() => this._displayDim = this._useIcons ? 50 : Number(SEARCH_THUMBNAIL_SIZE))}
onPointerLeave={action(() => this._displayDim = 50)} >
<DocumentView
- fitToBox={this.fitToBox}
- Document={newRenderDoc}
+ fitToBox={StrCast(this.props.doc.type).indexOf(DocumentType.COL) !== -1}
+ Document={this.props.doc}
addDocument={returnFalse}
removeDocument={returnFalse}
ScreenToLocalTransform={Transform.Identity}
@@ -239,15 +242,15 @@ export class SearchItem extends React.Component<SearchItemProps> {
if (this._previewDoc) {
DocServer.DeleteDocument(this._previewDoc[Id]);
}
- let button = layoutresult.indexOf(DocTypes.PDF) !== -1 ? faFilePdf :
- layoutresult.indexOf(DocTypes.IMG) !== -1 ? faImage :
- layoutresult.indexOf(DocTypes.TEXT) !== -1 ? faStickyNote :
- layoutresult.indexOf(DocTypes.VID) !== -1 ? faFilm :
- layoutresult.indexOf(DocTypes.COL) !== -1 ? faObjectGroup :
- layoutresult.indexOf(DocTypes.AUDIO) !== -1 ? faMusic :
- layoutresult.indexOf(DocTypes.LINK) !== -1 ? faLink :
- layoutresult.indexOf(DocTypes.HIST) !== -1 ? faChartBar :
- layoutresult.indexOf(DocTypes.WEB) !== -1 ? faGlobeAsia :
+ let button = layoutresult.indexOf(DocumentType.PDF) !== -1 ? faFilePdf :
+ layoutresult.indexOf(DocumentType.IMG) !== -1 ? faImage :
+ layoutresult.indexOf(DocumentType.TEXT) !== -1 ? faStickyNote :
+ layoutresult.indexOf(DocumentType.VID) !== -1 ? faFilm :
+ layoutresult.indexOf(DocumentType.COL) !== -1 ? faObjectGroup :
+ layoutresult.indexOf(DocumentType.AUDIO) !== -1 ? faMusic :
+ layoutresult.indexOf(DocumentType.LINK) !== -1 ? faLink :
+ layoutresult.indexOf(DocumentType.HIST) !== -1 ? faChartBar :
+ layoutresult.indexOf(DocumentType.WEB) !== -1 ? faGlobeAsia :
faCaretUp;
return <div onPointerDown={action(() => { this._useIcons = false; this._displayDim = Number(SEARCH_THUMBNAIL_SIZE); })} >
<FontAwesomeIcon icon={button} size="2x" />
@@ -281,7 +284,7 @@ export class SearchItem extends React.Component<SearchItemProps> {
pointerDown = (e: React.PointerEvent) => { e.preventDefault(); e.button === 0 && SearchBox.Instance.openSearch(e); }
highlightDoc = (e: React.PointerEvent) => {
- if (this.props.doc.type === DocTypes.LINK) {
+ if (this.props.doc.type === DocumentType.LINK) {
if (this.props.doc.anchor1 && this.props.doc.anchor2) {
let doc1 = Cast(this.props.doc.anchor1, Doc, null);
@@ -298,18 +301,18 @@ export class SearchItem extends React.Component<SearchItemProps> {
}
unHighlightDoc = (e: React.PointerEvent) => {
- if (this.props.doc.type === DocTypes.LINK) {
+ if (this.props.doc.type === DocumentType.LINK) {
if (this.props.doc.anchor1 && this.props.doc.anchor2) {
let doc1 = Cast(this.props.doc.anchor1, Doc, null);
let doc2 = Cast(this.props.doc.anchor2, Doc, null);
- doc1 && (doc1.libraryBrush = false);
- doc2 && (doc2.libraryBrush = false);
+ doc1 && (doc1.libraryBrush = undefined);
+ doc2 && (doc2.libraryBrush = undefined);
}
} else {
let docViews: DocumentView[] = DocumentManager.Instance.getAllDocumentViews(this.props.doc);
docViews.forEach(element => {
- element.props.Document.libraryBrush = false;
+ element.props.Document.libraryBrush = undefined;
});
}
}
@@ -341,12 +344,15 @@ export class SearchItem extends React.Component<SearchItemProps> {
<div className="search-item" onPointerEnter={this.highlightDoc} onPointerLeave={this.unHighlightDoc} id="result"
onClick={this.onClick} onPointerDown={this.pointerDown} >
<div className="main-search-info">
- <div title="Drag as document" onPointerDown={this.onPointerDown}> <FontAwesomeIcon icon="file" size="lg" /> </div>
- <div className="search-title" id="result" >{this.props.doc.title}</div>
+ <div title="Drag as document" onPointerDown={this.onPointerDown} style={{ marginRight: "7px" }}> <FontAwesomeIcon icon="file" size="lg" /> </div>
+ <div className="search-title-container">
+ <div className="search-title">{StrCast(this.props.doc.title)}</div>
+ <div className="search-highlighting">Matched fields: {this.props.highlighting.join(", ")}</div>
+ </div>
<div className="search-info" style={{ width: this._useIcons ? "15%" : "400px" }}>
<div className={`icon-${this._useIcons ? "icons" : "live"}`}>
- <div className="search-type" >{this.DocumentIcon()}</div>
- <div className="search-label">{this.props.doc.type}</div>
+ <div className="search-type" title="Click to Preview">{this.DocumentIcon}</div>
+ <div className="search-label">{this.props.doc.type ? this.props.doc.type : "Other"}</div>
</div>
<div className="link-container item">
<div className="link-count">{this.linkCount}</div>
@@ -356,7 +362,7 @@ export class SearchItem extends React.Component<SearchItemProps> {
</div>
</div>
<div className="searchBox-instances">
- {this.props.doc.type === DocTypes.LINK ? <LinkContextMenu doc1={Cast(this.props.doc.anchor1, Doc, new Doc())} doc2={Cast(this.props.doc.anchor2, Doc, new Doc())} /> :
+ {this.props.doc.type === DocumentType.LINK ? <LinkContextMenu doc1={Cast(this.props.doc.anchor1, Doc, new Doc())} doc2={Cast(this.props.doc.anchor2, Doc, new Doc())} /> :
<SelectorContextMenu {...this.props} />}
</div>
</div>
diff --git a/src/debug/Repl.tsx b/src/debug/Repl.tsx
index 91b711c79..4f4db13d2 100644
--- a/src/debug/Repl.tsx
+++ b/src/debug/Repl.tsx
@@ -6,6 +6,7 @@ import { CompileScript } from '../client/util/Scripting';
import { makeInterface } from '../new_fields/Schema';
import { ObjectField } from '../new_fields/ObjectField';
import { RefField } from '../new_fields/RefField';
+import { DocServer } from '../client/DocServer';
@observer
class Repl extends React.Component {
@@ -63,4 +64,7 @@ class Repl extends React.Component {
}
}
-ReactDOM.render(<Repl />, document.getElementById("root")); \ No newline at end of file
+(async function () {
+ DocServer.init(window.location.protocol, window.location.hostname, 4321, "repl");
+ ReactDOM.render(<Repl />, document.getElementById("root"));
+})(); \ No newline at end of file
diff --git a/src/debug/Viewer.tsx b/src/debug/Viewer.tsx
index c085b3483..24db3f934 100644
--- a/src/debug/Viewer.tsx
+++ b/src/debug/Viewer.tsx
@@ -181,9 +181,12 @@ class Viewer extends React.Component {
}
}
-ReactDOM.render((
- <div style={{ position: "absolute", width: "100%", height: "100%" }}>
- <Viewer />
- </div>),
- document.getElementById('root')
-); \ No newline at end of file
+(async function () {
+ await DocServer.init(window.location.protocol, window.location.hostname, 4321, "viewer");
+ ReactDOM.render((
+ <div style={{ position: "absolute", width: "100%", height: "100%" }}>
+ <Viewer />
+ </div>),
+ document.getElementById('root')
+ );
+})(); \ No newline at end of file
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index bfc1738fc..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';
@@ -33,7 +34,7 @@ class Uploader extends React.Component {
onClick = async () => {
try {
this.status = "initializing protos";
- await Docs.initProtos();
+ await Docs.Prototypes.initialize();
let imgPrev = document.getElementById("img_preview");
if (imgPrev) {
let files: FileList | null = inputRef.current!.files;
@@ -53,11 +54,11 @@ class Uploader extends React.Component {
const json = await res.json();
json.map(async (file: any) => {
let path = window.location.origin + file;
- var doc = Docs.ImageDocument(path, { nativeWidth: 200, width: 200, title: name });
+ var doc = Docs.Create.ImageDocument(path, { nativeWidth: 200, width: 200, title: name });
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/DateField.ts b/src/new_fields/DateField.ts
index fc8abb9d9..abec91e06 100644
--- a/src/new_fields/DateField.ts
+++ b/src/new_fields/DateField.ts
@@ -2,7 +2,9 @@ import { Deserializable } from "../client/util/SerializationHelper";
import { serializable, date } from "serializr";
import { ObjectField } from "./ObjectField";
import { Copy, ToScriptString } from "./FieldSymbols";
+import { scriptingGlobal, Scripting } from "../client/util/Scripting";
+@scriptingGlobal
@Deserializable("date")
export class DateField extends ObjectField {
@serializable(date())
@@ -21,3 +23,7 @@ export class DateField extends ObjectField {
return `new DateField(new Date(${this.date.toISOString()}))`;
}
}
+
+Scripting.addGlobal(function d(...dateArgs: any[]) {
+ return new DateField(new (Date as any)(...dateArgs));
+});
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index a07f56ca4..2ad6ae5f0 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -3,15 +3,26 @@ import { serializable, primitive, map, alias, list } from "serializr";
import { autoObject, SerializationHelper, Deserializable } from "../client/util/SerializationHelper";
import { DocServer } from "../client/DocServer";
import { setter, getter, getField, updateFunction, deleteProperty, makeEditable, makeReadOnly } from "./util";
-import { Cast, ToConstructor, PromiseValue, FieldValue, NumCast } from "./Types";
+import { Cast, ToConstructor, PromiseValue, FieldValue, NumCast, BoolCast, StrCast } from "./Types";
import { listSpec } from "./Schema";
import { ObjectField } from "./ObjectField";
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 = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
+ if (Field.IsField(field)) {
+ return (onDelegate ? "=" : "") + (field instanceof ComputedField ? `:=${field.script.originalScript}` : Field.toScriptString(field));
+ }
+ return "";
+ }
export function toScriptString(field: Field): string {
if (typeof field === "string") {
return `"${field}"`;
@@ -202,6 +213,18 @@ export namespace Doc {
}
return protos;
}
+
+ /**
+ * This function is intended to model Object.assign({}, {}) [https://mzl.la/1Mo3l21], which copies
+ * the values of the properties of a source object into the target.
+ *
+ * This is just a specific, Dash-authored version that serves the same role for our
+ * Doc class.
+ *
+ * @param doc the target document into which you'd like to insert the new fields
+ * @param fields the fields to project onto the target. Its type signature defines a mapping from some string key
+ * to a potentially undefined field, where each entry in this mapping is optional.
+ */
export function assign<K extends string>(doc: Doc, fields: Partial<Record<K, Opt<Field>>>) {
for (const key in fields) {
if (fields.hasOwnProperty(key)) {
@@ -264,8 +287,11 @@ export namespace Doc {
return true;
}
- export function ComputeContentBounds(doc: Doc) {
- let bounds = DocListCast(doc.data).reduce((bounds, doc) => {
+ //
+ // Computes the bounds of the contents of a set of documents.
+ //
+ export function ComputeContentBounds(docList: Doc[]) {
+ let bounds = docList.reduce((bounds, doc) => {
var [sptX, sptY] = [NumCast(doc.x), NumCast(doc.y)];
let [bptX, bptY] = [sptX + doc[WidthSym](), sptY + doc[HeightSym]()];
return {
@@ -288,16 +314,20 @@ export namespace Doc {
}
export function UpdateDocumentExtensionForField(doc: Doc, fieldKey: string) {
- if (doc[fieldKey + "_ext"] === undefined) {
+ let extensionDoc = doc[fieldKey + "_ext"];
+ 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)) {
proto = proto.proto;
}
(proto ? proto : doc)[fieldKey + "_ext"] = docExtensionForField;
}, 0);
+ } else if (extensionDoc instanceof Doc && extensionDoc.extendsDoc === undefined) {
+ setTimeout(() => (extensionDoc as Doc).extendsDoc = doc, 0);
}
}
export function MakeAlias(doc: Doc) {
@@ -316,19 +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;
}
- if (expandedTemplateLayout === undefined) {
+ 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;
+ let expandedDoc = Doc.MakeDelegate(templateLayoutDoc);
+ expandedDoc.title = templateLayoutDoc.title + "[" + StrCast(dataDoc.title).match(/\.\.\.[0-9]*/) + "]";
+ expandedDoc.isExpandedTemplate = templateLayoutDoc;
+ dataDoc[templateLayoutDoc[Id]] = expandedDoc;
}, 0);
}
- return templateLayoutDoc;
+ return templateLayoutDoc; // use the templateLayout when it's not a template or the expandedTemplate is pending.
}
+ let _pendingExpansions: Map<string, boolean> = new Map();
+
export function MakeCopy(doc: Doc, copyProto: boolean = false): Doc {
const copy = new Doc;
Object.keys(doc).forEach(key => {
@@ -342,6 +376,8 @@ export namespace Doc {
copy[key] = field;
} else if (field instanceof ObjectField) {
copy[key] = ObjectField.MakeCopy(field);
+ } else if (field instanceof Promise) {
+ field.then(f => (copy[key] === undefined) && (copy[key] = f)); //TODO what should we do here?
} else {
copy[key] = field;
}
@@ -354,11 +390,46 @@ 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;
+ }
+
+ export function MakeTemplate(fieldTemplate: Doc, metaKey: string, proto: Doc) {
+ // move data doc fields to layout doc as needed (nativeWidth/nativeHeight, data, ??)
+ let backgroundLayout = StrCast(fieldTemplate.backgroundLayout);
+ let fieldLayoutDoc = fieldTemplate;
+ if (fieldTemplate.layout instanceof Doc) {
+ fieldLayoutDoc = Doc.MakeDelegate(fieldTemplate.layout);
+ }
+ let layout = StrCast(fieldLayoutDoc.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
+ if (backgroundLayout) {
+ layout = StrCast(fieldLayoutDoc.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"} fieldExt={"annotations"}`);
+ backgroundLayout = backgroundLayout.replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
+ }
+ let nw = Cast(fieldTemplate.nativeWidth, "number");
+ let nh = Cast(fieldTemplate.nativeHeight, "number");
+
+ let layoutDelegate = fieldTemplate.layout instanceof Doc ? fieldLayoutDoc : fieldTemplate;
+ layoutDelegate.layout = layout;
+
+ fieldTemplate.title = metaKey;
+ fieldTemplate.layout = layoutDelegate !== fieldTemplate ? layoutDelegate : layout;
+ fieldTemplate.backgroundLayout = backgroundLayout;
+ fieldTemplate.nativeWidth = nw;
+ fieldTemplate.nativeHeight = nh;
+ fieldTemplate.isTemplate = true;
+ fieldTemplate.showTitle = "title";
+ 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/Schema.ts b/src/new_fields/Schema.ts
index 2355304d5..b1a449e08 100644
--- a/src/new_fields/Schema.ts
+++ b/src/new_fields/Schema.ts
@@ -104,7 +104,7 @@ export function makeStrictInterface<T extends Interface>(schema: T): (doc: Doc)
}
export function createSchema<T extends Interface>(schema: T): T & { proto: ToConstructor<Doc> } {
- schema.proto = Doc;
+ (schema as any).proto = Doc;
return schema as any;
}
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/Types.ts b/src/new_fields/Types.ts
index 39d384d64..f8a4a30b4 100644
--- a/src/new_fields/Types.ts
+++ b/src/new_fields/Types.ts
@@ -1,6 +1,7 @@
import { Field, Opt, FieldResult, Doc } from "./Doc";
import { List } from "./List";
import { RefField } from "./RefField";
+import { DateField } from "./DateField";
export type ToType<T extends InterfaceValue> =
T extends "string" ? string :
@@ -80,6 +81,9 @@ export function StrCast(field: FieldResult, defaultVal: string | null = "") {
export function BoolCast(field: FieldResult, defaultVal: boolean | null = null) {
return Cast(field, "boolean", defaultVal);
}
+export function DateCast(field: FieldResult) {
+ return Cast(field, DateField, null);
+}
type WithoutList<T extends Field> = T extends List<infer R> ? (R extends RefField ? (R | Promise<R>)[] : R[]) : T;
diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts
index b5e50d501..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()) {
@@ -50,6 +64,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
target.__fields[prop] = value;
}
if (value === undefined) target[Update]({ '$unset': { ["fields." + prop]: "" } });
+ if (typeof value === "object" && !(value instanceof ObjectField)) debugger;
else target[Update]({ '$set': { ["fields." + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : (value === undefined ? null : value) } });
UndoManager.AddEvent({
redo: () => receiver[prop] = value,
@@ -84,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/.gitignore b/src/scraping/acm/.gitignore
new file mode 100644
index 000000000..caca8b99c
--- /dev/null
+++ b/src/scraping/acm/.gitignore
@@ -0,0 +1,2 @@
+./citations.txt
+./results.txt \ No newline at end of file
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/scraping/acm/debug.log b/src/scraping/acm/debug.log
new file mode 100644
index 000000000..8c0a148f4
--- /dev/null
+++ b/src/scraping/acm/debug.log
@@ -0,0 +1,38 @@
+[0625/170004.768:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/170004.769:ERROR:exception_snapshot_win.cc(98)] thread ID 17604 not found in process
+[0625/171124.644:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/171124.645:ERROR:exception_snapshot_win.cc(98)] thread ID 14348 not found in process
+[0625/171853.989:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/171853.990:ERROR:exception_snapshot_win.cc(98)] thread ID 12080 not found in process
+[0625/171947.744:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/171947.745:ERROR:exception_snapshot_win.cc(98)] thread ID 16160 not found in process
+[0625/172007.424:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/172007.425:ERROR:exception_snapshot_win.cc(98)] thread ID 13472 not found in process
+[0625/172059.353:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/172059.354:ERROR:exception_snapshot_win.cc(98)] thread ID 6396 not found in process
+[0625/172402.795:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/172402.796:ERROR:exception_snapshot_win.cc(98)] thread ID 10720 not found in process
+[0625/172618.850:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/172618.850:ERROR:exception_snapshot_win.cc(98)] thread ID 21136 not found in process
+[0625/172819.875:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/172819.876:ERROR:exception_snapshot_win.cc(98)] thread ID 17624 not found in process
+[0625/172953.674:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/172953.675:ERROR:exception_snapshot_win.cc(98)] thread ID 15180 not found in process
+[0625/173412.182:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/173412.182:ERROR:exception_snapshot_win.cc(98)] thread ID 13952 not found in process
+[0625/173447.806:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/173447.807:ERROR:exception_snapshot_win.cc(98)] thread ID 1572 not found in process
+[0625/173516.188:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/173516.189:ERROR:exception_snapshot_win.cc(98)] thread ID 5472 not found in process
+[0625/173528.446:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/173528.447:ERROR:exception_snapshot_win.cc(98)] thread ID 20420 not found in process
+[0625/173539.436:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/173539.437:ERROR:exception_snapshot_win.cc(98)] thread ID 16192 not found in process
+[0625/173643.139:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/173643.140:ERROR:exception_snapshot_win.cc(98)] thread ID 15716 not found in process
+[0625/173659.376:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/173659.377:ERROR:exception_snapshot_win.cc(98)] thread ID 11828 not found in process
+[0625/201137.209:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/201137.210:ERROR:exception_snapshot_win.cc(98)] thread ID 7688 not found in process
+[0625/210240.476:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/210240.477:ERROR:exception_snapshot_win.cc(98)] thread ID 20828 not found in process
diff --git a/src/scraping/acm/index.js b/src/scraping/acm/index.js
index 51781dba8..b71d55226 100644
--- a/src/scraping/acm/index.js
+++ b/src/scraping/acm/index.js
@@ -276,4 +276,4 @@ log_read("target references");
readFile(target_source, {
encoding: "utf8"
-}, scrape_targets); \ No newline at end of file
+}, scrape_targets);
diff --git a/src/server/GarbageCollector.ts b/src/server/GarbageCollector.ts
index 268239481..ea5388004 100644
--- a/src/server/GarbageCollector.ts
+++ b/src/server/GarbageCollector.ts
@@ -59,7 +59,9 @@ function addDoc(doc: any, ids: string[], files: { [name: string]: string[] }) {
}
}
-async function GarbageCollect() {
+async function GarbageCollect(full: boolean = true) {
+ console.log("start GC");
+ const start = Date.now();
// await new Promise(res => setTimeout(res, 3000));
const cursor = await Database.Instance.query({}, { userDocumentId: 1 }, 'users');
const users = await cursor.toArray();
@@ -68,7 +70,7 @@ async function GarbageCollect() {
const files: { [name: string]: string[] } = {};
while (ids.length) {
- const count = Math.min(ids.length, 100);
+ const count = Math.min(ids.length, 1000);
const index = ids.length - count;
const fetchIds = ids.splice(index, count).filter(id => !visited.has(id));
if (!fetchIds.length) {
@@ -91,34 +93,58 @@ async function GarbageCollect() {
cursor.close();
- const toDeleteCursor = await Database.Instance.query({ _id: { $nin: Array.from(visited) } }, { _id: 1 });
+ const notToDelete = Array.from(visited);
+ const toDeleteCursor = await Database.Instance.query({ _id: { $nin: notToDelete } }, { _id: 1 });
const toDelete: string[] = (await toDeleteCursor.toArray()).map(doc => doc._id);
toDeleteCursor.close();
- const result = await Database.Instance.delete({ _id: { $in: toDelete } }, "newDocuments");
- console.log(`${result.deletedCount} documents deleted`);
+ if (!full) {
+ await Database.Instance.updateMany({ _id: { $nin: notToDelete } }, { $set: { "deleted": true } });
+ await Database.Instance.updateMany({ _id: { $in: notToDelete } }, { $unset: { "deleted": true } });
+ console.log(await Search.Instance.updateDocuments(
+ notToDelete.map<any>(id => ({
+ id, deleted: { set: null }
+ }))
+ .concat(toDelete.map(id => ({
+ id, deleted: { set: true }
+ })))));
+ console.log("Done with partial GC");
+ console.log(`Took ${(Date.now() - start) / 1000} seconds`);
+ } else {
+ let i = 0;
+ let deleted = 0;
+ while (i < toDelete.length) {
+ const count = Math.min(toDelete.length, 5000);
+ const toDeleteDocs = toDelete.slice(i, i + count);
+ i += count;
+ const result = await Database.Instance.delete({ _id: { $in: toDeleteDocs } }, "newDocuments");
+ deleted += result.deletedCount || 0;
+ }
+ // const result = await Database.Instance.delete({ _id: { $in: toDelete } }, "newDocuments");
+ console.log(`${deleted} documents deleted`);
- await Search.Instance.deleteDocuments(toDelete);
- console.log("Cleared search documents");
+ await Search.Instance.deleteDocuments(toDelete);
+ console.log("Cleared search documents");
- const folder = "./src/server/public/files/";
- fs.readdir(folder, (_, fileList) => {
- const filesToDelete = fileList.filter(file => {
- const ext = path.extname(file);
- let base = path.basename(file, ext);
- const existsInDb = (base in files || (base = base.substring(0, base.length - 2)) in files) && files[base].includes(ext);
- return file !== ".gitignore" && !existsInDb;
- });
- console.log(`Deleting ${filesToDelete.length} files`);
- filesToDelete.forEach(file => {
- console.log(`Deleting file ${file}`);
- try {
- fs.unlinkSync(folder + file);
- } catch {
- console.warn(`Couldn't delete file ${file}`);
- }
+ const folder = "./src/server/public/files/";
+ fs.readdir(folder, (_, fileList) => {
+ const filesToDelete = fileList.filter(file => {
+ const ext = path.extname(file);
+ let base = path.basename(file, ext);
+ const existsInDb = (base in files || (base = base.substring(0, base.length - 2)) in files) && files[base].includes(ext);
+ return file !== ".gitignore" && !existsInDb;
+ });
+ console.log(`Deleting ${filesToDelete.length} files`);
+ filesToDelete.forEach(file => {
+ console.log(`Deleting file ${file}`);
+ try {
+ fs.unlinkSync(folder + file);
+ } catch {
+ console.warn(`Couldn't delete file ${file}`);
+ }
+ });
+ console.log(`Deleted ${filesToDelete.length} files`);
});
- console.log(`Deleted ${filesToDelete.length} files`);
- });
+ }
}
-GarbageCollect();
+GarbageCollect(false);
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/Search.ts b/src/server/Search.ts
index 8591f8857..723dc101b 100644
--- a/src/server/Search.ts
+++ b/src/server/Search.ts
@@ -26,22 +26,18 @@ export class Search {
});
return res;
} catch (e) {
- // console.warn("Search error: " + e + document);
+ // console.warn("Search error: ", e, documents);
}
}
- public async search(query: string, start: number = 0) {
+ public async search(query: any) {
try {
const searchResults = JSON.parse(await rp.get(this.url + "dash/select", {
- qs: {
- q: query,
- fl: "id",
- start: start
- }
+ qs: query
}));
const { docs, numFound } = searchResults.response;
const ids = docs.map((field: any) => field.id);
- return { ids, numFound };
+ return { ids, numFound, highlighting: searchResults.highlighting };
} catch {
return { ids: [], numFound: -1 };
}
diff --git a/src/server/authentication/controllers/user_controller.ts b/src/server/authentication/controllers/user_controller.ts
index ca4fc171c..f5c6e1610 100644
--- a/src/server/authentication/controllers/user_controller.ts
+++ b/src/server/authentication/controllers/user_controller.ts
@@ -12,6 +12,9 @@ import * as nodemailer from 'nodemailer';
import c = require("crypto");
import { RouteStore } from "../../RouteStore";
import { Utils } from "../../../Utils";
+import { Schema } from "mongoose";
+import { Opt } from "../../../new_fields/Doc";
+import { MailOptions } from "nodemailer/lib/stream-transport";
/**
* GET /signup
@@ -45,21 +48,23 @@ export let postSignup = (req: Request, res: Response, next: NextFunction) => {
return res.redirect(RouteStore.signup);
}
- const email = req.body.email;
+ const email = req.body.email as String;
const password = req.body.password;
- const user = new User({
+ const model = {
email,
password,
userDocumentId: Utils.GenerateGuid()
- });
+ } as Partial<DashUserModel>;
+
+ const user = new User(model);
User.findOne({ email }, (err, existingUser) => {
if (err) { return next(err); }
if (existingUser) {
return res.redirect(RouteStore.login);
}
- user.save((err) => {
+ user.save((err: any) => {
if (err) { return next(err); }
req.logIn(user, (err) => {
if (err) { return next(err); }
@@ -72,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);
}
@@ -188,8 +194,8 @@ export let postForgot = function (req: Request, res: Response, next: NextFunctio
'Please click on the following link, or paste this into your browser to complete the process:\n\n' +
'http://' + req.headers.host + '/reset/' + token + '\n\n' +
'If you did not request this, please ignore this email and your password will remain unchanged.\n'
- };
- smtpTransport.sendMail(mailOptions, function (err) {
+ } as MailOptions;
+ smtpTransport.sendMail(mailOptions, function (err: Error | null) {
// req.flash('info', 'An e-mail has been sent to ' + user.email + ' with further instructions.');
done(null, err, 'done');
});
@@ -259,7 +265,7 @@ export let postReset = function (req: Request, res: Response) {
subject: 'Your password has been changed',
text: 'Hello,\n\n' +
'This is a confirmation that the password for your account ' + user.email + ' has just been changed.\n'
- };
+ } as MailOptions;
smtpTransport.sendMail(mailOptions, function (err) {
done(null, err);
});
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index e328d6e5c..1c52a3f11 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -10,8 +10,9 @@ import { CollectionView } from "../../../client/views/collections/CollectionView
import { Doc } from "../../../new_fields/Doc";
import { List } from "../../../new_fields/List";
import { listSpec } from "../../../new_fields/Schema";
-import { Cast, FieldValue } from "../../../new_fields/Types";
+import { Cast, FieldValue, StrCast } from "../../../new_fields/Types";
import { RouteStore } from "../../RouteStore";
+import { Utils } from "../../../Utils";
export class CurrentUserUtils {
private static curr_email: string;
@@ -37,47 +38,57 @@ export class CurrentUserUtils {
doc.gridGap = 5;
doc.xMargin = 5;
doc.yMargin = 5;
+ doc.boxShadow = "0 0";
doc.excludeFromLibrary = true;
- doc.optionalRightCollection = Docs.StackingDocument([], { title: "New mobile uploads" });
- // doc.library = Docs.TreeDocument([doc], { title: `Library: ${CurrentUserUtils.email}` });
+ doc.optionalRightCollection = Docs.Create.StackingDocument([], { title: "New mobile uploads" });
+ // doc.library = Docs.Create.TreeDocument([doc], { title: `Library: ${CurrentUserUtils.email}` });
// (doc.library as Doc).excludeFromLibrary = true;
return doc;
}
static updateUserDocument(doc: Doc) {
if (doc.workspaces === undefined) {
- const workspaces = Docs.TreeDocument([], { title: "Workspaces", height: 100 });
+ const workspaces = Docs.Create.TreeDocument([], { title: "Workspaces", height: 100 });
workspaces.excludeFromLibrary = true;
workspaces.workspaceLibrary = true;
+ workspaces.boxShadow = "0 0";
doc.workspaces = workspaces;
}
if (doc.recentlyClosed === undefined) {
- const recentlyClosed = Docs.TreeDocument([], { title: "Recently Closed", height: 75 });
+ const recentlyClosed = Docs.Create.TreeDocument([], { title: "Recently Closed", height: 75 });
recentlyClosed.excludeFromLibrary = true;
+ recentlyClosed.boxShadow = "0 0";
doc.recentlyClosed = recentlyClosed;
}
if (doc.sidebar === undefined) {
- const sidebar = Docs.StackingDocument([doc.workspaces as Doc, doc, doc.recentlyClosed as Doc], { title: "Sidebar" });
+ const sidebar = Docs.Create.StackingDocument([doc.workspaces as Doc, doc, doc.recentlyClosed as Doc], { title: "Sidebar" });
sidebar.excludeFromLibrary = true;
sidebar.gridGap = 5;
sidebar.xMargin = 5;
sidebar.yMargin = 5;
+ Doc.GetProto(sidebar).backgroundColor = "#aca3a6";
+ sidebar.boxShadow = "1 1 3";
doc.sidebar = sidebar;
}
+ StrCast(doc.title).indexOf("@") !== -1 && (doc.title = StrCast(doc.title).split("@")[0] + "'s Library");
}
- public static async loadCurrentUser(): Promise<any> {
- let userPromise = rp.get(DocServer.prepend(RouteStore.getCurrUser)).then(response => {
+ public static loadCurrentUser() {
+ return rp.get(Utils.prepend(RouteStore.getCurrUser)).then(response => {
if (response) {
- let obj = JSON.parse(response);
- CurrentUserUtils.curr_id = obj.id as string;
- CurrentUserUtils.curr_email = obj.email as string;
+ const result: { id: string, email: string } = JSON.parse(response);
+ return result;
} else {
throw new Error("There should be a user! Why does Dash think there isn't one?");
}
});
- let userDocPromise = await rp.get(DocServer.prepend(RouteStore.getUserDocumentId)).then(id => {
+ }
+
+ public static async loadUserDocument({ id, email }: { id: string, email: string }) {
+ this.curr_id = id;
+ this.curr_email = email;
+ await rp.get(Utils.prepend(RouteStore.getUserDocumentId)).then(id => {
if (id) {
return DocServer.GetRefField(id).then(async field => {
if (field instanceof Doc) {
@@ -102,7 +113,6 @@ export class CurrentUserUtils {
} catch (e) {
}
- return Promise.all([userPromise, userDocPromise]);
}
/* Northstar catalog ... really just for testing so this should eventually go away */
@@ -128,12 +138,12 @@ export class CurrentUserUtils {
// new AttributeTransformationModel(atmod, AggregateFunction.None),
// new AttributeTransformationModel(atmod, AggregateFunction.Count),
// new AttributeTransformationModel(atmod, AggregateFunction.Count));
- // schemaDocuments.push(Docs.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! }));
+ // schemaDocuments.push(Docs.Create.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! }));
// }
// })));
// return promises;
// }, [] as Promise<void>[]));
- // return CurrentUserUtils._northstarSchemas.push(Docs.TreeDocument(schemaDocuments, { width: 50, height: 100, title: schema.displayName! }));
+ // return CurrentUserUtils._northstarSchemas.push(Docs.Create.TreeDocument(schemaDocuments, { width: 50, height: 100, title: schema.displayName! }));
// });
// }
}
diff --git a/src/server/authentication/models/user_model.ts b/src/server/authentication/models/user_model.ts
index ee85e1c05..45fbf23b1 100644
--- a/src/server/authentication/models/user_model.ts
+++ b/src/server/authentication/models/user_model.ts
@@ -16,7 +16,7 @@ mongoose.connection.on('disconnected', function () {
console.log('connection closed');
});
export type DashUserModel = mongoose.Document & {
- email: string,
+ email: String,
password: string,
passwordResetToken?: string,
passwordResetExpires?: Date,
@@ -42,7 +42,7 @@ export type AuthToken = {
};
const userSchema = new mongoose.Schema({
- email: { type: String, unique: true },
+ email: String,
password: String,
passwordResetToken: String,
passwordResetExpires: Date,
diff --git a/src/server/database.ts b/src/server/database.ts
index 29a8ffafa..7f5331998 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -140,6 +140,17 @@ export class Database {
}
}
+ public updateMany(query: any, update: any, collectionName = "newDocuments") {
+ if (this.db) {
+ const db = this.db;
+ return new Promise<mongodb.WriteOpResult>(res => db.collection(collectionName).update(query, update, (_, result) => res(result)));
+ } else {
+ return new Promise<mongodb.WriteOpResult>(res => {
+ this.onConnect.push(() => this.updateMany(query, update, collectionName).then(res));
+ });
+ }
+ }
+
public print() {
console.log("db says hi!");
}
diff --git a/src/server/index.ts b/src/server/index.ts
index 64a2a6899..80dbd8a79 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -60,7 +60,7 @@ clientUtils = `//AUTO-GENERATED FILE: DO NOT EDIT\n${clientUtils.replace('"mode"
fs.writeFileSync("./src/client/util/ClientUtils.ts", clientUtils, "utf8");
const mongoUrl = 'mongodb://localhost:27017/Dash';
-mongoose.connect(mongoUrl);
+mongoose.connection.readyState === 0 && mongoose.connect(mongoUrl);
mongoose.connection.on('connected', () => console.log("connected"));
// SESSION MANAGEMENT AND AUTHENTICATION MIDDLEWARE
@@ -112,7 +112,7 @@ function addSecureRoute(method: Method,
if (req.user) {
handler(req.user, res, req);
} else {
- req.session!.target = `http://localhost:${port}${req.originalUrl}`;
+ req.session!.target = req.originalUrl;
onRejection(res, req);
}
};
@@ -147,15 +147,45 @@ const solrURL = "http://localhost:8983/solr/#/dash";
// GETTERS
app.get("/search", async (req, res) => {
- let query = req.query.query || "hello";
- let results = await Search.Instance.search(query);
+ const solrQuery: any = {};
+ ["q", "fq", "start", "rows", "hl", "hl.fl"].forEach(key => solrQuery[key] = req.query[key]);
+ if (solrQuery.q === undefined) {
+ res.send([]);
+ return;
+ }
+ let results = await Search.Instance.search(solrQuery);
res.send(results);
});
+function msToTime(duration: number) {
+ let milliseconds = Math.floor((duration % 1000) / 100),
+ seconds = Math.floor((duration / 1000) % 60),
+ minutes = Math.floor((duration / (1000 * 60)) % 60),
+ hours = Math.floor((duration / (1000 * 60 * 60)) % 24);
+
+ let hoursS = (hours < 10) ? "0" + hours : hours;
+ let minutesS = (minutes < 10) ? "0" + minutes : minutes;
+ let secondsS = (seconds < 10) ? "0" + seconds : seconds;
+
+ return hoursS + ":" + minutesS + ":" + secondsS + "." + milliseconds;
+}
+
+app.get("/whosOnline", (req, res) => {
+ let users: any = { active: {}, inactive: {} };
+ const now = Date.now();
+
+ for (const user in timeMap) {
+ const time = timeMap[user];
+ const key = ((now - time) / 1000) < (60 * 5) ? "active" : "inactive";
+ users[key][user] = `Last active ${msToTime(now - time)} ago`;
+ }
+
+ res.send(users);
+});
app.get("/thumbnail/:filename", (req, res) => {
let filename = req.params.filename;
let noExt = filename.substring(0, filename.length - ".png".length);
- let pagenumber = parseInt(noExt[noExt.length - 1]);
+ let pagenumber = parseInt(noExt.split('-')[1]);
fs.exists(uploadDir + filename, (exists: boolean) => {
console.log(`${uploadDir + filename} ${exists ? "exists" : "does not exist"}`);
if (exists) {
@@ -163,13 +193,14 @@ app.get("/thumbnail/:filename", (req, res) => {
probe(input, (err: any, result: any) => {
if (err) {
console.log(err);
+ console.log(`error on ${filename}`);
return;
}
res.send({ path: "/files/" + filename, width: result.width, height: result.height });
});
}
else {
- LoadPage(uploadDir + filename.substring(0, filename.length - "-n.png".length) + ".pdf", pagenumber, res);
+ LoadPage(uploadDir + filename.substring(0, filename.length - noExt.split('-')[1].length - ".PNG".length - 1) + ".pdf", pagenumber, res);
}
});
});
@@ -256,6 +287,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);
@@ -316,38 +361,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));
@@ -421,7 +434,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) {
@@ -453,12 +466,21 @@ interface Map {
}
let clients: Map = {};
+let socketMap = new Map<SocketIO.Socket, string>();
+let timeMap: { [id: string]: number } = {};
+
server.on("connection", function (socket: Socket) {
- console.log("a user has connected");
+ socket.use((packet, next) => {
+ let id = socketMap.get(socket);
+ if (id) {
+ timeMap[id] = Date.now();
+ }
+ next();
+ });
Utils.Emit(socket, MessageStore.Foo, "handshooken");
- Utils.AddServerHandler(socket, MessageStore.Bar, barReceived);
+ Utils.AddServerHandler(socket, MessageStore.Bar, guid => barReceived(socket, guid));
Utils.AddServerHandler(socket, MessageStore.SetField, (args) => setField(socket, args));
Utils.AddServerHandlerCallback(socket, MessageStore.GetField, getField);
Utils.AddServerHandlerCallback(socket, MessageStore.GetFields, getFields);
@@ -488,8 +510,10 @@ async function deleteAll() {
await Search.Instance.clear();
}
-function barReceived(guid: String) {
- clients[guid.toString()] = new Client(guid.toString());
+function barReceived(socket: SocketIO.Socket, guid: string) {
+ clients[guid] = new Client(guid.toString());
+ console.log(`User ${guid} has connected`);
+ socketMap.set(socket, guid);
}
function getField([id, callback]: [string, (result?: Transferable) => void]) {
diff --git a/src/server/updateProtos.ts b/src/server/updateProtos.ts
new file mode 100644
index 000000000..90490eb45
--- /dev/null
+++ b/src/server/updateProtos.ts
@@ -0,0 +1,15 @@
+import { Database } from "./database";
+
+const protos =
+ ["text", "histogram", "image", "web", "collection", "kvp",
+ "video", "audio", "pdf", "icon", "import", "linkdoc"];
+
+(async function () {
+ await Promise.all(
+ protos.map(protoId => new Promise(res => Database.Instance.update(protoId, {
+ $set: { "fields.baseProto": true }
+ }, res)))
+ );
+
+ console.log("done");
+})(); \ No newline at end of file