aboutsummaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
Diffstat (limited to 'src/client')
-rw-r--r--src/client/DocServer.ts333
-rw-r--r--src/client/documents/Documents.ts728
-rw-r--r--src/client/goldenLayout.js19
-rw-r--r--src/client/northstar/dash-nodes/HistogramBox.tsx2
-rw-r--r--src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx2
-rw-r--r--src/client/util/ClientUtils.ts.temp3
-rw-r--r--src/client/util/DocumentManager.ts120
-rw-r--r--src/client/util/DragManager.ts126
-rw-r--r--src/client/util/History.ts136
-rw-r--r--src/client/util/Import & Export/DirectoryImportBox.tsx376
-rw-r--r--src/client/util/Import & Export/ImportMetadataEntry.tsx149
-rw-r--r--src/client/util/LinkManager.ts245
-rw-r--r--src/client/util/ProsemirrorExampleTransfer.ts4
-rw-r--r--src/client/util/RichTextSchema.tsx118
-rw-r--r--src/client/util/Scripting.ts56
-rw-r--r--src/client/util/SearchUtil.ts76
-rw-r--r--src/client/util/SelectionManager.ts22
-rw-r--r--src/client/util/SerializationHelper.ts10
-rw-r--r--src/client/util/TooltipTextMenu.scss30
-rw-r--r--src/client/util/TooltipTextMenu.tsx453
-rw-r--r--src/client/util/UndoManager.ts1
-rw-r--r--src/client/util/request-image-size.js75
-rw-r--r--src/client/util/type_decls.d2
-rw-r--r--src/client/views/ContextMenu.tsx4
-rw-r--r--src/client/views/DocumentDecorations.scss56
-rw-r--r--src/client/views/DocumentDecorations.tsx171
-rw-r--r--src/client/views/EditableView.tsx12
-rw-r--r--src/client/views/GlobalKeyHandler.ts178
-rw-r--r--src/client/views/InkingCanvas.tsx10
-rw-r--r--src/client/views/InkingControl.tsx40
-rw-r--r--src/client/views/Main.scss59
-rw-r--r--src/client/views/Main.tsx33
-rw-r--r--src/client/views/MainOverlayTextBox.scss17
-rw-r--r--src/client/views/MainOverlayTextBox.tsx55
-rw-r--r--src/client/views/MainView.tsx347
-rw-r--r--src/client/views/MetadataEntryMenu.scss66
-rw-r--r--src/client/views/MetadataEntryMenu.tsx171
-rw-r--r--src/client/views/OverlayView.tsx47
-rw-r--r--src/client/views/PresentationView.tsx156
-rw-r--r--src/client/views/PreviewCursor.tsx13
-rw-r--r--src/client/views/ScriptBox.scss17
-rw-r--r--src/client/views/ScriptBox.tsx44
-rw-r--r--src/client/views/SearchBox.scss102
-rw-r--r--src/client/views/SearchBox.tsx52
-rw-r--r--src/client/views/SearchItem.tsx69
-rw-r--r--src/client/views/TemplateMenu.tsx12
-rw-r--r--src/client/views/Templates.tsx17
-rw-r--r--src/client/views/_nodeModuleOverrides.scss19
-rw-r--r--src/client/views/collections/CollectionBaseView.scss3
-rw-r--r--src/client/views/collections/CollectionBaseView.tsx103
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx319
-rw-r--r--src/client/views/collections/CollectionPDFView.tsx20
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx151
-rw-r--r--src/client/views/collections/CollectionStackingView.scss37
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx282
-rw-r--r--src/client/views/collections/CollectionSubView.tsx136
-rw-r--r--src/client/views/collections/CollectionTreeView.scss13
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx353
-rw-r--r--src/client/views/collections/CollectionVideoView.tsx74
-rw-r--r--src/client/views/collections/CollectionView.tsx18
-rw-r--r--src/client/views/collections/ParentDocumentSelector.tsx8
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss1
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx22
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx112
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss47
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx261
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.scss2
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx68
-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.scss16
-rw-r--r--src/client/views/globalCssVariables.scss.d.ts1
-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/CollectionFreeFormDocumentView.tsx34
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx56
-rw-r--r--src/client/views/nodes/DocumentView.tsx300
-rw-r--r--src/client/views/nodes/FieldView.tsx24
-rw-r--r--src/client/views/nodes/FormattedTextBox.scss2
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx193
-rw-r--r--src/client/views/nodes/ImageBox.scss18
-rw-r--r--src/client/views/nodes/ImageBox.tsx210
-rw-r--r--src/client/views/nodes/KeyValueBox.scss6
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx123
-rw-r--r--src/client/views/nodes/KeyValuePair.scss39
-rw-r--r--src/client/views/nodes/KeyValuePair.tsx88
-rw-r--r--src/client/views/nodes/LinkBox.scss66
-rw-r--r--src/client/views/nodes/LinkBox.tsx86
-rw-r--r--src/client/views/nodes/LinkEditor.scss171
-rw-r--r--src/client/views/nodes/LinkEditor.tsx396
-rw-r--r--src/client/views/nodes/LinkMenu.scss144
-rw-r--r--src/client/views/nodes/LinkMenu.tsx53
-rw-r--r--src/client/views/nodes/LinkMenuGroup.tsx102
-rw-r--r--src/client/views/nodes/LinkMenuItem.tsx131
-rw-r--r--src/client/views/nodes/PDFBox.scss79
-rw-r--r--src/client/views/nodes/PDFBox.tsx181
-rw-r--r--src/client/views/nodes/VideoBox.scss15
-rw-r--r--src/client/views/nodes/VideoBox.tsx230
-rw-r--r--src/client/views/nodes/WebBox.tsx35
-rw-r--r--src/client/views/pdf/Annotation.tsx153
-rw-r--r--src/client/views/pdf/PDFMenu.scss11
-rw-r--r--src/client/views/pdf/PDFMenu.tsx109
-rw-r--r--src/client/views/pdf/PDFViewer.scss89
-rw-r--r--src/client/views/pdf/PDFViewer.tsx637
-rw-r--r--src/client/views/pdf/Page.tsx174
-rw-r--r--src/client/views/presentationview/PresentationElement.tsx818
-rw-r--r--src/client/views/presentationview/PresentationList.tsx116
-rw-r--r--src/client/views/presentationview/PresentationView.scss (renamed from src/client/views/PresentationView.scss)22
-rw-r--r--src/client/views/presentationview/PresentationView.tsx838
-rw-r--r--src/client/views/search/CheckBox.scss59
-rw-r--r--src/client/views/search/CheckBox.tsx131
-rw-r--r--src/client/views/search/CollectionFilters.scss20
-rw-r--r--src/client/views/search/CollectionFilters.tsx83
-rw-r--r--src/client/views/search/FieldFilters.scss5
-rw-r--r--src/client/views/search/FieldFilters.tsx41
-rw-r--r--src/client/views/search/FilterBox.scss108
-rw-r--r--src/client/views/search/FilterBox.tsx394
-rw-r--r--src/client/views/search/IconBar.scss12
-rw-r--r--src/client/views/search/IconBar.tsx83
-rw-r--r--src/client/views/search/IconButton.scss52
-rw-r--r--src/client/views/search/IconButton.tsx192
-rw-r--r--src/client/views/search/NaviconButton.scss69
-rw-r--r--src/client/views/search/NaviconButton.tsx35
-rw-r--r--src/client/views/search/SearchBox.scss64
-rw-r--r--src/client/views/search/SearchBox.tsx340
-rw-r--r--src/client/views/search/SearchItem.scss215
-rw-r--r--src/client/views/search/SearchItem.tsx269
-rw-r--r--src/client/views/search/SelectorContextMenu.scss15
-rw-r--r--src/client/views/search/ToggleBar.scss36
-rw-r--r--src/client/views/search/ToggleBar.tsx86
130 files changed, 12027 insertions, 3033 deletions
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index cbcf751ee..8c64d2b2f 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -1,35 +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 {
- const _cache: { [id: string]: RefField | Promise<Opt<RefField>> } = {};
- const _socket = OpenSocket(`${window.location.protocol}//${window.location.hostname}:4321`);
- const GUID: string = Utils.GenerateGuid();
+ let _cache: { [id: string]: RefField | Promise<Opt<RefField>> } = {};
+ 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 makeReadOnly() {
- _CreateField = emptyFunction;
- _UpdateField = emptyFunction;
- _respondToUpdate = emptyFunction;
+ /**
+ * 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 async function GetRefField(id: string): Promise<Opt<RefField>> {
+ /**
+ * 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 namespace Util {
+
+ /**
+ * Emits a message to the server that wipes
+ * all documents in the database.
+ */
+ export function deleteDatabase() {
+ Utils.Emit(_socket, MessageStore.DeleteAll, {});
+ }
+
+ }
+
+ // 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;
@@ -38,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 {
@@ -84,70 +243,138 @@ 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;
+ };
+
+ let _GetRefFields: (ids: string[]) => Promise<{ [id: string]: Opt<RefField> }> = errorFunc;
+
+ export function GetRefFields(ids: string[]) {
+ return _GetRefFields(ids);
}
- let _UpdateField = (id: string, diff: any) => {
- if (id === updatingId) {
- return;
- }
- Utils.Emit(_socket, MessageStore.UpdateField, { id, diff });
- };
+ // 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);
}
- let _CreateField = (field: RefField) => {
+ function _CreateFieldImpl(field: RefField) {
_cache[field[Id]] = field;
const initialState = SerializationHelper.Serialize(field);
Utils.Emit(_socket, MessageStore.CreateField, initialState);
- };
+ }
- export function CreateField(field: RefField) {
- _CreateField(field);
+ let _CreateField: (field: RefField) => void = errorFunc;
+
+ // 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;
- let _respondToUpdate = (diff: any) => {
+ 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);
}
- };
- function respondToUpdate(diff: any) {
- _respondToUpdate(diff);
}
- function connected() {
- _socket.emit(MessageStore.Bar.Message, GUID);
+ export function DeleteDocument(id: string) {
+ Utils.Emit(_socket, MessageStore.DeleteField, id);
}
- Utils.AddServerHandler(_socket, MessageStore.Foo, connected);
- Utils.AddServerHandler(_socket, MessageStore.UpdateField, respondToUpdate);
+ export function DeleteDocuments(ids: string[]) {
+ Utils.Emit(_socket, MessageStore.DeleteFields, ids);
+ }
+
+
+ function _respondToDeleteImpl(ids: string | string[]) {
+ function deleteId(id: string) {
+ delete _cache[id];
+ }
+ if (typeof ids === "string") {
+ deleteId(ids);
+ } else if (Array.isArray(ids)) {
+ ids.map(deleteId);
+ }
+ }
+
+ let _RespondToUpdate = _respondToUpdateImpl;
+ let _respondToDelete = _respondToDeleteImpl;
+
+ function respondToUpdate(diff: any) {
+ _RespondToUpdate(diff);
+ }
+
+ function respondToDelete(ids: string | string[]) {
+ _respondToDelete(ids);
+ }
} \ No newline at end of file
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index de6c5bc6a..3c248760b 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -25,22 +25,43 @@ import { OmitKeys } 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 } 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";
-var requestImageSize = require('request-image-size');
+import { CollectionDockingView } from "../views/collections/CollectionDockingView";
+import { LinkManager } from "../util/LinkManager";
+import { DocumentManager } from "../util/DocumentManager";
+import DirectoryImportBox from "../util/Import & Export/DirectoryImportBox";
+import { Scripting } from "../util/Scripting";
+var requestImageSize = require('../util/request-image-size');
var path = require('path');
+export enum DocumentType {
+ NONE = "none",
+ TEXT = "text",
+ HIST = "histogram",
+ IMG = "image",
+ WEB = "web",
+ COL = "collection",
+ KVP = "kvp",
+ VID = "video",
+ AUDIO = "audio",
+ PDF = "pdf",
+ ICON = "icon",
+ IMPORT = "import",
+ LINK = "link",
+ LINKDOC = "linkdoc"
+}
+
export interface DocumentOptions {
x?: number;
y?: number;
- ink?: InkField;
+ type?: string;
width?: number;
height?: number;
nativeWidth?: number;
@@ -58,330 +79,443 @@ export interface DocumentOptions {
backgroundLayout?: string;
curPage?: number;
documentText?: string;
- borderRounding?: number;
+ borderRounding?: string;
schemaColumns?: List<string>;
dockingConfig?: string;
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") {
- let protoSrc = source.proto ? source.proto : source;
- let protoTarg = target.proto ? target.proto : target;
- UndoManager.RunInBatch(() => {
- let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: -1 });
- let linkDocProto = Doc.GetProto(linkDoc);
- linkDocProto.title = title === "" ? source.title + " to " + target.title : title;
- linkDocProto.linkDescription = description;
- linkDocProto.linkTags = tags;
- linkDocProto.linkedTo = target;
- linkDocProto.linkedFrom = source;
- linkDocProto.linkedToPage = target.curPage;
- linkDocProto.linkedFromPage = source.curPage;
- linkDocProto.linkedToContext = targetContext;
-
- let linkedFrom = Cast(protoTarg.linkedFromDocs, listSpec(Doc));
- let linkedTo = Cast(protoSrc.linkedToDocs, listSpec(Doc));
- !linkedFrom && (protoTarg.linkedFromDocs = linkedFrom = new List<Doc>());
- !linkedTo && (protoSrc.linkedToDocs = linkedTo = new List<Doc>());
- linkedFrom.push(linkDoc);
- linkedTo.push(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;
- 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";
-
- 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();
- });
- }
- 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 });
- 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 CreateHistogramPrototype(): Doc {
- let histoProto = setupPrototypeOptions(histoProtoId, "HISTO PROTO", CollectionView.LayoutString("annotations"),
- { x: 0, y: 0, width: 300, height: 300, backgroundColor: "black", backgroundLayout: HistogramBox.LayoutString() });
- 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) });
- return iconProto;
- }
- function CreateTextPrototype(): Doc {
- let textProto = setupPrototypeOptions(textProtoId, "TEXT_PROTO", FormattedTextBox.LayoutString(),
- { x: 0, y: 0, width: 300, backgroundColor: "#f1efeb" });
- 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 });
- return pdfProto;
- }
- function CreateWebPrototype(): Doc {
- let webProto = setupPrototypeOptions(webProtoId, "WEB_PROTO", WebBox.LayoutString(),
- { x: 0, y: 0, width: 300, height: 300 });
- return webProto;
- }
- function CreateCollectionPrototype(): Doc {
- let collProto = setupPrototypeOptions(collProtoId, "COLLECTION_PROTO", CollectionView.LayoutString("data"),
- { panX: 0, panY: 0, scale: 1, width: 500, height: 500 });
- return collProto;
- }
+ 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 CreateKVPPrototype(): Doc {
- let kvpProto = setupPrototypeOptions(kvpProtoId, "KVP_PROTO", KeyValueBox.LayoutString(),
- { x: 0, y: 0, width: 300, height: 150 });
- 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 });
- return videoProto;
- }
- function CreateAudioPrototype(): Doc {
- let audioProto = setupPrototypeOptions(audioProtoId, "AUDIO_PROTO", AudioBox.LayoutString(),
- { x: 0, y: 0, width: 300, height: 150 });
- return audioProto;
- }
+ // All document prototypes are initialized with at least these values
+ const defaultOptions: DocumentOptions = { x: 0, y: 0, width: 300 };
+ const suffix = "Proto";
- 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;
+ /**
+ * 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);
+
+ // 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 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);
- }
- export function PdfDocument(url: string, options: DocumentOptions = {}) {
- return CreateInstance(pdfProto, new PdfField(new URL(url)), options);
}
- 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;
+ /**
+ * Encapsulates the factory used to create new document instances
+ * delegated from top-level prototypes
+ */
+ export namespace Create {
+
+ 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);
+
+ 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 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 AudioDocument(url: string, options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(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 HistogramDocument(histoOp: HistogramOperation, options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.HIST), new HistogramField(histoOp), options);
+ }
- 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} isTopMost={isTopMost}/>
- </div>
-</div>
- `);
- }
- 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} isTopMost={isTopMost}/>
- </div>
- </div>
- `);
+ export function TextDocument(options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.TEXT), "", options);
+ }
+
+ export function IconDocument(icon: string, options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.ICON), new IconField(icon), options);
+ }
+
+ export function PdfDocument(url: string, options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.PDF), new PdfField(new URL(url)), options);
+ }
+
+ 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;
+ }
+ return Docs.Create.TreeDocument([], { width: 50, height: 100, title: schemaName });
+ }
+
+ export function WebDocument(url: string, options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.WEB), new WebField(new URL(url)), options);
+ }
+
+ 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);
+ }
}
- /*
+ export namespace Get {
- this template requires an additional style setting on the collectionView-cont to make the layout relative
-
-.collectionView-cont {
- position: relative;
- width: 100%;
- height: 100%;
+ 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 Percentaption() {
- return (`
- <div>
- <div style="margin:auto; height:85%; width:85%;">
- {layout}
- </div>
- <div style="height:15%; width:100%; position:absolute">
- <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"caption"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost}/>
- </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;
}
-} \ No newline at end of file
+
+}
+
+Scripting.addGlobal("Docs", Docs);
diff --git a/src/client/goldenLayout.js b/src/client/goldenLayout.js
index 54c9c6068..ad78139c1 100644
--- a/src/client/goldenLayout.js
+++ b/src/client/goldenLayout.js
@@ -2271,6 +2271,17 @@
this._dragListener.on('dragStop', this._createDragListener, this);
},
+ destroy: function () {
+ this._dragListener.destroy();
+ this._element = null;
+ this._itemConfig = null;
+ this._dragListener = null;
+ const index = this._layoutManager._dragSources.indexOf(this);
+ if (index > -1) {
+ this._layoutManager._dragSources.splice(index, 1);
+ }
+ },
+
/**
* Callback for the DragListener's dragStart event
*
@@ -3984,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/northstar/dash-nodes/HistogramBox.tsx b/src/client/northstar/dash-nodes/HistogramBox.tsx
index a60eaea85..b81eafbee 100644
--- a/src/client/northstar/dash-nodes/HistogramBox.tsx
+++ b/src/client/northstar/dash-nodes/HistogramBox.tsx
@@ -36,7 +36,7 @@ export class HistogramBox extends React.Component<FieldViewProps> {
@computed public get HistogramResult(): HistogramResult { return this.HistoOp.Result as HistogramResult; }
@observable public SizeConverter: SizeConverter = new SizeConverter();
- @computed get createOperationParamsCache() { trace(); return this.HistoOp.CreateOperationParameters(); }
+ @computed get createOperationParamsCache() { return this.HistoOp.CreateOperationParameters(); }
@computed get BinRanges() { return this.HistogramResult ? this.HistogramResult.binRanges : undefined; }
@computed get ChartType() {
return !this.BinRanges ? ChartType.SinglePoint : this.BinRanges[0] instanceof AggregateBinRange ?
diff --git a/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx
index 350987695..5a16b3782 100644
--- a/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx
+++ b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx
@@ -62,7 +62,6 @@ export class HistogramBoxPrimitives extends React.Component<HistogramPrimitivesP
}
private renderGridLinesAndLabels(axis: number) {
- trace();
if (!this.props.HistoBox.SizeConverter.Initialized) {
return (null);
}
@@ -111,7 +110,6 @@ export class HistogramBoxPrimitives extends React.Component<HistogramPrimitivesP
x={transXpercent} width={`${widthXpercent}`} y={transYpercent} height={`${heightYpercent}`} fill={color ? `${LABColor.RGBtoHexString(color)}` : "transparent"} />);
}
render() {
- trace();
return <div className="histogramboxprimitives-container">
{this.xaxislines}
{this.yaxislines}
diff --git a/src/client/util/ClientUtils.ts.temp b/src/client/util/ClientUtils.ts.temp
new file mode 100644
index 000000000..f9fad5ed9
--- /dev/null
+++ b/src/client/util/ClientUtils.ts.temp
@@ -0,0 +1,3 @@
+export namespace ClientUtils {
+ export const RELEASE = "mode";
+} \ No newline at end of file
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 862395d74..262194a40 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,14 +1,16 @@
-import { computed, observable } from 'mobx';
+import { computed, observable, action } from 'mobx';
import { DocumentView } from '../views/nodes/DocumentView';
import { Doc, DocListCast, Opt } from '../../new_fields/Doc';
-import { FieldValue, Cast, NumCast, BoolCast } from '../../new_fields/Types';
+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';
import { CollectionVideoView } from '../views/collections/CollectionVideoView';
import { Id } from '../../new_fields/FieldSymbols';
+import { LinkManager } from './LinkManager';
+import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
export class DocumentManager {
@@ -30,6 +32,29 @@ export class DocumentManager {
// this.DocumentViews = new Array<DocumentView>();
}
+ //gets all views
+ public getDocumentViewsById(id: string) {
+ let toReturn: DocumentView[] = [];
+ DocumentManager.Instance.DocumentViews.map(view => {
+ if (view.props.Document[Id] === id) {
+ toReturn.push(view);
+ }
+ });
+ if (toReturn.length === 0) {
+ DocumentManager.Instance.DocumentViews.map(view => {
+ let doc = view.props.Document.proto;
+ if (doc && doc[Id]) {
+ if (doc[Id] === id) { toReturn.push(view); }
+ }
+ });
+ }
+ return toReturn;
+ }
+
+ public getAllDocumentViews(doc: Doc) {
+ return this.getDocumentViewsById(doc[Id]);
+ }
+
public getDocumentViewById(id: string, preferredCollection?: CollectionView | CollectionPDFView | CollectionVideoView): DocumentView | null {
let toReturn: DocumentView | null = null;
@@ -66,13 +91,11 @@ export class DocumentManager {
//gets document view that is in a freeform canvas collection
DocumentManager.Instance.DocumentViews.map(view => {
let doc = view.props.Document;
- // if (view.props.ContainingCollectionView instanceof CollectionFreeFormView) {
if (doc === toFind) {
toReturn.push(view);
} else {
- let docSrc = FieldValue(doc.proto);
- if (docSrc && Object.is(docSrc, toFind)) {
+ if (Doc.AreProtosEqual(doc, toFind)) {
toReturn.push(view);
}
}
@@ -83,39 +106,29 @@ export class DocumentManager {
@computed
public get LinkedDocumentViews() {
- return DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false)).reduce((pairs, dv) => {
- let linksList = DocListCast(dv.props.Document.linkedToDocs);
- if (linksList && linksList.length) {
- pairs.push(...linksList.reduce((pairs, link) => {
- if (link) {
- let linkToDoc = FieldValue(Cast(link.linkedTo, Doc));
- 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 }[]));
- }
- linksList = DocListCast(dv.props.Document.linkedFromDocs);
- if (linksList && linksList.length) {
- pairs.push(...linksList.reduce((pairs, link) => {
- if (link) {
- let linkFromDoc = FieldValue(Cast(link.linkedFrom, Doc));
- if (linkFromDoc) {
- DocumentManager.Instance.getDocumentViews(linkFromDoc).map(docView1 =>
- pairs.push({ a: dv, b: docView1, l: link }));
- }
+ 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);
+ if (linkToDoc) {
+ DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => {
+ pairs.push({ a: dv, b: docView1, l: link });
+ });
}
- return pairs;
- }, pairs));
- }
+ }
+ return pairs;
+ }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]));
+ // }
return pairs;
}, [] as { a: DocumentView, b: DocumentView, l: Doc }[]);
+
+ return pairs;
}
+
@undoBatch
- public jumpToDocument = async (docDelegate: Doc, forceDockFunc: boolean = false, dockFunc?: (doc: Doc) => void, linkPage?: number, docContext?: Doc): Promise<void> => {
+ public jumpToDocument = async (docDelegate: Doc, willZoom: boolean, forceDockFunc: boolean = false, dockFunc?: (doc: Doc) => void, linkPage?: number, docContext?: Doc): Promise<void> => {
let doc = Doc.GetProto(docDelegate);
const contextDoc = await Cast(doc.annotationOn, Doc);
if (contextDoc) {
@@ -129,39 +142,64 @@ 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);
+ UndoManager.RunInBatch(() => {
+ docView!.props.focus(docView!.props.Document, willZoom);
+ }, "focus");
} else {
if (!contextDoc) {
if (docContext) {
let targetContextView: DocumentView | null;
if (!forceDockFunc && docContext && (targetContextView = DocumentManager.Instance.getDocumentView(docContext))) {
docContext.panTransformType = "Ease";
- targetContextView.props.focus(docDelegate);
+ targetContextView.props.focus(docDelegate, willZoom);
} else {
- (dockFunc || CollectionDockingView.Instance.AddRightSplit)(docContext);
+ (dockFunc || CollectionDockingView.Instance.AddRightSplit)(docContext, undefined);
setTimeout(() => {
- this.jumpToDocument(docDelegate, forceDockFunc, dockFunc, linkPage);
+ this.jumpToDocument(docDelegate, willZoom, forceDockFunc, dockFunc, linkPage);
}, 10);
}
} else {
const actualDoc = Doc.MakeAlias(docDelegate);
actualDoc.libraryBrush = true;
if (linkPage !== undefined) actualDoc.curPage = linkPage;
- (dockFunc || CollectionDockingView.Instance.AddRightSplit)(actualDoc);
+ (dockFunc || CollectionDockingView.Instance.AddRightSplit)(actualDoc, undefined);
}
} else {
let contextView: DocumentView | null;
docDelegate.libraryBrush = true;
if (!forceDockFunc && (contextView = DocumentManager.Instance.getDocumentView(contextDoc))) {
contextDoc.panTransformType = "Ease";
- contextView.props.focus(docDelegate);
+ contextView.props.focus(docDelegate, willZoom);
} else {
- (dockFunc || CollectionDockingView.Instance.AddRightSplit)(contextDoc);
+ (dockFunc || CollectionDockingView.Instance.AddRightSplit)(contextDoc, undefined);
setTimeout(() => {
- this.jumpToDocument(docDelegate, forceDockFunc, dockFunc, linkPage);
+ this.jumpToDocument(docDelegate, willZoom, forceDockFunc, dockFunc, linkPage);
}, 10);
}
}
}
}
+
+ @action
+ zoomIntoScale = (docDelegate: Doc, scale: number) => {
+ let doc = Doc.GetProto(docDelegate);
+
+ let docView: DocumentView | null;
+ docView = DocumentManager.Instance.getDocumentView(doc);
+ if (docView) {
+ docView.props.zoomToScale(scale);
+ }
+ }
+
+ getScaleOfDocView = (docDelegate: Doc) => {
+ let doc = Doc.GetProto(docDelegate);
+
+ let docView: DocumentView | null;
+ docView = DocumentManager.Instance.getDocumentView(doc);
+ if (docView) {
+ return docView.props.getScale();
+ } else {
+ return 1;
+ }
+ }
} \ No newline at end of file
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index c3c92daa5..323908302 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -1,66 +1,109 @@
-import { action, runInAction, observable } from "mobx";
-import { Doc, DocListCastAsync } from "../../new_fields/Doc";
+import { action, runInAction } from "mobx";
+import { Doc } from "../../new_fields/Doc";
import { Cast } from "../../new_fields/Types";
+import { URLField } from "../../new_fields/URLField";
import { emptyFunction } from "../../Utils";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
import * as globalCssVariables from "../views/globalCssVariables.scss";
-import { URLField } from "../../new_fields/URLField";
+import { DocumentManager } from "./DocumentManager";
+import { LinkManager } from "./LinkManager";
import { SelectionManager } from "./SelectionManager";
export type dropActionType = "alias" | "copy" | undefined;
-export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () => Doc | Promise<Doc>, moveFunc?: DragManager.MoveFunction, dropAction?: dropActionType, options?: any, dontHideOnDrop?: boolean) {
+export function SetupDrag(
+ _reference: React.RefObject<HTMLElement>,
+ docFunc: () => Doc | Promise<Doc>,
+ moveFunc?: DragManager.MoveFunction,
+ dropAction?: dropActionType,
+ options?: any,
+ dontHideOnDrop?: boolean,
+ dragStarted?: () => void
+) {
let onRowMove = async (e: PointerEvent) => {
e.stopPropagation();
e.preventDefault();
document.removeEventListener("pointermove", onRowMove);
document.removeEventListener('pointerup', onRowUp);
- var dragData = new DragManager.DocumentDragData([await docFunc()]);
+ let doc = await docFunc();
+ var dragData = new DragManager.DocumentDragData([doc], [undefined]);
dragData.dropAction = dropAction;
dragData.moveDocument = moveFunc;
dragData.options = options;
dragData.dontHideOnDrop = dontHideOnDrop;
DragManager.StartDocumentDrag([_reference.current!], dragData, e.x, e.y);
+ dragStarted && dragStarted();
};
let onRowUp = (): void => {
document.removeEventListener("pointermove", onRowMove);
document.removeEventListener('pointerup', onRowUp);
};
let onItemDown = async (e: React.PointerEvent) => {
- // if (this.props.isSelected() || this.props.isTopMost) {
if (e.button === 0) {
e.stopPropagation();
if (e.shiftKey && CollectionDockingView.Instance) {
- CollectionDockingView.Instance.StartOtherDrag([await docFunc()], e);
+ e.persist();
+ CollectionDockingView.Instance.StartOtherDrag({
+ pageX: e.pageX,
+ pageY: e.pageY,
+ preventDefault: emptyFunction,
+ button: 0
+ }, [await docFunc()]);
} else {
document.addEventListener("pointermove", onRowMove);
document.addEventListener("pointerup", onRowUp);
}
}
- //}
};
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);
+ 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) {
let srcTarg = sourceDoc.proto;
let draggedDocs: Doc[] = [];
- let draggedFromDocs: Doc[] = [];
+
if (srcTarg) {
- let linkToDocs = await DocListCastAsync(srcTarg.linkedToDocs);
- let linkFromDocs = await DocListCastAsync(srcTarg.linkedFromDocs);
- if (linkToDocs) draggedDocs = linkToDocs.map(linkDoc => Cast(linkDoc.linkedTo, Doc) as Doc);
- if (linkFromDocs) draggedFromDocs = linkFromDocs.map(linkDoc => Cast(linkDoc.linkedFrom, Doc) as Doc);
+ let linkDocs = LinkManager.Instance.getAllRelatedLinks(srcTarg);
+ if (linkDocs) {
+ draggedDocs = linkDocs.map(link => {
+ let opp = LinkManager.Instance.getOppositeAnchor(link, sourceDoc);
+ if (opp) return opp;
+ }) as Doc[];
+ }
}
- draggedDocs.push(...draggedFromDocs);
if (draggedDocs.length) {
let moddrag: Doc[] = [];
for (const draggedDoc of draggedDocs) {
let doc = await Cast(draggedDoc.annotationOn, Doc);
if (doc) moddrag.push(doc);
}
- let dragData = new DragManager.DocumentDragData(moddrag.length ? moddrag : draggedDocs);
- DragManager.StartDocumentDrag([dragEle], dragData, x, y, {
+ let dragdocs = moddrag.length ? moddrag : draggedDocs;
+ let dragData = new DragManager.DocumentDragData(dragdocs, dragdocs);
+ dragData.moveDocument = moveLinkedDocument;
+ DragManager.StartLinkedDocumentDrag([dragEle], dragData, x, y, {
handlers: {
dragComplete: action(emptyFunction),
},
@@ -69,6 +112,7 @@ export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: n
}
}
+
export namespace DragManager {
export function Root() {
const root = document.getElementById("root");
@@ -90,6 +134,10 @@ export namespace DragManager {
handlers: DragHandlers;
hideSource: boolean | (() => boolean);
+
+ dragHasStarted?: () => void;
+
+ withoutShiftDrag?: boolean;
}
export interface DragDropDisposer {
@@ -141,13 +189,15 @@ export namespace DragManager {
export type MoveFunction = (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
export class DocumentDragData {
- constructor(dragDoc: Doc[]) {
+ constructor(dragDoc: Doc[], dragDataDocs: (Doc | undefined)[]) {
this.draggedDocuments = dragDoc;
+ this.draggedDataDocs = dragDataDocs;
this.droppedDocuments = dragDoc;
this.xOffset = 0;
this.yOffset = 0;
}
draggedDocuments: Doc[];
+ draggedDataDocs: (Doc | undefined)[];
droppedDocuments: Doc[];
xOffset: number;
yOffset: number;
@@ -178,12 +228,38 @@ export namespace DragManager {
export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
runInAction(() => StartDragFunctions.map(func => func()));
StartDrag(eles, dragData, downX, downY, options,
- (dropData: { [id: string]: any }) =>
+ (dropData: { [id: string]: any }) => {
(dropData.droppedDocuments = dragData.userDropAction === "alias" || (!dragData.userDropAction && dragData.dropAction === "alias") ?
dragData.draggedDocuments.map(d => Doc.MakeAlias(d)) :
dragData.userDropAction === "copy" || (!dragData.userDropAction && dragData.dropAction === "copy") ?
dragData.draggedDocuments.map(d => Doc.MakeCopy(d, true)) :
- dragData.draggedDocuments));
+ dragData.draggedDocuments
+ );
+ });
+ }
+
+ 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 }) => {
+ 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) {
+ inContext.forEach(dv => droppedDocs.push(dv.props.Document));
+ } else {
+ droppedDocs.push(Doc.MakeAlias(d));
+ }
+ } else {
+ droppedDocs.push(Doc.MakeAlias(d));
+ }
+ return droppedDocs;
+ }, []);
+ dropData.droppedDocuments = droppedDocuments;
+ });
}
export function StartAnnotationDrag(eles: HTMLElement[], dragData: AnnotationDragData, downX: number, downY: number, options?: DragOptions) {
@@ -238,6 +314,8 @@ export namespace DragManager {
const docs: Doc[] =
dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnnotationDragData ? [dragData.dragDocument] : [];
+ const datadocs: (Doc | undefined)[] =
+ dragData instanceof DocumentDragData ? dragData.draggedDataDocs : dragData instanceof AnnotationDragData ? [dragData.dragDocument] : [];
let dragElements = eles.map(ele => {
const w = ele.offsetWidth,
h = ele.offsetHeight;
@@ -252,11 +330,13 @@ export namespace DragManager {
scaleYs.push(scaleY);
let dragElement = ele.cloneNode(true) as HTMLElement;
dragElement.style.opacity = "0.7";
+ dragElement.style.borderRadius = getComputedStyle(ele).borderRadius;
dragElement.style.position = "absolute";
dragElement.style.margin = "0";
dragElement.style.top = "0";
dragElement.style.bottom = "";
dragElement.style.left = "0";
+ dragElement.style.transition = "none";
dragElement.style.color = "black";
dragElement.style.transformOrigin = "0 0";
dragElement.style.zIndex = globalCssVariables.contextMenuZindex;// "1000";
@@ -312,14 +392,14 @@ export namespace DragManager {
if (dragData instanceof DocumentDragData) {
dragData.userDropAction = e.ctrlKey || e.altKey ? "alias" : undefined;
}
- if (e.shiftKey && CollectionDockingView.Instance) {
+ if (((options && !options.withoutShiftDrag) || !options) && e.shiftKey && CollectionDockingView.Instance) {
AbortDrag();
- CollectionDockingView.Instance.StartOtherDrag(docs, {
+ CollectionDockingView.Instance.StartOtherDrag({
pageX: e.pageX,
pageY: e.pageY,
preventDefault: emptyFunction,
button: 0
- });
+ }, docs, datadocs);
}
//TODO: Why can't we use e.movementX and e.movementY?
let moveX = e.pageX - lastX;
@@ -384,7 +464,7 @@ export namespace DragManager {
x: e.x,
y: e.y,
data: dragData,
- mods: e.altKey ? "AltKey" : ""
+ mods: e.altKey ? "AltKey" : e.ctrlKey ? "CtrlKey" : ""
}
})
);
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
new file mode 100644
index 000000000..75b0b52a7
--- /dev/null
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx
@@ -0,0 +1,376 @@
+import "fs";
+import React = require("react");
+import { Doc, Opt, DocListCast, DocListCastAsync } from "../../../new_fields/Doc";
+import { DocServer } from "../../DocServer";
+import { RouteStore } from "../../../server/RouteStore";
+import { action, observable, autorun, runInAction, computed } from "mobx";
+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 { 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";
+import { Utils } from "../../../Utils";
+import { DocumentManager } from "../DocumentManager";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { List } from "../../../new_fields/List";
+import { Cast, BoolCast, NumCast } from "../../../new_fields/Types";
+import { listSpec } from "../../../new_fields/Schema";
+
+const unsupported = ["text/html", "text/plain"];
+
+@observer
+export default class DirectoryImportBox extends React.Component<FieldViewProps> {
+ private selector = React.createRef<HTMLInputElement>();
+ @observable private top = 0;
+ @observable private left = 0;
+ private dimensions = 50;
+
+ @observable private entries: ImportMetadataEntry[] = [];
+
+ @observable private quota = 1;
+ @observable private remaining = 1;
+
+ @observable private uploading = false;
+ @observable private removeHover = false;
+
+ public static LayoutString() { return FieldView.LayoutString(DirectoryImportBox); }
+
+ constructor(props: FieldViewProps) {
+ super(props);
+ library.add(faTag, faPlus);
+ let doc = this.props.Document;
+ this.editingMetadata = this.editingMetadata || false;
+ this.persistent = this.persistent || false;
+ !Cast(doc.data, listSpec(Doc)) && (doc.data = new List<Doc>());
+ }
+
+ @computed
+ private get editingMetadata() {
+ return BoolCast(this.props.Document.editingMetadata);
+ }
+
+ private set editingMetadata(value: boolean) {
+ this.props.Document.editingMetadata = value;
+ }
+
+ @computed
+ private get persistent() {
+ return BoolCast(this.props.Document.persistent);
+ }
+
+ private set persistent(value: boolean) {
+ this.props.Document.persistent = value;
+ }
+
+ handleSelection = async (e: React.ChangeEvent<HTMLInputElement>) => {
+ runInAction(() => this.uploading = true);
+
+ let promises: Promise<void>[] = [];
+ let docs: Doc[] = [];
+
+ let files = e.target.files;
+ if (!files || files.length === 0) return;
+
+ let directory = (files.item(0) as any).webkitRelativePath.split("/", 1);
+
+ let validated: File[] = [];
+ for (let i = 0; i < files.length; i++) {
+ let file = files.item(i);
+ file && !unsupported.includes(file.type) && validated.push(file);
+ }
+
+ runInAction(() => this.quota = validated.length);
+
+ let sizes = [];
+ let modifiedDates = [];
+
+ for (let uploaded_file of validated) {
+ let formData = new FormData();
+ formData.append('file', uploaded_file);
+ let dropFileName = uploaded_file ? uploaded_file.name : "-empty-";
+ let type = uploaded_file.type;
+
+ sizes.push(uploaded_file.size);
+ modifiedDates.push(uploaded_file.lastModified);
+
+ runInAction(() => this.remaining++);
+
+ let prom = fetch(Utils.prepend(RouteStore.upload), {
+ method: 'POST',
+ body: formData
+ }).then(async (res: Response) => {
+ (await res.json()).map(action((file: any) => {
+ let docPromise = Docs.Get.DocumentFromType(type, Utils.prepend(file), { nativeWidth: 300, width: 300, title: dropFileName });
+ docPromise.then(doc => {
+ doc && docs.push(doc) && runInAction(() => this.remaining--);
+ });
+ }));
+ });
+ promises.push(prom);
+ }
+
+ await Promise.all(promises);
+
+ for (let i = 0; i < docs.length; i++) {
+ let doc = docs[i];
+ doc.size = sizes[i];
+ doc.modified = modifiedDates[i];
+ this.entries.forEach(entry => {
+ let target = entry.onDataDoc ? Doc.GetProto(doc) : doc;
+ target[entry.key] = entry.value;
+ });
+ }
+
+ let doc = this.props.Document;
+ let height: number = NumCast(doc.height) || 0;
+ let offset: number = this.persistent ? (height === 0 ? 0 : height + 30) : 0;
+ let options: DocumentOptions = {
+ title: `Import of ${directory}`,
+ width: 1105,
+ height: 500,
+ x: NumCast(doc.x),
+ y: NumCast(doc.y) + offset
+ };
+ let parent = this.props.ContainingCollectionView;
+ if (parent) {
+ 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);
+ DocumentManager.Instance.jumpToDocument(importContainer, true);
+
+ }
+
+ runInAction(() => {
+ this.uploading = false;
+ this.quota = 1;
+ this.remaining = 1;
+ });
+ }
+
+ componentDidMount() {
+ this.selector.current!.setAttribute("directory", "");
+ this.selector.current!.setAttribute("webkitdirectory", "");
+ }
+
+ @action
+ preserveCentering = (rect: ContentRect) => {
+ let bounds = rect.offset!;
+ if (bounds.width === 0 || bounds.height === 0) {
+ return;
+ }
+ let offset = this.dimensions / 2;
+ this.left = bounds.width / 2 - offset;
+ this.top = bounds.height / 2 - offset;
+ }
+
+ @action
+ addMetadataEntry = async () => {
+ let entryDoc = new Doc();
+ entryDoc.checked = false;
+ entryDoc.key = keyPlaceholder;
+ entryDoc.value = valuePlaceholder;
+ Doc.AddDocToList(this.props.Document, "data", entryDoc);
+ }
+
+ @action
+ remove = async (entry: ImportMetadataEntry) => {
+ let metadata = await DocListCastAsync(this.props.Document.data);
+ if (metadata) {
+ let index = this.entries.indexOf(entry);
+ if (index !== -1) {
+ runInAction(() => this.entries.splice(index, 1));
+ index = metadata.indexOf(entry.props.Document);
+ if (index !== -1) {
+ metadata.splice(index, 1);
+ }
+ }
+
+ }
+ }
+
+ render() {
+ let dimensions = 50;
+ let entries = DocListCast(this.props.Document.data);
+ let isEditing = this.editingMetadata;
+ let remaining = this.remaining;
+ let quota = this.quota;
+ let uploading = this.uploading;
+ let showRemoveLabel = this.removeHover;
+ let persistent = this.persistent;
+ let percent = `${100 - (remaining / quota * 100)}`;
+ percent = percent.split(".")[0];
+ percent = percent.startsWith("100") ? "99" : percent;
+ let marginOffset = (percent.length === 1 ? 5 : 0) - 1.6;
+ return (
+ <Measure offset onResize={this.preserveCentering}>
+ {({ measureRef }) =>
+ <div ref={measureRef} style={{ width: "100%", height: "100%", pointerEvents: "all" }} >
+ <input
+ id={"selector"}
+ ref={this.selector}
+ onChange={this.handleSelection}
+ type="file"
+ style={{
+ position: "absolute",
+ display: "none"
+ }} />
+ <label
+ htmlFor={"selector"}
+ style={{
+ opacity: isEditing ? 0 : 1,
+ pointerEvents: isEditing ? "none" : "all",
+ transition: "0.4s ease opacity"
+ }}
+ >
+ <div style={{
+ width: dimensions,
+ height: dimensions,
+ borderRadius: "50%",
+ background: "black",
+ position: "absolute",
+ left: this.left,
+ top: this.top
+ }} />
+ <div style={{
+ position: "absolute",
+ left: this.left + 8,
+ top: this.top + 10,
+ opacity: uploading ? 0 : 1,
+ transition: "0.4s opacity ease"
+ }}>
+ <FontAwesomeIcon icon={faCloudUploadAlt} color="#FFFFFF" size={"2x"} />
+ </div>
+ <img
+ style={{
+ width: 80,
+ height: 80,
+ transition: "0.4s opacity ease",
+ opacity: uploading ? 0.7 : 0,
+ position: "absolute",
+ top: this.top - 15,
+ left: this.left - 15
+ }}
+ src={"/assets/loading.gif"}></img>
+ </label>
+ <input
+ type={"checkbox"}
+ onChange={e => runInAction(() => this.persistent = e.target.checked)}
+ style={{
+ margin: 0,
+ position: "absolute",
+ left: 10,
+ bottom: 10,
+ opacity: isEditing || uploading ? 0 : 1,
+ transition: "0.4s opacity ease",
+ pointerEvents: isEditing || uploading ? "none" : "all"
+ }}
+ checked={this.persistent}
+ onPointerEnter={action(() => this.removeHover = true)}
+ onPointerLeave={action(() => this.removeHover = false)}
+ />
+ <p
+ style={{
+ position: "absolute",
+ left: 27,
+ bottom: 8.4,
+ fontSize: 12,
+ opacity: showRemoveLabel ? 1 : 0,
+ transition: "0.4s opacity ease"
+ }}>Template will be <span style={{ textDecoration: "underline", textDecorationColor: persistent ? "green" : "red", color: persistent ? "green" : "red" }}>{persistent ? "kept" : "removed"}</span> after upload</p>
+ <div
+ style={{
+ transition: "0.4s opacity ease",
+ opacity: uploading ? 1 : 0,
+ pointerEvents: "none",
+ position: "absolute",
+ left: 10,
+ top: this.top + 12.3,
+ fontSize: 18,
+ color: "white",
+ marginLeft: this.left + marginOffset
+ }}>{percent}%</div>
+ <div
+ style={{
+ position: "absolute",
+ top: 10,
+ right: 10,
+ borderRadius: "50%",
+ width: 25,
+ height: 25,
+ background: "black",
+ pointerEvents: uploading ? "none" : "all",
+ opacity: uploading ? 0 : 1,
+ transition: "0.4s opacity ease"
+ }}
+ title={isEditing ? "Back to Upload" : "Add Metadata"}
+ onClick={action(() => this.editingMetadata = !this.editingMetadata)}
+ />
+ <FontAwesomeIcon
+ style={{
+ pointerEvents: "none",
+ position: "absolute",
+ right: isEditing ? 16.3 : 14.5,
+ top: isEditing ? 15.4 : 16,
+ opacity: uploading ? 0 : 1,
+ transition: "0.4s opacity ease"
+ }}
+ icon={isEditing ? faCloudUploadAlt : faTag}
+ color="#FFFFFF"
+ size={"1x"}
+ />
+ <div
+ style={{
+ transition: "0.4s ease opacity",
+ width: "100%",
+ height: "100%",
+ pointerEvents: isEditing ? "all" : "none",
+ opacity: isEditing ? 1 : 0,
+ overflowY: "scroll"
+ }}
+ >
+ <div
+ style={{
+ borderRadius: "50%",
+ width: 25,
+ height: 25,
+ marginLeft: 10,
+ position: "absolute",
+ right: 41,
+ top: 10
+ }}
+ title={"Add Metadata Entry"}
+ onClick={this.addMetadataEntry}
+ >
+ <FontAwesomeIcon
+ style={{
+ pointerEvents: "none",
+ marginLeft: 6.4,
+ marginTop: 5.2
+ }}
+ icon={faPlus}
+ size={"1x"}
+ />
+ </div>
+ <p style={{ paddingLeft: 10, paddingTop: 8, paddingBottom: 7 }} >Add metadata to your import...</p>
+ <hr style={{ margin: "6px 10px 12px 10px" }} />
+ {entries.map(doc =>
+ <ImportMetadataEntry
+ Document={doc}
+ key={doc[Id]}
+ remove={this.remove}
+ ref={(el) => { if (el) this.entries.push(el); }}
+ next={this.addMetadataEntry}
+ />
+ )}
+ </div>
+ </div>
+ }
+ </Measure>
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/util/Import & Export/ImportMetadataEntry.tsx b/src/client/util/Import & Export/ImportMetadataEntry.tsx
new file mode 100644
index 000000000..f5198c39b
--- /dev/null
+++ b/src/client/util/Import & Export/ImportMetadataEntry.tsx
@@ -0,0 +1,149 @@
+import React = require("react");
+import { observer } from "mobx-react";
+import { EditableView } from "../../views/EditableView";
+import { observable, action, computed } from "mobx";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faPlus } from "@fortawesome/free-solid-svg-icons";
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { Opt, Doc } from "../../../new_fields/Doc";
+import { StrCast, BoolCast } from "../../../new_fields/Types";
+
+interface KeyValueProps {
+ Document: Doc;
+ remove: (self: ImportMetadataEntry) => void;
+ next: () => void;
+}
+
+export const keyPlaceholder = "Key";
+export const valuePlaceholder = "Value";
+
+@observer
+export default class ImportMetadataEntry extends React.Component<KeyValueProps> {
+
+ private keyRef = React.createRef<EditableView>();
+ private valueRef = React.createRef<EditableView>();
+ private checkRef = React.createRef<HTMLInputElement>();
+
+ constructor(props: KeyValueProps) {
+ super(props);
+ library.add(faPlus);
+ }
+
+ @computed
+ public get valid() {
+ return (this.key.length > 0 && this.key !== keyPlaceholder) && (this.value.length > 0 && this.value !== valuePlaceholder);
+ }
+
+ @computed
+ private get backing() {
+ return this.props.Document;
+ }
+
+ @computed
+ public get onDataDoc() {
+ return BoolCast(this.backing.checked);
+ }
+
+ public set onDataDoc(value: boolean) {
+ this.backing.checked = value;
+ }
+
+ @computed
+ public get key() {
+ return StrCast(this.backing.key);
+ }
+
+ public set key(value: string) {
+ this.backing.key = value;
+ }
+
+ @computed
+ public get value() {
+ return StrCast(this.backing.value);
+ }
+
+ public set value(value: string) {
+ this.backing.value = value;
+ }
+
+ @action
+ updateKey = (newKey: string) => {
+ this.key = newKey;
+ this.keyRef.current && this.keyRef.current.setIsFocused(false);
+ this.valueRef.current && this.valueRef.current.setIsFocused(true);
+ this.key.length === 0 && (this.key = keyPlaceholder);
+ return true;
+ }
+
+ @action
+ updateValue = (newValue: string, shiftDown: boolean) => {
+ this.value = newValue;
+ this.valueRef.current && this.valueRef.current.setIsFocused(false);
+ this.value.length > 0 && shiftDown && this.props.next();
+ this.value.length === 0 && (this.value = valuePlaceholder);
+ return true;
+ }
+
+ render() {
+ let keyValueStyle: React.CSSProperties = {
+ paddingLeft: 10,
+ width: "50%",
+ opacity: this.valid ? 1 : 0.5,
+ };
+ return (
+ <div
+ style={{
+ display: "flex",
+ flexDirection: "row",
+ paddingBottom: 5,
+ paddingRight: 5,
+ justifyContent: "center",
+ alignItems: "center",
+ alignContent: "center"
+ }}
+ >
+ <input
+ onChange={e => this.onDataDoc = e.target.checked}
+ ref={this.checkRef}
+ style={{ margin: "0 10px 0 15px" }}
+ type="checkbox"
+ title={"Add to Data Document?"}
+ checked={this.onDataDoc}
+ />
+ <div className={"key_container"} style={keyValueStyle}>
+ <EditableView
+ ref={this.keyRef}
+ contents={this.key}
+ SetValue={this.updateKey}
+ GetValue={() => ""}
+ oneLine={true}
+ />
+ </div>
+ <div
+ className={"value_container"}
+ style={keyValueStyle}>
+ <EditableView
+ ref={this.valueRef}
+ contents={this.value}
+ SetValue={this.updateValue}
+ GetValue={() => ""}
+ oneLine={true}
+ />
+ </div>
+ <div onClick={() => this.props.remove(this)} title={"Delete Entry"}>
+ <FontAwesomeIcon
+ icon={faPlus}
+ color={"red"}
+ size={"1x"}
+ style={{
+ marginLeft: 15,
+ marginRight: 15,
+ transform: "rotate(45deg)"
+ }}
+ />
+ </div>
+ </div>
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
new file mode 100644
index 000000000..a647f22c1
--- /dev/null
+++ b/src/client/util/LinkManager.ts
@@ -0,0 +1,245 @@
+import { observable, action } from "mobx";
+import { StrCast, Cast, FieldValue } from "../../new_fields/Types";
+import { Doc, DocListCast } from "../../new_fields/Doc";
+import { listSpec } from "../../new_fields/Schema";
+import { List } from "../../new_fields/List";
+import { Id } from "../../new_fields/FieldSymbols";
+import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
+import { Docs } from "../documents/Documents";
+
+
+/*
+ * link doc:
+ * - anchor1: doc
+ * - anchor1page: number
+ * - anchor1groups: list of group docs representing the groups anchor1 categorizes this link/anchor2 in
+ * - anchor2: doc
+ * - anchor2page: number
+ * - anchor2groups: list of group docs representing the groups anchor2 categorizes this link/anchor1 in
+ *
+ * group doc:
+ * - type: string representing the group type/name/category
+ * - metadata: doc representing the metadata kvps
+ *
+ * metadata doc:
+ * - user defined kvps
+ */
+export class LinkManager {
+
+ private static _instance: LinkManager;
+ public static get Instance(): LinkManager {
+ return this._instance || (this._instance = new this());
+ }
+ private constructor() {
+ }
+
+ // 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 Docs.Prototypes.MainLinkDocument();
+ }
+
+ public getAllLinks(): Doc[] {
+ return LinkManager.Instance.LinkManagerDoc ? LinkManager.Instance.LinkManagerDoc.allLinks ? DocListCast(LinkManager.Instance.LinkManagerDoc.allLinks) : [] : [];
+ }
+
+ public addLink(linkDoc: Doc): boolean {
+ let linkList = LinkManager.Instance.getAllLinks();
+ linkList.push(linkDoc);
+ if (LinkManager.Instance.LinkManagerDoc) {
+ LinkManager.Instance.LinkManagerDoc.allLinks = new List<Doc>(linkList);
+ return true;
+ }
+ return false;
+ }
+
+ public deleteLink(linkDoc: Doc): boolean {
+ let linkList = LinkManager.Instance.getAllLinks();
+ let index = LinkManager.Instance.getAllLinks().indexOf(linkDoc);
+ if (index > -1) {
+ linkList.splice(index, 1);
+ if (LinkManager.Instance.LinkManagerDoc) {
+ LinkManager.Instance.LinkManagerDoc.allLinks = new List<Doc>(linkList);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // finds all links that contain the given anchor
+ public getAllRelatedLinks(anchor: Doc): Doc[] {//List<Doc> {
+ let related = LinkManager.Instance.getAllLinks().filter(link => {
+ let protomatch1 = Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, null));
+ let protomatch2 = Doc.AreProtosEqual(anchor, Cast(link.anchor2, Doc, null));
+ return protomatch1 || protomatch2;
+ });
+ return related;
+ }
+
+ public deleteAllLinksOnAnchor(anchor: Doc) {
+ let related = LinkManager.Instance.getAllRelatedLinks(anchor);
+ related.forEach(linkDoc => LinkManager.Instance.deleteLink(linkDoc));
+ }
+
+ public addGroupType(groupType: string): boolean {
+ if (LinkManager.Instance.LinkManagerDoc) {
+ LinkManager.Instance.LinkManagerDoc[groupType] = new List<string>([]);
+ let groupTypes = LinkManager.Instance.getAllGroupTypes();
+ groupTypes.push(groupType);
+ LinkManager.Instance.LinkManagerDoc.allGroupTypes = new List<string>(groupTypes);
+ return true;
+ }
+ return false;
+ }
+
+ // removes all group docs from all links with the given group type
+ public deleteGroupType(groupType: string): boolean {
+ if (LinkManager.Instance.LinkManagerDoc) {
+ if (LinkManager.Instance.LinkManagerDoc[groupType]) {
+ let groupTypes = LinkManager.Instance.getAllGroupTypes();
+ let index = groupTypes.findIndex(type => type.toUpperCase() === groupType.toUpperCase());
+ if (index > -1) groupTypes.splice(index, 1);
+ LinkManager.Instance.LinkManagerDoc.allGroupTypes = new List<string>(groupTypes);
+ LinkManager.Instance.LinkManagerDoc[groupType] = undefined;
+ LinkManager.Instance.getAllLinks().forEach(async linkDoc => {
+ const anchor1 = await Cast(linkDoc.anchor1, Doc);
+ const anchor2 = await Cast(linkDoc.anchor2, Doc);
+ anchor1 && LinkManager.Instance.removeGroupFromAnchor(linkDoc, anchor1, groupType);
+ anchor2 && LinkManager.Instance.removeGroupFromAnchor(linkDoc, anchor2, groupType);
+ });
+ }
+ return true;
+ } else return false;
+ }
+
+ public getAllGroupTypes(): string[] {
+ if (LinkManager.Instance.LinkManagerDoc) {
+ if (LinkManager.Instance.LinkManagerDoc.allGroupTypes) {
+ return Cast(LinkManager.Instance.LinkManagerDoc.allGroupTypes, listSpec("string"), []);
+ } else {
+ LinkManager.Instance.LinkManagerDoc.allGroupTypes = new List<string>([]);
+ return [];
+ }
+ }
+ return [];
+ }
+
+ // gets the groups associates with an anchor in a link
+ public getAnchorGroups(linkDoc: Doc, anchor?: Doc): Array<Doc> {
+ if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, null))) {
+ return DocListCast(linkDoc.anchor1Groups);
+ } else {
+ return DocListCast(linkDoc.anchor2Groups);
+ }
+ }
+
+ // sets the groups of the given anchor in the given link
+ public setAnchorGroups(linkDoc: Doc, anchor: Doc, groups: Doc[]) {
+ if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, null))) {
+ linkDoc.anchor1Groups = new List<Doc>(groups);
+ } else {
+ linkDoc.anchor2Groups = new List<Doc>(groups);
+ }
+ }
+
+ public addGroupToAnchor(linkDoc: Doc, anchor: Doc, groupDoc: Doc, replace: boolean = false) {
+ let groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor);
+ let index = groups.findIndex(gDoc => {
+ return StrCast(groupDoc.type).toUpperCase() === StrCast(gDoc.type).toUpperCase();
+ });
+ if (index > -1 && replace) {
+ groups[index] = groupDoc;
+ }
+ if (index === -1) {
+ groups.push(groupDoc);
+ }
+ LinkManager.Instance.setAnchorGroups(linkDoc, anchor, groups);
+ }
+
+ // removes group doc of given group type only from given anchor on given link
+ public removeGroupFromAnchor(linkDoc: Doc, anchor: Doc, groupType: string) {
+ let groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor);
+ let newGroups = groups.filter(groupDoc => StrCast(groupDoc.type).toUpperCase() !== groupType.toUpperCase());
+ LinkManager.Instance.setAnchorGroups(linkDoc, anchor, newGroups);
+ }
+
+ // returns map of group type to anchor's links in that group type
+ public getRelatedGroupedLinks(anchor: Doc): Map<string, Array<Doc>> {
+ let related = this.getAllRelatedLinks(anchor);
+ let anchorGroups = new Map<string, Array<Doc>>();
+ related.forEach(link => {
+ let groups = LinkManager.Instance.getAnchorGroups(link, anchor);
+
+ if (groups.length > 0) {
+ groups.forEach(groupDoc => {
+ let groupType = StrCast(groupDoc.type);
+ if (groupType === "") {
+ let group = anchorGroups.get("*");
+ anchorGroups.set("*", group ? [...group, link] : [link]);
+ } else {
+ let group = anchorGroups.get(groupType);
+ anchorGroups.set(groupType, group ? [...group, link] : [link]);
+ }
+ });
+ } else {
+ // if link is in no groups then put it in default group
+ let group = anchorGroups.get("*");
+ anchorGroups.set("*", group ? [...group, link] : [link]);
+ }
+
+ });
+ return anchorGroups;
+ }
+
+ // gets a list of strings representing the keys of the metadata associated with the given group type
+ public getMetadataKeysInGroup(groupType: string): string[] {
+ if (LinkManager.Instance.LinkManagerDoc) {
+ return LinkManager.Instance.LinkManagerDoc[groupType] ? Cast(LinkManager.Instance.LinkManagerDoc[groupType], listSpec("string"), []) : [];
+ }
+ return [];
+ }
+
+ public setMetadataKeysForGroup(groupType: string, keys: string[]): boolean {
+ if (LinkManager.Instance.LinkManagerDoc) {
+ LinkManager.Instance.LinkManagerDoc[groupType] = new List<string>(keys);
+ return true;
+ }
+ return false;
+ }
+
+ // returns a list of all metadata docs associated with the given group type
+ public getAllMetadataDocsInGroup(groupType: string): Array<Doc> {
+ let md: Doc[] = [];
+ let allLinks = LinkManager.Instance.getAllLinks();
+ allLinks.forEach(linkDoc => {
+ let anchor1Groups = LinkManager.Instance.getAnchorGroups(linkDoc, Cast(linkDoc.anchor1, Doc, null));
+ let anchor2Groups = LinkManager.Instance.getAnchorGroups(linkDoc, Cast(linkDoc.anchor2, Doc, null));
+ anchor1Groups.forEach(groupDoc => { if (StrCast(groupDoc.type).toUpperCase() === groupType.toUpperCase()) { const meta = Cast(groupDoc.metadata, Doc, null); meta && md.push(meta); } });
+ anchor2Groups.forEach(groupDoc => { if (StrCast(groupDoc.type).toUpperCase() === groupType.toUpperCase()) { const meta = Cast(groupDoc.metadata, Doc, null); meta && md.push(meta); } });
+ });
+ return md;
+ }
+
+ // checks if a link with the given anchors exists
+ public doesLinkExist(anchor1: Doc, anchor2: Doc): boolean {
+ let allLinks = LinkManager.Instance.getAllLinks();
+ let index = allLinks.findIndex(linkDoc => {
+ return (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, null), anchor1) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, null), anchor2)) ||
+ (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, null), anchor2) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, null), anchor1));
+ });
+ return index !== -1;
+ }
+
+ // 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 | undefined {
+ if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, null))) {
+ return Cast(linkDoc.anchor2, Doc, null);
+ } else {
+ return Cast(linkDoc.anchor1, Doc, null);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index 091926d0a..fa9e2e5af 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -7,6 +7,8 @@ import { wrapInList, splitListItem, liftListItem, sinkListItem } from "prosemirr
import { undo, redo } from "prosemirror-history";
import { undoInputRule } from "prosemirror-inputrules";
import { Transaction, EditorState } from "prosemirror-state";
+import { TooltipTextMenu } from "./TooltipTextMenu";
+import { Statement } from "../northstar/model/idea/idea";
const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false;
@@ -96,5 +98,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
});
}
+ bind("Mod-s", TooltipTextMenu.insertStar);
+
return keys;
}
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 943cdb4d1..269de0f42 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -1,10 +1,11 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Schema, NodeSpec, MarkSpec, DOMOutputSpecArray, NodeType, Slice } from "prosemirror-model";
+import { Schema, NodeSpec, MarkSpec, DOMOutputSpecArray, NodeType, Slice, Mark, Node } from "prosemirror-model";
import { joinUp, lift, setBlockType, toggleMark, wrapIn, selectNodeForward, deleteSelection } from 'prosemirror-commands';
import { redo, undo } from 'prosemirror-history';
import { orderedList, bulletList, listItem, } from 'prosemirror-schema-list';
import { EditorState, Transaction, NodeSelection, TextSelection, Selection, } from "prosemirror-state";
import { EditorView, } from "prosemirror-view";
+import { View } from '@react-pdf/renderer';
const pDOM: DOMOutputSpecArray = ["p", 0], blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"],
preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0];
@@ -89,9 +90,9 @@ export const nodes: { [index: string]: NodeSpec } = {
inline: true,
attrs: {
visibility: { default: false },
- oldtext: { default: undefined },
- oldtextslice: { default: undefined },
- oldtextlen: { default: 0 }
+ text: { default: undefined },
+ textslice: { default: undefined },
+ textlen: { default: 0 }
},
group: "inline",
@@ -214,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]; }
@@ -280,8 +282,8 @@ export const marks: { [index: string]: MarkSpec } = {
toDOM: () => ['sup']
},
- collapse: {
- parseDOM: [{ style: 'color: blue' }],
+ highlight: {
+ parseDOM: [{ style: 'background: #d9dbdd' }],
toDOM() {
return ['span', {
style: 'color: blue'
@@ -347,8 +349,29 @@ export const marks: { [index: string]: MarkSpec } = {
}]
},
+ pFontColor: {
+ attrs: {
+ color: { default: "yellow" }
+ },
+ parseDOM: [{ style: 'background: #d9dbdd' }],
+ toDOM: (node) => {
+ return ['span', {
+ style: `color: ${node.attrs.color}`
+ }];
+ }
+ },
/** FONT SIZES */
+ pFontSize: {
+ attrs: {
+ fontSize: { default: 10 }
+ },
+ inclusive: false,
+ parseDOM: [{ style: 'font-size: 10px;' }],
+ toDOM: (node) => ['span', {
+ style: `font-size: ${node.attrs.fontSize}px;`
+ }]
+ },
p10: {
parseDOM: [{ style: 'font-size: 10px;' }],
@@ -378,6 +401,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', {
@@ -479,32 +516,48 @@ export class ImageResizeView {
export class SummarizedView {
// TODO: highlight text that is summarized. to find end of region, walk along mark
_collapsed: HTMLElement;
+ _view: any;
constructor(node: any, view: any, getPos: any) {
this._collapsed = document.createElement("span");
- this._collapsed.textContent = "㊉";
+ this._collapsed.textContent = node.attrs.visibility ? "㊀" : "㊉";
this._collapsed.style.opacity = "0.5";
- // this._collapsed.style.background = "yellow";
this._collapsed.style.position = "relative";
this._collapsed.style.width = "40px";
this._collapsed.style.height = "20px";
+ let self = this;
+ this._view = view;
+ const js = node.toJSON;
+ node.toJSON = function () {
+
+ return js.apply(this, arguments);
+ };
this._collapsed.onpointerdown = function (e: any) {
- console.log("star pressed!");
if (node.attrs.visibility) {
- node.attrs.visibility = !node.attrs.visibility;
- console.log("content is visible");
+ // node.attrs.visibility = !node.attrs.visibility;
let y = getPos();
- view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, y + 1, y + 1 + node.attrs.oldtextlen)));
- view.dispatch(view.state.tr.deleteSelection(view.state, () => { }));
- //this._collapsed.textContent = "㊀";
+ const attrs = { ...node.attrs };
+ attrs.visibility = !attrs.visibility;
+ let { from, to } = self.updateSummarizedText(y + 1, view.state.schema.marks.highlight);
+ let length = to - from;
+ let newSelection = TextSelection.create(view.state.doc, y + 1, y + 1 + length);
+ // update attrs of node
+ attrs.text = newSelection.content();
+ attrs.textslice = newSelection.content().toJSON();
+ view.dispatch(view.state.tr.setNodeMarkup(y, undefined, attrs));
+ view.dispatch(view.state.tr.setSelection(newSelection).deleteSelection(view.state, () => { }));
+ self._collapsed.textContent = "㊉";
} else {
- node.attrs.visibility = !node.attrs.visibility;
- console.log("content is invisible");
+ // node.attrs.visibility = !node.attrs.visibility;
let y = getPos();
- let mark = view.state.schema.mark(view.state.schema.marks.underline);
- console.log("PASTING " + node.attrs.oldtext.toString());
+ const attrs = { ...node.attrs };
+ attrs.visibility = !attrs.visibility;
+ view.dispatch(view.state.tr.setNodeMarkup(y, undefined, attrs));
+ let mark = view.state.schema.mark(view.state.schema.marks.highlight);
view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, y + 1, y + 1)));
- view.dispatch(view.state.tr.replaceSelection(node.attrs.oldtext).addMark(view.state.selection.from, view.state.selection.from + node.attrs.oldtextlen, mark));
- //this._collapsed.textContent = "㊉";
+ const from = view.state.selection.from;
+ let size = node.attrs.text.size;
+ view.dispatch(view.state.tr.replaceSelection(node.attrs.text).addMark(from, from + size, mark).removeStoredMark(mark));
+ self._collapsed.textContent = "㊀";
}
e.preventDefault();
e.stopPropagation();
@@ -515,6 +568,27 @@ export class SummarizedView {
selectNode() {
}
+ updateSummarizedText(start?: any, mark?: any) {
+ let $start = this._view.state.doc.resolve(start);
+ let endPos = start;
+
+ let _mark = this._view.state.schema.mark(this._view.state.schema.marks.highlight);
+ let visited = new Set();
+ for (let i: number = start + 1; i < this._view.state.doc.nodeSize - 1; i++) {
+ let skip = false;
+ this._view.state.doc.nodesBetween(start, i, (node: Node, pos: number, parent: Node, index: number) => {
+ if (node.isLeaf && !visited.has(node) && !skip) {
+ if (node.marks.includes(_mark)) {
+ visited.add(node);
+ endPos = i + node.nodeSize - 1;
+ }
+ else skip = true;
+ }
+ });
+ }
+ return { from: start, to: endPos };
+ }
+
deselectNode() {
}
}
@@ -533,7 +607,7 @@ const fromJson = schema.nodeFromJSON;
schema.nodeFromJSON = (json: any) => {
let node = fromJson(json);
if (json.type === "star") {
- node.attrs.oldtext = Slice.fromJSON(schema, node.attrs.oldtextslice);
+ node.attrs.text = Slice.fromJSON(schema, node.attrs.textslice);
}
return node;
}; \ No newline at end of file
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index 40e2ad6bb..62c2cfe85 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -7,12 +7,7 @@ let ts = (window as any).ts;
// @ts-ignore
import * as typescriptlib from '!!raw-loader!./type_decls.d';
-import { Docs } from "../documents/Documents";
import { Doc, Field } from '../../new_fields/Doc';
-import { ImageField, PdfField, VideoField, AudioField } from '../../new_fields/URLField';
-import { List } from '../../new_fields/List';
-import { RichTextField } from '../../new_fields/RichTextField';
-import { ScriptField, ComputedField } from '../../fields/ScriptField';
export interface ScriptSucccess {
success: true;
@@ -40,15 +35,45 @@ export interface CompileError {
export type CompileResult = CompiledScript | CompileError;
+export namespace Scripting {
+ export function addGlobal(global: { name: string }): void;
+ export function addGlobal(name: string, global: any): void;
+ export function addGlobal(nameOrGlobal: any, global?: any) {
+ let n: string;
+ let obj: any;
+ if (global !== undefined && typeof nameOrGlobal === "string") {
+ n = nameOrGlobal;
+ obj = global;
+ } else if (nameOrGlobal && typeof nameOrGlobal.name === "string") {
+ n = nameOrGlobal.name;
+ obj = nameOrGlobal;
+ } else {
+ throw new Error("Must either register an object with a name, or give a name and an object");
+ }
+ if (scriptingGlobals.hasOwnProperty(n)) {
+ throw new Error(`Global with name ${n} is already registered, choose another name`);
+ }
+ scriptingGlobals[n] = obj;
+ }
+}
+
+export function scriptingGlobal(constructor: { new(...args: any[]): any }) {
+ Scripting.addGlobal(constructor);
+}
+
+const scriptingGlobals: { [name: string]: any } = {};
+
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) {
return { compiled: false, errors: diagnostics };
}
- let fieldTypes = [Doc, ImageField, PdfField, VideoField, AudioField, List, RichTextField, ScriptField, ComputedField, CompileScript];
- let paramNames = ["Docs", ...fieldTypes.map(fn => fn.name)];
- let params: any[] = [Docs, ...fieldTypes];
+ let paramNames = Object.keys(scriptingGlobals);
+ let params = paramNames.map(key => scriptingGlobals[key]);
+ // let fieldTypes = [Doc, ImageField, PdfField, VideoField, AudioField, List, RichTextField, ScriptField, ComputedField, CompileScript];
+ // let paramNames = ["Docs", ...fieldTypes.map(fn => fn.name)];
+ // let params: any[] = [Docs, ...fieldTypes];
let compiledFunction = new Function(...paramNames, `return ${script}`);
let { capturedVariables = {} } = options;
let run = (args: { [name: string]: any } = {}): ScriptResult => {
@@ -64,10 +89,20 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an
}
}
let thisParam = args.this || capturedVariables.this;
+ let batch: { end(): void } | undefined = undefined;
try {
+ if (!options.editable) {
+ batch = Doc.MakeReadOnly();
+ }
const result = compiledFunction.apply(thisParam, params).apply(thisParam, argsArray);
+ if (batch) {
+ batch.end();
+ }
return { success: true, result };
} catch (error) {
+ if (batch) {
+ batch.end();
+ }
return { success: false, error };
}
};
@@ -133,6 +168,7 @@ export interface ScriptOptions {
params?: { [name: string]: string };
capturedVariables?: { [name: string]: Field };
typecheck?: boolean;
+ editable?: boolean;
}
export function CompileScript(script: string, options: ScriptOptions = {}): CompileResult {
@@ -168,4 +204,6 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
let diagnostics = ts.getPreEmitDiagnostics(program).concat(testResult.diagnostics);
return Run(outputText, paramNames, diagnostics, script, options);
-} \ No newline at end of file
+}
+
+Scripting.addGlobal(CompileScript); \ No newline at end of file
diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts
index 28ec8ca14..ee5a83710 100644
--- a/src/client/util/SearchUtil.ts
+++ b/src/client/util/SearchUtil.ts
@@ -2,25 +2,79 @@ 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 function Search(query: string, returnDocs: true): Promise<Doc[]>;
- export function Search(query: string, returnDocs: false): Promise<string[]>;
- export async function Search(query: string, returnDocs: boolean) {
- const ids = JSON.parse(await rp.get(DocServer.prepend("/search"), {
- qs: { query }
+ 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 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 ids;
+ return result;
}
+ const { ids, numFound, highlighting } = result;
const docMap = await DocServer.GetRefFields(ids);
- return ids.map((id: string) => docMap[id]).filter((doc: any) => doc instanceof Doc);
+ const docs = ids.map((id: string) => docMap[id]).filter((doc: any) => doc instanceof Doc);
+ return { docs, numFound, highlighting };
}
- export async function GetAliasesOfDocument(doc: Doc): Promise<Doc[]> {
- const proto = await Doc.GetT(doc, "proto", Doc, true);
- const protoId = (proto || doc)[Id];
- return Search(`proto_i:"${protoId}"`, true);
+ export async function GetAliasesOfDocument(doc: Doc): Promise<Doc[]>;
+ export async function GetAliasesOfDocument(doc: Doc, returnDocs: false): Promise<string[]>;
+ export async function GetAliasesOfDocument(doc: Doc, returnDocs = true): Promise<Doc[] | string[]> {
+ const proto = Doc.GetProto(doc);
+ const protoId = proto[Id];
+ if (returnDocs) {
+ return (await Search("", returnDocs, { fq: `proto_i:"${protoId}"` })).docs;
+ } else {
+ 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("", true, { fq: `proto_i:"${doc[Id]}"` });
+ return results.docs;
+ }
+
+ export async function GetContextsOfDocument(doc: Doc): Promise<{ contexts: Doc[], aliasContexts: Doc[] }> {
+ 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("", 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("", false, { fq: `data_l:"${doc[Id]}"` })).ids;
+ const aliases = await GetAliasesOfDocument(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;
+ }
} \ No newline at end of file
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 7dbb81e76..9efef888d 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -1,14 +1,18 @@
-import { observable, action, runInAction } from "mobx";
-import { Doc } from "../../new_fields/Doc";
+import { observable, action, runInAction, IReactionDisposer, reaction, autorun } from "mobx";
+import { Doc, Opt } from "../../new_fields/Doc";
import { DocumentView } from "../views/nodes/DocumentView";
import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
-import { NumCast } from "../../new_fields/Types";
+import { NumCast, StrCast } from "../../new_fields/Types";
+import { InkingControl } from "../views/InkingControl";
export namespace SelectionManager {
+
class Manager {
+
@observable IsDragging: boolean = false;
@observable SelectedDocuments: Array<DocumentView> = [];
+
@action
SelectDoc(docView: DocumentView, ctrlPressed: boolean): void {
// if doc is not in SelectedDocuments, add it
@@ -18,6 +22,7 @@ export namespace SelectionManager {
}
manager.SelectedDocuments.push(docView);
+ // console.log(manager.SelectedDocuments);
docView.props.whenActiveChanged(true);
}
}
@@ -38,6 +43,17 @@ export namespace SelectionManager {
}
const manager = new Manager();
+ reaction(() => manager.SelectedDocuments, sel => {
+ let targetColor = "#FFFFFF";
+ if (sel.length > 0) {
+ let firstView = sel[0];
+ let doc = firstView.props.Document;
+ let targetDoc = doc.isTemplate ? doc : Doc.GetProto(doc);
+ let stored = StrCast(targetDoc.backgroundColor);
+ stored.length > 0 && (targetColor = stored);
+ }
+ InkingControl.Instance.updateSelectedColor(targetColor);
+ }, { fireImmediately: true });
export function DeselectDoc(docView: DocumentView): void {
manager.DeselectDoc(docView);
diff --git a/src/client/util/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.scss b/src/client/util/TooltipTextMenu.scss
index 4d4eb386d..40ac3abb9 100644
--- a/src/client/util/TooltipTextMenu.scss
+++ b/src/client/util/TooltipTextMenu.scss
@@ -18,6 +18,7 @@
.ProseMirror-menuitem {
margin-right: 3px;
display: inline-block;
+ z-index: 100000;
}
.ProseMirror-menuseparator {
@@ -59,7 +60,6 @@
}
.ProseMirror-menu-dropdown-menu, .ProseMirror-menu-submenu {
- position: absolute;
background: $dark-color;
color:white;
border: 1px solid rgb(223, 223, 223);
@@ -67,9 +67,10 @@
}
.ProseMirror-menu-dropdown-menu {
- z-index: 15;
+ z-index: 100000;
min-width: 6em;
background: white;
+ position: absolute;
}
.linking {
@@ -80,6 +81,7 @@
cursor: pointer;
padding: 2px 8px 2px 4px;
width: auto;
+ z-index: 100000;
}
.ProseMirror-menu-dropdown-item:hover {
@@ -233,19 +235,20 @@
}
.tooltipMenu {
- position: absolute;
- z-index: 50;
- background: whitesmoke;
+ position: relative;
+ z-index: 2000;
+ background: #121721;
border: 1px solid silver;
border-radius: 15px;
- padding: 2px 10px;
- //margin-bottom: 100px;
+ //height: 60px;
+ //padding: 2px 10px;
+ //margin-top: 100px;
//-webkit-transform: translateX(-50%);
//transform: translateX(-50%);
- transform: translateY(-50%);
+ transform: translateY(-85px);
pointer-events: all;
- height: auto;
- width:inherit;
+ height: 30px;
+ width:550px;
.ProseMirror-example-setup-style hr {
padding: 2px 10px;
border: none;
@@ -306,3 +309,10 @@
font-size: 12px;
padding-right: 0px;
}
+ .summarize{
+ //margin-left: 15px;
+ color: white;
+ height: 20px;
+ // background-color: white;
+ text-align: center;
+ } \ No newline at end of file
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index f2559db74..a4c053de2 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -1,54 +1,38 @@
-import { action, IReactionDisposer, reaction } from "mobx";
-import { Dropdown, DropdownSubmenu, MenuItem, MenuItemSpec, renderGrouped, icons, } from "prosemirror-menu"; //no import css
-import { baseKeymap, lift, deleteSelection } from "prosemirror-commands";
-import { history, redo, undo } from "prosemirror-history";
-import { keymap } from "prosemirror-keymap";
-import { EditorState, Transaction, NodeSelection, TextSelection } from "prosemirror-state";
+import { action } from "mobx";
+import { Dropdown, MenuItem, icons, } from "prosemirror-menu"; //no import css
+import { EditorState, NodeSelection, TextSelection } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { schema } from "./RichTextSchema";
-import { Schema, NodeType, MarkType, Mark } from "prosemirror-model";
-import React = require("react");
+import { Schema, NodeType, MarkType, Mark, ResolvedPos } from "prosemirror-model";
+import { Node as ProsNode } from "prosemirror-model";
import "./TooltipTextMenu.scss";
-const { toggleMark, setBlockType, wrapIn } = require("prosemirror-commands");
+const { toggleMark, setBlockType } = require("prosemirror-commands");
import { library } from '@fortawesome/fontawesome-svg-core';
-import { wrapInList, bulletList, liftListItem, listItem, } from 'prosemirror-schema-list';
-import { liftTarget, RemoveMarkStep, AddMarkStep } from 'prosemirror-transform';
-import {
- faListUl, faGrinTongueSquint,
-} from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { wrapInList, liftListItem, } from 'prosemirror-schema-list';
+import { faListUl } from '@fortawesome/free-solid-svg-icons';
import { FieldViewProps } from "../views/nodes/FieldView";
-import { throwStatement } from "babel-types";
const { openPrompt, TextField } = require("./ProsemirrorCopy/prompt.js");
-import { View } from "@react-pdf/renderer";
import { DragManager } from "./DragManager";
import { Doc, Opt, Field } from "../../new_fields/Doc";
import { DocServer } from "../DocServer";
-import { CollectionFreeFormDocumentView } from "../views/nodes/CollectionFreeFormDocumentView";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
import { DocumentManager } from "./DocumentManager";
import { Id } from "../../new_fields/FieldSymbols";
+import { FormattedTextBoxProps } from "../views/nodes/FormattedTextBox";
import { Utils } from "../../Utils";
-// import { wrap } from "module";
-
-const SVG = "http://www.w3.org/2000/svg";
//appears above a selection of text in a RichTextBox to give user options such as Bold, Italics, etc.
export class TooltipTextMenu {
public tooltip: HTMLElement;
- private num_icons = 0;
private view: EditorView;
private fontStyles: MarkType[];
private fontSizes: MarkType[];
private listTypes: NodeType[];
- private editorProps: FieldViewProps;
- private state: EditorState;
+ private editorProps: FieldViewProps & FormattedTextBoxProps;
private fontSizeToNum: Map<MarkType, number>;
private fontStylesToName: Map<MarkType, string>;
private listTypeToIcon: Map<NodeType, string>;
- private fontSizeIndicator: HTMLSpanElement = document.createElement("span");
- private link: HTMLAnchorElement;
private linkEditor?: HTMLDivElement;
private linkText?: HTMLDivElement;
@@ -58,15 +42,23 @@ export class TooltipTextMenu {
private fontStyleDom?: Node;
private listTypeBtnDom?: Node;
- constructor(view: EditorView, editorProps: FieldViewProps) {
+ private _activeMarks: Mark[] = [];
+
+ private _collapseBtn?: MenuItem;
+
+ constructor(view: EditorView, editorProps: FieldViewProps & FormattedTextBoxProps) {
this.view = view;
- this.state = view.state;
this.editorProps = editorProps;
this.tooltip = document.createElement("div");
this.tooltip.className = "tooltipMenu";
+ this.dragElement(this.tooltip);
+ // this.createCollapse();
+ // if (this._collapseBtn) {
+ // this.tooltip.appendChild(this._collapseBtn.render(this.view).dom);
+ // }
//add the div which is the tooltip
- view.dom.parentNode!.parentNode!.appendChild(this.tooltip);
+ //view.dom.parentNode!.parentNode!.appendChild(this.tooltip);
//add additional icons
library.add(faListUl);
@@ -78,7 +70,7 @@ export class TooltipTextMenu {
{ command: toggleMark(schema.marks.strikethrough), dom: this.icon("S", "strikethrough", "Strikethrough") },
{ command: toggleMark(schema.marks.superscript), dom: this.icon("s", "superscript", "Superscript") },
{ command: toggleMark(schema.marks.subscript), dom: this.icon("s", "subscript", "Subscript") },
- { command: deleteSelection, dom: this.icon("C", 'collapse', 'Collapse') }
+ { command: toggleMark(schema.marks.highlight), dom: this.icon("H", 'blue', 'Blue') }
// { command: wrapInList(schema.nodes.bullet_list), dom: this.icon(":", "bullets") },
// { command: wrapInList(schema.nodes.ordered_list), dom: this.icon("1)", "bullets") },
// { command: lift, dom: this.icon("<", "lift") },
@@ -98,6 +90,7 @@ export class TooltipTextMenu {
});
});
+ this.updateLinkMenu();
//list of font styles
this.fontStylesToName = new Map();
@@ -116,10 +109,14 @@ 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);
this.fontSizeToNum.set(schema.marks.p72, 72);
+ this.fontSizeToNum.set(schema.marks.pFontSize, 10);
+ this.fontSizeToNum.set(schema.marks.pFontSize, 10);
this.fontSizes = Array.from(this.fontSizeToNum.keys());
//list types
@@ -128,14 +125,21 @@ export class TooltipTextMenu {
this.listTypeToIcon.set(schema.nodes.ordered_list, "1)");
this.listTypes = Array.from(this.listTypeToIcon.keys());
- this.link = document.createElement("a");
- this.link.target = "_blank";
- this.link.style.color = "white";
- this.tooltip.appendChild(this.link);
+ // this.tooltip.appendChild(this.createLink().render(this.view).dom);
+
+ this.tooltip.appendChild(this.createStar().render(this.view).dom);
+
- this.tooltip.appendChild(this.createLink().render(this.view).dom);
+
+ this.updateListItemDropdown(":", this.listTypeBtnDom);
this.update(view, undefined);
+
+ //view.dom.parentNode!.parentNode!.insertBefore(this.tooltip, view.dom.parentNode);
+
+ // quick and dirty null check
+ const outer_div = this.editorProps.outer_div;
+ outer_div && outer_div(this.tooltip);
}
//label of dropdown will change to given label
@@ -149,12 +153,15 @@ export class TooltipTextMenu {
fontSizeBtns.push(this.dropdownMarkBtn(String(number), "color: black; width: 50px;", mark, this.view, this.changeToMarkInGroup, this.fontSizes));
});
- if (this.fontSizeDom) { this.tooltip.removeChild(this.fontSizeDom); }
- this.fontSizeDom = (new Dropdown(cut(fontSizeBtns), {
+ let newfontSizeDom = (new Dropdown(cut(fontSizeBtns), {
label: label,
css: "color:black; min-width: 60px; padding-left: 5px; margin-right: 0;"
}) as MenuItem).render(this.view).dom;
- this.tooltip.appendChild(this.fontSizeDom);
+ if (this.fontSizeDom) { this.tooltip.replaceChild(newfontSizeDom, this.fontSizeDom); }
+ else {
+ this.tooltip.appendChild(newfontSizeDom);
+ }
+ this.fontSizeDom = newfontSizeDom;
}
//label of dropdown will change to given label
@@ -168,18 +175,22 @@ export class TooltipTextMenu {
fontBtns.push(this.dropdownMarkBtn(name, "color: black; font-family: " + name + ", sans-serif; width: 125px;", mark, this.view, this.changeToMarkInGroup, this.fontStyles));
});
- if (this.fontStyleDom) { this.tooltip.removeChild(this.fontStyleDom); }
- this.fontStyleDom = (new Dropdown(cut(fontBtns), {
+ let newfontStyleDom = (new Dropdown(cut(fontBtns), {
label: label,
css: "color:black; width: 125px; margin-left: -3px; padding-left: 2px;"
}) as MenuItem).render(this.view).dom;
+ if (this.fontStyleDom) { this.tooltip.replaceChild(newfontStyleDom, this.fontStyleDom); }
+ else {
+ this.tooltip.appendChild(newfontStyleDom);
+ }
+ this.fontStyleDom = newfontStyleDom;
- this.tooltip.appendChild(this.fontStyleDom);
}
updateLinkMenu() {
if (!this.linkEditor || !this.linkText) {
this.linkEditor = document.createElement("div");
+ this.linkEditor.className = "ProseMirror-icon menuicon";
this.linkEditor.style.color = "black";
this.linkText = document.createElement("div");
this.linkText.style.cssFloat = "left";
@@ -189,27 +200,27 @@ export class TooltipTextMenu {
this.linkText.style.whiteSpace = "nowrap";
this.linkText.style.width = "150px";
this.linkText.style.overflow = "hidden";
- this.linkText.style.color = "black";
+ this.linkText.style.color = "white";
this.linkText.onpointerdown = (e: PointerEvent) => { e.stopPropagation(); };
let linkBtn = document.createElement("div");
linkBtn.textContent = ">>";
linkBtn.style.width = "10px";
linkBtn.style.height = "10px";
- linkBtn.style.color = "black";
+ linkBtn.style.color = "white";
linkBtn.style.cssFloat = "left";
linkBtn.onpointerdown = (e: PointerEvent) => {
let node = this.view.state.selection.$from.nodeAfter;
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)) {
- DocumentManager.Instance.getDocumentView(f)!.props.focus(f);
+ DocumentManager.Instance.getDocumentView(f)!.props.focus(f, false);
}
- else if (CollectionDockingView.Instance) CollectionDockingView.Instance.AddRightSplit(f);
+ else if (CollectionDockingView.Instance) CollectionDockingView.Instance.AddRightSplit(f, undefined);
}
}));
}
@@ -220,45 +231,39 @@ export class TooltipTextMenu {
};
this.linkDrag = document.createElement("img");
this.linkDrag.src = "https://seogurusnyc.com/wp-content/uploads/2016/12/link-1.png";
- this.linkDrag.style.width = "20px";
- this.linkDrag.style.height = "20px";
+ this.linkDrag.style.width = "15px";
+ this.linkDrag.style.height = "15px";
+ this.linkDrag.title = "Drag to create link";
this.linkDrag.style.color = "black";
this.linkDrag.style.background = "black";
this.linkDrag.style.cssFloat = "left";
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
});
};
this.linkEditor.appendChild(this.linkDrag);
- this.linkEditor.appendChild(this.linkText);
- this.linkEditor.appendChild(linkBtn);
+ // this.linkEditor.appendChild(this.linkText);
+ // this.linkEditor.appendChild(linkBtn);
this.tooltip.appendChild(this.linkEditor);
-
- let starButton = document.createElement("span");
- // starButton.style.width = '10px';
- // starButton.style.height = '10px';
- starButton.style.marginLeft = '10px';
- starButton.textContent = "Summarize";
- starButton.style.color = 'black';
- starButton.style.height = '20px';
- starButton.style.backgroundColor = 'white';
- starButton.style.textAlign = 'center';
- starButton.onclick = () => {
- let state = this.view.state;
- this.insertStar(state, this.view.dispatch);
- };
-
- this.tooltip.appendChild(starButton);
}
let node = this.view.state.selection.$from.nodeAfter;
@@ -267,35 +272,75 @@ 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);
+ }
+
+ dragElement(elmnt: HTMLElement) {
+ var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
+ if (elmnt) {
+ // if present, the header is where you move the DIV from:
+ elmnt.onpointerdown = dragMouseDown;
+ }
+ const self = this;
+
+ function dragMouseDown(e: PointerEvent) {
+ e = e || window.event;
+ //e.preventDefault();
+ // get the mouse cursor position at startup:
+ pos3 = e.clientX;
+ pos4 = e.clientY;
+ document.onpointerup = closeDragElement;
+ // call a function whenever the cursor moves:
+ document.onpointermove = elementDrag;
+ }
+
+ function elementDrag(e: PointerEvent) {
+ e = e || window.event;
+ //e.preventDefault();
+ // calculate the new cursor position:
+ pos1 = pos3 - e.clientX;
+ pos2 = pos4 - e.clientY;
+ pos3 = e.clientX;
+ pos4 = e.clientY;
+ // set the element's new position:
+ elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
+ elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
+ }
+
+ function closeDragElement() {
+ // stop moving when mouse button is released:
+ document.onpointerup = null;
+ document.onpointermove = null;
+ //self.highlightSearchTerms(self.state, ["hello"]);
+ //FormattedTextBox.Instance.unhighlightSearchTerms();
+ }
}
- 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;
link = node && node.marks.find(m => m.type.name === "link");
}
- insertStar(state: EditorState<any>, dispatch: any) {
- console.log("creating star...");
- let newNode = schema.nodes.star.create({ visibility: false, oldtext: state.selection.content(), oldtextslice: state.selection.content().toJSON(), oldtextlen: state.selection.to - state.selection.from });
+ public static insertStar(state: EditorState<any>, dispatch: any) {
+ let newNode = schema.nodes.star.create({ visibility: false, text: state.selection.content(), textslice: state.selection.content().toJSON(), textlen: state.selection.to - state.selection.from });
if (dispatch) {
- console.log(newNode.attrs.oldtext.toString());
+ //console.log(newNode.attrs.text.toString());
dispatch(state.tr.replaceSelectionWith(newNode));
}
return true;
}
//will display a remove-list-type button if selection is in list, otherwise will show list type dropdown
- updateListItemDropdown(label: string, listTypeBtn: Node) {
+ updateListItemDropdown(label: string, listTypeBtn: any) {
//remove old btn
if (listTypeBtn) { this.tooltip.removeChild(listTypeBtn); }
@@ -318,8 +363,8 @@ export class TooltipTextMenu {
}
//for a specific grouping of marks (passed in), remove all and apply the passed-in one to the selected text
- changeToMarkInGroup(markType: MarkType, view: EditorView, fontMarks: MarkType[]) {
- let { empty, $cursor, ranges } = view.state.selection as TextSelection;
+ changeToMarkInGroup = (markType: MarkType, view: EditorView, fontMarks: MarkType[]) => {
+ let { $cursor, ranges } = view.state.selection as TextSelection;
let state = view.state;
let dispatch = view.dispatch;
@@ -331,25 +376,34 @@ export class TooltipTextMenu {
dispatch(state.tr.removeStoredMark(type));
}
} else {
- let has = false, tr = state.tr;
+ let has = false;
for (let i = 0; !has && i < ranges.length; i++) {
let { $from, $to } = ranges[i];
has = state.doc.rangeHasMark($from.pos, $to.pos, type);
}
for (let i of ranges) {
- let { $from, $to } = i;
if (has) {
toggleMark(type)(view.state, view.dispatch, view);
}
}
}
}
- }); //actually apply font
+ });
+ // fontsize
+ if (markType.name[0] === 'p') {
+ let size = this.fontSizeToNum.get(markType);
+ if (size) { this.updateFontSizeDropdown(String(size) + " pt"); }
+ }
+ else {
+ let fontName = this.fontStylesToName.get(markType);
+ if (fontName) { this.updateFontStyleDropdown(fontName); }
+ }
+ //actually apply font
return toggleMark(markType)(view.state, view.dispatch, view);
}
//remove all node typeand apply the passed-in one to the selected text
- changeToNodeType(nodeType: NodeType | undefined, view: EditorView, allNodes: NodeType[]) {
+ changeToNodeType(nodeType: NodeType | undefined, view: EditorView) {
//remove old
liftListItem(schema.nodes.list_item)(view.state, view.dispatch);
if (nodeType) { //add new
@@ -366,13 +420,69 @@ export class TooltipTextMenu {
execEvent: "",
class: "menuicon",
css: css,
- enable(state) { return true; },
+ enable() { return true; },
run() {
changeToMarkInGroup(markType, view, groupMarks);
}
});
}
+ createStar() {
+ return new MenuItem({
+ title: "Summarize",
+ label: "Summarize",
+ icon: icons.join,
+ css: "color:white;",
+ class: "summarize",
+ execEvent: "",
+ run: (state, dispatch) => {
+ TooltipTextMenu.insertStar(state, dispatch);
+ }
+
+ });
+ }
+
+ createCollapse() {
+ this._collapseBtn = new MenuItem({
+ title: "Collapse",
+ //label: "Collapse",
+ icon: icons.join,
+ execEvent: "",
+ css: "color:white;",
+ class: "summarize",
+ run: () => {
+ this.collapseToolTip();
+ }
+ });
+ }
+
+ collapseToolTip() {
+ if (this._collapseBtn) {
+ if (this._collapseBtn.spec.title === "Collapse") {
+ // const newcollapseBtn = new MenuItem({
+ // title: "Expand",
+ // icon: icons.join,
+ // execEvent: "",
+ // css: "color:white;",
+ // class: "summarize",
+ // run: (state, dispatch, view) => {
+ // this.collapseToolTip();
+ // }
+ // });
+ // this.tooltip.replaceChild(newcollapseBtn.render(this.view).dom, this._collapseBtn.render(this.view).dom);
+ // this._collapseBtn = newcollapseBtn;
+ this.tooltip.style.width = "30px";
+ this._collapseBtn.spec.title = "Expand";
+ this._collapseBtn.render(this.view);
+ }
+ else {
+ this._collapseBtn.spec.title = "Collapse";
+ this.tooltip.style.width = "550px";
+ this._collapseBtn.render(this.view);
+ }
+ }
+ }
+
createLink() {
let markType = schema.marks.link;
return new MenuItem({
@@ -385,16 +495,24 @@ export class TooltipTextMenu {
enable(state) { return !state.selection.empty; },
run: (state, dispatch, view) => {
// to remove link
+ let curLink = "";
if (this.markActive(state, markType)) {
- toggleMark(markType)(state, dispatch);
- return true;
+
+ let { from, $from, to, empty } = state.selection;
+ let node = state.doc.nodeAt(from);
+ node && node.marks.map(m => {
+ m.type === markType && (curLink = m.attrs.href);
+ });
+ //toggleMark(markType)(state, dispatch);
+ //return true;
}
// to create link
openPrompt({
title: "Create a link",
fields: {
href: new TextField({
- label: "Link target",
+ value: curLink,
+ label: "Link Target",
required: true
}),
title: new TextField({ label: "Title" })
@@ -419,7 +537,7 @@ export class TooltipTextMenu {
execEvent: "",
class: "menuicon",
css: css,
- enable(state) { return true; },
+ enable() { return true; },
run() {
changeToNodeInGroup(nodeType, view, groupNodes);
}
@@ -438,7 +556,7 @@ export class TooltipTextMenu {
span.className = name + " menuicon";
span.title = title;
span.textContent = text;
- span.style.color = "black";
+ span.style.color = "white";
return span;
}
@@ -502,90 +620,121 @@ export class TooltipTextMenu {
// Hide the tooltip if the selection is empty
if (state.selection.empty) {
- this.tooltip.style.display = "none";
- return;
+ //this.tooltip.style.display = "none";
+ //return;
}
- let linksInSelection = this.activeMarksOnSelection([schema.marks.link]);
- if (linksInSelection.length > 0) {
- let attributes = this.getMarksInSelection(this.view.state, [schema.marks.link])[0].attrs;
- this.link.href = attributes.href;
- this.link.textContent = attributes.title;
- this.link.style.visibility = "visible";
- } else this.link.style.visibility = "hidden";
-
- // Otherwise, reposition it and update its content
- this.tooltip.style.display = "";
- let { from, to } = state.selection;
- let start = view.coordsAtPos(from), end = view.coordsAtPos(to);
- // The box in which the tooltip is positioned, to use as base
- let box = this.tooltip.offsetParent!.getBoundingClientRect();
- // Find a center-ish x position from the selection endpoints (when
- // crossing lines, end may be more to the left)
- let left = Math.max((start.left + end.left) / 2, start.left + 3);
- this.tooltip.style.left = (left - box.left) * this.editorProps.ScreenToLocalTransform().Scale + "px";
- let width = Math.abs(start.left - end.left) / 2 * this.editorProps.ScreenToLocalTransform().Scale;
- let mid = Math.min(start.left, end.left) + width;
-
- //this.tooltip.style.width = 225 + "px";
- this.tooltip.style.bottom = (box.bottom - start.top) * this.editorProps.ScreenToLocalTransform().Scale + "px";
- this.tooltip.style.top = "-100px";
- //this.tooltip.style.height = "100px";
-
- // let transform = this.editorProps.ScreenToLocalTransform();
- // this.tooltip.style.width = `${225 / transform.Scale}px`;
- // Utils
//UPDATE LIST ITEM DROPDOWN
- this.listTypeBtnDom = this.updateListItemDropdown(":", this.listTypeBtnDom!);
//UPDATE FONT STYLE DROPDOWN
let activeStyles = this.activeMarksOnSelection(this.fontStyles);
- if (activeStyles.length === 1) {
- // if we want to update something somewhere with active font name
- let fontName = this.fontStylesToName.get(activeStyles[0]);
- if (fontName) { this.updateFontStyleDropdown(fontName); }
- } else if (activeStyles.length === 0) {
- //crimson on default
- this.updateFontStyleDropdown("Crimson Text");
- } else {
- this.updateFontStyleDropdown("Various");
+ if (activeStyles !== undefined) {
+ // activeStyles.forEach((markType) => {
+ // this._activeMarks.push(this.view.state.schema.mark(markType));
+ // });
+ if (activeStyles.length === 1) {
+ // if we want to update something somewhere with active font name
+ let fontName = this.fontStylesToName.get(activeStyles[0]);
+ if (fontName) { this.updateFontStyleDropdown(fontName); }
+ } else if (activeStyles.length === 0) {
+ //crimson on default
+ this.updateFontStyleDropdown("Crimson Text");
+ } else {
+ this.updateFontStyleDropdown("Various");
+ }
}
//UPDATE FONT SIZE DROPDOWN
let activeSizes = this.activeMarksOnSelection(this.fontSizes);
- if (activeSizes.length === 1) { //if there's only one active font size
- let size = this.fontSizeToNum.get(activeSizes[0]);
- if (size) { this.updateFontSizeDropdown(String(size) + " pt"); }
- } else if (activeSizes.length === 0) {
- //should be 14 on default
- this.updateFontSizeDropdown("14 pt");
- } else { //multiple font sizes selected
- this.updateFontSizeDropdown("Various");
+ if (activeSizes !== undefined) {
+ if (activeSizes.length === 1) { //if there's only one active font size
+ // activeSizes.forEach((markType) => {
+ // this._activeMarks.push(this.view.state.schema.mark(markType));
+ // });
+ let size = this.fontSizeToNum.get(activeSizes[0]);
+ if (size) { this.updateFontSizeDropdown(String(size) + " pt"); }
+ } else if (activeSizes.length === 0) {
+ //should be 14 on default
+ this.updateFontSizeDropdown("14 pt");
+ } else { //multiple font sizes selected
+ this.updateFontSizeDropdown("Various");
+ }
}
-
- this.updateLinkMenu();
+ this.view.dispatch(this.view.state.tr.setStoredMarks(this._activeMarks));
}
//finds all active marks on selection in given group
activeMarksOnSelection(markGroup: MarkType[]) {
//current selection
- let { empty, $cursor, ranges } = this.view.state.selection as TextSelection;
+ let { empty, ranges } = this.view.state.selection as TextSelection;
let state = this.view.state;
let dispatch = this.view.dispatch;
-
- let activeMarks = markGroup.filter(mark => {
- if (dispatch) {
- let has = false, tr = state.tr;
- for (let i = 0; !has && i < ranges.length; i++) {
- let { $from, $to } = ranges[i];
- return state.doc.rangeHasMark($from.pos, $to.pos, mark);
+ let activeMarks: MarkType[];
+ if (!empty) {
+ activeMarks = markGroup.filter(mark => {
+ if (dispatch) {
+ let has = false;
+ for (let i = 0; !has && i < ranges.length; i++) {
+ let { $from, $to } = ranges[i];
+ return state.doc.rangeHasMark($from.pos, $to.pos, mark);
+ }
}
+ return false;
+ });
+ }
+ else {
+ const pos = this.view.state.selection.$from;
+ const ref_node: ProsNode = this.reference_node(pos);
+ if (ref_node !== null && ref_node !== this.view.state.doc) {
+ if (ref_node.isText) {
+ }
+ else {
+ return [];
+ }
+
+ this._activeMarks = ref_node.marks;
+
+ activeMarks = markGroup.filter(mark_type => {
+ if (dispatch) {
+ let mark = state.schema.mark(mark_type);
+ return ref_node.marks.includes(mark);
+ }
+ return false;
+ });
}
- return false;
- });
+ else {
+ return [];
+ }
+
+ }
return activeMarks;
}
- destroy() { this.tooltip.remove(); }
+ reference_node(pos: ResolvedPos<any>): ProsNode {
+ let ref_node: ProsNode = this.view.state.doc;
+ if (pos.nodeAfter !== null && pos.nodeAfter !== undefined) {
+ ref_node = pos.nodeAfter;
+ }
+ else if (pos.nodeBefore !== null && pos.nodeBefore !== undefined) {
+ ref_node = pos.nodeBefore;
+ }
+ else if (pos.pos > 0) {
+ let skip = false;
+ for (let i: number = pos.pos - 1; i > 0; i--) {
+ this.view.state.doc.nodesBetween(i, pos.pos, (node: ProsNode) => {
+ if (node.isLeaf && !skip) {
+ ref_node = node;
+ skip = true;
+ }
+
+ });
+ }
+ }
+ return ref_node;
+ }
+
+ destroy() {
+ this.tooltip.remove();
+ }
}
diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index c0ed015bd..156390fd3 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -94,6 +94,7 @@ export namespace UndoManager {
}
export function PrintBatches(): void {
+ console.log("Open Undo Batches:");
GetOpenBatches().forEach(batch => console.log(batch.batchName));
}
diff --git a/src/client/util/request-image-size.js b/src/client/util/request-image-size.js
new file mode 100644
index 000000000..27605d167
--- /dev/null
+++ b/src/client/util/request-image-size.js
@@ -0,0 +1,75 @@
+/**
+ * request-image-size: Detect image dimensions via request.
+ * Licensed under the MIT license.
+ *
+ * https://github.com/FdezRomero/request-image-size
+ * © 2017 Rodrigo Fernández Romero
+ *
+ * Based on the work of Johannes J. Schmidt
+ * https://github.com/jo/http-image-size
+ */
+
+const request = require('request');
+const imageSize = require('image-size');
+const HttpError = require('standard-http-error');
+
+module.exports = function requestImageSize(options) {
+ let opts = {
+ encoding: null
+ };
+
+ if (options && typeof options === 'object') {
+ opts = Object.assign(options, opts);
+ } else if (options && typeof options === 'string') {
+ opts = Object.assign({
+ uri: options
+ }, opts);
+ } else {
+ return Promise.reject(new Error('You should provide an URI string or a "request" options object.'));
+ }
+
+ opts.encoding = null;
+
+ return new Promise((resolve, reject) => {
+ const req = request(opts);
+
+ req.on('response', res => {
+ if (res.statusCode >= 400) {
+ return reject(new HttpError(res.statusCode, res.statusMessage));
+ }
+
+ let buffer = new Buffer([]);
+ let size;
+ let imageSizeError;
+
+ res.on('data', chunk => {
+ buffer = Buffer.concat([buffer, chunk]);
+
+ try {
+ size = imageSize(buffer);
+ } catch (err) {
+ imageSizeError = err;
+ return;
+ }
+
+ if (size) {
+ resolve(size);
+ return req.abort();
+ }
+ });
+
+ res.on('error', err => reject(err));
+
+ res.on('end', () => {
+ if (!size) {
+ return reject(imageSizeError);
+ }
+
+ size.downloaded = buffer.length;
+ return resolve(size);
+ });
+ });
+
+ req.on('error', err => reject(err));
+ });
+}; \ No newline at end of file
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/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 69692dbb8..c163c56a0 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -163,10 +163,6 @@ export class ContextMenu extends React.Component {
let style = this._yRelativeToTop ? { left: this.pageX, top: this.pageY } :
{ left: this.pageX, bottom: this.pageY };
- console.log(this._pageX);
- console.log(this.pageX);
- console.log();
-
const contents = (
<>
<span>
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index ba9f32d7d..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;
}
@@ -26,6 +27,14 @@ $linkGap : 3px;
opacity: 0.8;
}
+ .documentDecorations-radius {
+ pointer-events: auto;
+ background: black;
+ opacity: 0.8;
+ transform: translate(10px, 10px);
+ grid-row: 4;
+ }
+
#documentDecorations-topLeftResizer,
#documentDecorations-leftResizer,
#documentDecorations-bottomLeftResizer {
@@ -45,11 +54,27 @@ $linkGap : 3px;
grid-column-end: 7;
}
+ #documentDecorations-borderRadius {
+ grid-column-start: 5;
+ grid-column-end: 7;
+ border-radius: 100%;
+
+ .borderRadiusTooltip {
+ width: 10px;
+ height: 10px;
+ position: absolute;
+ }
+ }
+
#documentDecorations-topLeftResizer,
#documentDecorations-bottomRightResizer {
cursor: nwse-resize;
}
+ #documentDecorations-bottomRightResizer {
+ grid-row: 4;
+ }
+
#documentDecorations-topRightResizer,
#documentDecorations-bottomLeftResizer {
cursor: nesw-resize;
@@ -64,7 +89,8 @@ $linkGap : 3px;
#documentDecorations-rightResizer {
cursor: ew-resize;
}
- .title{
+
+ .title {
background: $alt-accent;
grid-column-start: 3;
grid-column-end: 4;
@@ -107,7 +133,6 @@ $linkGap : 3px;
.linkFlyout {
grid-column: 2/4;
- margin-top: $linkGap;
}
.linkButton-empty:hover {
@@ -123,6 +148,7 @@ $linkGap : 3px;
}
.link-button-container {
+ margin-top: $linkGap;
grid-column: 1/4;
width: auto;
height: auto;
@@ -130,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;
@@ -147,7 +177,8 @@ $linkGap : 3px;
transform: scale(1.05);
}
-.linkButton-empty, .linkButton-nonempty {
+.linkButton-empty,
+.linkButton-nonempty {
height: 20px;
width: 20px;
border-radius: 50%;
@@ -173,8 +204,6 @@ $linkGap : 3px;
.templating-menu {
position: absolute;
- bottom: 0;
- left: 50px;
pointer-events: auto;
text-transform: uppercase;
letter-spacing: 2px;
@@ -186,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;
@@ -208,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 2c0e18bbb..fb5104915 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,18 +1,18 @@
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";
import { Doc } from "../../new_fields/Doc";
import { List } from "../../new_fields/List";
-import { listSpec } from "../../new_fields/Schema";
-import { Cast, NumCast, StrCast, BoolCast } from "../../new_fields/Types";
+import { BoolCast, Cast, NumCast, StrCast } from "../../new_fields/Types";
+import { URLField } from '../../new_fields/URLField';
import { emptyFunction, Utils } from "../../Utils";
import { Docs } from "../documents/Documents";
import { DocumentManager } from "../util/DocumentManager";
import { DragLinksAsDocuments, DragManager } from "../util/DragManager";
import { SelectionManager } from "../util/SelectionManager";
-import { undoBatch } from "../util/UndoManager";
+import { undoBatch, UndoManager } from "../util/UndoManager";
import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss";
import { CollectionView } from "./collections/CollectionView";
import './DocumentDecorations.scss';
@@ -24,12 +24,16 @@ import { LinkMenu } from "./nodes/LinkMenu";
import { TemplateMenu } from "./TemplateMenu";
import { Template, Templates } from "./Templates";
import React = require("react");
-import { URLField } from '../../new_fields/URLField';
+import { RichTextField } from '../../new_fields/RichTextField';
+import { LinkManager } from '../util/LinkManager';
+import { ObjectField } from '../../new_fields/ObjectField';
+import { MetadataEntryMenu } from './MetadataEntryMenu';
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 }> {
@@ -43,9 +47,12 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
private _linkButton = React.createRef<HTMLDivElement>();
private _linkerButton = React.createRef<HTMLDivElement>();
private _embedButton = React.createRef<HTMLDivElement>();
+ private _tooltipoff = React.createRef<HTMLDivElement>();
+ private _textDoc?: Doc;
private _downX = 0;
private _downY = 0;
private _iconDoc?: Doc = undefined;
+ private _resizeUndo?: UndoManager.Batch;
@observable private _minimizedX = 0;
@observable private _minimizedY = 0;
@observable private _title: string = "";
@@ -73,6 +80,13 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
if (text[0] === '#') {
this._fieldKey = text.slice(1, text.length);
this._title = this.selectionTitle;
+ } else if (text.startsWith(">")) {
+ let fieldTemplateView = SelectionManager.SelectedDocuments()[0];
+ SelectionManager.DeselectAll();
+ let fieldTemplate = fieldTemplateView.props.Document;
+ let docTemplate = fieldTemplateView.props.ContainingCollectionView!.props.Document;
+ let metaKey = text.slice(1, text.length);
+ Doc.MakeTemplate(fieldTemplate, metaKey, Doc.GetProto(docTemplate));
}
else {
if (SelectionManager.SelectedDocuments().length > 0) {
@@ -122,7 +136,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
@computed
get Bounds(): { x: number, y: number, b: number, r: number } {
return SelectionManager.SelectedDocuments().reduce((bounds, documentView) => {
- if (documentView.props.isTopMost) {
+ if (documentView.props.renderDepth === 0) {
return bounds;
}
let transform = (documentView.props.ScreenToLocalTransform().scale(documentView.props.ContentScaling())).inverse();
@@ -141,7 +155,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
document.addEventListener("pointermove", this.onBackgroundMove);
document.addEventListener("pointerup", this.onBackgroundUp);
e.stopPropagation();
- e.preventDefault();
}
@action
@@ -149,7 +162,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
let dragDocView = SelectionManager.SelectedDocuments()[0];
const [left, top] = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).inverse().transformPoint(0, 0);
const [xoff, yoff] = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).transformDirection(e.x - left, e.y - top);
- let dragData = new DragManager.DocumentDragData(SelectionManager.SelectedDocuments().map(dv => dv.props.Document));
+ let dragData = new DragManager.DocumentDragData(SelectionManager.SelectedDocuments().map(dv => dv.props.Document), SelectionManager.SelectedDocuments().map(dv => dv.props.DataDoc ? dv.props.DataDoc : dv.props.Document));
dragData.xOffset = xoff;
dragData.yOffset = yoff;
dragData.moveDocument = SelectionManager.SelectedDocuments()[0].props.moveDocument;
@@ -275,7 +288,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;
@@ -310,6 +323,37 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
iconDoc.y = where[1] + NumCast(selView.props.Document.y);
}
+ _radiusDown = [0, 0];
+ @action
+ onRadiusDown = (e: React.PointerEvent): void => {
+ e.stopPropagation();
+ if (e.button === 0) {
+ this._radiusDown = [e.clientX, e.clientY];
+ this._isPointerDown = true;
+ this._resizeUndo = UndoManager.StartBatch("DocDecs set radius");
+ document.removeEventListener("pointermove", this.onRadiusMove);
+ document.removeEventListener("pointerup", this.onRadiusUp);
+ document.addEventListener("pointermove", this.onRadiusMove);
+ document.addEventListener("pointerup", this.onRadiusUp);
+ }
+ }
+
+ onRadiusMove = (e: PointerEvent): void => {
+ let dist = Math.sqrt((e.clientX - this._radiusDown[0]) * (e.clientX - this._radiusDown[0]) + (e.clientY - this._radiusDown[1]) * (e.clientY - this._radiusDown[1]));
+ SelectionManager.SelectedDocuments().map(dv => dv.props.Document.borderRounding = Doc.GetProto(dv.props.Document).borderRounding = `${Math.min(100, dist)}%`);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ onRadiusUp = (e: PointerEvent): void => {
+ e.stopPropagation();
+ e.preventDefault();
+ this._isPointerDown = false;
+ this._resizeUndo && this._resizeUndo.end();
+ document.removeEventListener("pointermove", this.onRadiusMove);
+ document.removeEventListener("pointerup", this.onRadiusUp);
+ }
+
@action
onPointerDown = (e: React.PointerEvent): void => {
e.stopPropagation();
@@ -317,6 +361,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
this._isPointerDown = true;
this._resizing = e.currentTarget.id;
this.Interacting = true;
+ this._resizeUndo = UndoManager.StartBatch("DocDecs resize");
document.removeEventListener("pointermove", this.onPointerMove);
document.addEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
@@ -408,7 +453,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();
@@ -468,10 +512,9 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
let doc = PositionDocument(element.props.Document);
let nwidth = doc.nativeWidth || 0;
let nheight = doc.nativeHeight || 0;
- let zoomBasis = NumCast(doc.zoomBasis, 1);
- let width = (doc.width || 0) / zoomBasis;
- let height = (doc.height || (nheight / nwidth * width)) / zoomBasis;
- let scale = element.props.ScreenToLocalTransform().Scale;
+ let width = (doc.width || 0);
+ let height = (doc.height || (nheight / nwidth * width));
+ let scale = element.props.ScreenToLocalTransform().Scale * element.props.ContentScaling();
let actualdW = Math.max(width + (dW * scale), 20);
let actualdH = Math.max(height + (dH * scale), 20);
doc.x = (doc.x || 0) + dX * (actualdW - width);
@@ -485,25 +528,25 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
}
if (nwidth > 0 && nheight > 0) {
if (Math.abs(dW) > Math.abs(dH)) {
- if (!fixedAspect) proto.nativeWidth = zoomBasis * actualdW / (doc.width || 1) * NumCast(proto.nativeWidth);
- doc.width = zoomBasis * actualdW;
- // doc.zoomBasis = zoomBasis * width / actualdW;
+ if (!fixedAspect) {
+ Doc.SetInPlace(element.props.Document, "nativeWidth", actualdW / (doc.width || 1) * (doc.nativeWidth || 0), true);
+ }
+ doc.width = actualdW;
if (fixedAspect) doc.height = nheight / nwidth * doc.width;
- else doc.height = zoomBasis * actualdH;
- proto.nativeHeight = (doc.height || 0) / doc.width * NumCast(proto.nativeWidth);
+ else doc.height = actualdH;
}
else {
- if (!fixedAspect) proto.nativeHeight = zoomBasis * actualdH / (doc.height || 1) * NumCast(proto.nativeHeight);
- doc.height = zoomBasis * actualdH;
- //doc.zoomBasis = zoomBasis * height / actualdH;
+ if (!fixedAspect) {
+ Doc.SetInPlace(element.props.Document, "nativeHeight", actualdH / (doc.height || 1) * (doc.nativeHeight || 0), true);
+ }
+ doc.height = actualdH;
if (fixedAspect) doc.width = nwidth / nheight * doc.height;
- else doc.width = zoomBasis * actualdW;
- proto.nativeWidth = (doc.width || 0) / doc.height * NumCast(proto.nativeHeight);
+ else doc.width = actualdW;
}
} else {
- dW && (doc.width = zoomBasis * actualdW);
- dH && (doc.height = zoomBasis * actualdH);
- proto.autoHeight = undefined;
+ dW && (doc.width = actualdW);
+ dH && (doc.height = actualdH);
+ Doc.SetInPlace(element.props.Document, "autoHeight", undefined, true);
}
}
});
@@ -517,6 +560,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
if (e.button === 0) {
e.preventDefault();
this._isPointerDown = false;
+ this._resizeUndo && this._resizeUndo.end();
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
}
@@ -551,10 +595,51 @@ 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>
+ );
+ }
+
+ considerTooltip = () => {
+ let thisDoc = SelectionManager.SelectedDocuments()[0].props.Document;
+ let isTextDoc = thisDoc.data && thisDoc.data instanceof RichTextField;
+ if (!isTextDoc) return null;
+ this._textDoc = thisDoc;
+ return (
+ <div className="tooltipwrapper">
+ <div title="Hide Tooltip" className="linkButton-linker" ref={this._tooltipoff} onPointerDown={this.onTooltipOff}>
+ {/* <FontAwesomeIcon className="fa-image" icon="image" size="sm" /> */}
</div>
</div>
+
+ );
+ }
+
+ onTooltipOff = (e: React.PointerEvent): void => {
+ e.stopPropagation();
+ if (this._textDoc) {
+ if (this._tooltipoff.current) {
+ if (this._tooltipoff.current.title === "Hide Tooltip") {
+ this._tooltipoff.current.title = "Show Tooltip";
+ this._textDoc.tooltip = "hi";
+ }
+ else {
+ this._tooltipoff.current.title = "Hide Tooltip";
+ }
+ }
+ }
+ }
+
+ 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>
);
}
@@ -572,9 +657,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
let linkButton = null;
if (SelectionManager.SelectedDocuments().length > 0) {
let selFirst = SelectionManager.SelectedDocuments()[0];
- let linkToSize = Cast(selFirst.props.Document.linkedToDocs, listSpec(Doc), []).length;
- let linkFromSize = Cast(selFirst.props.Document.linkedFromDocs, listSpec(Doc), []).length;
- let linkCount = linkToSize + linkFromSize;
+ let linkCount = LinkManager.Instance.getAllRelatedLinks(selFirst.props.Document).length;
linkButton = (<Flyout
anchorPoint={anchorPoints.RIGHT_TOP}
content={<LinkMenu docView={selFirst}
@@ -610,6 +693,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",
@@ -617,7 +711,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
left: bounds.x - this._resizeBorderWidth / 2,
top: bounds.y - this._resizeBorderWidth / 2,
pointerEvents: this.Interacting ? "none" : "all",
- zIndex: SelectionManager.SelectedDocuments().length > 1 ? 1000 : 0,
+ zIndex: SelectionManager.SelectedDocuments().length > 1 ? 900 : 0,
}} onPointerDown={this.onBackgroundDown} onContextMenu={(e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); }} >
</div>
<div className="documentDecorations-container" style={{
@@ -632,7 +726,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
{this._edtingTitle ?
<input ref={this.keyinput} className="title" type="text" name="dynbox" value={this._title} onBlur={this.titleBlur} onChange={this.titleChanged} onKeyPress={this.titleEntered} /> :
<div className="title" onPointerDown={this.onTitleDown} ><span>{`${this.selectionTitle}`}</span></div>}
- <div className="documentDecorations-closeButton" onPointerDown={this.onCloseDown}>X</div>
+ <div className="documentDecorations-closeButton" title="Close Document" onPointerDown={this.onCloseDown}>X</div>
<div id="documentDecorations-topLeftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
<div id="documentDecorations-topResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
<div id="documentDecorations-topRightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
@@ -642,17 +736,22 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
<div id="documentDecorations-bottomLeftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
<div id="documentDecorations-bottomResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
<div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+ <div id="documentDecorations-borderRadius" className="documentDecorations-radius" onPointerDown={this.onRadiusDown} onContextMenu={(e) => e.preventDefault()}><span className="borderRadiusTooltip" title="Drag Corner Radius"></span></div>
<div className="link-button-container">
<div className="linkButtonWrapper">
<div title="View Links" className="linkFlyout" ref={this._linkButton}> {linkButton} </div>
</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>
</div >
</div>
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index 97a2d19dd..989fb1be9 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -14,7 +14,7 @@ export interface EditableProps {
* @param value - The string entered by the user to set the value to
* @returns `true` if setting the value was successful, `false` otherwise
* */
- SetValue(value: string): boolean;
+ SetValue(value: string, shiftDown?: boolean): boolean;
OnFillDown?(value: string): void;
@@ -25,6 +25,7 @@ export interface EditableProps {
*/
contents: any;
fontStyle?: string;
+ fontSize?: number;
height?: number;
display?: string;
oneLine?: boolean;
@@ -52,7 +53,7 @@ export class EditableView extends React.Component<EditableProps> {
this.props.OnTab && this.props.OnTab();
} else if (e.key === "Enter") {
if (!e.ctrlKey) {
- if (this.props.SetValue(e.currentTarget.value)) {
+ if (this.props.SetValue(e.currentTarget.value, e.shiftKey)) {
this._editing = false;
}
} else if (this.props.OnFillDown) {
@@ -76,11 +77,16 @@ export class EditableView extends React.Component<EditableProps> {
e.stopPropagation();
}
+ @action
+ setIsFocused = (value: boolean) => {
+ this._editing = value;
+ }
+
render() {
if (this._editing) {
return <input className="editableView-input" defaultValue={this.props.GetValue()} onKeyDown={this.onKeyDown} autoFocus
onBlur={action(() => this._editing = false)} onPointerDown={this.stopPropagation} onClick={this.stopPropagation} onPointerUp={this.stopPropagation}
- style={{ display: this.props.display }} />;
+ style={{ display: this.props.display, fontSize: this.props.fontSize }} />;
} else {
return (
<div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`}
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
new file mode 100644
index 000000000..f378b6c0c
--- /dev/null
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -0,0 +1,178 @@
+import { UndoManager, undoBatch } from "../util/UndoManager";
+import { SelectionManager } from "../util/SelectionManager";
+import { CollectionDockingView } from "./collections/CollectionDockingView";
+import { MainView } from "./MainView";
+import { DragManager } from "../util/DragManager";
+import { action } from "mobx";
+
+const modifiers = ["control", "meta", "shift", "alt"];
+type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo;
+type KeyControlInfo = {
+ preventDefault: boolean,
+ stopPropagation: boolean
+};
+
+export default class KeyManager {
+ public static Instance: KeyManager = new KeyManager();
+ private router = new Map<string, KeyHandler>();
+
+ constructor() {
+ let isMac = navigator.platform.toLowerCase().indexOf("mac") >= 0;
+
+ // SHIFT CONTROL ALT META
+ this.router.set("0000", this.unmodified);
+ this.router.set(isMac ? "0001" : "0100", this.ctrl);
+ this.router.set(isMac ? "0100" : "0010", this.alt);
+ this.router.set(isMac ? "1001" : "1100", this.ctrl_shift);
+ }
+
+ public handle = (e: KeyboardEvent) => {
+ let keyname = e.key.toLowerCase();
+ this.handleGreedy(keyname);
+
+ if (modifiers.includes(keyname)) {
+ return;
+ }
+
+ let bit = (value: boolean) => value ? "1" : "0";
+ let modifierIndex = bit(e.shiftKey) + bit(e.ctrlKey) + bit(e.altKey) + bit(e.metaKey);
+
+ let handleConstrained = this.router.get(modifierIndex);
+ if (!handleConstrained) {
+ return;
+ }
+
+ let control = handleConstrained(keyname, e);
+
+ control.stopPropagation && e.stopPropagation();
+ control.preventDefault && e.preventDefault();
+ }
+
+ private handleGreedy = action((keyname: string) => {
+ switch (keyname) {
+ }
+ });
+
+ private unmodified = action((keyname: string, e: KeyboardEvent) => {
+ switch (keyname) {
+ case "escape":
+ if (MainView.Instance.isPointerDown) {
+ DragManager.AbortDrag();
+ } else {
+ if (CollectionDockingView.Instance.HasFullScreen()) {
+ CollectionDockingView.Instance.CloseFullScreen();
+ } else {
+ SelectionManager.DeselectAll();
+ }
+ }
+ MainView.Instance.toggleColorPicker(true);
+ break;
+ case "delete":
+ case "backspace":
+ 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;
+ }
+
+ return {
+ stopPropagation: false,
+ preventDefault: false
+ };
+ });
+
+ private alt = action((keyname: string) => {
+ let stopPropagation = true;
+ let preventDefault = true;
+
+ switch (keyname) {
+ case "n":
+ let toggle = MainView.Instance.addMenuToggle.current!;
+ toggle.checked = !toggle.checked;
+ break;
+ }
+
+ return {
+ stopPropagation: stopPropagation,
+ preventDefault: preventDefault
+ };
+ });
+
+ 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":
+ MainView.Instance.isSearchVisible = !MainView.Instance.isSearchVisible;
+ break;
+ case "o":
+ let target = SelectionManager.SelectedDocuments()[0];
+ target && target.fullScreenClicked();
+ break;
+ case "r":
+ preventDefault = false;
+ break;
+ case "y":
+ UndoManager.Redo();
+ break;
+ case "z":
+ UndoManager.Undo();
+ break;
+ case "a":
+ case "c":
+ case "v":
+ case "x":
+ stopPropagation = false;
+ preventDefault = false;
+ break;
+ }
+
+ return {
+ stopPropagation: stopPropagation,
+ preventDefault: preventDefault
+ };
+ });
+
+ private ctrl_shift = action((keyname: string) => {
+ let stopPropagation = true;
+ let preventDefault = true;
+
+ switch (keyname) {
+ case "z":
+ UndoManager.Redo();
+ break;
+ }
+
+ return {
+ stopPropagation: stopPropagation,
+ preventDefault: preventDefault
+ };
+ });
+
+} \ No newline at end of file
diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx
index 5d4ea76cd..3e0d7b476 100644
--- a/src/client/views/InkingCanvas.tsx
+++ b/src/client/views/InkingCanvas.tsx
@@ -13,7 +13,9 @@ import { Cast, PromiseValue, NumCast } from "../../new_fields/Types";
interface InkCanvasProps {
getScreenTransform: () => Transform;
+ AnnotationDocument: Doc;
Document: Doc;
+ inkFieldKey: string;
children: () => JSX.Element[];
}
@@ -40,7 +42,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
}
componentDidMount() {
- PromiseValue(Cast(this.props.Document.ink, 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) =>
@@ -55,12 +57,12 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
@computed
get inkData(): Map<string, StrokeData> {
- let map = Cast(this.props.Document.ink, 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).ink = new InkField(value);
+ this.props.AnnotationDocument[this.props.inkFieldKey] = new InkField(value);
}
@action
@@ -150,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/InkingControl.tsx b/src/client/views/InkingControl.tsx
index 0837e07a9..c7f7bdb66 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -1,5 +1,5 @@
-import { observable, action, computed } from "mobx";
-import { CirclePicker, ColorResult } from 'react-color';
+import { observable, action, computed, runInAction } from "mobx";
+import { ColorResult } from 'react-color';
import React = require("react");
import { observer } from "mobx-react";
import "./InkingControl.scss";
@@ -8,6 +8,10 @@ import { faPen, faHighlighter, faEraser, faBan } from '@fortawesome/free-solid-s
import { SelectionManager } from "../util/SelectionManager";
import { InkTool } from "../../new_fields/InkField";
import { Doc } from "../../new_fields/Doc";
+import { undoBatch, UndoManager } from "../util/UndoManager";
+import { StrCast } from "../../new_fields/Types";
+import { FormattedTextBox } from "./nodes/FormattedTextBox";
+import { MainOverlayTextBox } from "./MainOverlayTextBox";
library.add(faPen, faHighlighter, faEraser, faBan);
@@ -17,7 +21,7 @@ export class InkingControl extends React.Component {
@observable private _selectedTool: InkTool = InkTool.None;
@observable private _selectedColor: string = "rgb(244, 67, 54)";
@observable private _selectedWidth: string = "25";
- @observable private _open: boolean = false;
+ @observable public _open: boolean = false;
constructor(props: Readonly<{}>) {
super(props);
@@ -36,11 +40,28 @@ export class InkingControl extends React.Component {
return number.toString(16).toUpperCase();
}
- @action
- switchColor = (color: ColorResult): void => {
+ @undoBatch
+ switchColor = action((color: ColorResult): void => {
this._selectedColor = color.hex + (color.rgb.a !== undefined ? this.decimalToHexString(Math.round(color.rgb.a * 255)) : "ff");
- SelectionManager.SelectedDocuments().forEach(doc => Doc.GetProto(doc.props.Document).backgroundColor = this._selectedColor);
- }
+ if (InkingControl.Instance.selectedTool === InkTool.None) {
+ if (MainOverlayTextBox.Instance.SetColor(color.hex)) return;
+ let selected = SelectionManager.SelectedDocuments();
+ let oldColors = selected.map(view => {
+ let targetDoc = view.props.Document.isTemplate ? view.props.Document : Doc.GetProto(view.props.Document);
+ let oldColor = StrCast(targetDoc.backgroundColor);
+ targetDoc.backgroundColor = this._selectedColor;
+ return {
+ target: targetDoc,
+ previous: oldColor
+ };
+ });
+ let captured = this._selectedColor;
+ UndoManager.AddEvent({
+ undo: () => oldColors.forEach(pair => pair.target.backgroundColor = pair.previous),
+ redo: () => oldColors.forEach(pair => pair.target.backgroundColor = captured)
+ });
+ }
+ });
@action
switchWidth = (width: string): void => {
@@ -57,6 +78,11 @@ export class InkingControl extends React.Component {
return this._selectedColor;
}
+ @action
+ updateSelectedColor(value: string) {
+ this._selectedColor = value;
+ }
+
@computed
get selectedWidth() {
return this._selectedWidth;
diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss
index 690139341..a16123476 100644
--- a/src/client/views/Main.scss
+++ b/src/client/views/Main.scss
@@ -20,18 +20,11 @@ div {
-ms-user-select: none;
}
-#dash-title {
- position: absolute;
- right: 46.5%;
- letter-spacing: 3px;
- top: 9px;
- font-size: 12px;
- color: $alt-accent;
- z-index: 9999;
-}
+
.jsx-parser {
width: 100%;
+ height:100%;
pointer-events: none;
border-radius: inherit;
}
@@ -43,8 +36,8 @@ p {
::-webkit-scrollbar {
-webkit-appearance: none;
- height: 10px;
- width: 10px;
+ height: 8px;
+ width: 8px;
}
::-webkit-scrollbar-thumb {
@@ -118,8 +111,8 @@ button:hover {
//toolbar stuff
#toolbar {
position: absolute;
- bottom: 62px;
- left: 24px;
+ right: 8px;
+ top: 5px;
.toolbar-button {
display: block;
@@ -151,7 +144,7 @@ button:hover {
#add-nodes-menu {
position: absolute;
bottom: 22px;
- left: 24px;
+ left: 250px;
> label {
background: $dark-color;
@@ -200,7 +193,7 @@ button:hover {
position: absolute;
top: 0;
left: 0;
- overflow: scroll;
+ overflow: auto;
z-index: 1;
}
@@ -210,6 +203,7 @@ button:hover {
position: absolute;
top: 0;
left: 0;
+ overflow: hidden;
}
#add-options-content {
@@ -231,4 +225,39 @@ ul#add-options-list {
display: inline-block;
padding: 0;
}
+}
+
+.mainView-libraryFlyout {
+ height: 100%;
+ position: absolute;
+ display: flex;
+ flex-direction:column;
+}
+
+.mainView-libraryHandle {
+ width: 20px;
+ height: 40px;
+ top: 50%;
+ border: 1px solid black;
+ border-radius: 5px;
+ position: absolute;
+ z-index: 1;
+}
+.svg-inline--fa {
+ vertical-align: unset;
+}
+.mainView-workspace {
+ height:200px;
+ position:relative;
+ display:flex;
+}
+.mainView-library {
+ height:75%;
+ position:relative;
+ display:flex;
+}
+.mainView-recentlyClosed {
+ height:25%;
+ position:relative;
+ display:flex;
} \ No newline at end of file
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 3d9750a85..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 f6a746e63..c9d44e194 100644
--- a/src/client/views/MainOverlayTextBox.scss
+++ b/src/client/views/MainOverlayTextBox.scss
@@ -1,20 +1,29 @@
@import "globalCssVariables";
+
.mainOverlayTextBox-textInput {
background-color: rgba(248, 6, 6, 0.001);
- width: 200px;
+ 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 {
+ // width: 0px;
+ z-index: 10000;
+ position: absolute;
+ pointer-events: none;
} \ No newline at end of file
diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx
index b4ad5f4d7..126efd11c 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';
@@ -10,7 +10,6 @@ import { Transform } from '../util/Transform';
import { CollectionDockingView } from './collections/CollectionDockingView';
import "./MainOverlayTextBox.scss";
import { FormattedTextBox } from './nodes/FormattedTextBox';
-import { For } from 'babel-types';
interface MainOverlayTextBoxProps {
}
@@ -26,8 +25,21 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
private _textProxyDiv: React.RefObject<HTMLDivElement>;
private _textBottom: boolean | undefined;
private _textAutoHeight: boolean | undefined;
+ private _setouterdiv = (outerdiv: HTMLElement | null) => { this._outerdiv = outerdiv; this.updateTooltip(); };
+ private _outerdiv: HTMLElement | null = null;
private _textBox: FormattedTextBox | undefined;
+ private _tooltip?: HTMLElement;
@observable public TextDoc?: Doc;
+ @observable public TextDataDoc?: Doc;
+
+ updateTooltip = () => {
+ this._outerdiv && this._tooltip && !this._outerdiv.contains(this._tooltip) && this._outerdiv.appendChild(this._tooltip);
+ }
+
+ public SetColor(color: string) {
+ return this._textBox && this._textBox.setFontColor(color);
+ }
+
constructor(props: MainOverlayTextBoxProps) {
super(props);
@@ -38,12 +50,17 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
this._textBox = box;
if (box) {
this.TextDoc = box.props.Document;
- let sxf = Utils.GetScreenTransform(box ? box.CurrentDiv : undefined);
- let xf = () => { box.props.ScreenToLocalTransform(); return new Transform(-sxf.translateX, -sxf.translateY, 1 / sxf.scale); };
+ this.TextDataDoc = box.props.DataDoc;
+ let xf = () => {
+ box.props.ScreenToLocalTransform();
+ let sxf = Utils.GetScreenTransform(box ? box.CurrentDiv : undefined);
+ return new Transform(-sxf.translateX, -sxf.translateY, 1 / sxf.scale);
+ };
this.setTextDoc(box.props.fieldKey, box.CurrentDiv, xf, BoolCast(box.props.Document.autoHeight, false) || box.props.height === "min-content");
}
else {
this.TextDoc = undefined;
+ this.TextDataDoc = undefined;
this.setTextDoc();
}
});
@@ -82,10 +99,10 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
}
@action
textBoxMove = (e: PointerEvent) => {
- if (e.movementX > 1 || e.movementY > 1) {
+ if ((e.movementX > 1 || e.movementY > 1) && FormattedTextBox.InputBoxOverlay) {
document.removeEventListener("pointermove", this.textBoxMove);
document.removeEventListener('pointerup', this.textBoxUp);
- let dragData = new DragManager.DocumentDragData(FormattedTextBox.InputBoxOverlay ? [FormattedTextBox.InputBoxOverlay.props.Document] : []);
+ let dragData = new DragManager.DocumentDragData([FormattedTextBox.InputBoxOverlay.props.Document], [FormattedTextBox.InputBoxOverlay.props.DataDoc]);
const [left, top] = this._textXf().inverse().transformPoint(0, 0);
dragData.xOffset = e.clientX - left;
dragData.yOffset = e.clientY - top;
@@ -102,26 +119,30 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
document.removeEventListener('pointerup', this.textBoxUp);
}
- addDocTab = (doc: Doc, location: string) => {
+ addDocTab = (doc: Doc, dataDoc: Doc | undefined, location: string) => {
if (true) { // location === "onRight") { need to figure out stack to add "inTab"
- CollectionDockingView.Instance.AddRightSplit(doc);
+ CollectionDockingView.Instance.AddRightSplit(doc, dataDoc);
}
}
render() {
- this.TextDoc;
+ this.TextDoc; this.TextDataDoc;
if (FormattedTextBox.InputBoxOverlay && this._textTargetDiv) {
let textRect = this._textTargetDiv.getBoundingClientRect();
let s = this._textXf().Scale;
let location = this._textBottom ? textRect.bottom : textRect.top;
let hgt = this._textAutoHeight || this._textBottom ? "auto" : this._textTargetDiv.clientHeight;
- return <div className="mainOverlayTextBox-textInput" style={{ transform: `translate(${textRect.left}px, ${location}px) scale(${1 / s},${1 / s})`, width: "auto", height: "0px" }} >
- <div className="mainOverlayTextBox-textInput" onPointerDown={this.textBoxDown} ref={this._textProxyDiv} onScroll={this.textScroll}
- style={{ width: `${textRect.width * s}px`, height: "0px" }}>
- <div style={{ height: hgt, width: "100%", position: "absolute", bottom: this._textBottom ? "0px" : undefined }}>
- <FormattedTextBox color={`${this._textColor}`} fieldKey={this.TextFieldKey} hideOnLeave={this._textHideOnLeave} isOverlay={true} Document={FormattedTextBox.InputBoxOverlay.props.Document}
- isSelected={returnTrue} select={emptyFunction} isTopMost={true} selectOnLoad={true}
- ContainingCollectionView={undefined} whenActiveChanged={emptyFunction} active={returnTrue}
- ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyFunction} addDocTab={this.addDocTab} />
+ return <div ref={this._setouterdiv} className="mainOverlayTextBox-unscaled_div" style={{ transform: `translate(${textRect.left}px, ${location}px)` }} >
+ <div className="mainOverlayTextBox-textInput" style={{ transform: `scale(${1 / s},${1 / s})`, width: "auto", height: "0px" }} >
+ <div className="mainOverlayTextBox-textInput" onPointerDown={this.textBoxDown} ref={this._textProxyDiv} onScroll={this.textScroll}
+ style={{ width: `${textRect.width * s}px`, height: "0px" }}>
+ <div style={{ height: hgt, width: "100%", position: "absolute", bottom: this._textBottom ? "0px" : undefined }}>
+ <FormattedTextBox color={`${this._textColor}`} fieldKey={this.TextFieldKey} fieldExt="" hideOnLeave={this._textHideOnLeave} isOverlay={true}
+ Document={FormattedTextBox.InputBoxOverlay.props.Document}
+ DataDoc={FormattedTextBox.InputBoxOverlay.props.DataDoc}
+ isSelected={returnTrue} select={emptyFunction} renderDepth={0} selectOnLoad={true}
+ ContainingCollectionView={undefined} whenActiveChanged={emptyFunction} active={returnTrue}
+ ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyFunction} addDocTab={this.addDocTab} outer_div={(tooltip: HTMLElement) => { this._tooltip = tooltip; this.updateTooltip(); }} />
+ </div>
</div>
</div>
</ div>;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 51630c29b..94a4835a1 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,74 +1,104 @@
import { IconName, library } from '@fortawesome/fontawesome-svg-core';
-import { faFilePdf, faFilm, faFont, faGlobeAsia, faImage, faMusic, faObjectGroup, faPenNib, faThumbtack, faRedoAlt, faTable, faTree, faUndoAlt, faBell, faCommentAlt } 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, trace } from 'mobx';
+import { action, computed, configure, observable, runInAction, reaction, trace } from 'mobx';
import { observer } from 'mobx-react';
-import { CirclePicker, SliderPicker, BlockPicker, TwitterPicker, SketchPicker } from 'react-color';
import "normalize.css";
import * as React from 'react';
+import { SketchPicker } from 'react-color';
import Measure from 'react-measure';
import * as request from 'request';
+import { Doc, DocListCast, Opt, HeightSym } from '../../new_fields/Doc';
+import { Id } from '../../new_fields/FieldSymbols';
+import { InkTool } from '../../new_fields/InkField';
+import { List } from '../../new_fields/List';
+import { listSpec } from '../../new_fields/Schema';
+import { 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, returnTrue, Utils, returnOne, returnZero } from '../../Utils';
+import { emptyFunction, returnOne, returnTrue, Utils } from '../../Utils';
+import { DocServer } from '../DocServer';
import { Docs } from '../documents/Documents';
-import { SetupDrag, DragManager } from '../util/DragManager';
+import { SetupDrag } from '../util/DragManager';
+import { HistoryUtil } from '../util/History';
import { Transform } from '../util/Transform';
import { UndoManager } from '../util/UndoManager';
-import { PresentationView } from './PresentationView';
+import { CollectionBaseView } from './collections/CollectionBaseView';
import { CollectionDockingView } from './collections/CollectionDockingView';
import { ContextMenu } from './ContextMenu';
import { DocumentDecorations } from './DocumentDecorations';
+import KeyManager from './GlobalKeyHandler';
import { InkingControl } from './InkingControl';
import "./Main.scss";
import { MainOverlayTextBox } from './MainOverlayTextBox';
import { DocumentView } from './nodes/DocumentView';
-import { PreviewCursor } from './PreviewCursor';
-import { SearchBox } from './SearchBox';
-import { SelectionManager } from '../util/SelectionManager';
-import { FieldResult, Field, Doc, Opt, DocListCast } from '../../new_fields/Doc';
-import { Cast, FieldValue, StrCast } from '../../new_fields/Types';
-import { DocServer } from '../DocServer';
-import { listSpec } from '../../new_fields/Schema';
-import { Id } from '../../new_fields/FieldSymbols';
-import { HistoryUtil } from '../util/History';
-import { CollectionBaseView } from './collections/CollectionBaseView';
+import { OverlayView } from './OverlayView';
import PDFMenu from './pdf/PDFMenu';
-import { InkTool } from '../../new_fields/InkField';
-
+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 {
public static Instance: MainView;
+ @observable addMenuToggle = React.createRef<HTMLInputElement>();
@observable private _workspacesShown: boolean = false;
@observable public pwidth: number = 0;
@observable public pheight: number = 0;
@computed private get mainContainer(): Opt<Doc> {
return FieldValue(Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc));
}
+ @computed get mainFreeform(): Opt<Doc> {
+ let docs = DocListCast(this.mainContainer!.data);
+ return (docs && docs.length > 1) ? docs[1] : undefined;
+ }
+ public isPointerDown = false;
private set mainContainer(doc: Opt<Doc>) {
if (doc) {
if (!("presentationView" in doc)) {
- doc.presentationView = 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);
+
+ reaction(() => {
+ let workspaces = CurrentUserUtils.UserDocument.workspaces;
+ let recent = CurrentUserUtils.UserDocument.recentlyClosed;
+ if (!(recent instanceof Doc)) return 0;
+ if (!(workspaces instanceof Doc)) return 0;
+ let workspacesDoc = workspaces;
+ let recentDoc = recent;
+ let libraryHeight = this.getPHeight() - workspacesDoc[HeightSym]() - recentDoc[HeightSym]() - 20 + CurrentUserUtils.UserDocument[HeightSym]() * 0.00001;
+ return libraryHeight;
+ }, (libraryHeight: number) => {
+ if (libraryHeight && Math.abs(CurrentUserUtils.UserDocument[HeightSym]() - libraryHeight) > 5) {
+ CurrentUserUtils.UserDocument.height = libraryHeight;
+ }
+ (Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc) as Doc).allowClear = true;
+ }, { fireImmediately: true });
+ }
+
+ componentWillUnMount() {
+ window.removeEventListener("keydown", KeyManager.Instance.handle);
+ }
+
constructor(props: Readonly<{}>) {
super(props);
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) {
@@ -80,7 +110,9 @@ export class MainView extends React.Component {
}
library.add(faFont);
- library.add(faImage);
+ library.add(faExclamation);
+ library.add(faPortrait);
+ library.add(faCat);
library.add(faFilePdf);
library.add(faObjectGroup);
library.add(faTable);
@@ -91,8 +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();
}
@@ -101,21 +139,9 @@ export class MainView extends React.Component {
// window.addEventListener("pointermove", (e) => this.reportLocation(e))
window.addEventListener("drop", (e) => e.preventDefault(), false); // drop event handler
window.addEventListener("dragover", (e) => e.preventDefault(), false); // drag event handler
- window.addEventListener("keydown", (e) => {
- if (e.key === "Escape") {
- DragManager.AbortDrag();
- SelectionManager.DeselectAll();
- } else if (e.key === "z" && e.ctrlKey) {
- e.preventDefault();
- UndoManager.Undo();
- } else if ((e.key === "y" && e.ctrlKey) || (e.key === "z" && e.ctrlKey && e.shiftKey)) {
- e.preventDefault();
- UndoManager.Redo();
- }
- }, false); // drag event handler
// click interactions for the context menu
- document.addEventListener("pointerdown", action(function (e: PointerEvent) {
-
+ document.addEventListener("pointerdown", action((e: PointerEvent) => {
+ this.isPointerDown = true;
const targets = document.elementsFromPoint(e.x, e.y);
if (targets && targets.length && targets[0].className.toString().indexOf("contextMenu") === -1) {
ContextMenu.Instance.closeMenu();
@@ -139,13 +165,21 @@ export class MainView extends React.Component {
}
}
+
@action
createNewWorkspace = async (id?: string) => {
- const list = Cast(CurrentUserUtils.UserDocument.data, listSpec(Doc));
+ let workspaces = Cast(CurrentUserUtils.UserDocument.workspaces, Doc);
+ 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}` });
- var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(CurrentUserUtils.UserDocument, 150), CollectionDockingView.makeDocumentConfig(freeformDoc, 600)] }] };
- let mainDoc = Docs.DockDocument([CurrentUserUtils.UserDocument, freeformDoc], JSON.stringify(dockingLayout), { title: `Workspace ${list.length + 1}` }, id);
+ 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.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>([]);
+ CurrentUserUtils.UserDocument.linkManagerDoc = linkManagerDoc;
+ }
list.push(mainDoc);
// bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container)
setTimeout(() => {
@@ -156,31 +190,37 @@ export class MainView extends React.Component {
}
}
- @observable _notifsCol: Opt<Doc>;
-
@action
openWorkspace = async (doc: Doc, fromHistory = false) => {
CurrentUserUtils.MainDocId = doc[Id];
this.mainContainer = doc;
- fromHistory || HistoryUtil.pushState({ type: "doc", docId: doc[Id], initializers: {} });
+ 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.Control.makeEditable();
+ }
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 () => {
if (col) {
const l = Cast(col.data, listSpec(Doc));
if (l) {
- runInAction(() => this._notifsCol = col);
+ runInAction(() => CollectionTreeView.NotifsCol = col);
}
}
}, 100);
}
- openNotifsCol = () => {
- if (this._notifsCol && CollectionDockingView.Instance) {
- CollectionDockingView.Instance.AddRightSplit(this._notifsCol);
- }
- }
-
onDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
@@ -198,36 +238,116 @@ export class MainView extends React.Component {
getPHeight = () => {
return this.pheight;
}
- @computed
- get mainContent() {
+
+ @observable flyoutWidth: number = 250;
+ @computed get dockingContent() {
+ let flyoutWidth = this.flyoutWidth;
let mainCont = this.mainContainer;
- let content = !mainCont ? (null) :
- <DocumentView Document={mainCont}
- addDocument={undefined}
- addDocTab={emptyFunction}
- removeDocument={undefined}
- ScreenToLocalTransform={Transform.Identity}
- ContentScaling={returnOne}
- PanelWidth={this.getPWidth}
- PanelHeight={this.getPHeight}
- isTopMost={true}
- selectOnLoad={false}
- focus={emptyFunction}
- parentActive={returnTrue}
- whenActiveChanged={emptyFunction}
- bringToFront={emptyFunction}
- ContainingCollectionView={undefined} />;
- const pres = mainCont ? FieldValue(Cast(mainCont.presentationView, Doc)) : undefined;
+ let castRes = mainCont ? FieldValue(Cast(mainCont.presentationView, listSpec(Doc))) : undefined;
return <Measure offset onResize={this.onResize}>
{({ measureRef }) =>
- <div ref={measureRef} id="mainContent-div" onDrop={this.onDrop}>
- {content}
- {pres ? <PresentationView Document={pres} key="presentation" /> : null}
+ <div ref={measureRef} id="mainContent-div" style={{ width: `calc(100% - ${flyoutWidth}px`, transform: `translate(${flyoutWidth}px, 0px)` }} onDrop={this.onDrop}>
+ {!mainCont ? (null) :
+ <DocumentView Document={mainCont}
+ DataDoc={undefined}
+ addDocument={undefined}
+ addDocTab={emptyFunction}
+ removeDocument={undefined}
+ ScreenToLocalTransform={Transform.Identity}
+ ContentScaling={returnOne}
+ PanelWidth={this.getPWidth}
+ PanelHeight={this.getPHeight}
+ renderDepth={0}
+ selectOnLoad={false}
+ focus={emptyFunction}
+ parentActive={returnTrue}
+ whenActiveChanged={emptyFunction}
+ bringToFront={emptyFunction}
+ ContainingCollectionView={undefined}
+ zoomToScale={emptyFunction}
+ getScale={returnOne}
+ />}
+ {castRes ? <PresentationView Documents={castRes} key="presentation" /> : null}
</div>
}
</Measure>;
}
+ _downsize = 0;
+ onPointerDown = (e: React.PointerEvent) => {
+ this._downsize = e.clientX;
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ document.addEventListener("pointermove", this.onPointerMove);
+ document.addEventListener("pointerup", this.onPointerUp);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ @action
+ onPointerMove = (e: PointerEvent) => {
+ this.flyoutWidth = Math.max(e.clientX, 0);
+ }
+ @action
+ onPointerUp = (e: PointerEvent) => {
+ this.isPointerDown = false;
+ if (Math.abs(e.clientX - this._downsize) < 4) {
+ if (this.flyoutWidth < 5) this.flyoutWidth = 250;
+ else this.flyoutWidth = 0;
+ }
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ }
+ flyoutWidthFunc = () => this.flyoutWidth;
+ addDocTabFunc = (doc: Doc) => {
+ if (doc.dockingConfig) {
+ this.openWorkspace(doc);
+ } else {
+ CollectionDockingView.Instance.AddRightSplit(doc, undefined);
+ }
+ }
+ @computed
+ get flyout() {
+ let sidebar = CurrentUserUtils.UserDocument.sidebar;
+ if (!(sidebar instanceof Doc)) return (null);
+ let sidebarDoc = sidebar;
+ return <DocumentView
+ Document={sidebarDoc}
+ DataDoc={undefined}
+ addDocument={undefined}
+ addDocTab={this.addDocTabFunc}
+ removeDocument={undefined}
+ ScreenToLocalTransform={Transform.Identity}
+ ContentScaling={returnOne}
+ PanelWidth={this.flyoutWidthFunc}
+ PanelHeight={this.getPHeight}
+ renderDepth={0}
+ selectOnLoad={false}
+ focus={emptyFunction}
+ parentActive={returnTrue}
+ whenActiveChanged={emptyFunction}
+ bringToFront={emptyFunction}
+ ContainingCollectionView={undefined}
+ zoomToScale={emptyFunction}
+ getScale={returnOne}>
+ </DocumentView>;
+ }
+ @computed
+ get mainContent() {
+ let sidebar = CurrentUserUtils.UserDocument.sidebar;
+ if (!(sidebar instanceof Doc)) return (null);
+ return <div>
+ <div className="mainView-libraryHandle"
+ style={{ cursor: "ew-resize", left: `${this.flyoutWidth - 10}px`, backgroundColor: `${StrCast(sidebar.backgroundColor, "lightGray")}` }}
+ onPointerDown={this.onPointerDown}>
+ <span title="library View Dragger" style={{ width: "100%", height: "100%", position: "absolute" }} />
+ </div>
+ <div className="mainView-libraryFlyout" style={{ width: `${this.flyoutWidth}px`, zIndex: 1 }}>
+ {this.flyout}
+ </div>
+ {this.dockingContent}
+ </div>;
+ }
+
selected = (tool: InkTool) => {
if (!InkingControl.Instance || InkingControl.Instance.selectedTool === InkTool.None) return { display: "none" };
if (InkingControl.Instance.selectedTool === tool) {
@@ -251,41 +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 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>(), "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" >
- <input type="checkbox" id="add-menu-toggle" />
- <label htmlFor="add-menu-toggle" title="Add Node"><p>+</p></label>
+ 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" 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="Redo" 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" 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>
@@ -295,33 +421,19 @@ export class MainView extends React.Component {
@action
- toggleColorPicker = () => {
- this._colorPickerDisplay = !this._colorPickerDisplay;
+ toggleColorPicker = (close = false) => {
+ this._colorPickerDisplay = close ? false : !this._colorPickerDisplay;
}
/* @TODO this should really be moved into a moveable toolbar component, but for now let's put it here to meet the deadline */
@computed
get miscButtons() {
- const length = this._notifsCol ? DocListCast(this._notifsCol.data).length : 0;
- const notifsRef = React.createRef<HTMLDivElement>();
- const dragNotifs = action(() => this._notifsCol!);
let logoutRef = React.createRef<HTMLDivElement>();
return [
- <div id="toolbar" key="toolbar">
- <div ref={notifsRef}>
- <button className="toolbar-button round-button" title="Notifs"
- onClick={this.openNotifsCol} onPointerDown={this._notifsCol ? SetupDrag(notifsRef, dragNotifs) : emptyFunction}>
- <FontAwesomeIcon icon={faBell} size="sm" />
- </button>
- <div className="main-notifs-badge" style={length > 0 ? { "display": "initial" } : { "display": "none" }}>
- {length}
- </div>
- </div>
- </div >,
- this.isSearchVisible ? <div className="main-searchDiv" key="search" style={{ top: '34px', right: '1px', position: 'absolute' }} > <SearchBox /> </div> : null,
+ 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={() => request.get(DocServer.prepend(RouteStore.logout), emptyFunction)}>Log Out</button></div>
+ <button onClick={() => window.location.assign(Utils.prepend(RouteStore.logout))}>Log Out</button></div>
];
}
@@ -332,6 +444,7 @@ export class MainView extends React.Component {
this.isSearchVisible = !this.isSearchVisible;
}
+
render() {
return (
<div id="main-div">
@@ -341,10 +454,10 @@ export class MainView extends React.Component {
<ContextMenu />
{this.nodesMenu()}
{this.miscButtons}
- <InkingControl />
<PDFMenu />
<MainOverlayTextBox />
- </div>
+ <OverlayView />
+ </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.tsx b/src/client/views/OverlayView.tsx
new file mode 100644
index 000000000..f8fc94274
--- /dev/null
+++ b/src/client/views/OverlayView.tsx
@@ -0,0 +1,47 @@
+import * as React from "react";
+import { observer } from "mobx-react";
+import { observable, action } from "mobx";
+import { Utils } from "../../Utils";
+
+export type OverlayDisposer = () => void;
+
+export type OverlayElementOptions = {
+ x: number;
+ y: number;
+ width?: number;
+ height?: number;
+};
+
+@observer
+export class OverlayView extends React.Component {
+ public static Instance: OverlayView;
+ @observable.shallow
+ private _elements: { ele: JSX.Element, id: string, options: OverlayElementOptions }[] = [];
+
+ constructor(props: any) {
+ super(props);
+ if (!OverlayView.Instance) {
+ OverlayView.Instance = this;
+ }
+ }
+
+ @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);
+ if (index !== -1) this._elements.splice(index, 1);
+ });
+ }
+
+ 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>
+ ))}
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/PresentationView.tsx b/src/client/views/PresentationView.tsx
deleted file mode 100644
index 1dacbb663..000000000
--- a/src/client/views/PresentationView.tsx
+++ /dev/null
@@ -1,156 +0,0 @@
-import { action, observable } from "mobx";
-import { observer } from "mobx-react";
-import { Doc, DocListCast } from "../../new_fields/Doc";
-import { Id } from "../../new_fields/FieldSymbols";
-import { List } from "../../new_fields/List";
-import { listSpec } from "../../new_fields/Schema";
-import { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../new_fields/Types";
-import { DocumentManager } from "../util/DocumentManager";
-import "./PresentationView.scss";
-import React = require("react");
-
-export interface PresViewProps {
- Document: Doc;
-}
-
-interface PresListProps extends PresViewProps {
- deleteDocument(index: number): void;
- gotoDocument(index: number): void;
-}
-
-@observer
-/**
- * Component that takes in a document prop and a boolean whether it's collapsed or not.
- */
-class PresentationViewList extends React.Component<PresListProps> {
-
-
- /**
- * Renders a single child document. It will just append a list element.
- * @param document The document to render.
- */
- renderChild = (document: Doc, index: number) => {
- let title = document.title;
-
- //to get currently selected presentation doc
- let selected = NumCast(this.props.Document.selectedDoc, 0);
-
- let className = "presentationView-item";
- if (selected === index) {
- //this doc is selected
- className += " presentationView-selected";
- }
- let onEnter = (e: React.PointerEvent) => { document.libraryBrush = true; };
- let onLeave = (e: React.PointerEvent) => { document.libraryBrush = false; };
- return (
- <div className={className} key={document[Id] + index}
- onPointerEnter={onEnter} onPointerLeave={onLeave}
- style={{
- outlineColor: "maroon",
- outlineStyle: "dashed",
- outlineWidth: BoolCast(document.libraryBrush, false) || BoolCast(document.protoBrush, false) ? `1px` : "0px",
- }}
- onClick={e => { this.props.gotoDocument(index); e.stopPropagation(); }}>
- <strong className="presentationView-name">
- {`${index + 1}. ${title}`}
- </strong>
- <button className="presentation-icon" onClick={e => { this.props.deleteDocument(index); e.stopPropagation(); }}>X</button>
- </div>
- );
-
- }
-
- render() {
- const children = DocListCast(this.props.Document.data);
-
- return (
- <div className="presentationView-listCont">
- {children.map(this.renderChild)}
- </div>
- );
- }
-}
-
-
-@observer
-export class PresentationView extends React.Component<PresViewProps> {
- public static Instance: PresentationView;
-
- //observable means render is re-called every time variable is changed
- @observable
- collapsed: boolean = false;
- closePresentation = action(() => this.props.Document.width = 0);
- next = () => {
- const current = NumCast(this.props.Document.selectedDoc);
- this.gotoDocument(current + 1);
-
- }
- back = () => {
- const current = NumCast(this.props.Document.selectedDoc);
- this.gotoDocument(current - 1);
- }
-
- @action
- public RemoveDoc = (index: number) => {
- const value = FieldValue(Cast(this.props.Document.data, listSpec(Doc)));
- if (value) {
- value.splice(index, 1);
- }
- }
-
- public gotoDocument = async (index: number) => {
- const list = FieldValue(Cast(this.props.Document.data, listSpec(Doc)));
- if (!list) {
- return;
- }
- if (index < 0 || index >= list.length) {
- return;
- }
-
- this.props.Document.selectedDoc = index;
- const doc = await list[index];
- DocumentManager.Instance.jumpToDocument(doc);
- }
-
- //initilize class variables
- constructor(props: PresViewProps) {
- super(props);
- PresentationView.Instance = this;
- }
-
- /**
- * Adds a document to the presentation view
- **/
- @action
- public PinDoc(doc: Doc) {
- //add this new doc to props.Document
- const data = Cast(this.props.Document.data, listSpec(Doc));
- if (data) {
- data.push(doc);
- } else {
- this.props.Document.data = new List([doc]);
- }
-
- this.props.Document.width = 300;
- }
-
- render() {
- let titleStr = StrCast(this.props.Document.title);
- let width = NumCast(this.props.Document.width);
-
- //TODO: next and back should be icons
- return (
- <div className="presentationView-cont" style={{ width: width, overflow: "hidden" }}>
- <div className="presentationView-heading">
- <div className="presentationView-title">{titleStr}</div>
- <button className='presentation-icon' onClick={this.closePresentation}>X</button>
- </div>
- <div className="presentation-buttons">
- <button className="presentation-button" onClick={this.back}>back</button>
- <button className="presentation-button" onClick={this.next}>next</button>
- </div>
- <PresentationViewList Document={this.props.Document} deleteDocument={this.RemoveDoc} gotoDocument={this.gotoDocument} />
- </div>
- );
- }
-} \ No newline at end of file
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/ScriptBox.scss b/src/client/views/ScriptBox.scss
new file mode 100644
index 000000000..28326624a
--- /dev/null
+++ b/src/client/views/ScriptBox.scss
@@ -0,0 +1,17 @@
+.scriptBox-outerDiv {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
+.scriptBox-toolbar {
+ width: 100%;
+}
+
+.scriptBox-textArea {
+ width: 100%;
+ height: 100%;
+ box-sizing: border-box;
+ resize: none;
+} \ No newline at end of file
diff --git a/src/client/views/ScriptBox.tsx b/src/client/views/ScriptBox.tsx
new file mode 100644
index 000000000..fa236c2da
--- /dev/null
+++ b/src/client/views/ScriptBox.tsx
@@ -0,0 +1,44 @@
+import * as React from "react";
+import { observer } from "mobx-react";
+import { observable, action } from "mobx";
+
+import "./ScriptBox.scss";
+
+export interface ScriptBoxProps {
+ onSave: (text: string, onError: (error: string) => void) => void;
+ onCancel?: () => void;
+ initialText?: string;
+}
+
+@observer
+export class ScriptBox extends React.Component<ScriptBoxProps> {
+ @observable
+ private _scriptText: string;
+
+ constructor(props: ScriptBoxProps) {
+ super(props);
+ this._scriptText = props.initialText || "";
+ }
+
+ @action
+ onChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
+ this._scriptText = e.target.value;
+ }
+
+ @action
+ onError = (error: string) => {
+ console.log(error);
+ }
+
+ render() {
+ return (
+ <div className="scriptBox-outerDiv">
+ <div className="scriptBox-toolbar">
+ <button onClick={e => { this.props.onSave(this._scriptText, this.onError); e.stopPropagation(); }}>Save</button>
+ <button onClick={e => { this.props.onCancel && this.props.onCancel(); e.stopPropagation(); }}>Cancel</button>
+ </div>
+ <textarea className="scriptBox-textarea" onChange={this.onChange} value={this._scriptText}></textarea>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/SearchBox.scss b/src/client/views/SearchBox.scss
deleted file mode 100644
index b38e6091d..000000000
--- a/src/client/views/SearchBox.scss
+++ /dev/null
@@ -1,102 +0,0 @@
-@import "globalCssVariables";
-
-.searchBox-bar {
- height: 32px;
- display: flex;
- justify-content: flex-end;
- align-items: center;
- padding-left: 2px;
- padding-right: 2px;
-
- .searchBox-input {
- width: 130px;
- -webkit-transition: width 0.4s;
- transition: width 0.4s;
- align-self: stretch;
- }
-
- .searchBox-input:focus {
- width: 500px;
- outline: 3px solid lightblue;
- }
-
- .searchBox-barChild {
- flex: 0 1 auto;
- margin-left: 2px;
- margin-right: 2px;
- }
-
- .searchBox-filter {
- align-self: stretch;
- }
-
- .searchBox-submit {
- color: $dark-color;
- }
-
- .searchBox-submit:hover {
- color: $main-accent;
- transform: scale(1.05);
- cursor: pointer;
- }
-}
-
-.searchBox-results {
- margin-left: 27px; //Is there a better way to do this?
-}
-
-.filter-form {
- background: $dark-color;
- height: 400px;
- width: 400px;
- position: relative;
- right: 1px;
- color: $light-color;
- padding: 10px;
- flex-direction: column;
-}
-
-#header {
- text-transform: uppercase;
- letter-spacing: 2px;
- font-size: 100%;
- height: 40px;
-}
-
-#option {
- height: 20px;
-}
-
-.searchBox-results {
- top: 300px;
- display: flex;
- flex-direction: column;
-
- .search-item {
- width: 500px;
- height: 50px;
- background: $light-color-secondary;
- display: flex;
- justify-content: space-between;
- align-items: center;
- transition: all 0.1s;
- border-width: 0.11px;
- border-style: none;
- border-color: $intermediate-color;
- border-bottom-style: solid;
- padding: 10px;
- white-space: nowrap;
- font-size: 13px;
- }
-
- .search-item:hover {
- transition: all 0.1s;
- background: $lighter-alt-accent;
- }
-
- .search-title {
- text-transform: uppercase;
- text-align: left;
- width: 8vw;
- }
-} \ No newline at end of file
diff --git a/src/client/views/SearchBox.tsx b/src/client/views/SearchBox.tsx
index 63d2065e2..33cb63df5 100644
--- a/src/client/views/SearchBox.tsx
+++ b/src/client/views/SearchBox.tsx
@@ -1,27 +1,19 @@
-import * as React from 'react';
-import { observer } from 'mobx-react';
-import { observable, action, runInAction } from 'mobx';
-import { Utils } from '../../Utils';
-import { MessageStore } from '../../server/Message';
-import "./SearchBox.scss";
-import { faSearch, faObjectGroup } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { library } from '@fortawesome/fontawesome-svg-core';
-// const app = express();
-// import * as express from 'express';
-import { Search } from '../../server/Search';
+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 { SearchItem } from './SearchItem';
-import { isString } from 'util';
-import { constant } from 'async';
-import { DocServer } from '../DocServer';
import { Doc } from '../../new_fields/Doc';
import { Id } from '../../new_fields/FieldSymbols';
-import { DocumentManager } from '../util/DocumentManager';
-import { SetupDrag } from '../util/DragManager';
-import { Docs } from '../documents/Documents';
-import { RouteStore } from '../../server/RouteStore';
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);
@@ -56,7 +48,7 @@ export class SearchBox extends React.Component {
@action
getResults = async (query: string) => {
- let response = await rp.get(DocServer.prepend('/search'), {
+ let response = await rp.get(Utils.prepend('/search'), {
qs: {
query
}
@@ -72,22 +64,6 @@ export class SearchBox extends React.Component {
}
return docs;
}
- public static async convertDataUri(imageUri: string, returnedFilename: string) {
- try {
- let posting = DocServer.prepend(RouteStore.dataUriToImage);
- const returnedUri = await rp.post(posting, {
- body: {
- uri: imageUri,
- name: returnedFilename
- },
- json: true,
- });
- return returnedUri;
-
- } catch (e) {
- console.log(e);
- }
- }
@action
handleClickFilter = (e: Event): void => {
@@ -166,7 +142,7 @@ 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}"` });
}
// Useful queries:
@@ -188,7 +164,7 @@ export class SearchBox extends React.Component {
</div>
{this._resultsOpen ? (
<div className="searchBox-results">
- {this._results.map(result => <SearchItem doc={result} key={result[Id]} />)}
+ {this._results.map(result => <SearchItem doc={result} key={result[Id]} highlighting={[]} />)}
</div>
) : null}
</div>
diff --git a/src/client/views/SearchItem.tsx b/src/client/views/SearchItem.tsx
deleted file mode 100644
index 6901f60da..000000000
--- a/src/client/views/SearchItem.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import React = require("react");
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Doc } from "../../new_fields/Doc";
-import { DocumentManager } from "../util/DocumentManager";
-import { SetupDrag } from "../util/DragManager";
-
-
-export interface SearchProps {
- doc: Doc;
-}
-
-library.add(faCaretUp);
-library.add(faObjectGroup);
-library.add(faStickyNote);
-library.add(faFilePdf);
-library.add(faFilm);
-
-export class SearchItem extends React.Component<SearchProps> {
-
- onClick = () => {
- DocumentManager.Instance.jumpToDocument(this.props.doc);
- }
-
- //needs help
- // @computed get layout(): string { const field = Cast(this.props.doc[fieldKey], IconField); return field ? field.icon : "<p>Error loading icon data</p>"; }
-
-
- public static DocumentIcon(layout: string) {
- let button = layout.indexOf("PDFBox") !== -1 ? faFilePdf :
- layout.indexOf("ImageBox") !== -1 ? faImage :
- layout.indexOf("Formatted") !== -1 ? faStickyNote :
- layout.indexOf("Video") !== -1 ? faFilm :
- layout.indexOf("Collection") !== -1 ? faObjectGroup :
- faCaretUp;
- return <FontAwesomeIcon icon={button} className="documentView-minimizedIcon" />;
- }
- onPointerEnter = (e: React.PointerEvent) => {
- this.props.doc.libraryBrush = true;
- Doc.SetOnPrototype(this.props.doc, "protoBrush", true);
- }
- onPointerLeave = (e: React.PointerEvent) => {
- this.props.doc.libraryBrush = false;
- Doc.SetOnPrototype(this.props.doc, "protoBrush", false);
- }
-
- collectionRef = React.createRef<HTMLDivElement>();
- startDocDrag = () => {
- let doc = this.props.doc;
- const isProto = Doc.GetT(doc, "isPrototype", "boolean", true);
- if (isProto) {
- return Doc.MakeDelegate(doc);
- } else {
- return Doc.MakeAlias(doc);
- }
- }
- render() {
- return (
- <div className="search-item" ref={this.collectionRef} id="result"
- onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}
- onClick={this.onClick} onPointerDown={SetupDrag(this.collectionRef, this.startDocDrag)} >
- <div className="search-title" id="result" >title: {this.props.doc.title}</div>
- {/* <div className="search-type" id="result" >Type: {this.props.doc.layout}</div> */}
- {/* <div className="search-type" >{SearchItem.DocumentIcon(this.layout)}</div> */}
- </div>
- );
- }
-} \ No newline at end of file
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index a9bc4d3d2..1b32f0ddd 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -6,6 +6,7 @@ import './DocumentDecorations.scss';
import { DocumentView } from "./nodes/DocumentView";
import { Template } from "./Templates";
import React = require("react");
+import { undoBatch } from "../util/UndoManager";
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -39,6 +40,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
super(props);
}
+ @undoBatch
@action
toggleTemplate = (event: React.ChangeEvent<HTMLInputElement>, template: Template): void => {
if (event.target.checked) {
@@ -62,6 +64,13 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
}
}
+ @undoBatch
+ @action
+ clearTemplates = (event: React.MouseEvent) => {
+ this.props.docs.map(d => d.clearTemplates());
+ Array.from(this.props.templates.keys()).map(t => this.props.templates.set(t, false));
+ }
+
@action
componentWillReceiveProps(nextProps: TemplateMenuProps) {
// this._templates = nextProps.templates;
@@ -79,9 +88,10 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
return (
<div className="templating-menu" >
- <div className="templating-button" onClick={() => this.toggleTemplateActivity()}>+</div>
+ <div title="Template Options" className="templating-button" onClick={() => this.toggleTemplateActivity()}>+</div>
<ul id="template-list" style={{ display: this._hidden ? "none" : "block" }}>
{templateMenu}
+ <button style={{ display: this._hidden ? "none" : "block" }} onClick={this.clearTemplates}>Clear</button>
</ul>
</div>
);
diff --git a/src/client/views/Templates.tsx b/src/client/views/Templates.tsx
index 3d5f7b6ea..236704fa2 100644
--- a/src/client/views/Templates.tsx
+++ b/src/client/views/Templates.tsx
@@ -49,7 +49,7 @@ export namespace Templates {
export const Title = new Template("Title", TemplatePosition.InnerTop,
`<div>
- <div style="height:25px; width:100%; background-color: rgba(0, 0, 0, .4); color: white; ">
+ <div style="height:25px; width:100%; background-color: rgba(0, 0, 0, .4); color: white; z-index: 100">
<span style="text-align:center;width:100%;font-size:20px;position:absolute;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">{props.Document.title}</span>
</div>
<div style="height:calc(100% - 25px);">
@@ -58,7 +58,7 @@ export namespace Templates {
</div>` );
export const Header = new Template("Header", TemplatePosition.InnerTop,
- `< div style = "display:flex; flex-direction:column; height:100%;" >
+ `<div style = "display:flex; flex-direction:column; height:100%;" >
<div style="width:100%; background-color: rgba(0, 0, 0, .4); color: white; ">
<FormattedTextBox {...props} height={"min-content"} color={"white"} fieldKey={"header"} />
</div>
@@ -69,8 +69,7 @@ export namespace Templates {
`< div >
<div style="height:100%; width:100%;position:absolute;">{layout}</div>
<div id="isExpander" style="height:15px; width:15px; margin-left:-16px; pointer-events:all; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white;">
- <img id="isExpander" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAMAAAAJbSJIAAAAZlBMVEX///8AAABmZmb7+/tYWFhgYGBFRUVSUlL4+Pg/Pz9jY2N5eXmcnJyioqKBgYFzc3NtbW1LS0s3NzfW1taWlpaOjo6IiIgvLy9WVlampqZcXFw5OTlvb28mJiYxMTHe3t7l5eUjIyMY8kIZAAAD2UlEQVR4nO2d61biMBRGW1FBEVHxfp15/5ecOVa5lHxtArmck/Xtn1BotjtNoXQtm4YQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEFIrX6UHEA1gsmrneceRjHm7cj28attKFOf/TRyKIliH4vzbZE+xE2zbZYkxRWX5Y9JT/BW0X3G+NtlR3Ahar7jcMtlS3Ba0XXG+Y7JW3BW0XHHZM/lR7AvaVewL/ijuC1pV3Bf8VnQJ2lR0CYriq/Nxg4puwfa1aZ7dz9yUHnEgN26NZ3luWkPFd7fEtHsWVDwpO+YgTgYKCuYn6tAU7TBecaygcGpZEQie7m5luKJPQQFUvCwx5iAuvQoK4KShvSIoOHVtCz7dnOUecxBn7kG/urc2eCz6T9EOcxXDCgpAUetyAwoOCBqrGF5QMKR4mCA8L+pTBIJwkRl95eifJjPHTDYTFQ8vePyrs3BsBfXLzfFHkvKKMY4j1ctNnCmmuGKslfCQT0RZiPdFVmnFmOcy36sDWYn7DU9hxdifRkKuEGQh/pWW0K/QiUlxtUxVxTTXyhQtN6kuI6mpmO5qpxJFIBjl1yMVimmvV4PfrnIq3iYsKICTRj7F9L84gIq38fYwCCj4HnMfRY/FPL8ZFayYo6BQbLlJeZrYpVDFXAUFcMtKWkUgmOhmnwKKOQsK4NaxdIp5CwqZj8X8gv27jNecJ9nZuXtnie/SzjhRQcHkt6Fnq1imoAAUY1csVVDIUrFcQSGDIhC8jriLQZIrli0oXKdVLF1QSFqxfEEBVLyI8NYXCgoKySaqhinakajimxrBRBX1FBQSVNRyDP4SXVGbYHRFfYJN8xhTESwyj5HHHEjEihoLCqDiXfAb3aksKESqCAoqEIxUUW9BAS03E+93mOhcZDYcXVF3QeHBPcI3v4qo4EPiUQcBKr75vHaiv6AAKt6NV0SCqgoKqOKYovpFZgOo+DmsOHkyUlA4ZKKamaIdQPEJK5oqKKCKM7D9zFZBIayiuYICWm5cFWef7o3vs486CP8VdQIEVRcU7sFE7VecgSmqvKDgVxEJqi8ogIof2xVnH2YLCuMT1fAU7RirOPtrXHCsovmCwlDFCgoKWNH4IrMBTdQ/NUzRjiu3CeCq9HAPAVSspaDgX9FkQcG3ollB34qGBf0UTQv6KBoXHFc0LzimWIFg0ywGBBelBxcHXLGKggKqWElBwV2xIkF3xaoEXYqVCe4rVifYV3wpPZwULOouKLzUXVBY1F1QeKm7oLCoXVAqVi7YNM7/F0YIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCG+/ANh4i1CHdc63QAAAABJRU5ErkJggg=="
- width="15px" height="15px" />
+ <img id="isExpander" src="/assets/downarrow.png" width="15px" height="15px" />
</div>
</div > `
);
@@ -84,6 +83,16 @@ export namespace Templates {
</div > `);
}
+ export function TitleBar(datastring: string) {
+ return (`<div>
+ <div style="height:25px; width:100%; background-color: rgba(0, 0, 0, .4); color: white; z-index: 100">
+ <span style="text-align:center;width:100%;font-size:20px;position:absolute;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">${datastring}</span>
+ </div>
+ <div style="height:calc(100% - 25px);">
+ <div style="width:100%;overflow:auto">{layout}</div>
+ </div>
+ </div>` );
+ }
export const TemplateList: Template[] = [Title, Header, Caption, Bullet];
export function sortTemplates(a: Template, b: Template) {
diff --git a/src/client/views/_nodeModuleOverrides.scss b/src/client/views/_nodeModuleOverrides.scss
index 6f97e60f8..b8a7db034 100644
--- a/src/client/views/_nodeModuleOverrides.scss
+++ b/src/client/views/_nodeModuleOverrides.scss
@@ -2,22 +2,21 @@
// goldenlayout stuff
div .lm_header {
- background: $dark-color;
- min-height: 2em;
+ background: $dark-color;
}
.lm_tab {
- margin-top: 0.6em !important;
- padding-top: 0.5em !important;
- min-height: 1.35em;
- padding-bottom: 0px;
- border-radius: 5px;
- font-family: $sans-serif !important;
+ // margin-top: 0.6em !important;
+ // padding-top: 0.5em !important;
+ // min-height: 1.35em;
+ // padding-bottom: 0px;
+ // border-radius: 5px;
+ font-family: $sans-serif !important;
}
.lm_header .lm_controls {
- right: 1em !important;
+ right: 1em !important;
}
// @TODO the ril__navgiation buttons in the img gallery are a lil messed up but I can't figure out
-// why. Low priority for now but it's bugging me. --Julie
+// why. Low priority for now but it's bugging me. --Julie \ 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 1e42593d1..eba69b448 100644
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -1,16 +1,16 @@
import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { Doc, DocListCast, Opt } from '../../../new_fields/Doc';
+import { Doc } from '../../../new_fields/Doc';
import { Id } from '../../../new_fields/FieldSymbols';
import { List } from '../../../new_fields/List';
import { listSpec } from '../../../new_fields/Schema';
-import { Cast, FieldValue, NumCast, 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';
import { FieldViewProps } from '../nodes/FieldView';
import './CollectionBaseView.scss';
-import { DocumentManager } from '../../util/DocumentManager';
export enum CollectionViewType {
Invalid,
@@ -36,7 +36,6 @@ export interface CollectionViewProps extends FieldViewProps {
contentRef?: React.Ref<HTMLDivElement>;
}
-
@observer
export class CollectionBaseView extends React.Component<CollectionViewProps> {
@observable private static _safeMode = false;
@@ -60,10 +59,12 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
}
}
+ @computed get dataDoc() { return Doc.resolvedFieldDataDoc(BoolCast(this.props.Document.isTemplate) ? this.props.DataDoc ? this.props.DataDoc : this.props.Document : this.props.Document, this.props.fieldKey, this.props.fieldExt); }
+ @computed get dataField() { return this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey; }
+
active = (): boolean => {
var isSelected = this.props.isSelected();
- var topMost = this.props.isTopMost;
- return isSelected || this._isChildActive || topMost;
+ return isSelected || this._isChildActive || this.props.renderDepth === 0 || BoolCast(this.props.Document.excludeFromLibrary);
}
//TODO should this be observable?
@@ -73,57 +74,26 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
this.props.whenActiveChanged(isActive);
}
- createsCycle(documentToAdd: Doc, containerDocument: Doc): boolean {
- if (!(documentToAdd instanceof Doc)) {
- return false;
- }
- let data = DocListCast(documentToAdd.data);
- for (const doc of data) {
- if (this.createsCycle(doc, containerDocument)) {
- return true;
- }
- }
- let annots = DocListCast(documentToAdd.annotations);
- for (const annot of annots) {
- if (this.createsCycle(annot, containerDocument)) {
- return true;
- }
- }
- for (let containerProto: Opt<Doc> = containerDocument; containerProto !== undefined; containerProto = FieldValue(containerProto.proto)) {
- if (containerProto[Id] === documentToAdd[Id]) {
- return true;
- }
- }
- return false;
- }
- @computed get isAnnotationOverlay() { return this.props.fieldKey === "annotations"; }
+ @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 {
- let props = this.props;
- var curPage = NumCast(props.Document.curPage, -1);
+ let self = this;
+ var curPage = NumCast(this.props.Document.curPage, -1);
Doc.GetProto(doc).page = curPage;
if (curPage >= 0) {
- Doc.GetProto(doc).annotationOn = props.Document;
+ Doc.GetProto(doc).annotationOn = this.props.Document;
}
- if (!this.createsCycle(doc, props.Document)) {
- //TODO This won't create the field if it doesn't already exist
- const value = Cast(props.Document[props.fieldKey], listSpec(Doc));
- let alreadyAdded = true;
- if (value !== undefined) {
- if (allowDuplicates || !value.some(v => v instanceof Doc && v[Id] === doc[Id])) {
- alreadyAdded = false;
- value.push(doc);
- }
- } else {
- alreadyAdded = false;
- Doc.SetOnPrototype(this.props.Document, this.props.fieldKey, new List([doc]));
- }
- // set the ZoomBasis only if hasn't already been set -- bcz: maybe set/resetting the ZoomBasis should be a parameter to addDocument?
- if (!alreadyAdded && (this.collectionViewType === CollectionViewType.Freeform || this.collectionViewType === CollectionViewType.Invalid)) {
- let zoom = NumCast(this.props.Document.scale, 1);
- // Doc.GetProto(doc).zoomBasis = zoom;
+ allowDuplicates = true;
+ let targetDataDoc = this.props.fieldExt || this.props.Document.isTemplate ? this.extensionDoc : this.props.Document;
+ let targetField = (this.props.fieldExt || this.props.Document.isTemplate) && this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey;
+ const value = Cast(targetDataDoc[targetField], listSpec(Doc));
+ if (value !== undefined) {
+ if (allowDuplicates || !value.some(v => v instanceof Doc && v[Id] === doc[Id])) {
+ value.push(doc);
}
+ } else {
+ Doc.GetProto(targetDataDoc)[targetField] = new List([doc]);
}
return true;
}
@@ -132,22 +102,14 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
removeDocument(doc: Doc): boolean {
let docView = DocumentManager.Instance.getDocumentView(doc, this.props.ContainingCollectionView);
docView && SelectionManager.DeselectDoc(docView);
- const props = this.props;
//TODO This won't create the field if it doesn't already exist
- const value = Cast(props.Document[props.fieldKey], listSpec(Doc), []);
- let index = -1;
- for (let i = 0; i < value.length; i++) {
- let v = value[i];
- if (v instanceof Doc && v[Id] === doc[Id]) {
- index = i;
- break;
- }
- }
- PromiseValue(Cast(doc.annotationOn, Doc)).then(annotationOn => {
- if (annotationOn === props.Document) {
- doc.annotationOn = undefined;
- }
- });
+ 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)
+ );
if (index !== -1) {
value.splice(index, 1);
@@ -161,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.props.Document, 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)) {
@@ -180,11 +145,13 @@ 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={{ 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>
);
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 69b9e77eb..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";
@@ -10,7 +10,7 @@ import { Id } from '../../../new_fields/FieldSymbols';
import { FieldId } from "../../../new_fields/RefField";
import { listSpec } from "../../../new_fields/Schema";
import { Cast, NumCast, StrCast, BoolCast } from "../../../new_fields/Types";
-import { emptyFunction, returnTrue, Utils } from "../../../Utils";
+import { emptyFunction, returnTrue, Utils, returnOne } from "../../../Utils";
import { DocServer } from "../../DocServer";
import { DocumentManager } from '../../util/DocumentManager';
import { DragLinksAsDocuments, DragManager } from "../../util/DragManager";
@@ -24,11 +24,17 @@ import { SubCollectionViewProps } from "./CollectionSubView";
import { ParentDocSelector } from './ParentDocumentSelector';
import React = require("react");
import { MainView } from '../MainView';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { library } from '@fortawesome/fontawesome-svg-core';
+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
export class CollectionDockingView extends React.Component<SubCollectionViewProps> {
public static Instance: CollectionDockingView;
- public static makeDocumentConfig(document: Doc, width?: number) {
+ public static makeDocumentConfig(document: Doc, dataDoc: Doc | undefined, width?: number) {
return {
type: 'react-component',
component: 'DocumentFrameRenderer',
@@ -36,6 +42,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
width: width,
props: {
documentId: document[Id],
+ dataDocumentId: dataDoc ? dataDoc[Id] : ""
//collectionDockingView: CollectionDockingView.Instance
}
};
@@ -46,6 +53,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
private _flush: boolean = false;
private _ignoreStateChange = "";
private _isPointerDown = false;
+ private _maximizedSrc: Opt<DocumentView>;
constructor(props: SubCollectionViewProps) {
super(props);
@@ -56,28 +64,63 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
}
hack: boolean = false;
undohack: any = null;
- public StartOtherDrag(dragDocs: Doc[], e: any) {
- this.hack = true;
- this.undohack = UndoManager.StartBatch("goldenDrag");
- dragDocs.map(dragDoc =>
- this.AddRightSplit(dragDoc, true).contentItems[0].tab._dragListener.
- onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 }));
+ public StartOtherDrag(e: any, dragDocs: Doc[], dragDataDocs: (Doc | undefined)[] = []) {
+ let config: any;
+ if (dragDocs.length === 1) {
+ config = CollectionDockingView.makeDocumentConfig(dragDocs[0], dragDataDocs[0]);
+ } else {
+ config = {
+ type: 'row',
+ content: dragDocs.map((doc, i) => {
+ CollectionDockingView.makeDocumentConfig(doc, dragDataDocs[i]);
+ })
+ };
+ }
+ const div = document.createElement("div");
+ const dragSource = this._goldenLayout.createDragSource(div, config);
+ dragSource._dragListener.on("dragStop", () => {
+ dragSource.destroy();
+ });
+ dragSource._dragListener.onMouseDown(e);
+ // dragSource.destroy();
+ // this.hack = true;
+ // this.undohack = UndoManager.StartBatch("goldenDrag");
+ // dragDocs.map((dragDoc, i) =>
+ // this.AddRightSplit(dragDoc, dragDataDocs[i], true).contentItems[0].tab._dragListener.
+ // onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 }));
}
@action
- public OpenFullScreen(document: Doc) {
+ public OpenFullScreen(docView: DocumentView) {
+ let document = Doc.MakeAlias(docView.props.Document);
+ let dataDoc = docView.dataDoc;
let newItemStackConfig = {
type: 'stack',
- content: [CollectionDockingView.makeDocumentConfig(document)]
+ content: [CollectionDockingView.makeDocumentConfig(document, dataDoc)]
};
var docconfig = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout);
this._goldenLayout.root.contentItems[0].addChild(docconfig);
docconfig.callDownwards('_$init');
this._goldenLayout._$maximiseItem(docconfig);
+ this._maximizedSrc = docView;
this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig());
this.stateChanged();
}
+ public CloseFullScreen = () => {
+ let target = this._goldenLayout._maximisedItem;
+ if (target !== null && this._maximizedSrc) {
+ this._goldenLayout._maximisedItem.remove();
+ SelectionManager.SelectDoc(this._maximizedSrc, false);
+ this._maximizedSrc = undefined;
+ this.stateChanged();
+ }
+ }
+
+ public HasFullScreen = () => {
+ return this._goldenLayout._maximisedItem !== null;
+ }
+
@undoBatch
@action
public CloseRightSplit = (document: Doc): boolean => {
@@ -123,22 +166,23 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
// Creates a vertical split on the right side of the docking view, and then adds the Document to that split
//
@action
- public AddRightSplit = (document: Doc, minimize: boolean = false) => {
+ public AddRightSplit = (document: Doc, dataDoc: Doc | undefined, minimize: boolean = false) => {
let docs = Cast(this.props.Document.data, listSpec(Doc));
if (docs) {
docs.push(document);
}
let newItemStackConfig = {
type: 'stack',
- content: [CollectionDockingView.makeDocumentConfig(document)]
+ content: [CollectionDockingView.makeDocumentConfig(document, dataDoc)]
};
var newContentItem = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout);
- if (this._goldenLayout.root.contentItems[0].isRow) {
+ if (this._goldenLayout.root.contentItems.length === 0) {
+ this._goldenLayout.root.addChild(newContentItem);
+ } else if (this._goldenLayout.root.contentItems[0].isRow) {
this._goldenLayout.root.contentItems[0].addChild(newContentItem);
- }
- else {
+ } else {
var collayout = this._goldenLayout.root.contentItems[0];
var newRow = collayout.layoutManager.createContentItem({ type: "row" }, this._goldenLayout);
collayout.parent.replaceChild(collayout, newRow);
@@ -160,12 +204,12 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
return newContentItem;
}
@action
- public AddTab = (stack: any, document: Doc) => {
+ public AddTab = (stack: any, document: Doc, dataDocument: Doc | undefined) => {
let docs = Cast(this.props.Document.data, listSpec(Doc));
if (docs) {
docs.push(document);
}
- let docContentConfig = CollectionDockingView.makeDocumentConfig(document);
+ let docContentConfig = CollectionDockingView.makeDocumentConfig(document, dataDocument);
var newContentItem = stack.layoutManager.createContentItem(docContentConfig, this._goldenLayout);
stack.addChild(newContentItem.contentItems[0], undefined);
this.layoutChanged();
@@ -216,6 +260,8 @@ 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 = "";
}, { fireImmediately: true });
@@ -225,6 +271,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
}
componentWillUnmount: () => void = () => {
try {
+ this.props.Document.workspaceBrush = false;
this._goldenLayout.unbind('itemDropped', this.itemDropped);
this._goldenLayout.unbind('tabCreated', this.tabCreated);
this._goldenLayout.unbind('stackCreated', this.stackCreated);
@@ -245,20 +292,27 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
var cur = this._containerRef.current;
// bcz: since GoldenLayout isn't a React component itself, we need to notify it to resize when its document container's size has changed
- this._goldenLayout.updateSize(cur!.getBoundingClientRect().width, cur!.getBoundingClientRect().height);
+ this._goldenLayout && this._goldenLayout.updateSize(cur!.getBoundingClientRect().width, cur!.getBoundingClientRect().height);
}
@action
onPointerUp = (e: React.PointerEvent): void => {
- this._isPointerDown = false;
if (this._flush) {
this._flush = false;
- setTimeout(() => this.stateChanged(), 10);
+ setTimeout(() => {
+ CollectionDockingView.Instance._ignoreStateChange = JSON.stringify(CollectionDockingView.Instance._goldenLayout.toConfig());
+ this.stateChanged();
+ }, 10);
}
}
@action
onPointerDown = (e: React.PointerEvent): void => {
this._isPointerDown = true;
+ let onPointerUp = action(() => {
+ window.removeEventListener("pointerup", onPointerUp);
+ this._isPointerDown = false;
+ });
+ window.addEventListener("pointerup", onPointerUp);
var className = (e.target as any).className;
if (className === "messageCounter") {
e.stopPropagation();
@@ -269,36 +323,10 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
let tab = (e.target as any).parentElement as HTMLElement;
DocServer.GetRefField(docid).then(action(async (sourceDoc: Opt<Field>) =>
(sourceDoc instanceof Doc) && DragLinksAsDocuments(tab, x, y, sourceDoc)));
- } else
- if ((className === "lm_title" || className === "lm_tab lm_active") && !e.shiftKey) {
- e.stopPropagation();
- e.preventDefault();
- let x = e.clientX;
- let y = e.clientY;
- let docid = (e.target as any).DashDocId;
- let tab = (e.target as any).parentElement as HTMLElement;
- let glTab = (e.target as any).Tab;
- if (glTab && glTab.contentItem && glTab.contentItem.parent) {
- glTab.contentItem.parent.setActiveContentItem(glTab.contentItem);
- }
- DocServer.GetRefField(docid).then(action((f: Opt<Field>) => {
- if (f instanceof Doc) {
- DragManager.StartDocumentDrag([tab], new DragManager.DocumentDragData([f]), x, y,
- {
- handlers: {
- dragComplete: emptyFunction,
- },
- hideSource: false
- });
- }
- }));
- }
+ }
if (className === "lm_drag_handle" || className === "lm_close" || className === "lm_maximise" || className === "lm_minimise" || className === "lm_close_tab") {
this._flush = true;
}
- if (this.props.active()) {
- e.stopPropagation();
- }
}
@undoBatch
@@ -318,6 +346,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
}
itemDropped = () => {
+ CollectionDockingView.Instance._ignoreStateChange = JSON.stringify(CollectionDockingView.Instance._goldenLayout.toConfig());
this.stateChanged();
}
@@ -333,33 +362,46 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
if (tab.contentItem.config.fixed) {
tab.contentItem.parent.config.fixed = true;
}
- DocServer.GetRefField(tab.contentItem.config.props.documentId).then(async doc => {
- if (doc instanceof Doc) {
- let counter: any = this.htmlToElement(`<span class="messageCounter">0</div>`);
- tab.element.append(counter);
- let upDiv = document.createElement("span");
- const stack = tab.contentItem.parent;
- // shifts the focus to this tab when another tab is dragged over it
- tab.element[0].onmouseenter = (e: any) => {
- if (!this._isPointerDown) return;
- var activeContentItem = tab.header.parent.getActiveContentItem();
- if (tab.contentItem !== activeContentItem) {
- tab.header.parent.setActiveContentItem(tab.contentItem);
- }
- tab.setActive(true);
- };
- ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={(doc, location) => CollectionDockingView.Instance.AddTab(stack, doc)} />, upDiv);
- tab.reactComponents = [upDiv];
- tab.element.append(upDiv);
- counter.DashDocId = tab.contentItem.config.props.documentId;
- tab.reactionDisposer = reaction(() => [doc.linkedFromDocs, doc.LinkedToDocs, doc.title],
- () => {
- counter.innerHTML = DocListCast(doc.linkedFromDocs).length + DocListCast(doc.linkedToDocs).length;
- tab.titleElement[0].textContent = doc.title;
- }, { fireImmediately: true });
- tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId;
- }
- });
+
+ 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) {
+ let dragSpan = document.createElement("span");
+ dragSpan.style.position = "relative";
+ dragSpan.style.bottom = "6px";
+ dragSpan.style.paddingLeft = "4px";
+ dragSpan.style.paddingRight = "2px";
+ let upDiv = document.createElement("span");
+ const stack = tab.contentItem.parent;
+ // shifts the focus to this tab when another tab is dragged over it
+ tab.element[0].onmouseenter = (e: any) => {
+ if (!this._isPointerDown) return;
+ var activeContentItem = tab.header.parent.getActiveContentItem();
+ if (tab.contentItem !== activeContentItem) {
+ tab.header.parent.setActiveContentItem(tab.contentItem);
+ }
+ tab.setActive(true);
+ };
+ ReactDOM.render(<span title="Drag as document" onPointerDown={
+ e => {
+ e.preventDefault();
+ e.stopPropagation();
+ DragManager.StartDocumentDrag([dragSpan], new DragManager.DocumentDragData([doc], [dataDoc]), e.clientX, e.clientY, {
+ handlers: { dragComplete: emptyFunction },
+ hideSource: false
+ });
+ }}><FontAwesomeIcon icon="file" size="lg" /></span>, dragSpan);
+ ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={doc => CollectionDockingView.Instance.AddTab(stack, doc, dataDoc)} />, upDiv);
+ tab.reactComponents = [dragSpan, upDiv];
+ tab.element.append(dragSpan);
+ tab.element.append(upDiv);
+ tab.reactionDisposer = reaction(() => [doc.title],
+ () => {
+ tab.titleElement[0].textContent = doc.title;
+ }, { fireImmediately: true });
+ //TODO why can't this just be doc instead of the id?
+ tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId;
+ }
}
tab.titleElement[0].Tab = tab;
tab.closeElement.off('click') //unbind the current click handler
@@ -371,9 +413,16 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
if (doc instanceof Doc) {
let theDoc = doc;
CollectionDockingView.Instance._removedDocs.push(theDoc);
+
+ 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());
});
}
@@ -388,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);
}
@@ -406,15 +465,24 @@ 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");
}));
}
render() {
+ if (this.props.renderDepth > 0) {
+ return <div style={{ width: "100%", height: "100%" }}>Nested workspaces can't be rendered</div>;
+ }
return (
- <div className="collectiondockingview-container" id="menuContainer"
- onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef} />
+ <Measure offset onResize={this.onResize}>
+ {({ measureRef }) =>
+ <div ref={measureRef}>
+ <div className="collectiondockingview-container" id="menuContainer"
+ onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef} />
+ </div>
+ }
+ </Measure>
);
}
@@ -422,6 +490,8 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
interface DockedFrameProps {
documentId: FieldId;
+ dataDocumentId: FieldId;
+ glContainer: any;
//collectionDockingView: CollectionDockingView
}
@observer
@@ -430,6 +500,10 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
@observable private _panelWidth = 0;
@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) {
@@ -439,9 +513,33 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
}
constructor(props: any) {
super(props);
- DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => this._document = f as Doc));
+ DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => {
+ this._document = f as Doc;
+ if (this.props.dataDocumentId && this.props.documentId !== this.props.dataDocumentId) {
+ DocServer.GetRefField(this.props.dataDocumentId).then(action((f: Opt<Field>) => this._dataDoc = f as Doc));
+ }
+ }));
+ }
+
+ 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);
@@ -476,45 +574,60 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
}
get previewPanelCenteringOffset() { return (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2; }
- addDocTab = (doc: Doc, location: string) => {
+ addDocTab = (doc: Doc, dataDoc: Doc | undefined, location: string) => {
if (doc.dockingConfig) {
MainView.Instance.openWorkspace(doc);
} else if (location === "onRight") {
- CollectionDockingView.Instance.AddRightSplit(doc);
+ CollectionDockingView.Instance.AddRightSplit(doc, dataDoc);
} else {
- CollectionDockingView.Instance.AddTab(this._stack, doc);
+ 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}, ${this.scaleToFitMultiplier})` }}>
- <DocumentView key={this._document[Id]} Document={this._document}
- bringToFront={emptyFunction}
- addDocument={undefined}
- removeDocument={undefined}
- ContentScaling={this.contentScaling}
- PanelWidth={this.nativeWidth}
- PanelHeight={this.nativeHeight}
- ScreenToLocalTransform={this.ScreenToLocalTransform}
- isTopMost={true}
- selectOnLoad={false}
- parentActive={returnTrue}
- whenActiveChanged={emptyFunction}
- focus={emptyFunction}
- addDocTab={this.addDocTab}
- ContainingCollectionView={undefined} />
+ style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px) scale(${this.scaleToFitMultiplier})` }}>
+ {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 b2d016934..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,22 +29,18 @@ 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 }
);
}
- public static LayoutString(fieldKey: string = "data") {
- return FieldView.LayoutString(CollectionPDFView, fieldKey);
+ componentWillUnmount() {
+ this._reactionDisposer && this._reactionDisposer();
+ }
+
+ 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 9cc8961e3..f72b1aa07 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -6,15 +6,15 @@ import { action, computed, observable, trace, untracked } from "mobx";
import { observer } from "mobx-react";
import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table";
import "react-table/react-table.css";
+import { emptyFunction, returnFalse, returnZero, returnOne } from "../../../Utils";
import { Doc, DocListCast, DocListCastAsync, Field } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
import { List } from "../../../new_fields/List";
import { listSpec } from "../../../new_fields/Schema";
-import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
-import { emptyFunction, returnFalse, returnZero } from "../../../Utils";
+import { Cast, FieldValue, NumCast, StrCast, BoolCast } from "../../../new_fields/Types";
import { Docs } from "../../documents/Documents";
import { Gateway } from "../../northstar/manager/Gateway";
-import { SetupDrag } from "../../util/DragManager";
+import { SetupDrag, DragManager } from "../../util/DragManager";
import { CompileScript } from "../../util/Scripting";
import { Transform } from "../../util/Transform";
import { COLLECTION_BORDER_WIDTH, MAX_ROW_HEIGHT } from '../../views/globalCssVariables.scss';
@@ -29,6 +29,8 @@ import "./CollectionSchemaView.scss";
import { CollectionSubView } from "./CollectionSubView";
import { CollectionVideoView } from "./CollectionVideoView";
import { CollectionView } from "./CollectionView";
+import { undoBatch } from "../../util/UndoManager";
+import { timesSeries } from "async";
library.add(faCog);
@@ -98,11 +100,13 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
renderCell = (rowProps: CellInfo) => {
let props: FieldViewProps = {
Document: rowProps.original,
+ DataDoc: rowProps.original,
fieldKey: rowProps.column.id as string,
+ fieldExt: "",
ContainingCollectionView: this.props.CollectionView,
isSelected: returnFalse,
select: emptyFunction,
- isTopMost: false,
+ renderDepth: this.props.renderDepth + 1,
selectOnLoad: false,
ScreenToLocalTransform: Transform.Identity,
focus: emptyFunction,
@@ -114,9 +118,10 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
};
let fieldContentView = <FieldView {...props} />;
let reference = React.createRef<HTMLDivElement>();
- let onItemDown = (e: React.PointerEvent) =>
- (this.props.CollectionView.props.isSelected() ?
- SetupDrag(reference, () => props.Document, this.props.moveDocument, this.props.Document.schemaDoc ? "copy" : undefined)(e) : undefined);
+ let onItemDown = (e: React.PointerEvent) => {
+ (!this.props.CollectionView.props.isSelected() ? undefined :
+ SetupDrag(reference, () => props.Document, this.props.moveDocument, this.props.Document.schemaDoc ? "copy" : undefined)(e));
+ };
let applyToDoc = (doc: Doc, run: (args?: { [name: string]: any }) => any) => {
const res = run({ this: doc });
if (!res.success) return false;
@@ -230,7 +235,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
onPointerDown = (e: React.PointerEvent): void => {
if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) {
if (this.props.isSelected()) e.stopPropagation();
- else e.preventDefault();
}
}
@@ -259,7 +263,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;
@@ -280,9 +284,9 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
@computed
get previewDocument(): Doc | undefined {
- const children = DocListCast(this.props.Document[this.props.fieldKey]);
- const selected = children.length > this._selectedIndex ? FieldValue(children[this._selectedIndex]) : undefined;
- return selected ? (this.previewScript && this.previewScript !== "this" ? FieldValue(Cast(selected[this.previewScript], Doc)) : selected) : undefined;
+ const selected = this.childDocs.length > this._selectedIndex ? this.childDocs[this._selectedIndex] : undefined;
+ let pdc = selected ? (this.previewScript && this.previewScript !== "this" ? FieldValue(Cast(selected[this.previewScript], Doc)) : selected) : undefined;
+ return pdc;
}
getPreviewTransform = (): Transform => this.props.ScreenToLocalTransform().translate(
@@ -329,7 +333,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
@computed
get reactTable() {
- trace();
let previewWidth = this.previewWidth() + 2 * this.borderWidth + this.DIVIDER_WIDTH + 1;
return <ReactTable style={{ position: "relative", float: "left", width: `calc(100% - ${previewWidth}px` }} data={this.childDocs} page={0} pageSize={this.childDocs.length} showPagination={false}
columns={this.tableColumns}
@@ -344,24 +347,38 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
<div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} />;
}
+
@computed
get previewPanel() {
- trace();
- return <CollectionSchemaPreview
- Document={this.previewDocument}
- width={this.previewWidth}
- height={this.previewHeight}
- getTransform={this.getPreviewTransform}
- CollectionView={this.props.CollectionView}
- moveDocument={this.props.moveDocument}
- addDocument={this.props.addDocument}
- removeDocument={this.props.removeDocument}
- active={this.props.active}
- whenActiveChanged={this.props.whenActiveChanged}
- addDocTab={this.props.addDocTab}
- setPreviewScript={this.setPreviewScript}
- previewScript={this.previewScript}
- />;
+ // let layoutDoc = this.previewDocument;
+ // let resolvedDataDoc = (layoutDoc !== this.props.DataDoc) ? this.props.DataDoc : undefined;
+ // if (layoutDoc && !(Cast(layoutDoc.layout, Doc) instanceof Doc) &&
+ // resolvedDataDoc && resolvedDataDoc !== layoutDoc) {
+ // // ... so 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.
+ // layoutDoc = Doc.expandTemplateLayout(layoutDoc, resolvedDataDoc);
+ // }
+
+ let layoutDoc = this.previewDocument ? Doc.expandTemplateLayout(this.previewDocument, this.props.DataDoc) : undefined;
+ return <div ref={this.createTarget}>
+ <CollectionSchemaPreview
+ Document={layoutDoc}
+ DataDocument={this.previewDocument !== this.props.DataDoc ? this.props.DataDoc : undefined}
+ childDocs={this.childDocs}
+ renderDepth={this.props.renderDepth}
+ width={this.previewWidth}
+ height={this.previewHeight}
+ getTransform={this.getPreviewTransform}
+ CollectionView={this.props.CollectionView}
+ moveDocument={this.props.moveDocument}
+ addDocument={this.props.addDocument}
+ removeDocument={this.props.removeDocument}
+ active={this.props.active}
+ whenActiveChanged={this.props.whenActiveChanged}
+ addDocTab={this.props.addDocTab}
+ setPreviewScript={this.setPreviewScript}
+ previewScript={this.previewScript}
+ />
+ </div>;
}
@action
setPreviewScript = (script: string) => {
@@ -369,7 +386,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
}
render() {
- trace();
return (
<div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} onWheel={this.onWheel}
onDrop={(e: React.DragEvent) => this.onDrop(e, {})} onContextMenu={this.onContextMenu} ref={this.createTarget}>
@@ -383,8 +399,13 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
}
interface CollectionSchemaPreviewProps {
Document?: Doc;
+ DataDocument?: Doc;
+ childDocs?: Doc[];
+ 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;
@@ -392,13 +413,15 @@ interface CollectionSchemaPreviewProps {
removeDocument: (document: Doc) => boolean;
active: () => boolean;
whenActiveChanged: (isActive: boolean) => void;
- addDocTab: (document: Doc, where: string) => void;
+ addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
setPreviewScript: (script: string) => void;
previewScript?: string;
}
@observer
export class CollectionSchemaPreview extends React.Component<CollectionSchemaPreviewProps>{
+ private dropDisposer?: DragManager.DragDropDisposer;
+ _mainCont?: HTMLDivElement;
private get nativeWidth() { return NumCast(this.props.Document!.nativeWidth, this.props.width()); }
private get nativeHeight() { return NumCast(this.props.Document!.nativeHeight, this.props.height()); }
private contentScaling = () => {
@@ -408,32 +431,80 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre
}
return wscale;
}
- private PanelWidth = () => this.nativeWidth * this.contentScaling();
- private PanelHeight = () => this.nativeHeight * this.contentScaling();
+ protected createDropTarget = (ele: HTMLDivElement) => {
+ }
+ private createTarget = (ele: HTMLDivElement) => {
+ this._mainCont = ele;
+ this.dropDisposer && this.dropDisposer();
+ if (ele) {
+ this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
+ }
+ }
+
+ @undoBatch
+ @action
+ drop = (e: Event, de: DragManager.DropEvent) => {
+ if (de.data instanceof DragManager.DocumentDragData) {
+ let docDrag = de.data;
+ this.props.childDocs && this.props.childDocs.map(otherdoc => {
+ Doc.GetProto(otherdoc).layout = Doc.MakeDelegate(docDrag.draggedDocuments[0]);
+ });
+ e.stopPropagation();
+ }
+ return true;
+ }
+ private PanelWidth = () => this.nativeWidth ? this.nativeWidth * this.contentScaling() : this.props.width();
+ private PanelHeight = () => this.nativeHeight ? this.nativeHeight * this.contentScaling() : this.props.height();
private getTransform = () => this.props.getTransform().translate(-this.centeringOffset, 0).scale(1 / this.contentScaling());
- get centeringOffset() { return (this.props.width() - this.nativeWidth * this.contentScaling()) / 2; }
+ get centeringOffset() { return this.nativeWidth ? (this.props.width() - this.nativeWidth * this.contentScaling()) / 2 : 0; }
@action
onPreviewScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.props.setPreviewScript(e.currentTarget.value);
}
+ @computed get borderRounding() {
+ let br = StrCast(this.props.Document!.borderRounding);
+ if (br.endsWith("%")) {
+ let percent = Number(br.substr(0, br.length - 1)) / 100;
+ let nativeDim = Math.min(NumCast(this.props.Document!.nativeWidth), NumCast(this.props.Document!.nativeHeight));
+ let minDim = percent * (nativeDim ? nativeDim : Math.min(this.PanelWidth(), this.PanelHeight()));
+ return minDim;
+ }
+ return undefined;
+ }
render() {
let input = this.props.previewScript === undefined ? (null) :
- <input className="collectionSchemaView-input" value={this.props.previewScript} onChange={this.onPreviewScriptChange}
- style={{ left: `calc(50% - ${Math.min(75, (this.props.Document ? this.PanelWidth() / 2 : 75))}px)` }} />;
+ <div ref={this.createTarget}><input className="collectionSchemaView-input" value={this.props.previewScript} onChange={this.onPreviewScriptChange}
+ style={{ left: `calc(50% - ${Math.min(75, (this.props.Document ? this.PanelWidth() / 2 : 75))}px)` }} /></div>;
return (<div className="collectionSchemaView-previewRegion" style={{ width: this.props.width(), height: "100%" }}>
{!this.props.Document || !this.props.width ? (null) : (
- <div className="collectionSchemaView-previewDoc" style={{ transform: `translate(${this.centeringOffset}px, 0px)`, height: "100%" }}>
- <DocumentView Document={this.props.Document} isTopMost={false} selectOnLoad={false}
- addDocument={this.props.addDocument} removeDocument={this.props.removeDocument} moveDocument={this.props.moveDocument}
+ <div className="collectionSchemaView-previewDoc"
+ style={{
+ transform: `translate(${this.centeringOffset}px, 0px)`,
+ borderRadius: this.borderRounding,
+ height: "100%"
+ }}>
+ <DocumentView
+ DataDoc={this.props.Document.layout instanceof Doc ? this.props.Document : this.props.DataDocument}
+ Document={this.props.Document}
+ 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}
ScreenToLocalTransform={this.getTransform}
ContentScaling={this.contentScaling}
- PanelWidth={this.PanelWidth} PanelHeight={this.PanelHeight}
+ PanelWidth={this.PanelWidth}
+ PanelHeight={this.PanelHeight}
ContainingCollectionView={this.props.CollectionView}
focus={emptyFunction}
parentActive={this.props.active}
whenActiveChanged={this.props.whenActiveChanged}
bringToFront={emptyFunction}
addDocTab={this.props.addDocTab}
+ zoomToScale={emptyFunction}
+ getScale={returnOne}
/>
</div>)}
{input}
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index af194aec9..7e886304d 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -16,7 +16,7 @@
align-items: center;
}
- .collectionStackingView-masonrySingle, .collectionStackingView-masonryGrid{
+ .collectionStackingView-masonrySingle, .collectionStackingView-masonryGrid {
width:100%;
height:100%;
position: absolute;
@@ -25,7 +25,17 @@
left: 0;
width: 100%;
position: absolute;
-
+ }
+ .collectionStackingView-masonrySingle {
+ width:100%;
+ height:100%;
+ position: absolute;
+ display:flex;
+ flex-direction: column;
+ top: 0;
+ left: 0;
+ width: 100%;
+ position: absolute;
}
.collectionStackingView-description {
@@ -38,4 +48,27 @@
background: $dark-color;
color: $light-color;
}
+
+ .collectionStackingView-columnDragger {
+ width: 15;
+ height: 15;
+ position: absolute;
+ margin-left: -5;
+ }
+
+ .collectionStackingView-columnDoc{
+ display: inline-block;
+ }
+
+ .collectionStackingView-columnDoc,
+ .collectionStackingView-masonryDoc {
+ margin-left: auto;
+ margin-right: auto;
+ }
+
+ .collectionStackingView-masonryDoc {
+ transform-origin: top left;
+ grid-column-end: span 1;
+ height: 100%;
+ }
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 368e94a8c..6d9e942c9 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -1,45 +1,42 @@
import React = require("react");
-import { action, computed, IReactionDisposer, reaction } from "mobx";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+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 } from "../../../new_fields/Types";
-import { emptyFunction, returnOne, Utils } from "../../../Utils";
+import { BoolCast, NumCast, Cast } from "../../../new_fields/Types";
+import { emptyFunction, Utils } from "../../../Utils";
import { ContextMenu } from "../ContextMenu";
-import { DocumentView } from "../nodes/DocumentView";
import { CollectionSchemaPreview } from "./CollectionSchemaView";
import "./CollectionStackingView.scss";
import { CollectionSubView } from "./CollectionSubView";
-import { auto } from "async";
+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[] = [];
+ @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); }
@computed get singleColumn() { return BoolCast(this.props.Document.singleColumn, true); }
@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]();
- }
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])],
- () => {
- if (this.singleColumn) {
- let children = this.childDocs.filter(d => !d.isMinimized);
- this.props.Document.height = children.reduce((height, d, i) =>
- height + this.singleColDocHeight(d) + (i === children.length - 1 ? this.yMargin : this.gridGap)
- , this.yMargin);
- }
- }, { fireImmediately: true });
+ () => this.singleColumn &&
+ (this.props.Document.height = 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();
@@ -47,98 +44,122 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
@action
moveDocument = (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean): boolean => {
- this.props.removeDocument(doc);
- addDocument(doc);
- return true;
- }
- getDocTransform(doc: Doc, dref: HTMLDivElement) {
- let { scale, translateX, translateY } = Utils.GetScreenTransform(dref);
- 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);
+ return this.props.removeDocument(doc) && addDocument(doc);
}
createRef = (ele: HTMLDivElement | null) => {
this._masonryGridRef = ele;
this.createDropTarget(ele!);
}
- @computed
- get singleColumnChildren() {
- let children = this.childDocs.filter(d => !d.isMinimized);
- return children.map((d, i) => {
- let dref = React.createRef<HTMLDivElement>();
- let script = undefined;
- let colWidth = () => d.nativeWidth ? Math.min(d[WidthSym](), this.columnWidth) : this.columnWidth;
- let rowHeight = () => this.singleColDocHeight(d);
- let dxf = () => this.getDocTransform(d, dref.current!).scale(this.columnWidth / d[WidthSym]());
- return <div className="collectionStackingView-masonryDoc"
- key={d[Id]}
- ref={dref}
- style={{ width: colWidth(), height: rowHeight(), marginLeft: "auto", marginRight: "auto" }} >
- <CollectionSchemaPreview
- Document={d}
- width={colWidth}
- height={rowHeight}
- 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={script}>
- </CollectionSchemaPreview>
- </div>;
- });
+ overlays = (doc: Doc) => {
+ return doc.type === DocumentType.IMG || doc.type === DocumentType.VID ? { title: "title", caption: "caption" } : {};
+ }
+
+ 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);
}
+ 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]);
+ }
+
@computed
get children() {
- return this.childDocs.filter(d => !d.isMinimized).map((d, i) => {
- let dref = React.createRef<HTMLDivElement>();
- let dxf = () => this.getDocTransform(d, dref.current!).scale(this.columnWidth / d[WidthSym]());
- let renderScale = this.columnWidth / NumCast(d.nativeWidth, this.columnWidth);
- let aspect = NumCast(d.nativeWidth) / NumCast(d.nativeHeight);
- let width = () => this.columnWidth;
- let height = () => aspect ? width() / aspect : d[HeightSym]()
- let rowSpan = Math.ceil((height() + this.gridGap) / (this._gridSize + this.gridGap));
- let childFocus = (doc: Doc) => {
- doc.libraryBrush = true;
- this.props.focus(this.props.Document); // just focus on this collection, not the underlying document because the API doesn't support adding an offset to focus on and we can't pan zoom our contents to be centered.
- };
- return (<div className="collectionStackingView-masonryDoc"
- key={d[Id]}
- ref={dref}
- style={{
- width: width(),
- height: height(),
- overflow: "auto",
- transformOrigin: "top left",
- gridRowEnd: `span ${rowSpan}`,
- gridColumnEnd: `span 1`,
- transform: `scale(${renderScale})`
- }} >
- <DocumentView key={d[Id]} Document={d}
- addDocument={this.props.addDocument}
- removeDocument={this.props.removeDocument}
- moveDocument={this.moveDocument}
- ContainingCollectionView={this.props.CollectionView}
- isTopMost={false}
- ScreenToLocalTransform={dxf}
- focus={childFocus}
- ContentScaling={returnOne}
- PanelWidth={width}
- PanelHeight={height}
- selectOnLoad={false}
- parentActive={this.props.active}
- addDocTab={this.props.addDocTab}
- bringToFront={emptyFunction}
- whenActiveChanged={this.props.whenActiveChanged}
- />
- </div>);
+ this._docXfs.length = 0;
+ return this.filteredChildren.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) {
+ let dxf = () => this.getSingleDocTransform(layoutDoc, i, width());
+ let rowHgtPcnt = height() / (this.props.Document[HeightSym]() - 2 * this.yMargin) * 100;
+ 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._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}` }} >
+ {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];
+ }
+ @action
+ onDividerMove = (e: PointerEvent): void => {
+ 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={{ 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({
@@ -147,15 +168,65 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
});
}
}
+
+ @undoBatch
+ @action
+ drop = (e: Event, de: DragManager.DropEvent) => {
+ let targInd = -1;
+ let where = [de.x, de.y];
+ if (de.data instanceof DragManager.DocumentDragData) {
+ 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];
+ let docs = this.childDocList;
+ if (docs) {
+ if (targInd === -1) targInd = docs.length;
+ else targInd = docs.indexOf(this.filteredChildren[targInd]);
+ let srcInd = docs.indexOf(newDoc);
+ docs.splice(srcInd, 1);
+ docs.splice(targInd > srcInd ? targInd - 1 : targInd, 0, newDoc);
+ }
+ }
+ return false;
+ }
+ @undoBatch
+ @action
+ onDrop = (e: React.DragEvent): void => {
+ let where = [e.clientX, e.clientY];
+ let targInd = -1;
+ 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];
+ let docs = this.childDocList;
+ if (docs) {
+ docs.splice(docs.length - 1, 1);
+ docs.splice(targInd, 0, newDoc);
+ }
+ }
+ });
+ }
render() {
- let cols = this.singleColumn ? 1 : Math.max(1, Math.min(this.childDocs.filter(d => !d.isMinimized).length,
+ 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 className="collectionStackingView" ref={this.createRef} onContextMenu={this.onContextMenu} onWheel={(e: React.WheelEvent) => e.stopPropagation()} >
+ <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",
@@ -167,7 +238,8 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
gridAutoRows: this.singleColumn ? undefined : `${this._gridSize}px`
}}
>
- {this.singleColumn ? this.singleColumnChildren : this.children}
+ {this.children}
+ {this.singleColumn ? (null) : this.columnDragger}
</div>
</div>
);
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 070abb51b..55ba71722 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -1,22 +1,26 @@
-import { action, runInAction } from "mobx";
-import React = require("react");
-import { undoBatch, UndoManager } from "../../util/UndoManager";
-import { DragManager } from "../../util/DragManager";
-import { Docs, DocumentOptions } from "../../documents/Documents";
-import { RouteStore } from "../../../server/RouteStore";
+import { action, computed } from "mobx";
+import * as rp from 'request-promise';
+import CursorField from "../../../new_fields/CursorField";
+import { Doc, DocListCast, Opt } 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, PromiseValue } 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, DocumentType } from "../../documents/Documents";
+import { DragManager } from "../../util/DragManager";
+import { undoBatch, UndoManager } from "../../util/UndoManager";
import { FieldViewProps } from "../nodes/FieldView";
-import * as rp from 'request-promise';
-import { CollectionView } from "./CollectionView";
+import { FormattedTextBox } from "../nodes/FormattedTextBox";
import { CollectionPDFView } from "./CollectionPDFView";
import { CollectionVideoView } from "./CollectionVideoView";
-import { Doc, Opt, FieldResult, DocListCast } from "../../../new_fields/Doc";
+import { CollectionView } from "./CollectionView";
+import React = require("react");
+import { MainView } from "../MainView";
+import { Utils } from "../../../Utils";
import { DocComponent } from "../DocComponent";
-import { listSpec } from "../../../new_fields/Schema";
-import { Cast, PromiseValue, FieldValue, ListSpec } from "../../../new_fields/Types";
-import { List } from "../../../new_fields/List";
-import { DocServer } from "../../DocServer";
-import CursorField from "../../../new_fields/CursorField";
export interface CollectionViewProps extends FieldViewProps {
addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
@@ -43,10 +47,19 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
this.createDropTarget(ele);
}
+ @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(this.props.Document[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(this.extensionDoc[this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey], listSpec(Doc));
}
@action
@@ -57,10 +70,17 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
let email = CurrentUserUtils.email;
let pos = { x: position[0], y: position[1] };
if (id && email) {
- const proto = await doc.proto;
+ const proto = Doc.GetProto(doc);
if (!proto) {
return;
}
+ // The following conditional detects a recurring bug we've seen on the server
+ 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);
+ throw new Error(`AHA! You were trying to set a cursor on a collection's proto, which is the original collection proto! Look at the two previously printed lines for document values!`);
+ }
let cursors = Cast(proto.cursors, listSpec(CursorField));
if (!cursors) {
proto.cursors = cursors = new List<CursorField>();
@@ -90,11 +110,9 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
return moved || added;
}, false);
} else if (de.data.moveDocument) {
- const move = de.data.moveDocument;
- added = de.data.droppedDocuments.reduce((added: boolean, d) => {
- let moved = move(d, this.props.Document, this.props.addDocument);
- return moved || added;
- }, false);
+ 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.DataDoc ? this.props.DataDoc :*/ this.props.Document, this.props.addDocument) || added, false);
} else {
added = de.data.droppedDocuments.reduce((added: boolean, d) => {
let moved = this.props.addDocument(d);
@@ -105,55 +123,15 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
return added;
}
else if (de.data instanceof DragManager.AnnotationDragData) {
+ e.stopPropagation();
return this.props.addDocument(de.data.dropDocument);
}
return false;
}
- protected async 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;
- }
- if (type.indexOf("video") !== -1) {
- ctor = Docs.VideoDocument;
- }
- if (type.indexOf("audio") !== -1) {
- ctor = Docs.AudioDocument;
- }
- if (type.indexOf("pdf") !== -1) {
- ctor = Docs.PdfDocument;
- options.nativeWidth = 1200;
- }
- if (type.indexOf("excel") !== -1) {
- ctor = Docs.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];
- 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;
- this.props.addDocument(alias, false);
- }
- });
- return undefined;
- }
- ctor = Docs.WebDocument;
- options = { height: options.width, ...options, title: path, nativeWidth: undefined };
- }
- return ctor ? ctor(path, options) : undefined;
- }
-
@undoBatch
@action
- protected onDrop(e: React.DragEvent, options: DocumentOptions): void {
+ protected onDrop(e: React.DragEvent, options: DocumentOptions, completed?: () => void) {
if (e.ctrlKey) {
e.stopPropagation(); // bcz: this is a hack to stop propagation when dropping an image on a text document with shift+ctrl
return;
@@ -179,23 +157,26 @@ 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;
}
if (html && !html.startsWith("<a")) {
- if (html.indexOf("<img") === 0) {
- let split = html.split("\"")[1];
- let doc = Docs.ImageDocument(split, { ...options, width: 300 });
+ let tags = html.split("<");
+ if (tags[0] === "") tags.splice(0, 1);
+ 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.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
@@ -203,7 +184,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;
@@ -211,7 +192,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;
}
@@ -224,11 +205,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) {
- this.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));
}
});
@@ -249,10 +230,9 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
body: formData
}).then(async (res: Response) => {
(await res.json()).map(action((file: any) => {
- let path = window.location.origin + file;
- let docPromise = this.getDocumentFromType(type, path, { ...options, nativeWidth: 300, width: 300, title: dropFileName });
-
- docPromise.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);
@@ -260,7 +240,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
}
if (promises.length) {
- Promise.all(promises).finally(() => batch.end());
+ Promise.all(promises).finally(() => { completed && completed(); batch.end(); });
} else {
batch.end();
}
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index a85604e58..5205f4313 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -43,13 +43,12 @@
display: inline;
}
-
- .coll-title {
- width: max-content;
+ .editableView-input, .editableView-container-editing {
display: block;
+ text-overflow: ellipsis;
font-size: 24px;
+ white-space: nowrap;
}
-
}
.collectionTreeView-keyHeader {
font-style: italic;
@@ -59,6 +58,12 @@
background: lightgray;
}
+.collectionTreeView-subtitle {
+ font-style: italic;
+ font-size: 8pt;
+ color: $intermediate-color;
+}
+
.docContainer {
position: relative;
text-overflow: ellipsis;
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index c51c16883..d05cc375e 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -1,15 +1,15 @@
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faAngleRight, faCaretDown, faCaretRight, faTrashAlt, faCaretSquareRight, faCaretSquareDown } 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, observable, computed } from "mobx";
+import { action, computed, observable, trace, untracked } from "mobx";
import { observer } from "mobx-react";
-import { Doc, DocListCast, HeightSym, WidthSym } from '../../../new_fields/Doc';
+import { Doc, DocListCast, HeightSym, WidthSym, Opt } from '../../../new_fields/Doc';
import { Id } from '../../../new_fields/FieldSymbols';
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 } 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';
@@ -25,14 +25,18 @@ import { CollectionSchemaPreview } from './CollectionSchemaView';
import { CollectionSubView } from "./CollectionSubView";
import "./CollectionTreeView.scss";
import React = require("react");
+import { LinkManager } from '../../util/LinkManager';
export interface TreeViewProps {
document: Doc;
+ dataDoc?: Doc;
+ containingCollection: Doc;
+ renderDepth: number;
deleteDoc: (doc: Doc) => boolean;
moveDocument: DragManager.MoveFunction;
dropAction: "alias" | "copy" | undefined;
- addDocTab: (doc: Doc, where: string) => void;
+ addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void;
panelWidth: () => number;
panelHeight: () => number;
addDocument: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean;
@@ -46,10 +50,15 @@ export interface TreeViewProps {
library.add(faTrashAlt);
library.add(faAngleRight);
+library.add(faBell);
+library.add(faTrash);
+library.add(faCamera);
+library.add(faExpand);
library.add(faCaretDown);
library.add(faCaretRight);
library.add(faCaretSquareDown);
library.add(faCaretSquareRight);
+library.add(faArrowsAltH);
@observer
/**
@@ -59,9 +68,34 @@ class TreeView extends React.Component<TreeViewProps> {
private _header?: React.RefObject<HTMLDivElement> = React.createRef();
private _treedropDisposer?: DragManager.DragDropDisposer;
private _dref = React.createRef<HTMLDivElement>();
- @observable _chosenKey: string = "data";
+ @observable __chosenKey: string = "";
+ @computed get _chosenKey() { return this.__chosenKey ? this.__chosenKey : this.fieldKey; }
+ @computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.document.maxEmbedHeight, 300); }
@observable _collapsed: boolean = true;
+ @computed get fieldKey() {
+ let keys = Array.from(Object.keys(this.resolvedDataDoc)); // bcz: Argh -- make untracked to avoid this rerunning whenever 'libraryBrush' is set
+ if (this.resolvedDataDoc.proto instanceof Doc) {
+ let arr = Array.from(Object.keys(this.resolvedDataDoc.proto));// bcz: Argh -- make untracked to avoid this rerunning whenever 'libraryBrush' is set
+ keys.push(...arr);
+ while (keys.indexOf("proto") !== -1) keys.splice(keys.indexOf("proto"), 1);
+ }
+ let keyList: string[] = [];
+ keys.map(key => {
+ let docList = Cast(this.resolvedDataDoc[key], listSpec(Doc));
+ if (docList && docList.length > 0) {
+ keyList.push(key);
+ }
+ });
+ let layout = StrCast(this.props.document.layout);
+ if (layout.indexOf("fieldKey={\"") !== -1) {
+ return layout.split("fieldKey={\"")[1].split("\"")[0];
+ }
+ return keyList.length ? keyList[0] : "data";
+ }
+
+ @computed get resolvedDataDoc() { return BoolCast(this.props.document.isTemplate) && this.props.dataDoc ? this.props.dataDoc : this.props.document; }
+
protected createTreeDropTarget = (ele: HTMLDivElement) => {
this._treedropDisposer && this._treedropDisposer();
if (ele) {
@@ -69,10 +103,10 @@ class TreeView extends React.Component<TreeViewProps> {
}
}
- @undoBatch delete = () => this.props.deleteDoc(this.props.document);
- @undoBatch openRight = async () => this.props.addDocTab(this.props.document, "openRight");
+ @undoBatch delete = () => this.props.deleteDoc(this.resolvedDataDoc);
+ @undoBatch openRight = async () => this.props.addDocTab(this.props.document, undefined, "onRight");
- onPointerDown = (e: React.PointerEvent) => e.stopPropagation()
+ onPointerDown = (e: React.PointerEvent) => e.stopPropagation();
onPointerEnter = (e: React.PointerEvent): void => {
this.props.active() && (this.props.document.libraryBrush = true);
if (e.buttons === 1 && SelectionManager.GetIsDragging()) {
@@ -81,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);
@@ -101,7 +135,7 @@ class TreeView extends React.Component<TreeViewProps> {
@action
remove = (document: Document, key: string): boolean => {
- let children = Cast(this.props.document[key], listSpec(Doc), []);
+ let children = Cast(this.resolvedDataDoc[key], listSpec(Doc), []);
if (children.indexOf(document) !== -1) {
children.splice(children.indexOf(document), 1);
return true;
@@ -114,38 +148,32 @@ class TreeView extends React.Component<TreeViewProps> {
return this.props.document !== target && this.props.deleteDoc(doc) && addDoc(doc);
}
@action
- indent = () => this.props.addDocument(this.props.document) && this.delete();
+ indent = () => this.props.addDocument(this.props.document) && this.delete()
renderBullet() {
- let docList = Cast(this.props.document["data"], listSpec(Doc));
- let doc = Cast(this.props.document["data"], Doc);
+ 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>;
}
- titleClicked = (e: React.MouseEvent) => {
- if (this._collapsed) return false;
- else {
- this.props.document.embed = !BoolCast(this.props.document.embed);
- return true;
- }
- }
static loadId = "";
editableView = (key: string, style?: string) => (<EditableView
oneLine={true}
display={"inline"}
- editing={this.props.document[Id] === TreeView.loadId}
+ editing={this.resolvedDataDoc[Id] === TreeView.loadId}
contents={StrCast(this.props.document[key])}
- onClick={this.titleClicked}
height={36}
fontStyle={style}
+ fontSize={12}
GetValue={() => StrCast(this.props.document[key])}
- SetValue={(value: string) => (Doc.GetProto(this.props.document)[key] = value) ? true : true}
+ SetValue={(value: string) => (Doc.GetProto(this.resolvedDataDoc)[key] = value) ? true : true}
OnFillDown={(value: string) => {
- Doc.GetProto(this.props.document)[key] = value;
- let doc = Docs.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
+ Doc.GetProto(this.resolvedDataDoc)[key] = value;
+ let doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
TreeView.loadId = doc[Id];
return this.props.addDocument(doc);
}}
@@ -153,50 +181,49 @@ class TreeView extends React.Component<TreeViewProps> {
/>)
@computed get keyList() {
- let keys = Array.from(Object.keys(this.props.document));
- if (this.props.document.proto instanceof Doc) {
- keys.push(...Array.from(Object.keys(this.props.document.proto)));
- while (keys.indexOf("proto") !== -1) keys.splice(keys.indexOf("proto"), 1);
+ let keys = Array.from(Object.keys(this.resolvedDataDoc));
+ if (this.resolvedDataDoc.proto instanceof Doc) {
+ keys.push(...Array.from(Object.keys(this.resolvedDataDoc.proto)));
}
- let keyList: string[] = [];
- keys.map(key => {
- let docList = Cast(this.props.document[key], listSpec(Doc));
- let doc = Cast(this.props.document[key], Doc);
- if (doc instanceof Doc || docList) {
- keyList.push(key);
- }
- });
- if (keyList.indexOf("data") !== -1) {
- keyList.splice(keyList.indexOf("data"), 1);
+ let keyList: string[] = keys.reduce((l, key) => {
+ let listspec = DocListCast(this.resolvedDataDoc[key]);
+ if (listspec && listspec.length) return [...l, key];
+ return l;
+ }, [] as string[]);
+ keys.map(key => Cast(this.resolvedDataDoc[key], Doc) instanceof Doc && keyList.push(key));
+ if (LinkManager.Instance.getAllRelatedLinks(this.props.document).length > 0) keyList.push("links");
+ if (keyList.indexOf(this.fieldKey) !== -1) {
+ keyList.splice(keyList.indexOf(this.fieldKey), 1);
}
- keyList.splice(0, 0, "data");
- return keyList;
+ keyList.splice(0, 0, this.fieldKey);
+ return keyList.filter((item, index) => keyList.indexOf(item) >= index);
}
/**
* Renders the EditableView title element for placement into the tree.
*/
renderTitle() {
let reference = React.createRef<HTMLDivElement>();
- let onItemDown = SetupDrag(reference, () => this.props.document, this.move, this.props.dropAction, this.props.treeViewId, true);
+ 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;
- this._chosenKey = this.keyList[ind];
+ this.__chosenKey = this.keyList[ind];
})} >
{this._chosenKey}
</span>);
- let dataDocs = CollectionDockingView.Instance ? Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc), []) : [];
- let openRight = dataDocs && dataDocs.indexOf(this.props.document) !== -1 ? (null) : (
+ let dataDocs = CollectionDockingView.Instance ? Cast(CollectionDockingView.Instance.props.Document[this.fieldKey], listSpec(Doc), []) : [];
+ let openRight = dataDocs && dataDocs.indexOf(this.resolvedDataDoc) !== -1 ? (null) : (
<div className="treeViewItem-openRight" onPointerDown={this.onPointerDown} onClick={this.openRight}>
<FontAwesomeIcon icon="angle-right" size="lg" />
</div>);
return <>
<div className="docContainer" id={`docContainer-${this.props.parentKey}`} ref={reference} onPointerDown={onItemDown}
style={{
- background: BoolCast(this.props.document.protoBrush, false) ? "#06123232" : BoolCast(this.props.document.libraryBrush, false) ? "#06121212" : "0",
+ 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"
}}
>
@@ -210,57 +237,58 @@ class TreeView extends React.Component<TreeViewProps> {
onWorkspaceContextMenu = (e: React.MouseEvent): void => {
if (!e.isPropagationStopped()) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
- ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => MainView.Instance.openWorkspace(this.props.document)) });
- ContextMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.KVPDocument(this.props.document, { width: 300, height: 300 }), "onRight"), icon: "layer-group" });
+ ContextMenu.Instance.addItem({ description: (BoolCast(this.props.document.embed) ? "Collapse" : "Expand") + " inline", event: () => this.props.document.embed = !BoolCast(this.props.document.embed), icon: "expand" });
if (NumCast(this.props.document.viewType) !== CollectionViewType.Docking) {
- ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, "inTab"), icon: "folder" });
- ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, "onRight"), icon: "caret-square-right" });
- if (DocumentManager.Instance.getDocumentViews(this.props.document).length) {
- ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.props.document).map(view => view.props.focus(this.props.document)) });
+ ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, this.resolvedDataDoc, "inTab"), icon: "folder" });
+ ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, this.resolvedDataDoc, "onRight"), icon: "caret-square-right" });
+ if (DocumentManager.Instance.getDocumentViews(this.resolvedDataDoc).length) {
+ ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.resolvedDataDoc).map(view => view.props.focus(this.props.document, true)), icon: "camera" });
}
- ContextMenu.Instance.addItem({ description: "Delete Item", event: undoBatch(() => this.props.deleteDoc(this.props.document)) });
+ ContextMenu.Instance.addItem({ description: "Delete Item", event: undoBatch(() => this.props.deleteDoc(this.props.document)), icon: "trash-alt" });
} else {
- ContextMenu.Instance.addItem({ description: "Delete Workspace", event: undoBatch(() => this.props.deleteDoc(this.props.document)) });
+ 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.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();
}
}
+
+ @undoBatch
treeDrop = (e: Event, de: DragManager.DropEvent) => {
let x = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y);
let rect = this._header!.current!.getBoundingClientRect();
let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2);
let before = x[1] < bounds[1];
let inside = x[0] > bounds[0] + 75 || (!before && !this._collapsed);
+ if (de.data instanceof DragManager.LinkDragData) {
+ let sourceDoc = de.data.linkSourceDocument;
+ let destDoc = this.props.document;
+ DocUtils.MakeLink(sourceDoc, destDoc);
+ e.stopPropagation();
+ }
if (de.data instanceof DragManager.DocumentDragData) {
- let addDoc = (doc: Doc) => this.props.addDocument(doc, this.props.document, before);
+ e.stopPropagation();
+ if (de.data.draggedDocuments[0] === this.props.document) return true;
+ let addDoc = (doc: Doc) => this.props.addDocument(doc, this.resolvedDataDoc, before);
if (inside) {
- let docList = Cast(this.props.document.data, listSpec(Doc));
+ let docList = Cast(this.resolvedDataDoc.data, listSpec(Doc));
if (docList !== undefined) {
addDoc = (doc: Doc) => { docList && docList.push(doc); return true; };
}
}
- e.stopPropagation();
let movedDocs = (de.data.options === this.props.treeViewId ? de.data.draggedDocuments : de.data.droppedDocuments);
return (de.data.dropAction || de.data.userDropAction) ?
- de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.props.document, before) || added, false)
+ de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.resolvedDataDoc, before) || added, false)
: (de.data.moveDocument) ?
- movedDocs.reduce((added: boolean, d) => de.data.moveDocument(d, this.props.document, addDoc) || added, false)
- : de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.props.document, before), false);
+ movedDocs.reduce((added: boolean, d) => de.data.moveDocument(d, this.resolvedDataDoc, addDoc) || added, false)
+ : de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.resolvedDataDoc, before), false);
}
return false;
}
- public static AddDocToList(target: Doc, key: string, doc: Doc, relativeTo?: Doc, before?: boolean) {
- let list = Cast(target[key], listSpec(Doc));
- if (list) {
- let ind = relativeTo ? list.indexOf(relativeTo) : -1;
- if (ind === -1) list.push(doc);
- else list.splice(before ? ind : ind + 1, 0, doc);
- }
- return true;
- }
-
docTransform = () => {
let { scale, translateX, translateY } = Utils.GetScreenTransform(this._dref.current!);
let outerXf = this.props.outerXf();
@@ -269,25 +297,77 @@ class TreeView extends React.Component<TreeViewProps> {
return finalXf;
}
+ renderLinks = () => {
+ let ele: JSX.Element[] = [];
+ let remDoc = (doc: Doc) => this.remove(doc, this._chosenKey);
+ 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: 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>
+ {
+ TreeView.GetChildElements(destLinks, this.props.treeViewId, this.props.document, this.props.dataDoc, "treeviewlink-" + groupType, addDoc, remDoc, this.move,
+ this.props.dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth)
+ }
+ </div>
+ );
+ });
+ return ele;
+ }
+
+ @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(DocListCast(layoutDoc.data));
+ }
+ docWidth = () => {
+ let aspect = NumCast(this.props.document.nativeHeight) / NumCast(this.props.document.nativeWidth);
+ if (aspect) return Math.min(this.props.document[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.panelWidth() - 5));
+ return NumCast(this.props.document.nativeWidth) ? Math.min(this.props.document[WidthSym](), this.props.panelWidth() - 5) : this.props.panelWidth() - 5;
+ }
+ docHeight = () => {
+ 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;
+ if (bounds) return this.docWidth() * (bounds.b - bounds.y) / (bounds.r - bounds.x);
+ return NumCast(this.props.document.height) ? NumCast(this.props.document.height) : 50;
+ })());
+ }
+
render() {
let contentElement: (JSX.Element | null) = null;
- let docList = Cast(this.props.document[this._chosenKey], listSpec(Doc));
+ let docList = Cast(this.resolvedDataDoc[this._chosenKey], listSpec(Doc));
let remDoc = (doc: Doc) => this.remove(doc, this._chosenKey);
- let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => TreeView.AddDocToList(this.props.document, this._chosenKey, doc, addBefore, before);
- let doc = Cast(this.props.document[this._chosenKey], Doc);
- let docWidth = () => NumCast(this.props.document.nativeWidth) ? Math.min(this.props.document[WidthSym](), this.props.panelWidth() - 5) : this.props.panelWidth() - 5;
+ let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.resolvedDataDoc, this._chosenKey, doc, addBefore, before);
+ let doc = Cast(this.resolvedDataDoc[this._chosenKey], Doc);
+
if (!this._collapsed) {
if (!this.props.document.embed) {
contentElement = <ul key={this._chosenKey + "more"}>
- {TreeView.GetChildElements(doc instanceof Doc ? [doc] : DocListCast(docList), this.props.treeViewId, this._chosenKey, addDoc, remDoc, this.move,
- this.props.dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth)}
+ {this._chosenKey === "links" ? this.renderLinks() :
+ TreeView.GetChildElements(doc instanceof Doc ? [doc] : DocListCast(docList), this.props.treeViewId, this.props.document, this.props.dataDoc, this._chosenKey, addDoc, remDoc, this.move,
+ this.props.dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth)}
</ul >;
} else {
- contentElement = <div ref={this._dref} style={{ display: "inline-block", height: this.props.panelHeight() }} key={this.props.document[Id]}>
+ 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] + this.props.document.title}>
<CollectionSchemaPreview
- Document={this.props.document}
- width={docWidth}
- height={this.props.panelHeight}
+ Document={layoutDoc}
+ DataDocument={this.resolvedDataDoc}
+ renderDepth={this.props.renderDepth}
+ fitToBox={this.boundsOfCollectionDocument !== undefined}
+ width={this.docWidth}
+ height={this.docHeight}
getTransform={this.docTransform}
CollectionView={undefined}
addDocument={emptyFunction as any}
@@ -298,7 +378,7 @@ class TreeView extends React.Component<TreeViewProps> {
addDocTab={this.props.addDocTab}
setPreviewScript={emptyFunction}>
</CollectionSchemaPreview>
- </div>
+ </div>;
}
}
return <div className="treeViewItem-container" ref={this.createTreeDropTarget} onContextMenu={this.onWorkspaceContextMenu}>
@@ -316,40 +396,46 @@ class TreeView extends React.Component<TreeViewProps> {
public static GetChildElements(
docs: Doc[],
treeViewId: string,
+ containingCollection: Doc,
+ dataDoc: Doc | undefined,
key: string,
add: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean,
remove: ((doc: Doc) => boolean),
move: DragManager.MoveFunction,
dropAction: dropActionType,
- addDocTab: (doc: Doc, where: string) => void,
+ addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void,
screenToLocalXf: () => Transform,
outerXf: () => { translateX: number, translateY: number },
active: () => boolean,
panelWidth: () => number,
+ renderDepth: number
) {
- let docList = docs.filter(child => !child.excludeFromLibrary && (key !== "data" || !child.isMinimized));
+ let docList = docs.filter(child => !child.excludeFromLibrary);
let rowWidth = () => panelWidth() - 20;
return docList.map((child, i) => {
let indent = i === 0 ? undefined : () => {
if (StrCast(docList[i - 1].layout).indexOf("CollectionView") !== -1) {
let fieldKeysub = StrCast(docList[i - 1].layout).split("fieldKey")[1];
let fieldKey = fieldKeysub.split("\"")[1];
- TreeView.AddDocToList(docList[i - 1], fieldKey, child);
+ Doc.AddDocToList(docList[i - 1], fieldKey, child);
remove(child);
}
- }
+ };
let addDocument = (doc: Doc, relativeTo?: Doc, before?: boolean) => {
return add(doc, relativeTo ? relativeTo : docList[i], before !== undefined ? before : false);
- }
+ };
let rowHeight = () => {
let aspect = NumCast(child.nativeWidth, 0) / NumCast(child.nativeHeight, 0);
return aspect ? Math.min(child[WidthSym](), rowWidth()) / aspect : child[HeightSym]();
- }
+ };
return <TreeView
document={child}
+ dataDoc={dataDoc}
+ containingCollection={containingCollection}
treeViewId={treeViewId}
- key={child[Id]}
+ key={child[Id] + "child " + i}
indentDocument={indent}
+ renderDepth={renderDepth}
deleteDoc={remove}
addDocument={addDocument}
panelWidth={rowWidth}
@@ -392,44 +478,85 @@ export class CollectionTreeView extends CollectionSubView(Document) {
}
onContextMenu = (e: React.MouseEvent): void => {
// need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout
- if (!e.isPropagationStopped() && this.props.Document.excludeFromLibrary) { // excludeFromLibrary means this is the user document
+ if (!e.isPropagationStopped() && this.props.Document.workspaceLibrary) { // excludeFromLibrary means this is the user document
ContextMenu.Instance.addItem({ description: "Create Workspace", event: undoBatch(() => MainView.Instance.createNewWorkspace()) });
ContextMenu.Instance.addItem({ description: "Delete Workspace", event: undoBatch(() => this.remove(this.props.Document)) });
+ e.stopPropagation();
+ e.preventDefault();
+ ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
}
}
+ @computed get resolvedDataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; }
+
outerXf = () => Utils.GetScreenTransform(this._mainEle!);
onTreeDrop = (e: React.DragEvent) => this.onDrop(e, {});
+
+ @observable static NotifsCol: Opt<Doc>;
+
+ openNotifsCol = () => {
+ if (CollectionTreeView.NotifsCol && CollectionDockingView.Instance) {
+ CollectionDockingView.Instance.AddRightSplit(CollectionTreeView.NotifsCol, undefined);
+ }
+ }
+ @computed get notifsButton() {
+ const length = CollectionTreeView.NotifsCol ? DocListCast(CollectionTreeView.NotifsCol.data).length : 0;
+ const notifsRef = React.createRef<HTMLDivElement>();
+ const dragNotifs = action(() => CollectionTreeView.NotifsCol!);
+ return <div id="toolbar" key="toolbar">
+ <div ref={notifsRef}>
+ <button className="toolbar-button round-button" title="Notifs"
+ onClick={this.openNotifsCol} onPointerDown={CollectionTreeView.NotifsCol ? SetupDrag(notifsRef, dragNotifs) : emptyFunction}>
+ <FontAwesomeIcon icon={faBell} size="sm" />
+ </button>
+ <div className="main-notifs-badge" style={length > 0 ? { "display": "initial" } : { "display": "none" }}>
+ {length}
+ </div>
+ </div>
+ </div >;
+ }
+ @computed get clearButton() {
+ return <div id="toolbar" key="toolbar">
+ <div >
+ <button className="toolbar-button round-button" title="Notifs"
+ onClick={undoBatch(action(() => Doc.GetProto(this.props.Document)[this.props.fieldKey] = undefined))}>
+ <FontAwesomeIcon icon={faTrash} size="sm" />
+ </button>
+ </div>
+ </div >;
+ }
+
+
render() {
let dropAction = StrCast(this.props.Document.dropAction) as dropActionType;
- let addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => TreeView.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before);
+ 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", background: StrCast(this.props.Document.backgroundColor, "lightgray") }}
onContextMenu={this.onContextMenu}
- onWheel={(e: React.WheelEvent) => this.props.isSelected() && e.stopPropagation()}
+ onWheel={(e: React.WheelEvent) => (e.target as any).scrollHeight > (e.target as any).clientHeight && e.stopPropagation()}
onDrop={this.onTreeDrop}
ref={this.createTreeDropTarget}>
- <div className="coll-title">
- <EditableView
- contents={this.props.Document.title}
- display={"inline"}
- height={72}
- GetValue={() => StrCast(this.props.Document.title)}
- SetValue={(value: string) => (Doc.GetProto(this.props.Document).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]) });
- TreeView.loadId = doc[Id];
- TreeView.AddDocToList(this.props.Document, this.props.fieldKey, doc, this.childDocs.length ? this.childDocs[0] : undefined, true);
- }} />
- </div>
- <ul className="no-indent">
+ <EditableView
+ contents={this.resolvedDataDoc.title}
+ display={"block"}
+ height={72}
+ GetValue={() => StrCast(this.resolvedDataDoc.title)}
+ SetValue={(value: string) => (Doc.GetProto(this.resolvedDataDoc).title = value) ? true : true}
+ OnFillDown={(value: string) => {
+ Doc.GetProto(this.props.Document).title = value;
+ let doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
+ TreeView.loadId = doc[Id];
+ Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, this.childDocs.length ? this.childDocs[0] : undefined, true);
+ }} />
+ {this.props.Document.workspaceLibrary ? this.notifsButton : (null)}
+ {this.props.Document.allowClear ? this.clearButton : (null)}
+ <ul className="no-indent" style={{ width: "max-content" }} >
{
- TreeView.GetChildElements(this.childDocs, this.props.Document[Id], this.props.fieldKey, addDoc, this.remove,
- moveDoc, dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.outerXf, this.props.active, this.props.PanelWidth)
+ TreeView.GetChildElements(this.childDocs, this.props.Document[Id], this.props.Document, this.props.DataDoc, this.props.fieldKey, addDoc, this.remove,
+ moveDoc, dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.outerXf, this.props.active, this.props.PanelWidth, this.props.renderDepth)
}
</ul>
</div >
diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx
index 7853544d5..d7d5773ba 100644
--- a/src/client/views/collections/CollectionVideoView.tsx
+++ b/src/client/views/collections/CollectionVideoView.tsx
@@ -1,33 +1,25 @@
-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 "../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");
@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 ([
+ return (VideoBox._showControls ? [] : [
<div className="collectionVideoView-time" key="time" onPointerDown={this.onResetDown} style={{ transform: `scale(${scaling}, ${scaling})` }}>
<span>{"" + Math.round(curTime)}</span>
<span style={{ fontSize: 8 }}>{" " + Math.round((curTime - Math.trunc(curTime)) * 100)}</span>
@@ -43,7 +35,7 @@ export class CollectionVideoView extends React.Component<FieldViewProps> {
@action
onPlayDown = () => {
- if (this._videoBox && this._videoBox.player) {
+ if (this._videoBox) {
if (this._videoBox.Playing) {
this._videoBox.Pause();
} else {
@@ -68,49 +60,6 @@ export class CollectionVideoView extends React.Component<FieldViewProps> {
this.props.Document.curPage = 0;
}
}
-
- 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"
- });
- }
- 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"
- });
- }
-
setVideoBox = (videoBox: VideoBox) => { this._videoBox = videoBox; };
private subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => {
@@ -122,9 +71,8 @@ export class CollectionVideoView extends React.Component<FieldViewProps> {
}
render() {
- trace();
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 68eefab4c..56750668d 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -2,8 +2,10 @@ import { library } from '@fortawesome/fontawesome-svg-core';
import { faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree } from '@fortawesome/free-solid-svg-icons';
import { observer } from "mobx-react";
import * as React from 'react';
+import { Doc } 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';
@@ -25,11 +27,11 @@ library.add(faThList);
@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 };
- switch (type) {
+ switch (this.isAnnotationOverlay ? CollectionViewType.Freeform : type) {
case CollectionViewType.Schema: return (<CollectionSchemaView {...props} CollectionView={this} />);
case CollectionViewType.Docking: return (<CollectionDockingView {...props} CollectionView={this} />);
case CollectionViewType.Tree: return (<CollectionTreeView {...props} CollectionView={this} />);
@@ -41,7 +43,7 @@ export class CollectionView extends React.Component<FieldViewProps> {
return (null);
}
- get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; } // bcz: ? Why do we need to compare Id's?
+ get isAnnotationOverlay() { return this.props.fieldExt ? true : false; }
onContextMenu = (e: React.MouseEvent): void => {
if (!this.isAnnotationOverlay && !e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
@@ -54,6 +56,16 @@ export class CollectionView extends React.Component<FieldViewProps> {
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" });
ContextMenu.Instance.addItem({ description: "View Modes...", subitems: subItems });
+ ContextMenu.Instance.addItem({
+ description: "Apply Template", event: undoBatch(() => {
+ let otherdoc = new Doc();
+ otherdoc.width = 100;
+ otherdoc.height = 50;
+ Doc.GetProto(otherdoc).title = "applied(" + this.props.Document.title + ")";
+ Doc.GetProto(otherdoc).layout = Doc.MakeDelegate(this.props.Document);
+ this.props.addDocTab && this.props.addDocTab(otherdoc, undefined, "onRight");
+ }), icon: "project-diagram"
+ });
}
}
diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx
index f11af04a3..c3e55d825 100644
--- a/src/client/views/collections/ParentDocumentSelector.tsx
+++ b/src/client/views/collections/ParentDocumentSelector.tsx
@@ -9,7 +9,7 @@ import { CollectionDockingView } from "./CollectionDockingView";
import { NumCast } from "../../../new_fields/Types";
import { CollectionViewType } from "./CollectionBaseView";
-type SelectorProps = { Document: Doc, addDocTab(doc: Doc, location: string): void };
+type SelectorProps = { Document: Doc, addDocTab(doc: Doc, dataDoc: Doc | undefined, location: string): void };
@observer
export class SelectorContextMenu extends React.Component<SelectorProps> {
@observable private _docs: { col: Doc, target: Doc }[] = [];
@@ -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)));
+ 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(() => {
@@ -43,7 +43,7 @@ export class SelectorContextMenu extends React.Component<SelectorProps> {
col.panX = newPanX;
col.panY = newPanY;
}
- this.props.addDocTab(col, "inTab");
+ this.props.addDocTab(col, undefined, "inTab"); // bcz: dataDoc?
};
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
index 7a0fd2b31..fc5212edd 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
@@ -9,6 +9,7 @@
opacity: 0.5;
transform: translate(10000px,10000px);
pointer-events: all;
+ cursor: pointer;
}
.collectionfreeformlinkview-linkText {
stroke: rgb(0,0,0);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index ba7e6cf9e..b546d1b78 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -25,18 +25,18 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : a[HeightSym]() / 2);
let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : b[WidthSym]() / 2);
let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : b[HeightSym]() / 2);
- this.props.LinkDocs.map(l => {
- let width = l[WidthSym]();
- l.x = (x1 + x2) / 2 - width / 2;
- l.y = (y1 + y2) / 2 + 10;
- if (!this.props.removeDocument(l)) this.props.addDocument(l, false);
- });
+ // this.props.LinkDocs.map(l => {
+ // let width = l[WidthSym]();
+ // l.x = (x1 + x2) / 2 - width / 2;
+ // l.y = (y1 + y2) / 2 + 10;
+ // if (!this.props.removeDocument(l)) this.props.addDocument(l, false);
+ // });
e.stopPropagation();
e.preventDefault();
}
}
render() {
- let l = this.props.LinkDocs;
+ // let l = this.props.LinkDocs;
let a = this.props.A;
let b = this.props.B;
let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.width) / NumCast(a.zoomBasis, 1) / 2);
@@ -44,13 +44,13 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.width) / NumCast(b.zoomBasis, 1) / 2);
let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.height) / NumCast(b.zoomBasis, 1) / 2);
let text = "";
- let first = this.props.LinkDocs[0];
- if (this.props.LinkDocs.length === 1) text += first.title + (first.linkDescription ? "(" + StrCast(first.linkDescription) + ")" : "");
- else text = "-multiple-";
+ // let first = this.props.LinkDocs[0];
+ // if (this.props.LinkDocs.length === 1) text += first.title + (first.linkDescription ? "(" + StrCast(first.linkDescription) + ")" : "");
+ // else text = "-multiple-";
return (
<>
<line key="linkLine" className="collectionfreeformlinkview-linkLine"
- style={{ strokeWidth: `${2 * l.length / 2}` }}
+ style={{ strokeWidth: `${2 * 1 / 2}` }}
x1={`${x1}`} y1={`${y1}`}
x2={`${x2}`} y2={`${y2}`} />
{/* <circle key="linkCircle" className="collectionfreeformlinkview-linkCircle"
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index be75c6c5c..2d94f1b8e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -17,58 +17,56 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
_brushReactionDisposer?: IReactionDisposer;
componentDidMount() {
- this._brushReactionDisposer = reaction(
- () => {
- let doclist = DocListCast(this.props.Document[this.props.fieldKey]);
- return { doclist: doclist ? doclist : [], xs: doclist.map(d => d.x) };
- },
- () => {
- let doclist = DocListCast(this.props.Document[this.props.fieldKey]);
- let views = doclist ? doclist.filter(doc => StrCast(doc.backgroundLayout).indexOf("istogram") !== -1) : [];
- views.forEach((dstDoc, i) => {
- views.forEach((srcDoc, j) => {
- let dstTarg = dstDoc;
- let srcTarg = srcDoc;
- let x1 = NumCast(srcDoc.x);
- let x2 = NumCast(dstDoc.x);
- let x1w = NumCast(srcDoc.width, -1) / NumCast(srcDoc.zoomBasis, 1);
- let x2w = NumCast(dstDoc.width, -1) / NumCast(srcDoc.zoomBasis, 1);
- if (x1w < 0 || x2w < 0 || i === j) { }
- else {
- let findBrush = (field: (Doc | Promise<Doc>)[]) => field.findIndex(brush => {
- let bdocs = brush instanceof Doc ? Cast(brush.brushingDocs, listSpec(Doc), []) : undefined;
- return bdocs && bdocs.length && ((bdocs[0] === dstTarg && bdocs[1] === srcTarg)) ? true : false;
- });
- let brushAction = (field: (Doc | Promise<Doc>)[]) => {
- let found = findBrush(field);
- if (found !== -1) {
- console.log("REMOVE BRUSH " + srcTarg.title + " " + dstTarg.title);
- field.splice(found, 1);
- }
- };
- if (Math.abs(x1 + x1w - x2) < 20) {
- let linkDoc: Doc = new Doc();
- linkDoc.title = "Histogram Brush";
- linkDoc.linkDescription = "Brush between " + StrCast(srcTarg.title) + " and " + StrCast(dstTarg.Title);
- linkDoc.brushingDocs = new List([dstTarg, srcTarg]);
+ // this._brushReactionDisposer = reaction(
+ // () => {
+ // let doclist = DocListCast(this.props.Document[this.props.fieldKey]);
+ // return { doclist: doclist ? doclist : [], xs: doclist.map(d => d.x) };
+ // },
+ // () => {
+ // let doclist = DocListCast(this.props.Document[this.props.fieldKey]);
+ // let views = doclist ? doclist.filter(doc => StrCast(doc.backgroundLayout).indexOf("istogram") !== -1) : [];
+ // views.forEach((dstDoc, i) => {
+ // views.forEach((srcDoc, j) => {
+ // let dstTarg = dstDoc;
+ // let srcTarg = srcDoc;
+ // let x1 = NumCast(srcDoc.x);
+ // let x2 = NumCast(dstDoc.x);
+ // let x1w = NumCast(srcDoc.width, -1) / NumCast(srcDoc.zoomBasis, 1);
+ // let x2w = NumCast(dstDoc.width, -1) / NumCast(srcDoc.zoomBasis, 1);
+ // if (x1w < 0 || x2w < 0 || i === j) { }
+ // else {
+ // let findBrush = (field: (Doc | Promise<Doc>)[]) => field.findIndex(brush => {
+ // let bdocs = brush instanceof Doc ? Cast(brush.brushingDocs, listSpec(Doc), []) : undefined;
+ // return bdocs && bdocs.length && ((bdocs[0] === dstTarg && bdocs[1] === srcTarg)) ? true : false;
+ // });
+ // let brushAction = (field: (Doc | Promise<Doc>)[]) => {
+ // let found = findBrush(field);
+ // if (found !== -1) {
+ // field.splice(found, 1);
+ // }
+ // };
+ // if (Math.abs(x1 + x1w - x2) < 20) {
+ // let linkDoc: Doc = new Doc();
+ // linkDoc.title = "Histogram Brush";
+ // linkDoc.linkDescription = "Brush between " + StrCast(srcTarg.title) + " and " + StrCast(dstTarg.Title);
+ // linkDoc.brushingDocs = new List([dstTarg, srcTarg]);
- brushAction = (field: (Doc | Promise<Doc>)[]) => {
- if (findBrush(field) === -1) {
- console.log("ADD BRUSH " + srcTarg.title + " " + dstTarg.title);
- field.push(linkDoc);
- }
- };
- }
- if (dstTarg.brushingDocs === undefined) dstTarg.brushingDocs = new List<Doc>();
- if (srcTarg.brushingDocs === undefined) srcTarg.brushingDocs = new List<Doc>();
- let dstBrushDocs = Cast(dstTarg.brushingDocs, listSpec(Doc), []);
- let srcBrushDocs = Cast(srcTarg.brushingDocs, listSpec(Doc), []);
- brushAction(dstBrushDocs);
- brushAction(srcBrushDocs);
- }
- });
- });
- });
+ // brushAction = (field: (Doc | Promise<Doc>)[]) => {
+ // if (findBrush(field) === -1) {
+ // field.push(linkDoc);
+ // }
+ // };
+ // }
+ // if (dstTarg.brushingDocs === undefined) dstTarg.brushingDocs = new List<Doc>();
+ // if (srcTarg.brushingDocs === undefined) srcTarg.brushingDocs = new List<Doc>();
+ // let dstBrushDocs = Cast(dstTarg.brushingDocs, listSpec(Doc), []);
+ // let srcBrushDocs = Cast(srcTarg.brushingDocs, listSpec(Doc), []);
+ // brushAction(dstBrushDocs);
+ // brushAction(srcBrushDocs);
+ // }
+ // });
+ // });
+ // });
}
componentWillUnmount() {
if (this._brushReactionDisposer) {
@@ -97,6 +95,7 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => {
let srcViews = this.documentAnchors(connection.a);
let targetViews = this.documentAnchors(connection.b);
+
let possiblePairs: { a: Doc, b: Doc, }[] = [];
srcViews.map(sv => targetViews.map(tv => possiblePairs.push({ a: sv.props.Document, b: tv.props.Document })));
possiblePairs.map(possiblePair => {
@@ -114,19 +113,16 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
});
return drawnPairs;
}, [] as { a: Doc, b: Doc, l: Doc[] }[]);
- return connections.map(c => {
- let x = c.l.reduce((p, l) => p + l[Id], "");
- return <CollectionFreeFormLinkView key={x} A={c.a} B={c.b} LinkDocs={c.l}
- removeDocument={this.props.removeDocument} addDocument={this.props.addDocument} />;
- });
+ return connections.map(c => <CollectionFreeFormLinkView key={c.l.reduce((p, l) => p + l[Id], "")} A={c.a} B={c.b} LinkDocs={c.l}
+ removeDocument={this.props.removeDocument} addDocument={this.props.addDocument} />);
}
render() {
return (
<div className="collectionfreeformlinksview-container">
- <svg className="collectionfreeformlinksview-svgCanvas">
+ {/* <svg className="collectionfreeformlinksview-svgCanvas">
{this.uniqueConnections}
- </svg>
+ </svg> */}
{this.props.children}
</div>
);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index 5ac2e1f9c..00407d39a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -1,7 +1,7 @@
@import "../../globalCssVariables";
.collectionfreeformview-ease {
- position: absolute;
+ position: inherit;
top: 0;
left: 0;
width: 100%;
@@ -25,8 +25,9 @@
height: 100%;
width: 100%;
}
+
>.jsx-parser {
- z-index:0;
+ z-index: 0;
}
//nested freeform views
@@ -35,56 +36,20 @@
// 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;
- .marqueeView {
- overflow: hidden;
- }
- top: 0;
- left: 0;
- width: 100%;
- 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...?
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 5ee16970b..4669ff142 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,23 +1,25 @@
import { action, computed } from "mobx";
import { observer } from "mobx-react";
-import { Doc, HeightSym, WidthSym } from "../../../../new_fields/Doc";
+import { Doc, DocListCastAsync, HeightSym, WidthSym, DocListCast } from "../../../../new_fields/Doc";
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";
import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
import { DocumentContentsView } from "../../nodes/DocumentContentsView";
import { DocumentViewProps, positionSchema } from "../../nodes/DocumentView";
import { pageSchema } from "../../nodes/ImageBox";
+import PDFMenu from "../../pdf/PDFMenu";
import { CollectionSubView } from "../CollectionSubView";
import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView";
import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors";
@@ -25,13 +27,19 @@ import "./CollectionFreeFormView.scss";
import { MarqueeView } from "./MarqueeView";
import React = require("react");
import v5 = require("uuid/v5");
-import PDFMenu from "../../pdf/PDFMenu";
import { Timeline } from "../../nodes/Timeline";
+import { ScriptField } from "../../../../new_fields/ScriptField";
+import { OverlayView, OverlayElementOptions } from "../../OverlayView";
+import { ScriptBox } from "../../ScriptBox";
+import { CompileScript } from "../../../util/Scripting";
+
export const panZoomSchema = createSchema({
panX: "number",
panY: "number",
- scale: "number"
+ scale: "number",
+ arrangeScript: ScriptField,
+ arrangeInit: ScriptField,
});
type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof positionSchema, typeof pageSchema]>;
@@ -45,13 +53,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 && this.props.fieldKey === "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.Document.panX || 0;
- private panY = () => this.Document.panY || 0;
- private zoomScaling = () => 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());
@@ -79,6 +96,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) => {
@@ -86,10 +108,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 => {
@@ -110,10 +131,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;
@@ -152,18 +172,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.props.Document.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);
@@ -191,14 +211,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
onPointerWheel = (e: React.WheelEvent): void => {
+ if (BoolCast(this.props.Document.lockedPosition)) return;
// if (!this.props.active()) {
// return;
// }
+ if (this.props.Document.type === "pdf") {
+ return;
+ }
let childSelected = this.childDocs.some(doc => {
var dv = DocumentManager.Instance.getDocumentView(doc);
return dv && SelectionManager.IsSelected(dv) ? true : false;
});
- if (!this.props.isSelected() && !childSelected && !this.props.isTopMost) {
+ if (!this.props.isSelected() && !childSelected && this.props.renderDepth > 0) {
return;
}
e.stopPropagation();
@@ -217,7 +241,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
} else {
// if (modes[e.deltaMode] === 'pixels') coefficient = 50;
// else if (modes[e.deltaMode] === 'lines') coefficient = 1000; // This should correspond to line-height??
- let deltaScale = (1 - (e.deltaY / coefficient));
+ let deltaScale = e.deltaY > 0 ? (1 / 1.1) : 1.1;
if (deltaScale * this.zoomScaling() < 1 && this.isAnnotationOverlay) {
deltaScale = 1 / this.zoomScaling();
}
@@ -234,13 +258,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
setPan(panX: number, panY: number) {
-
+ if (BoolCast(this.props.Document.lockedPosition)) return;
this.props.Document.panTransformType = "None";
var scale = this.getLocalTransform().inverse().Scale;
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 - scale * this.props.Document[HeightSym]();
+ }
}
@action
@@ -262,11 +291,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
doc.zIndex = docs.length + 1;
}
- focusDocument = (doc: Doc) => {
+ focusDocument = (doc: Doc, willZoom: boolean) => {
const panX = this.Document.panX;
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) {
@@ -283,28 +313,87 @@ 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);
+
this.props.Document.panTransformType = "Ease";
this.props.focus(this.props.Document);
+ if (willZoom) {
+ this.setScaleToZoom(doc);
+ }
+
+ }
+
+ setScaleToZoom = (doc: Doc) => {
+ let p = this.props;
+ let PanelHeight = p.PanelHeight();
+ let panelWidth = p.PanelWidth();
+
+ let docHeight = NumCast(doc.height);
+ let docWidth = NumCast(doc.width);
+ let targetHeight = 0.5 * PanelHeight;
+ let targetWidth = 0.5 * panelWidth;
+
+ let maxScaleX: number = targetWidth / docWidth;
+ let maxScaleY: number = targetHeight / docHeight;
+ let maxApplicableScale = Math.min(maxScaleX, maxScaleY);
+ this.Document.scale = maxApplicableScale;
}
+ zoomToScale = (scale: number) => {
+ this.Document.scale = scale;
+ }
+
+ getScale = () => {
+ if (this.Document.scale) {
+ return this.Document.scale;
+ }
+ return 1;
+ }
- getDocumentViewProps(document: Doc): DocumentViewProps {
+
+ getChildDocumentViewProps(childDocLayout: Doc): DocumentViewProps {
+ 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,
+ Document: layoutDoc,
+ addDocument: this.props.addDocument,
+ removeDocument: this.props.removeDocument,
+ moveDocument: this.props.moveDocument,
+ ScreenToLocalTransform: this.getTransform,
+ renderDepth: this.props.renderDepth + 1,
+ selectOnLoad: layoutDoc[Id] === this._selectOnLoaded,
+ PanelWidth: layoutDoc[WidthSym],
+ PanelHeight: layoutDoc[HeightSym],
+ ContentScaling: returnOne,
+ ContainingCollectionView: this.props.CollectionView,
+ focus: this.focusDocument,
+ parentActive: this.props.active,
+ whenActiveChanged: this.props.whenActiveChanged,
+ bringToFront: this.bringToFront,
+ addDocTab: this.props.addDocTab,
+ zoomToScale: this.zoomToScale,
+ getScale: this.getScale
+ };
+ }
+ getDocumentViewProps(layoutDoc: Doc): DocumentViewProps {
return {
- Document: document,
+ DataDoc: this.props.DataDoc,
+ Document: this.props.Document,
addDocument: this.props.addDocument,
removeDocument: this.props.removeDocument,
moveDocument: this.props.moveDocument,
ScreenToLocalTransform: this.getTransform,
- isTopMost: false,
- selectOnLoad: document[Id] === this._selectOnLoaded,
- PanelWidth: document[WidthSym],
- PanelHeight: document[HeightSym],
+ renderDepth: this.props.renderDepth + 1,
+ selectOnLoad: layoutDoc[Id] === this._selectOnLoaded,
+ PanelWidth: layoutDoc[WidthSym],
+ PanelHeight: layoutDoc[HeightSym],
ContentScaling: returnOne,
ContainingCollectionView: this.props.CollectionView,
focus: this.focusDocument,
@@ -312,19 +401,41 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
whenActiveChanged: this.props.whenActiveChanged,
bringToFront: this.bringToFront,
addDocTab: this.props.addDocTab,
+ zoomToScale: this.zoomToScale,
+ getScale: this.getScale
};
}
+ getCalculatedPositions(script: ScriptField, params: { doc: Doc, index: number, collection: Doc, docs: Doc[], state: any }): { x?: number, y?: number, width?: number, height?: number, state?: any } {
+ const result = script.script.run(params);
+ if (!result.success) {
+ return {};
+ }
+ return result.result === undefined ? {} : result.result;
+ }
+
@computed.struct
get views() {
let curPage = FieldValue(this.Document.curPage, -1);
- let docviews = this.childDocs.reduce((prev, doc) => {
+ const initScript = this.Document.arrangeInit;
+ const script = this.Document.arrangeScript;
+ let state: any = undefined;
+ const docs = this.childDocs;
+ if (initScript) {
+ const initResult = initScript.script.run({ docs, collection: this.Document });
+ if (initResult.success) {
+ state = initResult.result;
+ }
+ }
+ 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) {
- prev.push(<CollectionFreeFormDocumentView key={doc[Id]} {...this.getDocumentViewProps(doc)} />);
+ const pos = script ? this.getCalculatedPositions(script, { doc, index: prev.length, collection: this.Document, docs, state }) : {};
+ state = pos.state === undefined ? state : pos.state;
+ prev.push(<CollectionFreeFormDocumentView key={doc[Id]} x={pos.x} y={pos.y} width={pos.width} height={pos.height} {...this.getChildDocumentViewProps(doc)} />);
}
}
return prev;
@@ -340,25 +451,84 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY));
}
+ onContextMenu = () => {
+ ContextMenu.Instance.addItem({
+ description: "Arrange contents in grid",
+ event: async () => {
+ const docs = await DocListCastAsync(this.Document[this.props.fieldKey]);
+ 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 = 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,
+ requiredType,
+ typecheck: false
+ });
+ if (!script.compiled) {
+ onError(script.errors.map(error => error.messageText).join("\n"));
+ return;
+ }
+ const docs = DocListCast(this.Document[this.props.fieldKey]);
+ docs.map(d => d.transition = "transform 1s");
+ this.Document[key] = new ScriptField(script);
+ overlayDisposer();
+ setTimeout(() => docs.map(d => d.transition = undefined), 1200);
+ }} />;
+ overlayDisposer = OverlayView.Instance.addElement(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}");
+ }
+ });
+ }
+
private childViews = () => [
<CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />,
...this.views
]
render() {
- const containerName = `collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`;
const easing = () => this.props.Document.panTransformType === "Ease";
+
+ 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" }}
- onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} >
+ <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.props.Document} >
+ <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} AnnotationDocument={this.fieldExtensionDoc} inkFieldKey={"ink"} >
{this.childViews}
</InkingCanvas>
</CollectionFreeFormLinksView>
@@ -376,7 +546,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> {
@computed get overlayView() {
return (<DocumentContentsView {...this.props} layoutKey={"overlayLayout"}
- isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />);
+ renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />);
}
render() {
return this.overlayView;
@@ -386,8 +556,9 @@ class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps &
@observer
class CollectionFreeFormBackgroundView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> {
@computed get backgroundView() {
+ let props = this.props;
return (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"}
- isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />);
+ renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />);
}
render() {
return this.props.Document.backgroundLayout ? this.backgroundView : (null);
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
index 6e8ec8662..9fc2e44fb 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
@@ -21,6 +21,6 @@
white-space:nowrap;
}
.marquee-legend::after {
- content: "Press: c (collection), s (summary), r (replace) or Delete"
+ content: "Press: c (collection), s (summary), or Delete"
}
} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 3f7efcb66..b765517a2 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -1,7 +1,7 @@
import * as htmlToImage from "html-to-image";
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
-import { Doc } from "../../../../new_fields/Doc";
+import { Doc, FieldResult } from "../../../../new_fields/Doc";
import { Id } from "../../../../new_fields/FieldSymbols";
import { InkField, StrokeData } from "../../../../new_fields/InkField";
import { List } from "../../../../new_fields/List";
@@ -14,7 +14,6 @@ import { Transform } from "../../../util/Transform";
import { undoBatch } from "../../../util/UndoManager";
import { InkingCanvas } from "../../InkingCanvas";
import { PreviewCursor } from "../../PreviewCursor";
-import { SearchBox } from "../../SearchBox";
import { Templates } from "../../Templates";
import { CollectionViewType } from "../CollectionBaseView";
import { CollectionFreeFormView } from "./CollectionFreeFormView";
@@ -45,14 +44,12 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
_commandExecuted = false;
@action
- cleanupInteractions = (all: boolean = false, rem_keydown: boolean = true) => {
+ cleanupInteractions = (all: boolean = false) => {
if (all) {
document.removeEventListener("pointerup", this.onPointerUp, true);
document.removeEventListener("pointermove", this.onPointerMove, true);
}
- if (all) {
- document.removeEventListener("keydown", this.marqueeCommand, true);
- }
+ document.removeEventListener("keydown", this.marqueeCommand, true);
this._visible = false;
}
@@ -79,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;
});
@@ -89,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: 30, 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);
}
@@ -137,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);
}
@@ -150,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);
@@ -184,20 +180,18 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
@action
onPointerUp = (e: PointerEvent): void => {
- console.log("pointer up!");
+ if (!this.props.container.props.active()) this.props.selectDocuments([this.props.container.props.Document]);
+ // console.log("pointer up!");
if (this._visible) {
- console.log("visible");
+ // console.log("visible");
let mselect = this.marqueeSelect();
if (!e.shiftKey) {
SelectionManager.DeselectAll(mselect.length ? undefined : this.props.container.props.Document);
}
this.props.selectDocuments(mselect.length ? mselect : [this.props.container.props.Document]);
- mselect.length ? this.cleanupInteractions(true, false) : this.cleanupInteractions(true);
- }
- else {
- console.log("invisible");
- this.cleanupInteractions(true);
}
+ //console.log("invisible");
+ this.cleanupInteractions(true);
if (e.altKey) {
e.preventDefault();
@@ -230,6 +224,18 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
return { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) };
}
+ get ink() {
+ let container = this.props.container.Document;
+ let containerKey = this.props.container.props.fieldKey;
+ return Cast(container[containerKey + "_ink"], InkField);
+ }
+
+ set ink(value: InkField | undefined) {
+ let container = Doc.GetProto(this.props.container.Document);
+ let containerKey = this.props.container.props.fieldKey;
+ container[containerKey + "_ink"] = value;
+ }
+
@undoBatch
@action
marqueeCommand = async (e: KeyboardEvent) => {
@@ -241,15 +247,14 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
e.stopPropagation();
(e as any).propagationIsStopped = true;
this.marqueeSelect().map(d => this.props.removeDocument(d));
- let ink = Cast(this.props.container.props.Document.ink, InkField);
- if (ink) {
- this.marqueeInkDelete(ink.inkData);
+ if (this.ink) {
+ this.marqueeInkDelete(this.ink.inkData);
}
SelectionManager.DeselectAll();
this.cleanupInteractions(false);
e.stopPropagation();
}
- if (e.key === "c" || e.key === "s" || e.key === "S" || e.key === "e" || e.key === "p") {
+ if (e.key === "c" || e.key === "s" || e.key === "S") {
this._commandExecuted = true;
e.stopPropagation();
e.preventDefault();
@@ -265,21 +270,18 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
return d;
});
}
- let ink = Cast(this.props.container.props.Document.ink, InkField);
- let inkData = ink ? ink.inkData : undefined;
- let zoomBasis = NumCast(this.props.container.props.Document.scale, 1);
- let newCollection = Docs.FreeformDocument(selected, {
+ let inkData = this.ink ? this.ink.inkData : undefined;
+ let newCollection = Docs.Create.FreeformDocument(selected, {
x: bounds.left,
y: bounds.top,
panX: 0,
panY: 0,
- borderRounding: e.key === "e" ? -1 : undefined,
backgroundColor: this.props.container.isAnnotationOverlay ? undefined : "white",
width: bounds.width,
height: bounds.height,
- ink: inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined,
- title: e.key === "s" || e.key === "S" ? "-summary-" : e.key === "p" ? "-summary-" : "a nested collection",
+ title: e.key === "s" || e.key === "S" ? "-summary-" : "a nested collection",
});
+ newCollection.data_ink = inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined;
this.marqueeInkDelete(inkData);
if (e.key === "s") {
@@ -290,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);
// });
@@ -309,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;
@@ -356,7 +358,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
let idata = new Map();
ink.forEach((value: StrokeData, key: string, map: any) =>
!InkingCanvas.IntersectStrokeRect(value, this.Bounds) && idata.set(key, value));
- Doc.SetOnPrototype(this.props.container.props.Document, "ink", new InkField(idata));
+ this.ink = new InkField(idata);
}
}
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 838d4d9ac..6dffee586 100644
--- a/src/client/views/globalCssVariables.scss
+++ b/src/client/views/globalCssVariables.scss
@@ -9,27 +9,33 @@ $main-accent: #aaaaa3;
//$alt-accent: #59dff7;
$alt-accent: #c2c2c5;
$lighter-alt-accent: rgb(207, 220, 240);
+$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;
MAX_ROW_HEIGHT: $MAX_ROW_HEIGHT;
+ SEARCH_THUMBNAIL_SIZE: $search-thumnail-size;
} \ No newline at end of file
diff --git a/src/client/views/globalCssVariables.scss.d.ts b/src/client/views/globalCssVariables.scss.d.ts
index 9788d31f7..d95cec9d8 100644
--- a/src/client/views/globalCssVariables.scss.d.ts
+++ b/src/client/views/globalCssVariables.scss.d.ts
@@ -4,6 +4,7 @@ interface IGlobalScss {
COLLECTION_BORDER_WIDTH: string;
MINIMIZED_ICON_SIZE: string;
MAX_ROW_HEIGHT: string;
+ SEARCH_THUMBNAIL_SIZE: string;
}
declare const globalCssVariables: IGlobalScss;
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/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 858959d81..b09538d1a 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,14 +1,19 @@
import { computed } from "mobx";
import { observer } from "mobx-react";
import { createSchema, makeInterface } from "../../../new_fields/Schema";
-import { BoolCast, FieldValue, NumCast } from "../../../new_fields/Types";
+import { BoolCast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
import { Transform } from "../../util/Transform";
import { DocComponent } from "../DocComponent";
import { DocumentView, DocumentViewProps, positionSchema } from "./DocumentView";
import "./DocumentView.scss";
import React = require("react");
+import { Doc } from "../../../new_fields/Doc";
export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
+ x?: number;
+ y?: number;
+ width?: number;
+ height?: number;
}
const schema = createSchema({
@@ -23,13 +28,13 @@ const FreeformDocument = makeInterface(schema, positionSchema);
@observer
export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, FreeformDocument>(FreeformDocument) {
@computed get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) scale(${this.zoom}) `; }
- @computed get X() { return FieldValue(this.Document.x, 0); }
- @computed get Y() { return FieldValue(this.Document.y, 0); }
+ @computed get X() { return this.props.x !== undefined ? this.props.x : this.Document.x || 0; }
+ @computed get Y() { return this.props.y !== undefined ? this.props.y : this.Document.y || 0; }
+ @computed get width(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : this.props.width !== undefined ? this.props.width : this.Document.width || 0; }
+ @computed get height(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : this.props.height !== undefined ? this.props.height : this.Document.height || 0; }
@computed get zoom(): number { return 1 / FieldValue(this.Document.zoomBasis, 1); }
@computed get nativeWidth(): number { return FieldValue(this.Document.nativeWidth, 0); }
@computed get nativeHeight(): number { return FieldValue(this.Document.nativeHeight, 0); }
- @computed get width(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : FieldValue(this.Document.width, 0); }
- @computed get height(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : FieldValue(this.Document.height, 0); }
set width(w: number) {
this.Document.width = w;
@@ -43,12 +48,13 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
this.Document.width = this.nativeWidth / this.nativeHeight * h;
}
}
+ @computed get scaleToOverridingWidth() { return this.width / NumCast(this.props.Document.width, this.width); }
contentScaling = () => this.nativeWidth > 0 ? this.width / this.nativeWidth : 1;
panelWidth = () => this.props.PanelWidth();
panelHeight = () => this.props.PanelHeight();
getTransform = (): Transform => this.props.ScreenToLocalTransform()
.translate(-this.X, -this.Y)
- .scale(1 / this.contentScaling()).scale(1 / this.zoom)
+ .scale(1 / this.contentScaling()).scale(1 / this.zoom / this.scaleToOverridingWidth)
animateBetweenIcon = (icon: number[], stime: number, maximizing: boolean) => {
this.props.bringToFront(this.props.Document);
@@ -65,11 +71,14 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
}
borderRounding = () => {
- let br = NumCast(this.props.Document.borderRounding);
- return br >= 0 ? br :
- NumCast(this.props.Document.nativeWidth) === 0 ?
- Math.min(this.props.PanelWidth(), this.props.PanelHeight())
- : Math.min(this.Document.nativeWidth || 0, this.Document.nativeHeight || 0);
+ let br = StrCast(this.props.Document.layout instanceof Doc ? this.props.Document.layout.borderRounding : this.props.Document.borderRounding);
+ if (br.endsWith("%")) {
+ let percent = Number(br.substr(0, br.length - 1)) / 100;
+ let nativeDim = Math.min(NumCast(this.props.Document.nativeWidth), NumCast(this.props.Document.nativeHeight));
+ let minDim = percent * (nativeDim ? nativeDim : Math.min(this.props.PanelWidth(), this.props.PanelHeight()));
+ return minDim;
+ }
+ return undefined;
}
render() {
@@ -79,8 +88,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
transformOrigin: "left top",
position: "absolute",
backgroundColor: "transparent",
- borderRadius: `${this.borderRounding()}px`,
+ borderRadius: this.borderRounding(),
transform: this.transform,
+ transition: StrCast(this.props.Document.transition),
width: this.width,
height: this.height,
zIndex: this.Document.zIndex || 0,
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 02396c3af..ed6b224a7 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -23,6 +23,9 @@ import { FieldViewProps } from "./FieldView";
import { Without, OmitKeys } from "../../../Utils";
import { Cast, StrCast, NumCast } from "../../../new_fields/Types";
import { List } from "../../../new_fields/List";
+import { Doc } from "../../../new_fields/Doc";
+import DirectoryImportBox from "../../util/Import & Export/DirectoryImportBox";
+import { CollectionViewType } from "../collections/CollectionBaseView";
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
type BindingProps = Without<FieldViewProps, 'fieldKey'>;
@@ -47,11 +50,11 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
hideOnLeave?: boolean
}> {
@computed get layout(): string {
- const layout = Cast(this.props.Document[this.props.layoutKey], "string");
+ const layout = Cast(this.layoutDoc[this.props.layoutKey], "string");
if (layout === undefined) {
return this.props.Document.data ?
"<FieldView {...props} fieldKey='data' />" :
- KeyValueBox.LayoutString(this.props.Document.proto ? "proto" : "");
+ KeyValueBox.LayoutString(this.layoutDoc.proto ? "proto" : "");
} else if (typeof layout === "string") {
return layout;
} else {
@@ -59,8 +62,23 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
}
}
+ get dataDoc() {
+ if (this.props.DataDoc === undefined && this.props.Document.layout instanceof Doc) {
+ // if there is no dataDoc (ie, we're not rendering a temlplate layout), but this document
+ // has a template layout document, then we will render the template layout but use
+ // this document as the data document for the layout.
+ return this.props.Document;
+ }
+ return this.props.DataDoc;
+ }
+ get layoutDoc() {
+ // if this document's layout field contains a document (ie, a rendering template), then we will use that
+ // to determine the render JSX string, otherwise the layout field should directly contain a JSX layout string.
+ return this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document;
+ }
+
CreateBindings(): JsxBindings {
- return { props: OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit };
+ return { props: { ...OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit, Document: this.layoutDoc, DataDoc: this.dataDoc } };
}
@computed get templates(): List<string> {
@@ -71,39 +89,15 @@ 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.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() {
+ let self = this;
+ if (this.props.renderDepth > 7) return (null);
if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null);
return <ObserverJsxParser
- components={{ FormattedTextBox, ImageBox, IconBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }}
+ components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }}
bindings={this.CreateBindings()}
jsx={this.finalLayout}
showWarnings={true}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 247e3223b..fb8319934 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";
@@ -23,15 +23,18 @@ import { CollectionVideoView } from "../collections/CollectionVideoView";
import { CollectionView } from "../collections/CollectionView";
import { ContextMenu } from "../ContextMenu";
import { DocComponent } from "../DocComponent";
-import { PresentationView } from "../PresentationView";
-import { Template } from "./../Templates";
+import { PresentationView } from "../presentationview/PresentationView";
+import { Template, Templates } from "./../Templates";
import { DocumentContentsView } from "./DocumentContentsView";
import * as rp from "request-promise";
import "./DocumentView.scss";
import React = require("react");
import { Id, Copy } from '../../../new_fields/FieldSymbols';
import { ContextMenuProps } from '../ContextMenuItem';
+import { list, object, createSimpleSchema } from 'serializr';
+import { LinkManager } from '../../util/LinkManager';
import { RouteStore } from '../../../server/RouteStore';
+import { FormattedTextBox } from './FormattedTextBox';
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
library.add(fa.faTrash);
@@ -44,6 +47,7 @@ library.add(fa.faAlignCenter);
library.add(fa.faCaretSquareRight);
library.add(fa.faSquare);
library.add(fa.faConciergeBell);
+library.add(fa.faWindowRestore);
library.add(fa.faFolder);
library.add(fa.faMapPin);
library.add(fa.faLink);
@@ -53,34 +57,41 @@ library.add(fa.faDesktop);
library.add(fa.faUnlock);
library.add(fa.faLock);
-const linkSchema = createSchema({
- title: "string",
- linkDescription: "string",
- linkTags: "string",
- linkedTo: Doc,
- linkedFrom: Doc
-});
-type LinkDoc = makeInterface<[typeof linkSchema]>;
-const LinkDoc = makeInterface(linkSchema);
+// const linkSchema = createSchema({
+// title: "string",
+// linkDescription: "string",
+// linkTags: "string",
+// linkedTo: Doc,
+// linkedFrom: Doc
+// });
+
+// type LinkDoc = makeInterface<[typeof linkSchema]>;
+// const LinkDoc = makeInterface(linkSchema);
export interface DocumentViewProps {
ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
Document: Doc;
+ DataDoc?: Doc;
+ fitToBox?: boolean;
addDocument?: (doc: Doc, allowDuplicates?: boolean) => boolean;
removeDocument?: (doc: Doc) => boolean;
moveDocument?: (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
ScreenToLocalTransform: () => Transform;
- isTopMost: boolean;
+ renderDepth: number;
+ showOverlays?: (doc: Doc) => { title?: string, caption?: string };
ContentScaling: () => number;
PanelWidth: () => number;
PanelHeight: () => number;
- focus: (doc: Doc) => void;
+ focus: (doc: Doc, willZoom: boolean) => void;
selectOnLoad: boolean;
parentActive: () => boolean;
whenActiveChanged: (isActive: boolean) => void;
bringToFront: (doc: Doc) => void;
- addDocTab: (doc: Doc, where: string) => void;
+ addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void;
+ collapseToPoint?: (scrpt: number[], expandedDocs: Doc[] | undefined) => void;
+ zoomToScale: (scale: number) => void;
+ getScale: () => number;
animateBetweenIcon?: (iconPos: number[], startTime: number, maximizing: boolean) => void;
}
@@ -89,8 +100,8 @@ const schema = createSchema({
nativeWidth: "number",
nativeHeight: "number",
backgroundColor: "string",
- hidden: "boolean",
- opacity: "number"
+ opacity: "number",
+ hidden: "boolean"
});
export const positionSchema = createSchema({
@@ -120,7 +131,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
public get ContentDiv() { return this._mainCont.current; }
@computed get active(): boolean { return SelectionManager.IsSelected(this) || this.props.parentActive(); }
- @computed get topMost(): boolean { return this.props.isTopMost; }
+ @computed get topMost(): boolean { return this.props.renderDepth === 0; }
@computed get templates(): List<string> {
let field = this.props.Document.templates;
if (field && field instanceof List) {
@@ -209,11 +220,21 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
e.stopPropagation();
}
+ get dataDoc() {
+ if (this.props.DataDoc === undefined && this.props.Document.layout instanceof Doc) {
+ // if there is no dataDoc (ie, we're not rendering a temlplate layout), but this document
+ // has a template layout document, then we will render the template layout but use
+ // this document as the data document for the layout.
+ return this.props.Document;
+ }
+ return this.props.DataDoc !== this.props.Document ? this.props.DataDoc : undefined;
+ }
startDragging(x: number, y: number, dropAction: dropActionType, dragSubBullets: boolean) {
if (this._mainCont.current) {
let allConnected = [this.props.Document, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])];
+ let alldataConnected = [this.dataDoc, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])];
const [left, top] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(0, 0);
- let dragData = new DragManager.DocumentDragData(allConnected);
+ let dragData = new DragManager.DocumentDragData(allConnected, alldataConnected);
const [xoff, yoff] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top);
dragData.dropAction = dropAction;
dragData.xOffset = xoff;
@@ -238,14 +259,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
static _undoBatch?: UndoManager.Batch = undefined;
@action
- public collapseTargetsToPoint = async (scrpt: number[], expandedDocs: Doc[] | undefined): Promise<void> => {
+ public collapseTargetsToPoint = (scrpt: number[], expandedDocs: Doc[] | undefined): void => {
SelectionManager.DeselectAll();
if (expandedDocs) {
if (!DocumentView._undoBatch) {
DocumentView._undoBatch = UndoManager.StartBatch("iconAnimating");
}
let isMinimized: boolean | undefined;
- expandedDocs.map(d => Doc.GetProto(d)).map(maximizedDoc => {
+ expandedDocs.map(maximizedDoc => {
let iconAnimating = Cast(maximizedDoc.isIconAnimating, List);
if (!iconAnimating || (Date.now() - iconAnimating[2] > 1000)) {
if (isMinimized === undefined) {
@@ -267,10 +288,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
e.stopPropagation();
let altKey = e.altKey;
let ctrlKey = e.ctrlKey;
- if (this._doubleTap && !this.props.isTopMost) {
- this.props.addDocTab(this.props.Document, "inTab");
+ if (this._doubleTap && this.props.renderDepth) {
+ let fullScreenAlias = Doc.MakeAlias(this.props.Document);
+ 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 &&
@@ -282,8 +305,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
let subBulletDocs = await DocListCastAsync(this.props.Document.subBulletDocs);
let maximizedDocs = await DocListCastAsync(this.props.Document.maximizedDocs);
let summarizedDocs = await DocListCastAsync(this.props.Document.summarizedDocs);
- let linkedToDocs = await DocListCastAsync(this.props.Document.linkedToDocs, []);
- let linkedFromDocs = await DocListCastAsync(this.props.Document.linkedFromDocs, []);
+ let linkedDocs = LinkManager.Instance.getAllRelatedLinks(this.props.Document);
let expandedDocs: Doc[] = [];
expandedDocs = subBulletDocs ? [...subBulletDocs, ...expandedDocs] : expandedDocs;
expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs;
@@ -311,30 +333,26 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
if (dataDocs) {
expandedDocs.forEach(maxDoc =>
(!CollectionDockingView.Instance.CloseRightSplit(Doc.GetProto(maxDoc)) &&
- this.props.addDocTab(getDispDoc(maxDoc), maxLocation)));
+ this.props.addDocTab(getDispDoc(maxDoc), undefined, maxLocation)));
}
} else {
let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(NumCast(this.Document.width) / 2, NumCast(this.Document.height) / 2);
this.collapseTargetsToPoint(scrpt, expandedProtoDocs);
}
}
- else if (linkedToDocs.length || linkedFromDocs.length) {
- let linkedFwdDocs = [
- linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : expandedDocs[0],
- linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : expandedDocs[0]];
+ else if (linkedDocs.length) {
+ 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 = [
- linkedToDocs.length ? await (linkedToDocs[0].linkedToContext) as Doc : linkedFromDocs.length ? await PromiseValue(linkedFromDocs[0].linkedFromContext) as Doc : undefined,
- linkedFromDocs.length ? await (linkedFromDocs[0].linkedFromContext) as Doc : linkedToDocs.length ? await PromiseValue(linkedToDocs[0].linkedToContext) as Doc : undefined];
+ // @TODO: shouldn't always follow target context
+ let linkedFwdContextDocs = [first.length ? await (first[0].targetContext) as Doc : undefined, undefined];
- let linkedFwdPage = [
- linkedToDocs.length ? NumCast(linkedToDocs[0].linkedToPage, undefined) : linkedFromDocs.length ? NumCast(linkedFromDocs[0].linkedFromPage, undefined) : undefined,
- linkedFromDocs.length ? NumCast(linkedFromDocs[0].linkedFromPage, undefined) : linkedToDocs.length ? NumCast(linkedToDocs[0].linkedToPage, undefined) : undefined];
+ let linkedFwdPage = [first.length ? NumCast(first[0].linkedToPage, undefined) : undefined, undefined];
if (!linkedFwdDocs.some(l => l instanceof Promise)) {
- let maxLocation = StrCast(linkedFwdDocs[altKey ? 1 : 0].maximizeLocation, "inTab");
+ let maxLocation = StrCast(linkedFwdDocs[0].maximizeLocation, "inTab");
let targetContext = !Doc.AreProtosEqual(linkedFwdContextDocs[altKey ? 1 : 0], this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document) ? linkedFwdContextDocs[altKey ? 1 : 0] : undefined;
- DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, document => this.props.addDocTab(document, maxLocation), linkedFwdPage[altKey ? 1 : 0], targetContext);
+ DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, false, document => this.props.addDocTab(document, undefined, maxLocation), linkedFwdPage[altKey ? 1 : 0], targetContext);
}
}
}
@@ -344,23 +362,23 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
this._downX = e.clientX;
this._downY = e.clientY;
this._hitExpander = DocListCast(this.props.Document.subBulletDocs).length > 0;
- if (e.shiftKey && e.buttons === 1 && CollectionDockingView.Instance) {
- CollectionDockingView.Instance.StartOtherDrag([Doc.MakeAlias(this.props.Document)], e);
- e.stopPropagation();
- } else {
- if (this.active) e.stopPropagation(); // events stop at the lowest document that is active.
- document.removeEventListener("pointermove", this.onPointerMove);
- document.addEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- document.addEventListener("pointerup", this.onPointerUp);
- }
+ // if (e.shiftKey && e.buttons === 1 && CollectionDockingView.Instance) {
+ // CollectionDockingView.Instance.StartOtherDrag(e, [Doc.MakeAlias(this.props.Document)], [this.dataDoc]);
+ // e.stopPropagation();
+ // } else {
+ if (this.active) e.stopPropagation(); // events stop at the lowest document that is active.
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.addEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ document.addEventListener("pointerup", this.onPointerUp);
+ // }
}
onPointerMove = (e: PointerEvent): void => {
if (!e.cancelBubble && this.active) {
- if (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 (!this.props.Document.excludeFromLibrary && (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3)) {
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);
}
}
@@ -375,8 +393,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
this._lastTap = Date.now();
}
- deleteClicked = (): void => { this.props.removeDocument && this.props.removeDocument(this.props.Document); };
- fieldsClicked = (): void => { this.props.addDocTab(Docs.KVPDocument(this.props.Document, { width: 300, height: 300 }), "onRight"); };
+ @undoBatch
+ deleteClicked = (): void => { SelectionManager.DeselectAll(); this.props.removeDocument && this.props.removeDocument(this.props.Document); }
+
+ @undoBatch
+ fieldsClicked = (): void => { let kvp = Docs.Create.KVPDocument(this.props.Document, { width: 300, height: 300 }); this.props.addDocTab(kvp, this.dataDoc, "onRight"); }
+
+ @undoBatch
makeBtnClicked = (): void => {
let doc = Doc.GetProto(this.props.Document);
doc.isButton = !BoolCast(doc.isButton, false);
@@ -389,14 +412,32 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
doc.nativeWidth = doc.nativeHeight = undefined;
}
}
- fullScreenClicked = (): void => {
- CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(Doc.MakeCopy(this.props.Document, false));
+
+ @undoBatch
+ public fullScreenClicked = (): void => {
+ CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(this);
SelectionManager.DeselectAll();
}
@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;
@@ -414,8 +455,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;
}
}
}
@@ -448,7 +490,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
this.templates = this.templates;
}
+ @action
+ clearTemplates = () => {
+ this.templates.length = 0;
+ this.templates = this.templates;
+ }
+ @undoBatch
+ @action
freezeNativeDimensions = (): void => {
let proto = Doc.GetProto(this.props.Document);
if (proto.ignoreAspect === undefined && !proto.nativeWidth) {
@@ -459,6 +508,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
proto.ignoreAspect = !BoolCast(proto.ignoreAspect, false);
}
+ @undoBatch
+ @action
+ toggleLockPosition = (): void => {
+ this.props.Document.lockedPosition = BoolCast(this.props.Document.lockedPosition) ? undefined : true;
+ }
+
@action
onContextMenu = async (e: React.MouseEvent): Promise<void> => {
e.persist();
@@ -473,46 +528,64 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
const cm = ContextMenu.Instance;
let subitems: ContextMenuProps[] = [];
subitems.push({ description: "Open Full Screen", event: this.fullScreenClicked, icon: "desktop" });
- subitems.push({ description: "Open Tab", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, "inTab"), icon: "folder" });
- subitems.push({ description: "Open Tab Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), "inTab"), icon: "folder" });
- subitems.push({ description: "Open Right", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, "onRight"), icon: "caret-square-right" });
- subitems.push({ description: "Open Right Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), "onRight"), icon: "caret-square-right" });
+ subitems.push({ description: "Open Tab", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, this.dataDoc, "inTab"), icon: "folder" });
+ subitems.push({ description: "Open Tab Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.dataDoc, "inTab"), icon: "folder" });
+ subitems.push({ description: "Open Right", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, this.dataDoc, "onRight"), icon: "caret-square-right" });
+ subitems.push({ description: "Open Right Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.dataDoc, "onRight"), icon: "caret-square-right" });
subitems.push({ description: "Open Fields", event: this.fieldsClicked, icon: "layer-group" });
cm.addItem({ description: "Open...", subitems: subitems, icon: "external-link-alt" });
cm.addItem({ description: BoolCast(this.props.Document.ignoreAspect, false) || !this.props.Document.nativeWidth || !this.props.Document.nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "edit" });
cm.addItem({ description: "Pin to Pres", event: () => PresentationView.Instance.PinDoc(this.props.Document), icon: "map-pin" });
- cm.addItem({ description: BoolCast(this.props.Document.lockedPosition) ? "Unlock Pos" : "Lock Pos", event: () => this.props.Document.lockedPosition = BoolCast(this.props.Document.lockedPosition) ? undefined : true, icon: BoolCast(this.props.Document.lockedPosition) ? "unlock" : "lock" });
+ cm.addItem({ description: BoolCast(this.props.Document.lockedPosition) ? "Unlock Pos" : "Lock Pos", event: this.toggleLockPosition, icon: BoolCast(this.props.Document.lockedPosition) ? "unlock" : "lock" });
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.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.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, {}), "onRight");
+ this.props.addDocTab && this.props.addDocTab(Docs.Create.SchemaDocument(["title"], aliases, {}), undefined, "onRight"); // bcz: dataDoc?
}, icon: "search"
});
- cm.addItem({ description: "Center View", event: () => this.props.focus(this.props.Document), icon: "crosshairs" });
- cm.addItem({ description: "Copy URL", event: () => Utils.CopyText(DocServer.prepend("/doc/" + this.props.Document[Id])), icon: "link" });
+ cm.addItem({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" });
+ cm.addItem({ description: "Zoom to Document", event: () => this.props.focus(this.props.Document, true), icon: "search" });
+ cm.addItem({ description: "Copy URL", event: () => Utils.CopyText(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) {
@@ -527,7 +600,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); };
@@ -535,39 +608,72 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@computed get nativeWidth() { return this.Document.nativeWidth || 0; }
@computed get nativeHeight() { return this.Document.nativeHeight || 0; }
@computed get contents() {
- return (
- <DocumentContentsView {...this.props} isSelected={this.isSelected} select={this.select} selectOnLoad={this.props.selectOnLoad} layoutKey={"layout"} />);
+ return (<DocumentContentsView {...this.props} isSelected={this.isSelected} select={this.select} selectOnLoad={this.props.selectOnLoad} layoutKey={"layout"} DataDoc={this.dataDoc} />);
}
render() {
if (this.Document.hidden) {
return null;
}
- var scaling = this.props.ContentScaling();
+ let self = this;
+ let backgroundColor = this.props.Document.layout instanceof Doc ? StrCast(this.props.Document.layout.backgroundColor) : this.Document.backgroundColor;
+ 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.props.isTopMost ? "-topmost" : ""}`}
+ <div className={`documentView-node${this.topMost ? "-topmost" : ""}`}
ref={this._mainCont}
style={{
+ color: foregroundColor,
outlineColor: "maroon",
outlineStyle: "dashed",
- outlineWidth: BoolCast(this.props.Document.libraryBrush, false) ||
- BoolCast(this.props.Document.protoBrush, false) ?
- `${1 * this.props.ScreenToLocalTransform().Scale}px`
- : "0px",
+ outlineWidth: BoolCast(this.props.Document.libraryBrush) && !StrCast(this.props.Document.borderRounding) ?
+ `${this.props.ScreenToLocalTransform().Scale}px` : "0px",
+ border: BoolCast(this.props.Document.libraryBrush) && StrCast(this.props.Document.borderRounding) ?
+ `dashed maroon ${this.props.ScreenToLocalTransform().Scale}px` : undefined,
borderRadius: "inherit",
- background: this.Document.backgroundColor || "",
+ background: backgroundColor,
width: nativeWidth,
height: nativeHeight,
- transform: `scale(${scaling}, ${scaling})`,
- opacity: this.Document.opacity || 1
+ transform: `scale(${this.props.ContentScaling()})`,
+ opacity: this.Document.opacity
}}
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",
+ overflow: "hidden", width: `${100 * this.props.ContentScaling()}%`, height: 25, background: "rgba(0, 0, 0, .4)", color: "white",
+ transformOrigin: "top left", transform: `scale(${1 / this.props.ContentScaling()})`
+ }}>
+ <span>{this.props.Document[showTitle]}</span>
+ </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/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 1f1582f22..ea6730cd0 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -7,19 +7,17 @@ import { IconField } from "../../../new_fields/IconField";
import { List } from "../../../new_fields/List";
import { RichTextField } from "../../../new_fields/RichTextField";
import { AudioField, ImageField, VideoField } from "../../../new_fields/URLField";
-import { emptyFunction, returnFalse, returnOne } from "../../../Utils";
import { Transform } from "../../util/Transform";
import { CollectionPDFView } from "../collections/CollectionPDFView";
import { CollectionVideoView } from "../collections/CollectionVideoView";
import { CollectionView } from "../collections/CollectionView";
import { AudioBox } from "./AudioBox";
-import { DocumentContentsView } from "./DocumentContentsView";
import { FormattedTextBox } from "./FormattedTextBox";
import { IconBox } from "./IconBox";
import { ImageBox } from "./ImageBox";
-import { VideoBox } from "./VideoBox";
import { PDFBox } from "./PDFBox";
-
+import { VideoBox } from "./VideoBox";
+import { Id } from "../../../new_fields/FieldSymbols";
//
// these properties get assigned through the render() method of the DocumentView when it creates this node.
@@ -28,14 +26,18 @@ import { PDFBox } from "./PDFBox";
//
export interface FieldViewProps {
fieldKey: string;
+ fieldExt: string;
+ leaveNativeSize?: boolean;
+ fitToBox?: boolean;
ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
Document: Doc;
+ DataDoc?: Doc;
isSelected: () => boolean;
select: (isCtrlPressed: boolean) => void;
- isTopMost: boolean;
+ renderDepth: number;
selectOnLoad: boolean;
addDocument?: (document: Doc, allowDuplicates?: boolean) => boolean;
- addDocTab: (document: Doc, where: string) => void;
+ addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
removeDocument?: (document: Doc) => boolean;
moveDocument?: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
ScreenToLocalTransform: () => Transform;
@@ -50,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
@@ -71,7 +73,7 @@ export class FieldView extends React.Component<FieldViewProps> {
return <FormattedTextBox {...this.props} />;
}
else if (field instanceof ImageField) {
- return <ImageBox {...this.props} />;
+ return <ImageBox {...this.props} leaveNativeSize={true} />;
}
else if (field instanceof IconField) {
return <IconBox {...this.props} />;
@@ -85,7 +87,7 @@ export class FieldView extends React.Component<FieldViewProps> {
return <p>{field.date.toLocaleString()}</p>;
}
else if (field instanceof Doc) {
- return <p><b>{field.title}</b></p>;
+ return <p><b>{field.title + " : id= " + field[Id]}</b></p>;
// let returnHundred = () => 100;
// return (
// <DocumentContentsView Document={field}
@@ -96,7 +98,7 @@ export class FieldView extends React.Component<FieldViewProps> {
// ContentScaling={returnOne}
// PanelWidth={returnHundred}
// PanelHeight={returnHundred}
- // isTopMost={true} //TODO Why is this top most?
+ // renderDepth={0} //TODO Why is renderDepth reset?
// selectOnLoad={false}
// focus={emptyFunction}
// isSelected={this.props.isSelected}
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index 4a29c1949..d3045ae2f 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -1,7 +1,7 @@
@import "../globalCssVariables";
.ProseMirror {
width: 100%;
- height: auto;
+ height: 100%;
min-height: 100%;
font-family: $serif;
}
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 376b5a574..99801ecff 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 } from "mobx";
+import { action, IReactionDisposer, observable, reaction, runInAction, computed, trace } from "mobx";
import { observer } from "mobx-react";
import { baseKeymap } from "prosemirror-commands";
import { history } from "prosemirror-history";
@@ -9,11 +9,11 @@ import { NodeType } 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';
@@ -33,6 +33,9 @@ import { Templates } from '../Templates';
import { FieldView, FieldViewProps } from "./FieldView";
import "./FormattedTextBox.scss";
import React = require("react");
+import { DateField } from '../../../new_fields/DateField';
+import { thisExpression } from 'babel-types';
+import { Utils } from '../../../Utils';
library.add(faEdit);
library.add(faSmile);
@@ -45,6 +48,7 @@ export interface FormattedTextBoxProps {
hideOnLeave?: boolean;
height?: string;
color?: string;
+ outer_div?: (domminus: HTMLElement) => void;
}
const richTextSchema = createSchema({
@@ -60,12 +64,14 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
return FieldView.LayoutString(FormattedTextBox, fieldStr);
}
private _ref: React.RefObject<HTMLDivElement>;
+ private _outerdiv?: (dominus: HTMLElement) => void;
private _proseRef?: HTMLDivElement;
private _editorView: Opt<EditorView>;
private _toolTipTextMenu: TooltipTextMenu | undefined = undefined;
private _applyingChange: boolean = false;
private _linkClicked = "";
private _reactionDisposer: Opt<IReactionDisposer>;
+ private _textReactionDisposer: Opt<IReactionDisposer>;
private _proxyReactionDisposer: Opt<IReactionDisposer>;
private dropDisposer?: DragManager.DragDropDisposer;
public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
@@ -87,16 +93,31 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
public static GetDocFromUrl(url: string) {
if (url.startsWith(document.location.origin)) {
- let start = url.indexOf(window.location.origin);
- let path = url.substr(start, url.length - start);
- let docid = path.replace(DocServer.prepend("/doc/"), "").split("?")[0];
+ const split = new URL(url).pathname.split("doc/");
+ const docid = split[split.length - 1];
return docid;
}
return "";
}
+ @undoBatch
+ public setFontColor(color: string) {
+ let self = this;
+ if (this._editorView!.state.selection.from === this._editorView!.state.selection.to) return false;
+ if (this._editorView!.state.selection.to - this._editorView!.state.selection.from > this._editorView!.state.doc.nodeSize - 3) {
+ this.props.Document.color = color;
+ }
+ let colorMark = this._editorView!.state.schema.mark(this._editorView!.state.schema.marks.pFontColor, { color: color });
+ this._editorView!.dispatch(this._editorView!.state.tr.addMark(this._editorView!.state.selection.from,
+ this._editorView!.state.selection.to, colorMark));
+ return true;
+ }
+
constructor(props: FieldViewProps) {
super(props);
+ if (this.props.outer_div) {
+ this._outerdiv = this.props.outer_div;
+ }
this._ref = React.createRef();
if (this.props.isOverlay) {
@@ -104,20 +125,24 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
+ @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) {
const state = this._editorView.state.apply(tx);
this._editorView.updateState(state);
this._applyingChange = true;
- Doc.SetOnPrototype(this.props.Document, this.props.fieldKey, new RichTextField(JSON.stringify(state.toJSON())));
- Doc.SetOnPrototype(this.props.Document, "documentText", 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.props.Document.title);
+ 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.props.Document.proto ? this.props.Document.proto : this.props.Document;
- target.title = "-" + titlestr + (str.length > 40 ? "..." : "");
+ this.dataDoc.title = "-" + titlestr + (str.length > 40 ? "..." : "");
}
}
}
@@ -144,25 +169,28 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
e.stopPropagation();
} else {
if (de.data instanceof DragManager.DocumentDragData) {
- let ldocs = Cast(this.props.Document.subBulletDocs, listSpec(Doc));
- if (!ldocs) {
- this.props.Document.subBulletDocs = new List<Doc>([]);
- }
- ldocs = Cast(this.props.Document.subBulletDocs, listSpec(Doc));
- if (!ldocs) return;
- if (!ldocs || !ldocs[0] || ldocs[0] instanceof Promise || StrCast((ldocs[0] as Doc).layout).indexOf("CollectionView") === -1) {
- ldocs.splice(0, 0, Docs.StackingDocument([], { title: StrCast(this.props.Document.title) + "-subBullets", x: NumCast(this.props.Document.x), y: NumCast(this.props.Document.y) + NumCast(this.props.Document.height), width: 300, height: 300 }));
- this.props.addDocument && this.props.addDocument(ldocs[0] as Doc);
- this.props.Document.templates = new List<string>([Templates.Bullet.Layout]);
- this.props.Document.isBullet = true;
- }
- let stackDoc = (ldocs[0] as Doc);
- if (de.data.moveDocument) {
- de.data.moveDocument(de.data.draggedDocuments[0], stackDoc, (doc) => {
- Cast(stackDoc.data, listSpec(Doc))!.push(doc);
- return true;
- });
- }
+ this.props.Document.layout = de.data.draggedDocuments[0];
+ de.data.draggedDocuments[0].isTemplate = true;
+ e.stopPropagation();
+ // let ldocs = Cast(this.dataDoc.subBulletDocs, listSpec(Doc));
+ // if (!ldocs) {
+ // this.dataDoc.subBulletDocs = new List<Doc>([]);
+ // }
+ // ldocs = Cast(this.dataDoc.subBulletDocs, listSpec(Doc));
+ // if (!ldocs) return;
+ // if (!ldocs || !ldocs[0] || ldocs[0] instanceof Promise || StrCast((ldocs[0] as Doc).layout).indexOf("CollectionView") === -1) {
+ // ldocs.splice(0, 0, Docs.StackingDocument([], { title: StrCast(this.dataDoc.title) + "-subBullets", x: NumCast(this.props.Document.x), y: NumCast(this.props.Document.y) + NumCast(this.props.Document.height), width: 300, height: 300 }));
+ // this.props.addDocument && this.props.addDocument(ldocs[0] as Doc);
+ // this.props.Document.templates = new List<string>([Templates.Bullet.Layout]);
+ // this.props.Document.isBullet = true;
+ // }
+ // let stackDoc = (ldocs[0] as Doc);
+ // if (de.data.moveDocument) {
+ // de.data.moveDocument(de.data.draggedDocuments[0], stackDoc, (doc) => {
+ // Cast(stackDoc.data, listSpec(Doc))!.push(doc);
+ // return true;
+ // });
+ // }
}
}
}
@@ -201,21 +229,41 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
this._reactionDisposer = reaction(
() => {
- const field = this.props.Document ? Cast(this.props.Document[this.props.fieldKey], RichTextField) : undefined;
+ 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 => {
+ if (StrCast(this.props.Document.layout).indexOf("\"" + this.props.fieldKey + "\"") !== -1) { // bcz: UGH! why is this needed... something is happening out of order. test with making a collection, then adding a text note and converting that to a template field.
+ this._editorView && !this._applyingChange &&
+ this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field2)));
+ }
+ }
);
- this.setupEditor(config, this.props.Document, this.props.fieldKey);
+
+ 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);
}
private setupEditor(config: any, doc: Doc, fieldKey: string) {
let field = doc ? Cast(doc[fieldKey], RichTextField) : undefined;
let startup = StrCast(doc.documentText);
startup = startup.startsWith("@@@") ? startup.replace("@@@", "") : "";
- if (!startup && !field && doc) {
- startup = StrCast(doc[fieldKey]);
+ if (!field && doc) {
+ let text = StrCast(doc[fieldKey]);
+ if (text) {
+ startup = text;
+ } else if (Cast(doc[fieldKey], "number")) {
+ startup = NumCast(doc[fieldKey], 99).toString();
+ }
}
if (this._proseRef) {
this._editorView = new EditorView(this._proseRef, {
@@ -235,46 +283,60 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
if (this.props.selectOnLoad) {
if (!this.props.isOverlay) this.props.select(false);
else this._editorView!.focus();
+ this.tryUpdateHeight();
}
}
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 => {
if (e.button === 0 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) {
e.stopPropagation();
if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip) {
- this._toolTipTextMenu.tooltip.style.opacity = "0";
+ //this._toolTipTextMenu.tooltip.style.opacity = "0";
}
}
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;
+ 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, document => this.props.addDocTab(document, "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];
}
@@ -289,7 +351,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
onPointerUp = (e: React.PointerEvent): void => {
if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip) {
- this._toolTipTextMenu.tooltip.style.opacity = "1";
+ //this._toolTipTextMenu.tooltip.style.opacity = "1";
}
if (e.buttons === 1 && this.props.isSelected() && !e.altKey) {
e.stopPropagation();
@@ -307,7 +369,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();
}
}
@@ -334,6 +397,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
return self._toolTipTextMenu = new TooltipTextMenu(_editorView, myprops);
}
});
+ //this.props.Document.tooltip = self._toolTipTextMenu;
}
tooltipLinkingMenuPlugin() {
@@ -360,23 +424,28 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
// stop propagation doesn't seem to stop propagation of native keyboard events.
// so we set a flag on the native event that marks that the event's been handled.
(e.nativeEvent as any).DASHFormattedTextBoxHandled = true;
- if (StrCast(this.props.Document.title).startsWith("-") && this._editorView) {
+ 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.props.Document.proto ? this.props.Document.proto : this.props.Document;
- target.title = "-" + titlestr + (str.length > 40 ? "..." : "");
+ this.dataDoc.title = "-" + titlestr + (str.length > 40 ? "..." : "");
}
if (!this._undoTyping) {
this._undoTyping = UndoManager.StartBatch("undoTyping");
}
+ this.tryUpdateHeight();
+ }
+
+ @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.props.Document.nativeHeight, 0);
+ 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.props.Document.proto!.nativeHeight = nh ? sh : undefined;
+ this.dataDoc.nativeHeight = nh ? sh : undefined;
}
}
@@ -398,16 +467,18 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: subitems });
}
render() {
+ let self = this;
let style = this.props.isOverlay ? "scroll" : "hidden";
- let rounded = NumCast(this.props.Document.borderRounding) < 0 ? "-rounded" : "";
+ 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={{
height: this.props.height ? this.props.height : undefined,
background: this.props.hideOnLeave ? "rgba(0,0,0,0.4)" : undefined,
opacity: this.props.hideOnLeave ? (this._entered || this.props.isSelected() || this.props.Document.libraryBrush ? 1 : 0.1) : 1,
- color: this.props.color ? this.props.color : this.props.hideOnLeave ? "white" : "initial",
+ color: this.props.color ? this.props.color : this.props.hideOnLeave ? "white" : "inherit",
pointerEvents: interactive ? "all" : "none",
fontSize: "13px"
}}
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 f56a2d926..c3ee1e823 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,14 +1,14 @@
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faImage } from '@fortawesome/free-solid-svg-icons';
-import { action, observable } 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
-import { Doc, HeightSym, WidthSym } from '../../../new_fields/Doc';
+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 } from '../../../new_fields/Types';
-import { ImageField } from '../../../new_fields/URLField';
+import { Cast, FieldValue, NumCast, StrCast, BoolCast } from '../../../new_fields/Types';
+import { ImageField, AudioField } from '../../../new_fields/URLField';
import { Utils } from '../../../Utils';
import { DragManager } from '../../util/DragManager';
import { undoBatch } from '../../util/UndoManager';
@@ -20,32 +20,50 @@ import { positionSchema } from './DocumentView';
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';
+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"
+ 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);
@observer
export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageDocument) {
- public static LayoutString() { return FieldView.LayoutString(ImageBox); }
+ public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(ImageBox, fieldKey); }
private _imgRef: React.RefObject<HTMLImageElement> = React.createRef();
private _downX: number = 0;
private _downY: number = 0;
private _lastTap: number = 0;
- @observable private _photoIndex: number = 0;
@observable private _isOpen: boolean = false;
private dropDisposer?: DragManager.DragDropDisposer;
+ @computed get dataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; }
+
protected createDropTarget = (ele: HTMLDivElement) => {
if (this.dropDisposer) {
@@ -61,23 +79,29 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
console.log("IMPLEMENT ME PLEASE");
}
+ @computed get extensionDoc() { return Doc.resolvedFieldDataDoc(this.dataDoc, this.props.fieldKey, "Alternates"); }
@undoBatch
drop = (e: Event, de: DragManager.DropEvent) => {
if (de.data instanceof DragManager.DocumentDragData) {
de.data.droppedDocuments.forEach(action((drop: Doc) => {
- let layout = StrCast(drop.backgroundLayout);
- if (layout.indexOf(ImageBox.name) !== -1) {
- let imgData = this.props.Document[this.props.fieldKey];
- if (imgData instanceof ImageField) {
- Doc.SetOnPrototype(this.props.Document, "data", new List([imgData]));
- }
- let imgList = Cast(this.props.Document[this.props.fieldKey], listSpec(ImageField), [] as any[]);
- if (imgList) {
- let field = drop.data;
- if (field instanceof ImageField) imgList.push(field);
- else if (field instanceof List) imgList.concat(field);
+ if (de.mods === "AltKey" && /*this.dataDoc !== this.props.Document &&*/ drop.data instanceof ImageField) {
+ Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new ImageField(drop.data.url);
+ e.stopPropagation();
+ } else if (de.mods === "CtrlKey") {
+ if (this.extensionDoc !== this.dataDoc) {
+ let layout = StrCast(drop.backgroundLayout);
+ if (layout.indexOf(ImageBox.name) !== -1) {
+ let imgData = this.extensionDoc.Alternates;
+ if (!imgData) {
+ Doc.GetProto(this.extensionDoc).Alternates = new List([]);
+ }
+ let imgList = Cast(this.extensionDoc.Alternates, listSpec(Doc), [] as any[]);
+ imgList && imgList.push(drop);
+ e.stopPropagation();
+ }
}
+ } else if (!this.props.isSelected()) {
e.stopPropagation();
}
}));
@@ -88,7 +112,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
onPointerDown = (e: React.PointerEvent): void => {
if (e.shiftKey && e.ctrlKey) {
e.stopPropagation(); // allows default system drag drop of images with shift+ctrl only
- } else e.preventDefault();
+ }
// if (Date.now() - this._lastTap < 300) {
// if (e.buttons === 1) {
// this._downX = e.clientX;
@@ -109,31 +133,71 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
e.stopPropagation();
}
+ @action
lightbox = (images: string[]) => {
if (this._isOpen) {
return (<Lightbox
- mainSrc={images[this._photoIndex]}
- nextSrc={images[(this._photoIndex + 1) % images.length]}
- prevSrc={images[(this._photoIndex + images.length - 1) % images.length]}
+ mainSrc={images[this.Document.curPage || 0]}
+ nextSrc={images[((this.Document.curPage || 0) + 1) % images.length]}
+ prevSrc={images[((this.Document.curPage || 0) + images.length - 1) % images.length]}
onCloseRequest={action(() =>
this._isOpen = false
)}
onMovePrevRequest={action(() =>
- this._photoIndex = (this._photoIndex + images.length - 1) % images.length
+ this.Document.curPage = ((this.Document.curPage || 0) + images.length - 1) % images.length
)}
onMoveNextRequest={action(() =>
- this._photoIndex = (this._photoIndex + 1) % images.length
+ this.Document.curPage = ((this.Document.curPage || 0) + 1) % images.length
)}
/>);
}
}
+ 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({ description: "Record 1sec audio", event: this.recordAudioAnnotation, icon: "expand-arrows-alt" });
subitems.push({
description: "Rotate", event: action(() => {
let proto = Doc.GetProto(this.props.Document);
@@ -154,7 +218,6 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
@action
onDotDown(index: number) {
- this._photoIndex = index;
this.Document.curPage = index;
}
@@ -164,7 +227,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
let left = (nativeWidth - paths.length * dist) / 2;
return paths.map((p, i) =>
<div className="imageBox-placer" key={i} >
- <div className="imageBox-dot" style={{ background: (i === this._photoIndex ? "black" : "gray"), transform: `translate(${i * dist + left}px, 0px)` }} onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); this.onDotDown(i); }} />
+ <div className="imageBox-dot" style={{ background: (i === this.Document.curPage ? "black" : "gray"), transform: `translate(${i * dist + left}px, 0px)` }} onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); this.onDotDown(i); }} />
</div>
);
}
@@ -175,7 +238,8 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
return url.href;
}
let ext = path.extname(url.href);
- return url.href.replace(ext, this._curSuffix + ext);
+ const suffix = this.props.renderDepth <= 1 ? "_o" : this._curSuffix;
+ return url.href.replace(ext, suffix + ext);
}
@observable _smallRetryCount = 1;
@@ -193,6 +257,68 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
}
}
_curSuffix = "_m";
+
+ resize(srcpath: string, layoutdoc: Doc) {
+ requestImageSize(srcpath)
+ .then((size: any) => {
+ let aspect = size.height / size.width;
+ let rotation = NumCast(this.dataDoc.rotation) % 180;
+ if (rotation === 90 || rotation === 270) aspect = 1 / aspect;
+ if (Math.abs(layoutdoc[HeightSym]() / layoutdoc[WidthSym]() - aspect) > 0.01) {
+ setTimeout(action(() => {
+ layoutdoc.height = layoutdoc[WidthSym]() * aspect;
+ layoutdoc.nativeHeight = size.height;
+ layoutdoc.nativeWidth = size.width;
+ }), 0);
+ }
+ })
+ .catch((err: any) => {
+ console.log(err);
+ });
+ }
+
+ @observable _audioState = 0;
+
+ @action
+ onPointerEnter = () => {
+ let self = this;
+ let audioAnnos = DocListCast(this.extensionDoc.audioAnnotations);
+ if (audioAnnos.length && this._audioState === 0) {
+ let 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;
@@ -203,33 +329,47 @@ 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) {
- let field = this.Document[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];
// if (w < 100 && this._smallRetryCount < 10) this._curSuffix = "_s";
// else if (w < 600 && this._mediumRetryCount < 10) this._curSuffix = "_m";
// else if (this._largeRetryCount < 10) this._curSuffix = "_l";
if (field instanceof ImageField) paths = [this.choosePath(field.url)];
- else if (field instanceof List) paths = field.filter(val => val instanceof ImageField).map(p => this.choosePath((p as ImageField).url));
+ paths.push(...altpaths);
// }
let interactive = InkingControl.Instance.selectedTool ? "" : "-interactive";
- let rotation = NumCast(this.props.Document.rotation, 0);
- let aspect = (rotation % 180) ? this.props.Document[HeightSym]() / this.props.Document[WidthSym]() : 1;
+ let rotation = NumCast(this.dataDoc.rotation, 0);
+ let aspect = (rotation % 180) ? this.dataDoc[HeightSym]() / this.dataDoc[WidthSym]() : 1;
let shift = (rotation % 180) ? (nativeHeight - nativeWidth / aspect) / 2 : 0;
+ let srcpath = paths[Math.min(paths.length, this.Document.curPage || 0)];
+
+ if (!this.props.Document.ignoreAspect && !this.props.leaveNativeSize) this.resize(srcpath, this.props.Document);
+
return (
<div id={id} className={`imageBox-cont${interactive}`} style={{ background: "transparent" }}
onPointerDown={this.onPointerDown}
onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}>
<img id={id}
key={this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys
- src={paths[Math.min(paths.length, this._photoIndex)]}
+ src={srcpath}
style={{ transform: `translate(0px, ${shift}px) rotate(${rotation}deg) scale(${aspect})` }}
- // style={{ objectFit: (this._photoIndex === 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)} */}
</div>);
}
diff --git a/src/client/views/nodes/KeyValueBox.scss b/src/client/views/nodes/KeyValueBox.scss
index 20cae03d4..87a9565e8 100644
--- a/src/client/views/nodes/KeyValueBox.scss
+++ b/src/client/views/nodes/KeyValueBox.scss
@@ -91,12 +91,12 @@ $header-height: 30px;
width: 4px;
float: left;
height: 30px;
- width: 10px;
+ width: 5px;
z-index: 20;
right: 0;
top: 0;
- border-radius: 10px;
- background: gray;
+ border-radius: 0;
+ background: black;
pointer-events: all;
}
.keyValueBox-dividerDragger{
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 3d626eef0..c9dd9a64e 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -2,18 +2,36 @@
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";
import React = require("react");
-import { NumCast, Cast, FieldValue } from "../../../new_fields/Types";
-import { Doc, Field } from "../../../new_fields/Doc";
-import { ComputedField } from "../../../fields/ScriptField";
+import { NumCast, Cast, FieldValue, StrCast } from "../../../new_fields/Types";
+import { Doc, Field, FieldResult, DocListCastAsync } from "../../../new_fields/Doc";
+import { ComputedField, ScriptField } from "../../../new_fields/ScriptField";
+import { SetupDrag } from "../../util/DragManager";
+import { Docs } from "../../documents/Documents";
+import { RawDataOperationParameters } from "../../northstar/model/idea/idea";
+import { Templates } from "../Templates";
+import { List } from "../../../new_fields/List";
+import { TextField } from "../../util/ProsemirrorCopy/prompt";
+import { RichTextField } from "../../../new_fields/RichTextField";
+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>();
+ private _keyHeader = React.createRef<HTMLTableHeaderCellElement>();
+ @observable private rows: KeyValuePair[] = [];
public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(KeyValueBox, fieldStr); }
@observable private _keyInput: string = "";
@@ -36,31 +54,46 @@ 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("=");
value = eq ? value.substr(1) : value;
- let dubEq = value.startsWith(":=");
+ const dubEq = value.startsWith(":=") ? "computed" : value.startsWith(";=") ? "script" : false;
value = dubEq ? value.substr(2) : value;
- let options: ScriptOptions = { addReturn: true };
+ 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;
}
- let field = new ComputedField(script);
- if (!dubEq) {
- let res = script.run();
+ 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 (type === "computed") {
+ field = new ComputedField(script);
+ } else if (type === "script") {
+ field = new ScriptField(script);
+ } else {
+ let res = script.run({ this: target });
if (!res.success) return false;
field = res.result;
}
if (Field.IsField(field, true)) {
- let target = eq ? doc : Doc.GetProto(doc);
target[key] = field;
return true;
}
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();
@@ -89,8 +122,16 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
let rows: JSX.Element[] = [];
let i = 0;
+ const self = this;
for (let key of Object.keys(ids).sort()) {
- rows.push(<KeyValuePair doc={realDoc} keyWidth={100 - this.splitPercentage} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} key={key} keyName={key} />);
+ rows.push(<KeyValuePair doc={realDoc} ref={(function () {
+ let oldEl: KeyValuePair | undefined;
+ return (el: KeyValuePair) => {
+ if (oldEl) self.rows.splice(self.rows.indexOf(oldEl), 1);
+ oldEl = el;
+ if (el) self.rows.push(el);
+ };
+ })()} keyWidth={100 - this.splitPercentage} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} key={key} keyName={key} />);
}
return rows;
}
@@ -134,6 +175,56 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
document.addEventListener('pointerup', this.onDividerUp);
}
+ getTemplate = async () => {
+ 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)) {
+ await this.createTemplateField(parent, row);
+ row.uncheck();
+ }
+ return parent;
+ }
+
+ createTemplateField = async (parentStackingDoc: Doc, row: KeyValuePair) => {
+ let metaKey = row.props.keyName;
+ let sourceDoc = await Cast(this.props.Document.data, Doc);
+ if (!sourceDoc) {
+ return;
+ }
+
+ let fieldTemplate = await this.inferType(sourceDoc[metaKey], metaKey);
+ let previousViewType = fieldTemplate.viewType;
+ Doc.MakeTemplate(fieldTemplate, metaKey, Doc.GetProto(parentStackingDoc));
+ previousViewType && (fieldTemplate.viewType = previousViewType);
+
+ Cast(parentStackingDoc.data, listSpec(Doc))!.push(fieldTemplate);
+ }
+
+ 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.Create.TextDocument(options);
+ } else if (data instanceof List) {
+ if (data.length === 0) {
+ return Docs.Create.StackingDocument([], options);
+ }
+ let first = await Cast(data[0], Doc);
+ if (!first) {
+ return Docs.Create.StackingDocument([], options);
+ }
+ switch (first.type) {
+ case "image":
+ return Docs.Create.StackingDocument([], options);
+ case "text":
+ return Docs.Create.TreeDocument([], options);
+ }
+ } else if (data instanceof ImageField) {
+ return Docs.Create.ImageDocument("https://image.flaticon.com/icons/png/512/23/23765.png", options);
+ }
+ return new Doc;
+ }
+
render() {
let dividerDragger = this.splitPercentage === 0 ? (null) :
<div className="keyValueBox-dividerDragger" style={{ transform: `translate(calc(${100 - this.splitPercentage}% - 5px), 0px)` }}>
@@ -144,7 +235,9 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
<table className="keyValueBox-table">
<tbody className="keyValueBox-tbody">
<tr className="keyValueBox-header">
- <th className="keyValueBox-key" style={{ width: `${100 - this.splitPercentage}%` }}>Key</th>
+ <th className="keyValueBox-key" style={{ width: `${100 - this.splitPercentage}%` }} ref={this._keyHeader}
+ onPointerDown={SetupDrag(this._keyHeader, this.getTemplate)}
+ >Key</th>
<th className="keyValueBox-fields" style={{ width: `${this.splitPercentage}%` }}>Fields</th>
</tr>
{this.createTable()}
@@ -154,4 +247,4 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
{dividerDragger}
</div>);
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/KeyValuePair.scss b/src/client/views/nodes/KeyValuePair.scss
index a1c5d5537..f78767234 100644
--- a/src/client/views/nodes/KeyValuePair.scss
+++ b/src/client/views/nodes/KeyValuePair.scss
@@ -3,6 +3,7 @@
.keyValuePair-td-key {
display:inline-block;
+
.keyValuePair-td-key-container{
width:100%;
height:100%;
@@ -10,14 +11,23 @@
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
+ align-items: center;
.keyValuePair-td-key-delete{
position: relative;
background-color: transparent;
color:red;
}
+ .keyValuePair-td-key-check {
+ position: relative;
+ margin: 0;
+ }
.keyValuePair-keyField {
width:100%;
- text-align: center;
+ margin-left: 20px;
+ margin-top: -1px;
+ font-family: monospace;
+ // text-align: center;
+ align-self: center;
position: relative;
overflow: auto;
}
@@ -26,12 +36,25 @@
.keyValuePair-td-value {
display:inline-block;
overflow: scroll;
- img {
- max-height: 36px;
- width: auto;
- }
- .videoBox-cont{
- width: auto;
- max-height: 36px;
+ font-family: monospace;
+ height: 30px;
+ .keyValuePair-td-value-container {
+ display: flex;
+ align-items: center;
+ align-content: center;
+ flex-direction: row;
+ justify-content: space-between;
+ flex-wrap: nowrap;
+ width: 100%;
+ height: 100%;
+
+ img {
+ max-height: 36px;
+ width: auto;
+ }
+ .videoBox-cont{
+ width: auto;
+ max-height: 36px;
+ }
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 420a1ad94..064f3edcc 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -11,8 +11,11 @@ import "./KeyValuePair.scss";
import React = require("react");
import { Doc, Opt, Field } from '../../../new_fields/Doc';
import { FieldValue } from '../../../new_fields/Types';
-import { ComputedField } from '../../../fields/ScriptField';
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
@@ -24,15 +27,41 @@ export interface KeyValuePairProps {
}
@observer
export class KeyValuePair extends React.Component<KeyValuePairProps> {
+ @observable private isPointerOver = false;
+ @observable public isChecked = false;
+ private checkbox = React.createRef<HTMLInputElement>();
+
+ @action
+ handleCheck = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this.isChecked = e.currentTarget.checked;
+ }
+
+ @action
+ uncheck = () => {
+ this.checkbox.current!.checked = false;
+ 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,
+ DataDoc: this.props.doc,
ContainingCollectionView: undefined,
fieldKey: this.props.keyName,
+ fieldExt: "",
isSelected: returnFalse,
select: emptyFunction,
- isTopMost: false,
+ renderDepth: 1,
selectOnLoad: false,
active: returnFalse,
whenActiveChanged: emptyFunction,
@@ -43,12 +72,26 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
addDocTab: returnZero,
};
let contents = <FieldView {...props} />;
- let fieldKey = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? props.fieldKey : "(" + props.fieldKey + ")";
+ // let fieldKey = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? props.fieldKey : "(" + props.fieldKey + ")";
+ 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 };
+
return (
- <tr className={this.props.rowStyle}>
+ <tr className={this.props.rowStyle} onPointerEnter={action(() => this.isPointerOver = true)} onPointerLeave={action(() => this.isPointerOver = false)}>
<td className="keyValuePair-td-key" style={{ width: `${this.props.keyWidth}%` }}>
<div className="keyValuePair-td-key-container">
- <button className="keyValuePair-td-key-delete" onClick={() => {
+ <button style={hover} className="keyValuePair-td-key-delete" onClick={() => {
if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1) {
props.Document[props.fieldKey] = undefined;
}
@@ -56,22 +99,29 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
}}>
X
</button>
- <div className="keyValuePair-keyField">{fieldKey}</div>
+ <input
+ className={"keyValuePair-td-key-check"}
+ type="checkbox"
+ style={hover}
+ onChange={this.handleCheck}
+ ref={this.checkbox}
+ />
+ <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}%` }} onContextMenu={this.onContextMenu}>
+ <div className="keyValuePair-td-value-container">
+ <EditableView
+ contents={contents}
+ height={36}
+ GetValue={() => {
+ return Field.toKeyValueString(props.Document, props.fieldKey);
+ }}
+ SetValue={(value: string) =>
+ KeyValueBox.SetField(props.Document, props.fieldKey, value)}>
+ </EditableView>
</div>
</td>
- <td className="keyValuePair-td-value" style={{ width: `${100 - this.props.keyWidth}%` }}>
- <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 "";
- }}
- SetValue={(value: string) =>
- KeyValueBox.SetField(props.Document, props.fieldKey, value)}>
- </EditableView></td>
</tr>
);
}
diff --git a/src/client/views/nodes/LinkBox.scss b/src/client/views/nodes/LinkBox.scss
deleted file mode 100644
index 639f83b38..000000000
--- a/src/client/views/nodes/LinkBox.scss
+++ /dev/null
@@ -1,66 +0,0 @@
-@import "../globalCssVariables";
-.link-container {
- width: 100%;
- height: 50px;
- display: flex;
- flex-direction: row;
- border-top: 0.5px solid #bababa;
-}
-
-.info-container {
- width: 65%;
- padding-top: 5px;
- padding-left: 5px;
- display: flex;
- flex-direction: column
-}
-
-.link-name {
- font-size: 11px;
-}
-
-.doc-name {
- font-size: 8px;
-}
-
-.button-container {
- width: 35%;
- padding-top: 8px;
- display: flex;
- flex-direction: row;
-}
-
-.button {
- height: 20px;
- width: 20px;
- margin: 8px 4px;
- border-radius: 50%;
- opacity: 0.9;
- pointer-events: auto;
- background-color: $dark-color;
- color: $light-color;
- text-transform: uppercase;
- letter-spacing: 2px;
- font-size: 60%;
- transition: transform 0.2s;
-}
-
-.button:hover {
- background: $main-accent;
- cursor: pointer;
-}
-
-// .fa-icon-view {
-// margin-left: 3px;
-// margin-top: 5px;
-// }
-
-.fa-icon-edit {
- margin-left: 6px;
- margin-top: 6px;
-}
-
-.fa-icon-delete {
- margin-left: 7px;
- margin-top: 6px;
-} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
deleted file mode 100644
index 68b692aad..000000000
--- a/src/client/views/nodes/LinkBox.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faEdit, faEye, faTimes } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { observer } from "mobx-react";
-import { DocumentManager } from "../../util/DocumentManager";
-import { undoBatch } from "../../util/UndoManager";
-import { CollectionDockingView } from "../collections/CollectionDockingView";
-import './LinkBox.scss';
-import React = require("react");
-import { Doc } from '../../../new_fields/Doc';
-import { Cast, NumCast } from '../../../new_fields/Types';
-import { listSpec } from '../../../new_fields/Schema';
-import { action } from 'mobx';
-
-
-library.add(faEye);
-library.add(faEdit);
-library.add(faTimes);
-
-interface Props {
- linkDoc: Doc;
- linkName: String;
- pairedDoc: Doc;
- type: String;
- showEditor: () => void;
-}
-
-@observer
-export class LinkBox extends React.Component<Props> {
-
- @undoBatch
- onViewButtonPressed = async (e: React.PointerEvent): Promise<void> => {
- e.stopPropagation();
- DocumentManager.Instance.jumpToDocument(this.props.pairedDoc, e.altKey);
- }
-
- onEditButtonPressed = (e: React.PointerEvent): void => {
- e.stopPropagation();
-
- this.props.showEditor();
- }
-
- @action
- onDeleteButtonPressed = async (e: React.PointerEvent): Promise<void> => {
- e.stopPropagation();
- const [linkedFrom, linkedTo] = await Promise.all([Cast(this.props.linkDoc.linkedFrom, Doc), Cast(this.props.linkDoc.linkedTo, Doc)]);
- if (linkedFrom) {
- const linkedToDocs = Cast(linkedFrom.linkedToDocs, listSpec(Doc));
- if (linkedToDocs) {
- linkedToDocs.splice(linkedToDocs.indexOf(this.props.linkDoc), 1);
- }
- }
- if (linkedTo) {
- const linkedFromDocs = Cast(linkedTo.linkedFromDocs, listSpec(Doc));
- if (linkedFromDocs) {
- linkedFromDocs.splice(linkedFromDocs.indexOf(this.props.linkDoc), 1);
- }
- }
- }
-
- render() {
-
- return (
- //<LinkEditor linkBox={this} linkDoc={this.props.linkDoc} />
- <div className="link-container">
- <div className="info-container" onPointerDown={this.onViewButtonPressed}>
- <div className="link-name">
- <p>{this.props.linkName}</p>
- </div>
- <div className="doc-name">
- <p>{this.props.type}{this.props.pairedDoc.Title}</p>
- </div>
- </div>
-
- <div className="button-container">
- {/* <div title="Follow Link" className="button" onPointerDown={this.onViewButtonPressed}>
- <FontAwesomeIcon className="fa-icon-view" icon="eye" size="sm" /></div> */}
- <div title="Edit Link" className="button" onPointerDown={this.onEditButtonPressed}>
- <FontAwesomeIcon className="fa-icon-edit" icon="edit" size="sm" /></div>
- <div title="Delete Link" className="button" onPointerDown={this.onDeleteButtonPressed}>
- <FontAwesomeIcon className="fa-icon-delete" icon="times" size="sm" /></div>
- </div>
- </div>
- );
- }
-} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkEditor.scss b/src/client/views/nodes/LinkEditor.scss
index 9629585d7..fc5f2410c 100644
--- a/src/client/views/nodes/LinkEditor.scss
+++ b/src/client/views/nodes/LinkEditor.scss
@@ -1,42 +1,145 @@
@import "../globalCssVariables";
-.edit-container {
+
+.linkEditor {
width: 100%;
height: auto;
+ font-size: 12px; // TODO
+}
+
+.linkEditor-back {
+ margin-bottom: 6px;
+}
+
+.linkEditor-info {
+ border-bottom: 0.5px solid $light-color-secondary;
+ padding-bottom: 6px;
+ margin-bottom: 6px;
+ display: flex;
+ justify-content: space-between;
+
+ .linkEditor-linkedTo {
+ width: calc(100% - 26px);
+ }
+}
+
+.linkEditor-button {
+ width: 20px;
+ height: 20px;
+ margin-left: 6px;
+ padding: 0;
+ // font-size: 12px;
+ border-radius: 10px;
+
+ &:disabled {
+ background-color: gray;
+ }
+}
+
+.linkEditor-groupsLabel {
display: flex;
- flex-direction: column;
+ justify-content: space-between;
}
-.name-input {
- margin-bottom: 10px;
- padding: 5px;
+.linkEditor-group {
+ background-color: $light-color-secondary;
+ padding: 6px;
+ margin: 3px 0;
+ border-radius: 3px;
+
+ .linkEditor-group-row {
+ display: flex;
+ margin-bottom: 3px;
+
+ .linkEditor-group-row-label {
+ margin-right: 6px;
+ }
+ }
+
+ .linkEditor-metadata-row {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 6px;
+
+ .linkEditor-error {
+ border-color: red;
+ }
+
+ input {
+ width: calc(50% - 16px);
+ height: 20px;
+ }
+
+ button {
+ width: 20px;
+ height: 20px;
+ margin-left: 3px;
+ padding: 0;
+ font-size: 10px;
+ }
+ }
+}
+
+
+.linkEditor-dropdown {
+ width: 100%;
+ position: relative;
+ z-index: 999;
+
+ input {
+ width: 100%;
+ }
+
+ .linkEditor-options-wrapper {
+ width: 100%;
+ position: absolute;
+ top: 19px;
+ left: 0;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .linkEditor-option {
+ background-color: $light-color-secondary;
+ border: 1px solid $intermediate-color;
+ border-top: 0;
+ padding: 3px;
+ cursor: pointer;
+
+ &: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;
- border: 1px solid #bababa;
-}
-
-.description-input {
- font-size: 11px;
- padding: 5px;
- margin-bottom: 10px;
- border: 1px solid #bababa;
-}
-
-.save-button {
- width: 50px;
- height: 22px;
- pointer-events: auto;
- background-color: $dark-color;
- color: $light-color;
- text-transform: uppercase;
- letter-spacing: 2px;
- padding: 2px;
- font-size: 10px;
- margin: 0 auto;
- transition: transform 0.2s;
- text-align: center;
- line-height: 20px;
-}
-
-.save-button:hover {
- background: $main-accent;
- cursor: pointer;
+ font-weight: bold;
+
+ &:hover {
+ background-color: $light-color;
+ }
+}
+
+.linkEditor-group-buttons {
+ height: 20px;
+ display: flex;
+ justify-content: flex-end;
+ margin-top: 5px;
+
+ .linkEditor-button {
+ margin-left: 6px;
+ }
} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index 71a423338..afde85b69 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -1,57 +1,399 @@
-import { observable, computed, action } from "mobx";
+import { observable, computed, action, trace } from "mobx";
import React = require("react");
-import { SelectionManager } from "../../util/SelectionManager";
import { observer } from "mobx-react";
import './LinkEditor.scss';
-import { props } from "bluebird";
-import { DocumentView } from "./DocumentView";
-import { link } from "fs";
-import { StrCast } from "../../../new_fields/Types";
+import { StrCast, Cast, FieldValue } from "../../../new_fields/Types";
import { Doc } from "../../../new_fields/Doc";
+import { LinkManager } from "../../util/LinkManager";
+import { Docs } from "../../documents/Documents";
+import { Utils } from "../../../Utils";
+import { faArrowLeft, faEllipsisV, faTable, faTrash, faCog, faExchangeAlt, faTimes, faPlus } from '@fortawesome/free-solid-svg-icons';
+import { library } from "@fortawesome/fontawesome-svg-core";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { SetupDrag } from "../../util/DragManager";
+
+library.add(faArrowLeft, faEllipsisV, faTable, faTrash, faCog, faExchangeAlt, faTimes, faPlus);
-interface Props {
- linkDoc: Doc;
- showLinks: () => void;
-}
+interface GroupTypesDropdownProps {
+ groupType: string;
+ setGroupType: (group: string) => void;
+}
+// this dropdown could be generalized
@observer
-export class LinkEditor extends React.Component<Props> {
+class GroupTypesDropdown extends React.Component<GroupTypesDropdownProps> {
+ @observable private _searchTerm: string = this.props.groupType;
+ @observable private _groupType: string = this.props.groupType;
+ @observable private _isEditing: boolean = false;
- @observable private _nameInput: string = StrCast(this.props.linkDoc.title);
- @observable private _descriptionInput: string = StrCast(this.props.linkDoc.linkDescription);
+ @action
+ createGroup = (groupType: string): void => {
+ this.props.setGroupType(groupType);
+ LinkManager.Instance.addGroupType(groupType);
+ }
+ @action
+ onChange = (val: string): void => {
+ this._searchTerm = val;
+ this._groupType = val;
+ this._isEditing = true;
+ }
- onSaveButtonPressed = (e: React.PointerEvent): void => {
- e.stopPropagation();
+ @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());
- let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
- linkDoc.title = this._nameInput;
- linkDoc.linkDescription = this._descriptionInput;
+ if (exactFound > -1) {
+ let groupType = groupOptions[exactFound];
+ this.props.setGroupType(groupType);
+ this._groupType = groupType;
+ } else {
+ this.createGroup(this._searchTerm);
+ this._groupType = this._searchTerm;
+ }
- this.props.showLinks();
+ 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 => {
+ if (this._searchTerm === "") return <></>;
+
+ 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()) > -1;
+
+ let options = groupOptions.map(groupType => {
+ 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 !== "") {
+ 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() {
+ 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>;
+ }
+ }
+}
+
+interface LinkMetadataEditorProps {
+ id: string;
+ groupType: string;
+ mdDoc: Doc;
+ mdKey: string;
+ mdValue: string;
+ changeMdIdKey: (id: string, newKey: string) => void;
+}
+@observer
+class LinkMetadataEditor extends React.Component<LinkMetadataEditorProps> {
+ @observable private _key: string = this.props.mdKey;
+ @observable private _value: string = this.props.mdValue;
+ @observable private _keyError: boolean = false;
+
+ @action
+ setMetadataKey = (value: string): void => {
+ let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);
+
+ // don't allow user to create existing key
+ let newIndex = groupMdKeys.findIndex(key => key.toUpperCase() === value.toUpperCase());
+ if (newIndex > -1) {
+ this._keyError = true;
+ this._key = value;
+ return;
+ } else {
+ this._keyError = false;
+ }
+
+ // set new value for key
+ let currIndex = groupMdKeys.findIndex(key => {
+ return StrCast(key).toUpperCase() === this._key.toUpperCase();
+ });
+ if (currIndex === -1) console.error("LinkMetadataEditor: key was not found");
+ groupMdKeys[currIndex] = value;
+
+ this.props.changeMdIdKey(this.props.id, value);
+ this._key = value;
+ LinkManager.Instance.setMetadataKeysForGroup(this.props.groupType, [...groupMdKeys]);
+ }
+
+ @action
+ setMetadataValue = (value: string): void => {
+ if (!this._keyError) {
+ this._value = value;
+ this.props.mdDoc[this._key] = value;
+ }
+ }
+
+ @action
+ removeMetadata = (): void => {
+ let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);
+
+ let index = groupMdKeys.findIndex(key => key.toUpperCase() === this._key.toUpperCase());
+ if (index === -1) console.error("LinkMetadataEditor: key was not found");
+ groupMdKeys.splice(index, 1);
+
+ LinkManager.Instance.setMetadataKeysForGroup(this.props.groupType, groupMdKeys);
+ this._key = "";
+ }
+
+ render() {
return (
- <div className="edit-container">
- <input onChange={this.onNameChanged} className="name-input" type="text" value={this._nameInput} placeholder="Name . . ."></input>
- <textarea onChange={this.onDescriptionChanged} className="description-input" value={this._descriptionInput} placeholder="Description . . ."></textarea>
- <div className="save-button" onPointerDown={this.onSaveButtonPressed}>SAVE</div>
+ <div className="linkEditor-metadata-row">
+ <input className={this._keyError ? "linkEditor-error" : ""} type="text" value={this._key === "new key" ? "" : this._key} placeholder="key" onChange={e => this.setMetadataKey(e.target.value)}></input>:
+ <input type="text" value={this._value} placeholder="value" onChange={e => this.setMetadataValue(e.target.value)}></input>
+ <button onClick={() => this.removeMetadata()}><FontAwesomeIcon icon="times" size="sm" /></button>
</div>
+ );
+ }
+}
+interface LinkGroupEditorProps {
+ sourceDoc: Doc;
+ linkDoc: Doc;
+ groupDoc: Doc;
+}
+@observer
+export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
+
+ private _metadataIds: Map<string, string> = new Map();
+
+ constructor(props: LinkGroupEditorProps) {
+ super(props);
+
+ let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(StrCast(props.groupDoc.type));
+ groupMdKeys.forEach(key => {
+ this._metadataIds.set(key, Utils.GenerateGuid());
+ });
+ }
+
+ @action
+ setGroupType = (groupType: string): void => {
+ this.props.groupDoc.type = groupType;
+ }
+
+ removeGroupFromLink = (groupType: string): void => {
+ LinkManager.Instance.removeGroupFromAnchor(this.props.linkDoc, this.props.sourceDoc, groupType);
+ }
+
+ deleteGroup = (groupType: string): void => {
+ LinkManager.Instance.deleteGroupType(groupType);
+ }
+
+ copyGroup = async (groupType: string): Promise<void> => {
+ let sourceGroupDoc = this.props.groupDoc;
+ const sourceMdDoc = await Cast(sourceGroupDoc.metadata, Doc);
+ if (!sourceMdDoc) return;
+
+ let destDoc = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
+ // let destGroupList = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, destDoc);
+ let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
+
+ // create new metadata doc with copied kvp
+ let destMdDoc = new Doc();
+ destMdDoc.anchor1 = StrCast(sourceMdDoc.anchor2);
+ destMdDoc.anchor2 = StrCast(sourceMdDoc.anchor1);
+ keys.forEach(key => {
+ let val = sourceMdDoc[key] === undefined ? "" : StrCast(sourceMdDoc[key]);
+ destMdDoc[key] = val;
+ });
+
+ // create new group doc with new metadata doc
+ let destGroupDoc = new Doc();
+ destGroupDoc.type = groupType;
+ destGroupDoc.metadata = destMdDoc;
+
+ if (destDoc) {
+ LinkManager.Instance.addGroupToAnchor(this.props.linkDoc, destDoc, destGroupDoc, true);
+ }
+ }
+
+ @action
+ addMetadata = (groupType: string): void => {
+ this._metadataIds.set("new key", Utils.GenerateGuid());
+ let mdKeys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
+ // only add "new key" if there is no other key with value "new key"; prevents spamming
+ if (mdKeys.indexOf("new key") === -1) mdKeys.push("new key");
+ LinkManager.Instance.setMetadataKeysForGroup(groupType, mdKeys);
+ }
+
+ // for key rendering purposes
+ changeMdIdKey = (id: string, newKey: string) => {
+ this._metadataIds.set(newKey, id);
+ }
+
+ renderMetadata = (): JSX.Element[] => {
+ let metadata: Array<JSX.Element> = [];
+ let groupDoc = this.props.groupDoc;
+ const mdDoc = FieldValue(Cast(groupDoc.metadata, Doc));
+ if (!mdDoc) {
+ return [];
+ }
+ let groupType = StrCast(groupDoc.type);
+ let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
+
+ groupMdKeys.forEach((key) => {
+ let val = StrCast(mdDoc[key]);
+ metadata.push(
+ <LinkMetadataEditor key={"mded-" + this._metadataIds.get(key)} id={this._metadataIds.get(key)!} groupType={groupType} mdDoc={mdDoc} mdKey={key} mdValue={val} changeMdIdKey={this.changeMdIdKey} />
+ );
+ });
+ return metadata;
+ }
+
+ viewGroupAsTable = (groupType: string): JSX.Element => {
+ let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
+ let index = keys.indexOf("");
+ if (index > -1) keys.splice(index, 1);
+ let cols = ["anchor1", "anchor2", ...[...keys]];
+ let docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType);
+ 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>;
+ }
+
+ render() {
+ let groupType = StrCast(this.props.groupDoc.type);
+ // if ((groupType && LinkManager.Instance.getMetadataKeysInGroup(groupType).length > 0) || groupType === "") {
+ let buttons;
+ if (groupType === "") {
+ buttons = (
+ <>
+ <button className="linkEditor-button" disabled={true} title="Add KVP"><FontAwesomeIcon icon="plus" size="sm" /></button>
+ <button className="linkEditor-button" disabled title="Copy group to opposite anchor"><FontAwesomeIcon icon="exchange-alt" size="sm" /></button>
+ <button className="linkEditor-button" onClick={() => this.removeGroupFromLink(groupType)} title="Remove group from link"><FontAwesomeIcon icon="times" size="sm" /></button>
+ <button className="linkEditor-button" disabled title="Delete group"><FontAwesomeIcon icon="trash" size="sm" /></button>
+ <button className="linkEditor-button" disabled title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button>
+ </>
+ );
+ } else {
+ buttons = (
+ <>
+ <button className="linkEditor-button" onClick={() => this.addMetadata(groupType)} title="Add KVP"><FontAwesomeIcon icon="plus" size="sm" /></button>
+ <button className="linkEditor-button" onClick={() => this.copyGroup(groupType)} title="Copy group to opposite anchor"><FontAwesomeIcon icon="exchange-alt" size="sm" /></button>
+ <button className="linkEditor-button" onClick={() => this.removeGroupFromLink(groupType)} title="Remove group from link"><FontAwesomeIcon icon="times" size="sm" /></button>
+ <button className="linkEditor-button" onClick={() => this.deleteGroup(groupType)} title="Delete group"><FontAwesomeIcon icon="trash" size="sm" /></button>
+ {this.viewGroupAsTable(groupType)}
+ </>
+ );
+ }
+ return (
+ <div className="linkEditor-group">
+ <div className="linkEditor-group-row ">
+ <p className="linkEditor-group-row-label">type:</p>
+ <GroupTypesDropdown groupType={groupType} setGroupType={this.setGroupType} />
+ </div>
+ {this.renderMetadata().length > 0 ? <p className="linkEditor-group-row-label">metadata:</p> : <></>}
+ {this.renderMetadata()}
+ <div className="linkEditor-group-buttons">
+ {buttons}
+ </div>
+ </div>
);
}
+}
+
+
+interface LinkEditorProps {
+ sourceDoc: Doc;
+ linkDoc: Doc;
+ showLinks: () => void;
+}
+@observer
+export class LinkEditor extends React.Component<LinkEditorProps> {
@action
- onNameChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
- this._nameInput = e.target.value;
+ deleteLink = (): void => {
+ LinkManager.Instance.deleteLink(this.props.linkDoc);
+ this.props.showLinks();
}
@action
- onDescriptionChanged = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
- this._descriptionInput = e.target.value;
+ addGroup = (): void => {
+ // create new metadata document for group
+ let mdDoc = new Doc();
+ mdDoc.anchor1 = 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();
+ groupDoc.type = "";
+ groupDoc.metadata = mdDoc;
+
+ LinkManager.Instance.addGroupToAnchor(this.props.linkDoc, this.props.sourceDoc, groupDoc);
+ }
+
+ render() {
+ let destination = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
+
+ let groupList = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, this.props.sourceDoc);
+ let groups = groupList.map(groupDoc => {
+ return <LinkGroupEditor key={"gred-" + StrCast(groupDoc.type)} linkDoc={this.props.linkDoc} sourceDoc={this.props.sourceDoc} groupDoc={groupDoc} />;
+ });
+
+ 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>
+
+ );
+ }
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkMenu.scss b/src/client/views/nodes/LinkMenu.scss
index dedcce6ef..a4018bd2d 100644
--- a/src/client/views/nodes/LinkMenu.scss
+++ b/src/client/views/nodes/LinkMenu.scss
@@ -1,21 +1,137 @@
-#linkMenu-container {
+@import "../globalCssVariables";
+
+.linkMenu {
width: 100%;
height: auto;
- display: flex;
- flex-direction: column;
}
-#linkMenu-searchBar {
- width: 100%;
- padding: 5px;
- margin-bottom: 10px;
+.linkMenu-list {
+ max-height: 200px;
+ overflow-y: scroll;
+}
+
+.linkMenu-group {
+ border-bottom: 0.5px solid lightgray;
+ padding: 5px 0;
+
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ .linkMenu-group-name {
+ display: flex;
+
+ &:hover {
+ p {
+ background-color: lightgray;
+ }
+ p.expand-one {
+ width: calc(100% - 26px);
+ }
+ .linkEditor-tableButton {
+ display: block;
+ }
+ }
+
+ p {
+ width: 100%;
+ padding: 4px 6px;
+ line-height: 12px;
+ border-radius: 5px;
+ font-weight: bold;
+ }
+
+ .linkEditor-tableButton {
+ display: none;
+ }
+ }
+}
+
+.linkMenu-item {
+ // border-top: 0.5px solid $main-accent;
+ position: relative;
+ display: flex;
font-size: 12px;
- border: 1px solid #bababa;
+
+
+ .link-name {
+ position: relative;
+
+ p {
+ padding: 4px 6px;
+ line-height: 12px;
+ border-radius: 5px;
+ overflow-wrap: break-word;
+ }
+ }
+
+ .linkMenu-item-content {
+ width: 100%;
+ }
+
+ .link-metadata {
+ padding: 0 10px 0 16px;
+ margin-bottom: 4px;
+ color: $main-accent;
+ font-style: italic;
+ font-size: 10.5px;
+ }
+
+ &:hover {
+ .linkMenu-item-buttons {
+ display: flex;
+ }
+ .linkMenu-item-content {
+ &.expand-two p {
+ width: calc(100% - 52px);
+ background-color: lightgray;
+ }
+ &.expand-three p {
+ width: calc(100% - 84px);
+ background-color: lightgray;
+ }
+ }
+ }
}
-#linkMenu-list {
- margin-top: 5px;
- width: 100%;
- height: 100px;
- overflow-y: scroll;
-} \ No newline at end of file
+.linkMenu-item-buttons {
+ display: none;
+ position: absolute;
+ top: 50%;
+ right: 0;
+ transform: translateY(-50%);
+
+ .button {
+ width: 20px;
+ height: 20px;
+ margin: 0;
+ margin-right: 6px;
+ border-radius: 50%;
+ cursor: pointer;
+ pointer-events: auto;
+ background-color: $dark-color;
+ color: $light-color;
+ font-size: 65%;
+ transition: transform 0.2s;
+ text-align: center;
+ position: relative;
+
+ .fa-icon {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ }
+
+ &:last-child {
+ margin-right: 0;
+ }
+ &:hover {
+ background: $main-accent;
+ }
+ }
+}
+
+
+
diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx
index 3f09d6214..1eda7d1fb 100644
--- a/src/client/views/nodes/LinkMenu.tsx
+++ b/src/client/views/nodes/LinkMenu.tsx
@@ -1,13 +1,20 @@
import { action, observable } from "mobx";
import { observer } from "mobx-react";
import { DocumentView } from "./DocumentView";
-import { LinkBox } from "./LinkBox";
import { LinkEditor } from "./LinkEditor";
import './LinkMenu.scss';
import React = require("react");
-import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { Doc } from "../../../new_fields/Doc";
+import { LinkManager } from "../../util/LinkManager";
+import { LinkMenuGroup } from "./LinkMenuGroup";
+import { faTrash } from '@fortawesome/free-solid-svg-icons';
+import { library } from "@fortawesome/fontawesome-svg-core";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+library.add(faTrash);
import { Cast, FieldValue, StrCast } from "../../../new_fields/Types";
import { Id } from "../../../new_fields/FieldSymbols";
+import { DocumentType } from "../../documents/Documents";
interface Props {
docView: DocumentView;
@@ -19,34 +26,46 @@ export class LinkMenu extends React.Component<Props> {
@observable private _editingLink?: Doc;
- renderLinkItems(links: Doc[], key: string, type: string) {
- return links.map(link => {
- let doc = FieldValue(Cast(link[key], Doc));
- if (doc) {
- return <LinkBox key={doc[Id]} linkDoc={link} linkName={StrCast(link.title)} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={type} />;
- }
+ @action
+ componentWillReceiveProps() {
+ this._editingLink = undefined;
+ }
+
+ clearAllLinks = () => {
+ LinkManager.Instance.deleteAllLinksOnAnchor(this.props.docView.props.Document);
+ }
+
+ renderAllGroups = (groups: Map<string, Array<Doc>>): Array<JSX.Element> => {
+ let linkItems: Array<JSX.Element> = [];
+ groups.forEach((group, groupType) => {
+ linkItems.push(
+ <LinkMenuGroup key={groupType} sourceDoc={this.props.docView.props.Document} group={group} groupType={groupType} showEditor={action((linkDoc: Doc) => this._editingLink = linkDoc)} />
+ );
});
+
+ // if source doc has no links push message
+ if (linkItems.length === 0) linkItems.push(<p key="">No links have been created yet. Drag the linking button onto another document to create a link.</p>);
+
+ return linkItems;
}
render() {
- //get list of links from document
- let linkFrom = DocListCast(this.props.docView.props.Document.linkedFromDocs);
- let linkTo = DocListCast(this.props.docView.props.Document.linkedToDocs);
+ let sourceDoc = this.props.docView.props.Document;
+ let groups: Map<string, Doc[]> = LinkManager.Instance.getRelatedGroupedLinks(sourceDoc);
if (this._editingLink === undefined) {
return (
- <div id="linkMenu-container">
+ <div className="linkMenu">
+ <button className="linkEditor-button linkEditor-clearButton" onClick={() => this.clearAllLinks()} title="Clear all links"><FontAwesomeIcon icon="trash" size="sm" /></button>
{/* <input id="linkMenu-searchBar" type="text" placeholder="Search..."></input> */}
- <div id="linkMenu-list">
- {this.renderLinkItems(linkTo, "linkedTo", "Destination: ")}
- {this.renderLinkItems(linkFrom, "linkedFrom", "Source: ")}
+ <div className="linkMenu-list">
+ {this.renderAllGroups(groups)}
</div>
</div>
);
} else {
return (
- <LinkEditor linkDoc={this._editingLink} showLinks={action(() => this._editingLink = undefined)}></LinkEditor>
+ <LinkEditor sourceDoc={this.props.docView.props.Document} linkDoc={this._editingLink} showLinks={action(() => this._editingLink = undefined)}></LinkEditor>
);
}
-
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkMenuGroup.tsx b/src/client/views/nodes/LinkMenuGroup.tsx
new file mode 100644
index 000000000..ae97bed2f
--- /dev/null
+++ b/src/client/views/nodes/LinkMenuGroup.tsx
@@ -0,0 +1,102 @@
+import { action, observable } from "mobx";
+import { observer } from "mobx-react";
+import { DocumentView } from "./DocumentView";
+import { LinkMenuItem } from "./LinkMenuItem";
+import { LinkEditor } from "./LinkEditor";
+import './LinkMenu.scss';
+import React = require("react");
+import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { LinkManager } from "../../util/LinkManager";
+import { DragLinksAsDocuments, DragManager, SetupDrag } from "../../util/DragManager";
+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;
+ group: Doc[];
+ groupType: string;
+ showEditor: (linkDoc: Doc) => void;
+}
+
+@observer
+export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
+
+ private _drag = React.createRef<HTMLDivElement>();
+ private _table = React.createRef<HTMLDivElement>();
+
+ onLinkButtonDown = (e: React.PointerEvent): void => {
+ e.stopPropagation();
+ document.removeEventListener("pointermove", this.onLinkButtonMoved);
+ document.addEventListener("pointermove", this.onLinkButtonMoved);
+ document.removeEventListener("pointerup", this.onLinkButtonUp);
+ document.addEventListener("pointerup", this.onLinkButtonUp);
+ }
+
+ onLinkButtonUp = (e: PointerEvent): void => {
+ document.removeEventListener("pointermove", this.onLinkButtonMoved);
+ document.removeEventListener("pointerup", this.onLinkButtonUp);
+ e.stopPropagation();
+ }
+
+
+ onLinkButtonMoved = async (e: PointerEvent) => {
+ 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 => {
+ 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], dragData, e.x, e.y, {
+ handlers: {
+ dragComplete: action(emptyFunction),
+ },
+ hideSource: false
+ });
+ }
+ }, "drag links");
+ e.stopPropagation();
+ }
+
+ viewGroupAsTable = (groupType: string): JSX.Element => {
+ let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
+ let index = keys.indexOf("");
+ if (index > -1) keys.splice(index, 1);
+ let cols = ["anchor1", "anchor2", ...[...keys]];
+ let docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType);
+ 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>;
+ }
+
+ render() {
+ let groupItems = this.props.group.map(linkDoc => {
+ let destination = LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc);
+ 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 (
+ <div className="linkMenu-group">
+ <div className="linkMenu-group-name">
+ <p ref={this._drag} onPointerDown={this.onLinkButtonDown}
+ className={this.props.groupType === "*" || this.props.groupType === "" ? "" : "expand-one"} > {this.props.groupType}:</p>
+ {this.props.groupType === "*" || this.props.groupType === "" ? <></> : this.viewGroupAsTable(this.props.groupType)}
+ </div>
+ <div className="linkMenu-group-wrapper">
+ {groupItems}
+ </div>
+ </div >
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkMenuItem.tsx b/src/client/views/nodes/LinkMenuItem.tsx
new file mode 100644
index 000000000..a0c37a719
--- /dev/null
+++ b/src/client/views/nodes/LinkMenuItem.tsx
@@ -0,0 +1,131 @@
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faEdit, faEye, faTimes, faArrowRight, faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { observer } from "mobx-react";
+import { DocumentManager } from "../../util/DocumentManager";
+import { undoBatch } from "../../util/UndoManager";
+import './LinkMenu.scss';
+import React = require("react");
+import { Doc } from '../../../new_fields/Doc';
+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';
+import { CollectionDockingView } from '../collections/CollectionDockingView';
+library.add(faEye, faEdit, faTimes, faArrowRight, faChevronDown, faChevronUp);
+
+
+interface LinkMenuItemProps {
+ groupType: string;
+ linkDoc: Doc;
+ sourceDoc: Doc;
+ destinationDoc: Doc;
+ showEditor: (linkDoc: Doc) => void;
+}
+
+@observer
+export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
+ private _drag = React.createRef<HTMLDivElement>();
+ @observable private _showMore: boolean = false;
+ @action toggleShowMore() { this._showMore = !this._showMore; }
+
+ @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, 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 {
+ 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));
+ }
+ }
+ }
+
+ onEdit = (e: React.PointerEvent): void => {
+ e.stopPropagation();
+ this.props.showEditor(this.props.linkDoc);
+ }
+
+ renderMetadata = (): JSX.Element => {
+ let groups = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, this.props.sourceDoc);
+ let index = groups.findIndex(groupDoc => StrCast(groupDoc.type).toUpperCase() === this.props.groupType.toUpperCase());
+ let groupDoc = index > -1 ? groups[index] : undefined;
+
+ let mdRows: Array<JSX.Element> = [];
+ if (groupDoc) {
+ let mdDoc = Cast(groupDoc.metadata, Doc, null);
+ if (mdDoc) {
+ let keys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);//groupMetadataKeys.get(this.props.groupType);
+ mdRows = keys.map(key => {
+ return (<div key={key} className="link-metadata-row"><b>{key}</b>: {StrCast(mdDoc[key])}</div>);
+ });
+ }
+ }
+
+ return (<div className="link-metadata">{mdRows}</div>);
+ }
+
+ onLinkButtonDown = (e: React.PointerEvent): void => {
+ e.stopPropagation();
+ document.removeEventListener("pointermove", this.onLinkButtonMoved);
+ document.addEventListener("pointermove", this.onLinkButtonMoved);
+ document.removeEventListener("pointerup", this.onLinkButtonUp);
+ document.addEventListener("pointerup", this.onLinkButtonUp);
+ }
+
+ onLinkButtonUp = (e: PointerEvent): void => {
+ document.removeEventListener("pointermove", this.onLinkButtonMoved);
+ document.removeEventListener("pointerup", this.onLinkButtonUp);
+ 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);
+
+ DragLinkAsDocument(this._drag.current, e.x, e.y, this.props.linkDoc, this.props.sourceDoc);
+ }
+ e.stopPropagation();
+ }
+
+ render() {
+
+ let keys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);//groupMetadataKeys.get(this.props.groupType);
+ let canExpand = keys ? keys.length > 0 : false;
+
+ return (
+ <div className="linkMenu-item">
+ <div className={canExpand ? "linkMenu-item-content expand-three" : "linkMenu-item-content expand-two"}>
+ <div className="link-name">
+ <p ref={this._drag} onPointerDown={this.onLinkButtonDown}>{StrCast(this.props.destinationDoc.title)}</p>
+ <div className="linkMenu-item-buttons">
+ {canExpand ? <div title="Show more" className="button" onPointerDown={() => this.toggleShowMore()}>
+ <FontAwesomeIcon className="fa-icon" icon={this._showMore ? "chevron-up" : "chevron-down"} size="sm" /></div> : <></>}
+ <div title="Edit link" className="button" onPointerDown={this.onEdit}><FontAwesomeIcon className="fa-icon" icon="edit" size="sm" /></div>
+ <div title="Follow link" className="button" onPointerDown={this.onFollowLink}><FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div>
+ </div>
+ </div>
+ {this._showMore ? this.renderMetadata() : <></>}
+ </div>
+
+ </div >
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss
index 8bcae4f1e..e7655d598 100644
--- a/src/client/views/nodes/PDFBox.scss
+++ b/src/client/views/nodes/PDFBox.scss
@@ -32,16 +32,26 @@
height: 100px;
}
-.pdfBox-cont {
- pointer-events: none;
+.pdfBox-cont,
+.pdfBox-cont-interactive {
display: flex;
flex-direction: row;
+ height: 100%;
+ overflow-y: scroll;
+ overflow-x: hidden;
+}
+
+.pdfBox-cont {
+ pointer-events: none;
+
.textlayer {
pointer-events: none;
+
span {
pointer-events: none !important;
}
}
+
.page-cont {
pointer-events: none;
}
@@ -51,6 +61,7 @@
pointer-events: all;
display: flex;
flex-direction: row;
+
.textlayer {
span {
pointer-events: all !important;
@@ -62,4 +73,68 @@
.pdfBox-contentContainer {
position: absolute;
transform-origin: left top;
+}
+
+.pdfBox-settingsCont {
+ position: absolute;
+ right: 0;
+ top: 0;
+
+ .pdfBox-settingsButton {
+ border-bottom-left-radius: 50%;
+ display: flex;
+ justify-content: space-evenly;
+ align-items: center;
+ height: 70px;
+ background: none;
+ padding: 0;
+
+ .pdfBox-settingsButton-arrow {
+ width: 0;
+ height: 0;
+ border-top: 25px solid transparent;
+ border-bottom: 25px solid transparent;
+ border-right: 25px solid #121721;
+ transition: all 0.5s;
+ }
+
+ .pdfBox-settingsButton-iconCont {
+ background: #121721;
+ height: 50px;
+ width: 70px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-left: -2px;
+ border-radius: 3px;
+ }
+ }
+
+ .pdfBox-settingsButton:hover {
+ background: none;
+ }
+
+ .pdfBox-settingsFlyout {
+ width: 600px;
+ position: absolute;
+ background: #323232;
+ box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
+ left: -400px;
+ border-radius: 7px;
+ padding: 20px;
+ display: flex;
+ flex-direction: column;
+ font-size: 30px;
+ transition: all 0.5s;
+
+ .pdfBox-settingsFlyout-title {
+ color: white;
+ }
+
+ .pdfBox-settingsFlyout-kvpInput {
+ margin-top: 20px;
+ display: grid;
+ grid-template-columns: 47.5% 5% 47.5%;
+ }
+ }
} \ No newline at end of file
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index d2de1cb1c..5a5e6e6dd 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -1,9 +1,9 @@
-import { action, IReactionDisposer, observable, reaction, trace, untracked } from 'mobx';
+import { action, IReactionDisposer, observable, reaction, trace, untracked, computed } from 'mobx';
import { observer } from "mobx-react";
import 'react-image-lightbox/style.css';
-import { WidthSym } from "../../../new_fields/Doc";
+import { WidthSym, Doc } from "../../../new_fields/Doc";
import { makeInterface } from "../../../new_fields/Schema";
-import { Cast, NumCast } from "../../../new_fields/Types";
+import { Cast, NumCast, BoolCast } from "../../../new_fields/Types";
import { PdfField } from "../../../new_fields/URLField";
//@ts-ignore
// import { Document, Page } from "react-pdf";
@@ -11,15 +11,23 @@ import { PdfField } from "../../../new_fields/URLField";
import { RouteStore } from "../../../server/RouteStore";
import { DocComponent } from "../DocComponent";
import { InkingControl } from "../InkingControl";
+import { FilterBox } from "../search/FilterBox";
+import { Annotation } from './Annotation';
import { PDFViewer } from "../pdf/PDFViewer";
import { positionSchema } from "./DocumentView";
import { FieldView, FieldViewProps } from './FieldView';
import { pageSchema } from "./ImageBox";
import "./PDFBox.scss";
import React = require("react");
+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) {
@@ -27,27 +35,59 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
@observable private _alt = false;
@observable private _scrollY: number = 0;
+ @computed get dataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; }
+
+ @observable private _flyout: boolean = false;
+ private _mainCont: React.RefObject<HTMLDivElement>;
private _reactionDisposer?: IReactionDisposer;
+ private _keyValue: string = "";
+ private _valueValue: string = "";
+ private _scriptValue: string = "";
+ private _keyRef: React.RefObject<HTMLInputElement>;
+ private _valueRef: React.RefObject<HTMLInputElement>;
+ private _scriptRef: React.RefObject<HTMLInputElement>;
+
+ constructor(props: FieldViewProps) {
+ super(props);
+
+ this._mainCont = React.createRef();
+ this._reactionDisposer = reaction(
+ () => this.props.Document.scrollY,
+ () => {
+ if (this._mainCont.current) {
+ this._mainCont.current && this._mainCont.current.scrollTo({ top: NumCast(this.Document.scrollY), behavior: "auto" });
+ }
+ }
+ );
+
+ this._keyRef = React.createRef();
+ this._valueRef = React.createRef();
+ this._scriptRef = React.createRef();
+ }
componentDidMount() {
if (this.props.setPdfBox) this.props.setPdfBox(this);
}
+ componentWillUnmount() {
+ this._reactionDisposer && this._reactionDisposer();
+ }
+
public GetPage() {
- return Math.floor(NumCast(this.props.Document.scrollY) / NumCast(this.Document.pdfHeight)) + 1;
+ return Math.floor(NumCast(this.props.Document.scrollY) / NumCast(this.dataDoc.pdfHeight)) + 1;
}
public BackPage() {
- let cp = Math.ceil(NumCast(this.props.Document.scrollY) / NumCast(this.Document.pdfHeight)) + 1;
+ let cp = Math.ceil(NumCast(this.props.Document.scrollY) / NumCast(this.dataDoc.pdfHeight)) + 1;
cp = cp - 1;
if (cp > 0) {
this.props.Document.curPage = cp;
- this.props.Document.scrollY = (cp - 1) * NumCast(this.Document.pdfHeight);
+ this.props.Document.scrollY = (cp - 1) * NumCast(this.dataDoc.pdfHeight);
}
}
public GotoPage(p: number) {
if (p > 0 && p <= NumCast(this.props.Document.numPages)) {
this.props.Document.curPage = p;
- this.props.Document.scrollY = (p - 1) * NumCast(this.Document.pdfHeight);
+ this.props.Document.scrollY = (p - 1) * NumCast(this.dataDoc.pdfHeight);
}
}
@@ -55,20 +95,114 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
let cp = this.GetPage() + 1;
if (cp <= NumCast(this.props.Document.numPages)) {
this.props.Document.curPage = cp;
- this.props.Document.scrollY = (cp - 1) * NumCast(this.Document.pdfHeight);
+ this.props.Document.scrollY = (cp - 1) * NumCast(this.dataDoc.pdfHeight);
+ }
+ }
+
+ private newKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this._keyValue = e.currentTarget.value;
+ }
+
+ private newValueChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this._valueValue = e.currentTarget.value;
+ }
+
+ @action
+ private newScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this._scriptValue = e.currentTarget.value;
+ }
+
+ private applyFilter = () => {
+ let scriptText = "";
+ if (this._scriptValue.length > 0) {
+ scriptText = this._scriptValue;
+ } else if (this._keyValue.length > 0 && this._valueValue.length > 0) {
+ scriptText = `return this.${this._keyValue} === ${this._valueValue}`;
+ }
+ else {
+ scriptText = "return true";
+ }
+ let script = CompileScript(scriptText, { params: { this: Doc.name } });
+ if (script.compiled) {
+ this.props.Document.filterScript = new ScriptField(script);
+ }
+ }
+
+ @action
+ private toggleFlyout = () => {
+ this._flyout = !this._flyout;
+ }
+
+ @action
+ private resetFilters = () => {
+ this._keyValue = this._valueValue = "";
+ this._scriptValue = "return true";
+ if (this._keyRef.current) {
+ this._keyRef.current.value = "";
+ }
+ if (this._valueRef.current) {
+ this._valueRef.current.value = "";
+ }
+ if (this._scriptRef.current) {
+ this._scriptRef.current.value = "";
}
+ this.applyFilter();
}
- createRef = (ele: HTMLDivElement | null) => {
- if (this._reactionDisposer) this._reactionDisposer();
- this._reactionDisposer = reaction(() => this.props.Document.scrollY, () => {
- ele && ele.scrollTo({ top: NumCast(this.Document.scrollY), behavior: "auto" });
- });
+ scrollTo(y: number) {
+ if (this._mainCont.current) {
+ this._mainCont.current.scrollTo({ top: y, behavior: "auto" });
+ }
+ }
+
+ settingsPanel() {
+ return !this.props.active() ? (null) :
+ (
+ <div className="pdfBox-settingsCont" onPointerDown={(e) => e.stopPropagation()}>
+ <button className="pdfBox-settingsButton" onClick={this.toggleFlyout} title="Open Annotation Settings"
+ style={{ marginTop: `${NumCast(this.props.ContainingCollectionView!.props.Document.panY)}px` }}>
+ <div className="pdfBox-settingsButton-arrow"
+ style={{
+ borderTop: `25px solid ${this._flyout ? "#121721" : "transparent"}`,
+ borderBottom: `25px solid ${this._flyout ? "#121721" : "transparent"}`,
+ borderRight: `25px solid ${this._flyout ? "transparent" : "#121721"}`,
+ transform: `scaleX(${this._flyout ? -1 : 1})`
+ }}></div>
+ <div className="pdfBox-settingsButton-iconCont">
+ <FontAwesomeIcon style={{ color: "white" }} icon="cog" size="3x" />
+ </div>
+ </button>
+ <div className="pdfBox-settingsFlyout" style={{ left: `${this._flyout ? -600 : 100}px` }} >
+ <div className="pdfBox-settingsFlyout-title">
+ Annotation View Settings
+ </div>
+ <div className="pdfBox-settingsFlyout-kvpInput">
+ <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" onKeyDown={handleBackspace} onChange={this.newValueChange}
+ style={{ gridColumn: 3 }} ref={this._valueRef} />
+ </div>
+ <div className="pdfBox-settingsFlyout-kvpInput">
+ <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}>
+ <FontAwesomeIcon style={{ color: "white" }} icon="trash" size="lg" />
+ &nbsp; Reset Filters
+ </button>
+ <button style={{ gridColumn: 3 }} onClick={this.applyFilter}>
+ <FontAwesomeIcon style={{ color: "white" }} icon="check" size="lg" />
+ &nbsp; Apply
+ </button>
+ </div>
+ </div>
+ </div>
+ );
}
loaded = (nw: number, nh: number, np: number) => {
if (this.props.Document) {
- let doc = this.props.Document.proto ? this.props.Document.proto : this.props.Document;
+ let doc = this.dataDoc;
doc.numPages = np;
if (doc.nativeWidth && doc.nativeHeight) return;
let oldaspect = NumCast(doc.nativeHeight) / NumCast(doc.nativeWidth, 1);
@@ -85,32 +219,39 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
@action
onScroll = (e: React.UIEvent<HTMLDivElement>) => {
+
if (e.currentTarget) {
this._scrollY = e.currentTarget.scrollTop;
let ccv = this.props.ContainingCollectionView;
if (ccv) {
+ ccv.props.Document.panTransformType = "None";
ccv.props.Document.scrollY = this._scrollY;
}
}
}
+
+ @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, new PdfField(window.origin + RouteStore.corsProxy + "/https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf"));
+ const pdfUrl = Cast(this.props.Document.data, PdfField);
+ if (!(pdfUrl instanceof PdfField)) return <div>{`pdf, ${this.props.Document.data}, not found`}</div>;
let classname = "pdfBox-cont" + (this.props.active() && !InkingControl.Instance.selectedTool && !this._alt ? "-interactive" : "");
return (
- <div onScroll={this.onScroll}
+ <div className={classname}
+ onScroll={this.onScroll}
style={{
- height: "100%",
- overflowY: "scroll", overflowX: "hidden",
marginTop: `${NumCast(this.props.ContainingCollectionView!.props.Document.panY)}px`
}}
- ref={this.createRef}
+ ref={this._mainCont}
onWheel={(e: React.WheelEvent) => {
e.stopPropagation();
- }} className={classname}>
+ }}>
<PDFViewer url={pdfUrl.url.pathname} loaded={this.loaded} scrollY={this._scrollY} parent={this} />
{/* <div style={{ width: "100px", height: "300px" }}></div> */}
+ {this.settingsPanel()}
</div>
);
}
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 1239b498f..0f2d18f6b 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;
@@ -40,37 +54,58 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
}
}
- @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, 500));
+ this.updateTimecode();
}
- @action public Pause() {
+ @action public Seek(time: number) {
+ this._youtubePlayer && this._youtubePlayer.seekTo(Math.round(time), true);
+ }
+
+ @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,59 +120,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.preventDefault();
- 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..f0a9ec6d8 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -6,12 +6,45 @@ 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";
+export function onYouTubeIframeAPIReady() {
+ console.log("player");
+ return;
+ let player = new YT.Player('player', {
+ events: {
+ 'onReady': onPlayerReady
+ }
+ });
+}
+// must cast as any to set property on window
+const _global = (window /* browser */ || global /* node */) as any;
+_global.onYouTubeIframeAPIReady = onYouTubeIframeAPIReady;
+
+function onPlayerReady(event: any) {
+ event.target.playVideo();
+}
@observer
export class WebBox extends React.Component<FieldViewProps> {
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 +79,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
new file mode 100644
index 000000000..ed7081b1d
--- /dev/null
+++ b/src/client/views/pdf/Annotation.tsx
@@ -0,0 +1,153 @@
+import React = require("react");
+import { Doc, DocListCast, WidthSym, HeightSym } from "../../../new_fields/Doc";
+import { AnnotationTypes, Viewer, scale } from "./PDFViewer";
+import { observer } from "mobx-react";
+import { observable, IReactionDisposer, reaction, action } from "mobx";
+import { BoolCast, NumCast, FieldValue, Cast, StrCast } from "../../../new_fields/Types";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { List } from "../../../new_fields/List";
+import PDFMenu from "./PDFMenu";
+import { DocumentManager } from "../../util/DocumentManager";
+import { PresentationView } from "../presentationview/PresentationView";
+
+interface IAnnotationProps {
+ anno: Doc;
+ index: number;
+ parent: Viewer;
+}
+
+export default class Annotation extends React.Component<IAnnotationProps> {
+ render() {
+ let annotationDocs = DocListCast(this.props.anno.annotations);
+ let res = annotationDocs.map(a => {
+ let type = NumCast(a.type);
+ switch (type) {
+ // case AnnotationTypes.Pin:
+ // return <PinAnnotation parent={this} document={a} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />;
+ case AnnotationTypes.Region:
+ return <RegionAnnotation parent={this.props.parent} document={a} index={this.props.index} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />;
+ default:
+ return <div></div>;
+ }
+ });
+ return res;
+ }
+}
+
+interface IRegionAnnotationProps {
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+ index: number;
+ parent: Viewer;
+ document: Doc;
+}
+
+@observer
+class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
+ @observable private _backgroundColor: string = "red";
+
+ private _reactionDisposer?: IReactionDisposer;
+ private _scrollDisposer?: IReactionDisposer;
+ private _mainCont: React.RefObject<HTMLDivElement>;
+
+ constructor(props: IRegionAnnotationProps) {
+ super(props);
+
+ this._mainCont = React.createRef();
+ }
+
+ componentDidMount() {
+ this._reactionDisposer = reaction(
+ () => BoolCast(this.props.document.delete),
+ () => {
+ if (BoolCast(this.props.document.delete)) {
+ if (this._mainCont.current) {
+ this._mainCont.current.style.display = "none";
+ }
+ }
+ },
+ { fireImmediately: true }
+ );
+
+ this._scrollDisposer = reaction(
+ () => this.props.parent.Index,
+ () => {
+ if (this.props.parent.Index === this.props.index) {
+ this.props.parent.scrollTo(this.props.y * scale);
+ }
+ }
+ );
+ }
+
+ componentWillUnmount() {
+ this._reactionDisposer && this._reactionDisposer();
+ this._scrollDisposer && this._scrollDisposer();
+ }
+
+ deleteAnnotation = () => {
+ 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.fieldExtensionDoc.annotations = new List<Doc>(newAnnotations);
+ }
+
+ if (group) {
+ let groupAnnotations = DocListCast(group.annotations);
+ groupAnnotations.forEach(anno => anno.delete = true);
+ }
+
+ PDFMenu.Instance.fadeOut(true);
+ }
+
+ pinToPres = () => {
+ let group = FieldValue(Cast(this.props.document.group, Doc));
+ if (group) {
+ PresentationView.Instance.PinDoc(group);
+ }
+ }
+
+ @action
+ onPointerDown = (e: React.PointerEvent) => {
+ if (e.button === 0) {
+ let targetDoc = Cast(this.props.document.target, Doc, null);
+ if (targetDoc) {
+ DocumentManager.Instance.jumpToDocument(targetDoc, false);
+ }
+ }
+ if (e.button === 2) {
+ PDFMenu.Instance.Status = "annotation";
+ PDFMenu.Instance.Delete = this.deleteAnnotation.bind(this);
+ PDFMenu.Instance.Pinned = false;
+ PDFMenu.Instance.AddTag = this.addTag.bind(this);
+ PDFMenu.Instance.PinToPres = this.pinToPres;
+ PDFMenu.Instance.jumpTo(e.clientX, e.clientY, true);
+ }
+ }
+
+ addTag = (key: string, value: string): boolean => {
+ let group = FieldValue(Cast(this.props.document.group, Doc));
+ if (group) {
+ let valNum = parseInt(value);
+ group[key] = isNaN(valNum) ? value : valNum;
+ return true;
+ }
+ return false;
+ }
+
+ render() {
+ return (
+ <div className="pdfViewer-annotationBox" onPointerDown={this.onPointerDown} ref={this._mainCont}
+ style={{
+ top: this.props.y * scale,
+ left: this.props.x * scale,
+ width: this.props.width * scale,
+ height: this.props.height * scale,
+ pointerEvents: "all",
+ backgroundColor: this.props.parent.Index === this.props.index ? "green" : StrCast(this.props.document.color)
+ }}></div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/pdf/PDFMenu.scss b/src/client/views/pdf/PDFMenu.scss
index 22868082a..b06d19c53 100644
--- a/src/client/views/pdf/PDFMenu.scss
+++ b/src/client/views/pdf/PDFMenu.scss
@@ -21,5 +21,16 @@
.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 {
+ display: grid;
+ width: 200px;
+ padding: 5px;
+ grid-template-columns: 90px 20px 90px;
}
} \ No newline at end of file
diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx
index 39b15fb11..27c2a8f1a 100644
--- a/src/client/views/pdf/PDFMenu.tsx
+++ b/src/client/views/pdf/PDFMenu.tsx
@@ -1,10 +1,11 @@
import React = require("react");
import "./PDFMenu.scss";
-import { observable, action } from "mobx";
+import { observable, action, runInAction } from "mobx";
import { observer } from "mobx-react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { emptyFunction } from "../../../Utils";
+import { emptyFunction, returnFalse } from "../../../Utils";
import { Doc } from "../../../new_fields/Doc";
+import { handleBackspace } from "../nodes/PDFBox";
@observer
export default class PDFMenu extends React.Component {
@@ -16,26 +17,34 @@ export default class PDFMenu extends React.Component {
@observable private _transition: string = "opacity 0.5s";
@observable private _transitionDelay: string = "";
- @observable public Pinned: boolean = false;
- StartDrag: (e: PointerEvent) => 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;
+ AddTag: (key: string, value: string) => boolean = returnFalse;
+ PinToPres: () => void = emptyFunction;
@observable public Highlighting: boolean = false;
- @observable public Status: "pdf" | "annotation" | "" = "";
+ @observable public Status: "pdf" | "annotation" | "snippet" | "" = "";
+ @observable public Pinned: boolean = false;
+
+ public Marquee: { left: number; top: number; width: number; height: number; } | undefined;
private _offsetY: number = 0;
private _offsetX: number = 0;
- private _mainCont: React.RefObject<HTMLDivElement>;
+ private _mainCont: 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 = "";
+ @observable private _valueValue: string = "";
+ @observable private _added: boolean = false;
constructor(props: Readonly<{}>) {
super(props);
PDFMenu.Instance = this;
-
- this._mainCont = React.createRef();
}
pointerDown = (e: React.PointerEvent) => {
@@ -56,7 +65,7 @@ export default class PDFMenu extends React.Component {
return;
}
- this.StartDrag(e);
+ this.StartDrag(e, this._commentCont.current!);
this._dragging = true;
}
@@ -171,34 +180,88 @@ export default class PDFMenu extends React.Component {
e.preventDefault();
}
+ snippetStart = (e: React.PointerEvent) => {
+ document.removeEventListener("pointermove", this.snippetDrag);
+ document.addEventListener("pointermove", this.snippetDrag);
+ document.removeEventListener("pointerup", this.snippetEnd);
+ document.addEventListener("pointerup", this.snippetEnd);
+
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ snippetDrag = (e: PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ if (this._dragging) {
+ return;
+ }
+ this._dragging = true;
+
+ if (this.Marquee) {
+ this.Snippet(this.Marquee);
+ }
+ }
+
+ snippetEnd = (e: PointerEvent) => {
+ this._dragging = false;
+ document.removeEventListener("pointermove", this.snippetDrag);
+ document.removeEventListener("pointerup", this.snippetEnd);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ @action
+ keyChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this._keyValue = e.currentTarget.value;
+ }
+
+ @action
+ valueChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this._valueValue = e.currentTarget.value;
+ }
+
+ @action
+ addTag = (e: React.PointerEvent) => {
+ if (this._keyValue.length > 0 && this._valueValue.length > 0) {
+ this._added = this.AddTag(this._keyValue, this._valueValue);
+
+ setTimeout(
+ () => {
+ runInAction(() => {
+ this._added = false;
+ });
+ }, 1000
+ );
+ }
+ }
+
render() {
- let buttons = this.Status === "pdf" ? [
- <button className="pdfMenu-button" title="Click to Highlight" onClick={this.highlightClicked}
+ let buttons = this.Status === "pdf" || this.Status === "snippet" ? [
+ <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" onPointerDown={this.pointerDown}><FontAwesomeIcon icon="comment-alt" size="lg" /></button>,
- <button className="pdfMenu-button" title="Pin Menu" onClick={this.togglePin}
+ <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 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" /></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 onKeyDown={handleBackspace} onChange={this.keyChanged} placeholder="Key" style={{ gridColumn: 1 }} />
+ <input onKeyDown={handleBackspace} onChange={this.valueChanged} placeholder="Value" style={{ gridColumn: 3 }} />
+ </div>,
+ <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 (
<div className="pdfMenu-cont" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu}
style={{ left: this._left, top: this._top, opacity: this._opacity, transition: this._transition, transitionDelay: this._transitionDelay }}>
{buttons}
- {/* <button className="pdfMenu-button" title="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="Annotate" onPointerDown={this.pointerDown}><FontAwesomeIcon icon="comment-alt" size="lg" /></button>
- <button 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> */}
<div className="pdfMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} />
</div >
);
diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss
index 53c33ce0b..0fde764d0 100644
--- a/src/client/views/pdf/PDFViewer.scss
+++ b/src/client/views/pdf/PDFViewer.scss
@@ -1,9 +1,9 @@
+
.textLayer {
div {
user-select: text;
}
}
-
.viewer-button-cont {
position: absolute;
display: flex;
@@ -23,6 +23,93 @@
.textLayer {
user-select: auto;
}
+.viewer {
+ // position: absolute;
+ // top: 0;
+}
+
+.pdfViewer-text {
+
+ .page {
+ .canvasWrapper {
+ display: none;
+ }
+
+ .textLayer {
+ position: relative;
+ user-select: none;
+ }
+ }
+}
+.pdfViewer-viewerCont {
+ width:100%;
+}
+
+.page-cont {
+ .textLayer {
+ user-select: auto;
+
+ div {
+ user-select: text;
+ }
+ }
+}
+
+.pdfViewer-overlayCont {
+ position: absolute;
+ width: 100%;
+ height: 100px;
+ background: #121721;
+ bottom: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 20px;
+ overflow: hidden;
+ transition: left .5s;
+}
+
+.pdfViewer-overlaySearchBar {
+ width: 20%;
+ height: 100%;
+ font-size: 30px;
+ padding: 5px;
+}
+
+.pdfViewer-overlayButton {
+ border-bottom-left-radius: 50%;
+ display: flex;
+ justify-content: space-evenly;
+ align-items: center;
+ height: 70px;
+ background: none;
+ padding: 0;
+ position: absolute;
+
+ .pdfViewer-overlayButton-arrow {
+ width: 0;
+ height: 0;
+ border-top: 25px solid transparent;
+ border-bottom: 25px solid transparent;
+ border-right: 25px solid #121721;
+ transition: all 0.5s;
+ }
+
+ .pdfViewer-overlayButton-iconCont {
+ background: #121721;
+ height: 50px;
+ width: 70px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-left: -2px;
+ border-radius: 3px;
+ }
+}
+
+.pdfViewer-overlayButton:hover {
+ background: none;
+}
.pdfViewer-annotationBox {
position: absolute;
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 7000352e7..b7ded7e06 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -4,22 +4,23 @@ 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 { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { emptyFunction, Utils } from "../../../Utils";
import { Docs, DocUtils } from "../../documents/Documents";
-import { DocumentManager } from "../../util/DocumentManager";
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, 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;
interface IPDFViewerProps {
@@ -44,7 +45,7 @@ export class PDFViewer extends React.Component<IPDFViewerProps> {
render() {
return (
- <div ref={this._mainDiv}>
+ <div className="pdfViewer-viewerCont" ref={this._mainDiv}>
{!this._pdf ? (null) :
<Viewer pdf={this._pdf} loaded={this.props.loaded} scrollY={this.props.scrollY} parent={this.props.parent} mainCont={this._mainDiv} url={this.props.url} />}
</div>
@@ -61,13 +62,11 @@ interface IViewerProps {
url: string;
}
-const PinRadius = 25;
-
/**
* Handles rendering and virtualization of the pdf
*/
@observer
-class Viewer extends React.Component<IViewerProps> {
+export class Viewer extends React.Component<IViewerProps> {
// _visibleElements is the array of JSX elements that gets rendered
@observable.shallow private _visibleElements: JSX.Element[] = [];
// _isPage is an array that tells us whether or not an index is rendered as a page or as a placeholder
@@ -75,12 +74,32 @@ class Viewer extends React.Component<IViewerProps> {
@observable private _pageSizes: { width: number, height: number }[] = [];
@observable private _annotations: Doc[] = [];
@observable private _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>();
+ @observable private _script: CompileResult | undefined;
+ @observable private _searching: boolean = false;
+
+ @observable public Index: number = -1;
private _pageBuffer: number = 1;
private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
private _reactionDisposer?: IReactionDisposer;
private _annotationReactionDisposer?: IReactionDisposer;
private _dropDisposer?: DragManager.DragDropDisposer;
+ private _filterReactionDisposer?: IReactionDisposer;
+ private _viewer: React.RefObject<HTMLDivElement>;
+ private _mainCont: React.RefObject<HTMLDivElement>;
+ private _pdfViewer: any;
+ // private _textContent: Pdfjs.TextContent[] = [];
+ private _pdfFindController: any;
+ private _searchString: string = "";
+
+ constructor(props: IViewerProps) {
+ super(props);
+
+ let scriptfield = Cast(this.props.parent.Document.filterScript, ScriptField);
+ this._script = scriptfield ? scriptfield.script : CompileScript("return true");
+ this._viewer = React.createRef();
+ this._mainCont = React.createRef();
+ }
componentDidUpdate = (prevProps: IViewerProps) => {
if (this.scrollY !== prevProps.scrollY) {
@@ -99,15 +118,57 @@ 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 });
+
+
+ if (this.props.parent.props.ContainingCollectionView) {
+ this._filterReactionDisposer = reaction(
+ () => this.props.parent.Document.filterScript,
+ () => {
+ runInAction(() => {
+ let scriptfield = Cast(this.props.parent.Document.filterScript, ScriptField);
+ this._script = scriptfield ? scriptfield.script : CompileScript("return true");
+ if (this.props.parent.props.ContainingCollectionView) {
+ 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);
+ if (run.success) {
+ d.opacity = run.result ? 1 : 0;
+ }
+ }
+ });
+ }
+ this.Index = -1;
+ });
+ }
+ );
+ }
+
+ if (this._mainCont.current) {
+ this._dropDisposer = this._mainCont.current && DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } });
+ }
}
componentWillUnmount = () => {
this._reactionDisposer && this._reactionDisposer();
this._annotationReactionDisposer && this._annotationReactionDisposer();
+ this._filterReactionDisposer && this._filterReactionDisposer();
+ this._dropDisposer && this._dropDisposer();
+ }
+
+ scrollTo(y: number) {
+ if (this.props.mainCont.current) {
+ this.props.parent.scrollTo(y - this.props.mainCont.current.clientHeight);
+ }
}
@action
@@ -115,35 +176,58 @@ class Viewer extends React.Component<IViewerProps> {
if (this._pageSizes.length === 0) {
let pageSizes = Array<{ width: number, height: number }>(this.props.pdf.numPages);
this._isPage = Array<string>(this.props.pdf.numPages);
+ // this._textContent = Array<Pdfjs.TextContent>(this.props.pdf.numPages);
+ const proms: Pdfjs.PDFPromise<any>[] = [];
for (let i = 0; i < this.props.pdf.numPages; i++) {
- await this.props.pdf.getPage(i + 1).then(page => runInAction(() => {
- // pageSizes[i] = { width: page.view[2] * scale, height: page.view[3] * scale };
- let x = page.getViewport(scale);
- pageSizes[i] = { width: x.width, height: x.height };
- }));
+ proms.push(this.props.pdf.getPage(i + 1).then(page => runInAction(() => {
+ pageSizes[i] = {
+ width: (page.view[page.rotate === 0 || page.rotate === 180 ? 2 : 3] - page.view[page.rotate === 0 || page.rotate === 180 ? 0 : 1]) * scale,
+ height: (page.view[page.rotate === 0 || page.rotate === 180 ? 3 : 2] - page.view[page.rotate === 0 || page.rotate === 180 ? 1 : 0]) * scale
+ };
+ // let x = page.getViewport(scale);
+ // page.getTextContent().then((text: Pdfjs.TextContent) => {
+ // // let tc = new Pdfjs.TextContentItem()
+ // // let tc = {str: }
+ // this._textContent[i] = text;
+ // // text.items.forEach(t => {
+ // // tcStr += t.str;
+ // // })
+ // });
+ // pageSizes[i] = { width: x.width, height: x.height };
+ })));
}
+ await Promise.all(proms);
runInAction(() =>
Array.from(Array((this._pageSizes = pageSizes).length).keys()).map(this.getPlaceholderPage));
this.props.loaded(Math.max(...pageSizes.map(i => i.width)), pageSizes[0].height, this.props.pdf.numPages);
// this.props.loaded(Math.max(...pageSizes.map(i => i.width)), pageSizes[0].height, this.props.pdf.numPages);
- }
- }
- private mainCont = (div: HTMLDivElement | null) => {
- this._dropDisposer && this._dropDisposer();
- if (div) {
- this._dropDisposer = div && DragManager.MakeDropTarget(div, { handlers: { drop: this.drop.bind(this) } });
+ let startY = NumCast(this.props.parent.Document.startY);
+ let ccv = this.props.parent.props.ContainingCollectionView;
+ if (ccv) {
+ ccv.props.Document.panY = startY;
+ }
+ this.props.parent.Document.scrollY = 0;
+ this.props.parent.Document.scrollY = startY + 1;
}
}
+ @action
makeAnnotationDocument = (sourceDoc: Doc | undefined, s: number, color: string): Doc => {
let annoDocs: Doc[] = [];
- let mainAnnoDoc = new Doc();
+ let mainAnnoDoc = Docs.Create.InstanceFromProto(new Doc(), "", {});
+
+ mainAnnoDoc.title = "Annotation on " + StrCast(this.props.parent.Document.title);
+ mainAnnoDoc.pdfDoc = this.props.parent.props.Document;
+ let minY = Number.MAX_VALUE;
this._savedAnnotations.forEach((key: number, value: HTMLDivElement[]) => {
for (let anno of value) {
let annoDoc = new Doc();
if (anno.style.left) annoDoc.x = parseInt(anno.style.left) / scale;
- if (anno.style.top) annoDoc.y = parseInt(anno.style.top) / scale;
+ if (anno.style.top) {
+ annoDoc.y = parseInt(anno.style.top) / scale;
+ minY = Math.min(parseInt(anno.style.top), minY);
+ }
if (anno.style.height) annoDoc.height = parseInt(anno.style.height) / scale;
if (anno.style.width) annoDoc.width = parseInt(anno.style.width) / scale;
annoDoc.page = key;
@@ -156,11 +240,13 @@ class Viewer extends React.Component<IViewerProps> {
}
});
+ mainAnnoDoc.y = Math.max(minY, 0);
mainAnnoDoc.annotations = new List<Doc>(annoDocs);
if (sourceDoc) {
DocUtils.MakeLink(sourceDoc, mainAnnoDoc, undefined, `Annotation from ${StrCast(this.props.parent.Document.title)}`, "", StrCast(this.props.parent.Document.title));
}
this._savedAnnotations.clear();
+ this.Index = -1;
return mainAnnoDoc;
}
@@ -168,13 +254,14 @@ 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();
}
@@ -187,6 +274,7 @@ class Viewer extends React.Component<IViewerProps> {
pageLoaded = (index: number, page: Pdfjs.PDFPageViewport): void => {
this.props.loaded(page.width, page.height, this.props.pdf.numPages);
}
+
@action
getPlaceholderPage = (page: number) => {
if (this._isPage[page] !== "none") {
@@ -197,12 +285,14 @@ class Viewer extends React.Component<IViewerProps> {
);
}
}
+
@action
getRenderedPage = (page: number) => {
if (this._isPage[page] !== "page") {
this._isPage[page] = "page";
this._visibleElements[page] = (
<Page
+ size={this._pageSizes[page]}
pdf={this.props.pdf}
page={page}
numPages={this.props.pdf.numPages}
@@ -215,7 +305,7 @@ class Viewer extends React.Component<IViewerProps> {
createAnnotation={this.createAnnotation}
sendAnnotations={this.receiveAnnotations}
makeAnnotationDocuments={this.makeAnnotationDocument}
- receiveAnnotations={this.sendAnnotations}
+ getScrollFromPage={this.getScrollFromPage}
{...this.props} />
);
}
@@ -229,10 +319,14 @@ class Viewer extends React.Component<IViewerProps> {
if (this._isPage[page] !== "image") {
this._isPage[page] = "image";
const address = this.props.url;
- let res = JSON.parse(await rp.get(DocServer.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` }} />);
+ try {
+ 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);
+ }
}
}
@@ -287,28 +381,6 @@ class Viewer extends React.Component<IViewerProps> {
return this._savedAnnotations.getValue(page);
}
- // createPinAnnotation = (x: number, y: number, page: number): void => {
- // let targetDoc = Docs.TextDocument({ width: 100, height: 50, title: "New Pin Annotation" });
- // let pinAnno = new Doc();
- // pinAnno.x = x;
- // pinAnno.y = y + this.getScrollFromPage(page);
- // pinAnno.width = pinAnno.height = PinRadius;
- // pinAnno.page = page;
- // pinAnno.target = targetDoc;
- // pinAnno.type = AnnotationTypes.Pin;
- // // this._annotations.push(pinAnno);
- // let annoDoc = new Doc();
- // annoDoc.annotations = new List<Doc>([pinAnno]);
- // let annotations = DocListCast(this.props.parent.Document.annotations);
- // if (annotations && annotations.length) {
- // annotations.push(annoDoc);
- // this.props.parent.Document.annotations = new List<Doc>(annotations);
- // }
- // else {
- // this.props.parent.Document.annotations = new List<Doc>([annoDoc]);
- // }
- // }
-
// get the page index that the vertical offset passed in is on
getPageFromScroll = (vOffset: number) => {
let index = 0;
@@ -344,37 +416,250 @@ class Viewer extends React.Component<IViewerProps> {
}
}
- renderAnnotation = (anno: Doc): JSX.Element[] => {
- let annotationDocs = DocListCast(anno.annotations);
- let res = annotationDocs.map(a => {
- let type = NumCast(a.type);
- switch (type) {
- // case AnnotationTypes.Pin:
- // return <PinAnnotation parent={this} document={a} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />;
- case AnnotationTypes.Region:
- return <RegionAnnotation parent={this} document={a} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />;
- default:
- return <div></div>;
+ renderAnnotation = (anno: Doc, index: number): JSX.Element => {
+ return <Annotation anno={anno} index={index} parent={this} key={`${anno[Id]}-annotation`} />;
+ }
+
+ @action
+ pointerDown = () => {
+ // this._searching = false;
+ }
+
+ @action
+ search = (searchString: string) => {
+ if (this._pdfViewer._pageViewsReady) {
+ this._pdfFindController.executeCommand('find',
+ {
+ caseSensitive: false,
+ findPrevious: undefined,
+ highlightAll: true,
+ phraseSearch: true,
+ query: searchString
+ });
+ }
+ 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',
+ {
+ caseSensitive: false,
+ findPrevious: undefined,
+ highlightAll: true,
+ phraseSearch: true,
+ query: searchString
+ });
+ });
+ }
+ }
+
+ // let viewer = this._viewer.current;
+
+ // if (!this._pdfFindController) {
+ // if (container && viewer) {
+ // let simpleLinkService = new SimpleLinkService();
+ // let pdfViewer = new PDFJSViewer.PDFViewer({
+ // container: container,
+ // viewer: viewer,
+ // linkService: simpleLinkService
+ // });
+ // simpleLinkService.setPdf(this.props.pdf);
+ // container.addEventListener("pagesinit", () => {
+ // pdfViewer.currentScaleValue = 1;
+ // });
+ // container.addEventListener("pagerendered", () => {
+ // console.log("rendered");
+ // this._pdfFindController.executeCommand('find',
+ // {
+ // caseSensitive: false,
+ // findPrevious: undefined,
+ // highlightAll: true,
+ // phraseSearch: true,
+ // query: searchString
+ // });
+ // });
+ // pdfViewer.setDocument(this.props.pdf);
+ // this._pdfFindController = new PDFJSViewer.PDFFindController(pdfViewer);
+ // // this._pdfFindController._linkService = pdfLinkService;
+ // pdfViewer.findController = this._pdfFindController;
+ // }
+ // }
+ // else {
+ // this._pdfFindController.executeCommand('find',
+ // {
+ // caseSensitive: false,
+ // findPrevious: undefined,
+ // highlightAll: true,
+ // phraseSearch: true,
+ // query: searchString
+ // });
+ // }
+ }
+
+ searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this._searchString = e.currentTarget.value;
+ }
+
+ @action
+ toggleSearch = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ this._searching = !this._searching;
+
+ if (this._searching) {
+ let container = this._mainCont.current;
+ let viewer = this._viewer.current;
+
+ if (!this._pdfFindController) {
+ if (container && viewer) {
+ let simpleLinkService = new SimpleLinkService();
+ this._pdfViewer = new PDFJSViewer.PDFViewer({
+ container: container,
+ viewer: viewer,
+ linkService: simpleLinkService
+ });
+ simpleLinkService.setPdf(this.props.pdf);
+ container.addEventListener("pagesinit", () => {
+ this._pdfViewer.currentScaleValue = 1;
+ });
+ container.addEventListener("pagerendered", () => {
+ console.log("rendered");
+ });
+ this._pdfViewer.setDocument(this.props.pdf);
+ this._pdfFindController = new PDFJSViewer.PDFFindController(this._pdfViewer);
+ // this._pdfFindController._linkService = pdfLinkService;
+ this._pdfViewer.findController = this._pdfFindController;
+ }
+ }
+ }
+ else {
+ 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();
+ }
+ }
+ }
+ }
+
+ @action
+ prevAnnotation = (e: React.MouseEvent) => {
+ e.stopPropagation();
+
+ // if (this.Index > 0) {
+ // this.Index--;
+ // }
+ this.Index = Math.max(this.Index - 1, 0);
+ }
+
+ @action
+ nextAnnotation = (e: React.MouseEvent) => {
+ e.stopPropagation();
+
+ let compiled = this._script;
+ let filtered = this._annotations.filter(anno => {
+ if (compiled && compiled.compiled) {
+ let run = compiled.run({ this: anno });
+ if (run.success) {
+ return run.result;
+ }
}
+ return true;
});
- return res;
+ this.Index = Math.min(this.Index + 1, filtered.length - 1);
+ }
+
+ nextResult = () => {
+ // if (this._viewer.current) {
+ // let results = this._pdfFindController.pageMatches;
+ // if (results && results.length) {
+ // if (this._pageIndex === this.props.pdf.numPages && this._matchIndex === results[this._pageIndex].length - 1) {
+ // return;
+ // }
+ // if (this._pageIndex === -1 || this._matchIndex === results[this._pageIndex].length - 1) {
+ // this._matchIndex = 0;
+ // this._pageIndex++;
+ // }
+ // else {
+ // this._matchIndex++;
+ // }
+ // this._pdfFindController._nextMatch()
+ // let nextMatch = this._viewer.current.children[this._pageIndex].children[1].children[results[this._pageIndex][this._matchIndex]];
+ // rconsole.log(nextMatch);
+ // this.props.parent.scrollTo(nextMatch.getBoundingClientRect().top);
+ // nextMatch.setAttribute("style", nextMatch.getAttribute("style") ? nextMatch.getAttribute("style") + ", background-color: green" : "background-color: green");
+ // }
+ // }
}
render() {
+ let compiled = this._script;
return (
- <div ref={this.mainCont} style={{ pointerEvents: "all" }}>
- <div className="viewer">
+ <div ref={this._mainCont} style={{ pointerEvents: "all" }} onPointerDown={this.pointerDown}>
+ <div className="viewer" style={this._searching ? { position: "absolute", top: 0 } : {}}>
{this._visibleElements}
</div>
+ <div className="pdfViewer-text" ref={this._viewer} style={{ transform: "scale(1.5)", transformOrigin: "top left" }} />
<div className="pdfViewer-annotationLayer"
style={{
height: this.props.parent.Document.nativeHeight, width: `100%`,
pointerEvents: this.props.parent.props.active() ? "none" : "all"
}}>
<div className="pdfViewer-annotationLayer-subCont" ref={this._annotationLayer}>
- {this._annotations.map(anno => this.renderAnnotation(anno))}
+ {this._annotations.filter(anno => {
+ if (compiled && compiled.compiled) {
+ let run = compiled.run({ this: anno });
+ if (run.success) {
+ return run.result;
+ }
+ }
+ return true;
+ }).sort((a: Doc, b: Doc) => NumCast(a.y) - NumCast(b.y))
+ .map((anno: Doc, index: number) => this.renderAnnotation(anno, index))}
</div>
</div>
+ <div className="pdfViewer-overlayCont" onPointerDown={(e) => e.stopPropagation()}
+ style={{
+ bottom: -this.props.scrollY,
+ left: `${this._searching ? 0 : 100}%`
+ }}>
+ <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 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"
+ style={{ bottom: -this.props.scrollY + 280, right: 10, display: this.props.parent.props.active() ? "flex" : "none" }}>
+ <div className="pdfViewer-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}>
+ <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-up"} size="3x" />
+ </div>
+ </button>
+ <button className="pdfViewer-overlayButton" onClick={this.nextAnnotation} title="Next Annotation"
+ style={{ bottom: -this.props.scrollY + 200, right: 10, display: this.props.parent.props.active() ? "flex" : "none" }}>
+ <div className="pdfViewer-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}>
+ <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-down"} size="3x" />
+ </div>
+ </button>
+ <button className="pdfViewer-overlayButton" onClick={this.toggleSearch} title="Open Search Bar"
+ style={{ bottom: -this.props.scrollY + 10, right: 0, display: this.props.parent.props.active() ? "flex" : "none" }}>
+ <div className="pdfViewer-overlayButton-arrow" onPointerDown={(e) => e.stopPropagation()}></div>
+ <div className="pdfViewer-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}>
+ <FontAwesomeIcon style={{ color: "white" }} icon={this._searching ? "times" : "search"} size="3x" />
+ </div>
+ </button>
</div >
);
}
@@ -384,203 +669,37 @@ export enum AnnotationTypes {
Region
}
-interface IAnnotationProps {
- x: number;
- y: number;
- width: number;
- height: number;
- parent: Viewer;
- document: Doc;
-}
-
-// @observer
-// class PinAnnotation extends React.Component<IAnnotationProps> {
-// @observable private _backgroundColor: string = "green";
-// @observable private _display: string = "initial";
-
-// private _mainCont: React.RefObject<HTMLDivElement>;
-
-// constructor(props: IAnnotationProps) {
-// super(props);
-// this._mainCont = React.createRef();
-// }
-
-// componentDidMount = () => {
-// let selected = this.props.document.selected;
-// if (!BoolCast(selected)) {
-// runInAction(() => {
-// this._backgroundColor = "red";
-// this._display = "none";
-// });
-// }
-// if (selected) {
-// if (BoolCast(selected)) {
-// runInAction(() => {
-// this._backgroundColor = "green";
-// this._display = "initial";
-// });
-// }
-// else {
-// runInAction(() => {
-// this._backgroundColor = "red";
-// this._display = "none";
-// });
-// }
-// }
-// else {
-// runInAction(() => {
-// this._backgroundColor = "red";
-// this._display = "none";
-// });
-// }
-// }
-
-// @action
-// pointerDown = (e: React.PointerEvent) => {
-// let selected = this.props.document.selected;
-// if (selected && BoolCast(selected)) {
-// this._backgroundColor = "red";
-// this._display = "none";
-// this.props.document.selected = false;
-// }
-// else {
-// this._backgroundColor = "green";
-// this._display = "initial";
-// this.props.document.selected = true;
-// }
-// e.preventDefault();
-// e.stopPropagation();
-// }
-
-// @action
-// doubleClick = (e: React.MouseEvent) => {
-// if (this._mainCont.current) {
-// let annotations = DocListCast(this.props.parent.props.parent.Document.annotations);
-// if (annotations && annotations.length) {
-// let index = annotations.indexOf(this.props.document);
-// annotations.splice(index, 1);
-// this.props.parent.props.parent.Document.annotations = new List<Doc>(annotations);
-// }
-// // this._mainCont.current.childNodes.forEach(e => e.remove());
-// this._mainCont.current.style.display = "none";
-// // if (this._mainCont.current.parentElement) {
-// // this._mainCont.current.remove();
-// // }
-// }
-// e.stopPropagation();
-// }
-
-// render() {
-// let targetDoc = Cast(this.props.document.target, Doc);
-// if (targetDoc instanceof Doc) {
-// return (
-// <div className="pdfViewer-pinAnnotation" onPointerDown={this.pointerDown}
-// onDoubleClick={this.doubleClick} ref={this._mainCont}
-// style={{
-// top: this.props.y * scale - PinRadius / 2, left: this.props.x * scale - PinRadius / 2, width: PinRadius,
-// height: PinRadius, pointerEvents: "all", backgroundColor: this._backgroundColor
-// }}>
-// <div style={{
-// position: "absolute", top: "25px", left: "25px", transform: "scale(3)", transformOrigin: "top left",
-// display: this._display, width: targetDoc[WidthSym](), height: targetDoc[HeightSym]()
-// }}>
-// <DocumentView Document={targetDoc}
-// ContainingCollectionView={undefined}
-// ScreenToLocalTransform={this.props.parent.props.parent.props.ScreenToLocalTransform}
-// isTopMost={false}
-// ContentScaling={() => 1}
-// PanelWidth={() => NumCast(this.props.parent.props.parent.Document.nativeWidth)}
-// PanelHeight={() => NumCast(this.props.parent.props.parent.Document.nativeHeight)}
-// focus={emptyFunction}
-// selectOnLoad={false}
-// parentActive={this.props.parent.props.parent.props.active}
-// whenActiveChanged={this.props.parent.props.parent.props.whenActiveChanged}
-// bringToFront={emptyFunction}
-// addDocTab={this.props.parent.props.parent.props.addDocTab}
-// />
-// </div>
-// </div >
-// );
-// }
-// return null;
-// }
-// }
-
-class RegionAnnotation extends React.Component<IAnnotationProps> {
- @observable private _backgroundColor: string = "red";
-
- private _reactionDisposer?: IReactionDisposer;
- private _mainCont: React.RefObject<HTMLDivElement>;
+class SimpleLinkService {
+ externalLinkTarget: any = null;
+ externalLinkRel: any = null;
+ pdf: any = null;
- constructor(props: IAnnotationProps) {
- super(props);
+ navigateTo() { }
- this._mainCont = React.createRef();
- }
+ getDestinationHash() { return "#"; }
- componentDidMount() {
- this._reactionDisposer = reaction(
- () => BoolCast(this.props.document.delete),
- () => {
- if (BoolCast(this.props.document.delete)) {
- if (this._mainCont.current) {
- this._mainCont.current.style.display = "none";
- }
- }
- },
- { fireImmediately: true }
- );
- }
+ getAnchorUrl() { return "#"; }
- componentWillUnmount() {
- this._reactionDisposer && this._reactionDisposer();
- }
+ setHash() { }
- deleteAnnotation = () => {
- let annotation = DocListCast(this.props.parent.props.parent.Document.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);
- }
+ executeNamedAction() { }
- if (group) {
- let groupAnnotations = DocListCast(group.annotations);
- groupAnnotations.forEach(anno => anno.delete = true);
- }
+ cachePageRef() { }
- PDFMenu.Instance.fadeOut(true);
+ get pagesCount() {
+ return this.pdf ? this.pdf.numPages : 0;
}
+ get page() {
+ return 0;
+ }
- // annotateThis = (e: PointerEvent) => {
- // e.preventDefault();
- // e.stopPropagation();
- // // document that this annotation is linked to
- // let targetDoc = Docs.TextDocument({ width: 200, height: 200, title: "New Annotation" });
- // let group = FieldValue(Cast(this.props.document.group, Doc));
- // }
-
- @action
- onPointerDown = (e: React.PointerEvent) => {
- if (e.button === 0) {
- let targetDoc = Cast(this.props.document.target, Doc, null);
- if (targetDoc) {
- DocumentManager.Instance.jumpToDocument(targetDoc);
- }
- }
- if (e.button === 2) {
- PDFMenu.Instance.Status = "annotation";
- PDFMenu.Instance.Delete = this.deleteAnnotation;
- PDFMenu.Instance.Pinned = false;
- PDFMenu.Instance.jumpTo(e.clientX, e.clientY, true);
- }
+ setPdf(pdf: any) {
+ this.pdf = pdf;
}
- render() {
- return (
- <div className="pdfViewer-annotationBox" onPointerDown={this.onPointerDown} ref={this._mainCont}
- style={{ top: this.props.y * scale, left: this.props.x * scale, width: this.props.width * scale, height: this.props.height * scale, pointerEvents: "all", backgroundColor: StrCast(this.props.document.color) }}></div>
- );
+ get rotation() {
+ return 0;
}
+ set rotation(value: any) { }
} \ No newline at end of file
diff --git a/src/client/views/pdf/Page.tsx b/src/client/views/pdf/Page.tsx
index 734dff7fc..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,15 +10,17 @@ 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";
import PDFMenu from "./PDFMenu";
import { UndoManager } from "../../util/UndoManager";
+import { copy } from "typescript-collections/dist/lib/arrays";
interface IPageProps {
+ size: { width: number, height: number };
pdf: Opt<Pdfjs.PDFDocumentProxy>;
name: string;
numPages: number;
@@ -28,16 +30,16 @@ interface IPageProps {
renderAnnotations: (annotations: Doc[], removeOld: boolean) => void;
makePin: (x: number, y: number, page: number) => void;
sendAnnotations: (annotations: HTMLDivElement[], page: number) => void;
- receiveAnnotations: (page: number) => HTMLDivElement[] | undefined;
createAnnotation: (div: HTMLDivElement, page: number) => void;
- makeAnnotationDocuments: (doc: Doc | undefined, scale: number, color: string) => Doc;
+ makeAnnotationDocuments: (doc: Doc | undefined, scale: number, color: string, linkTo: boolean) => Doc;
+ getScrollFromPage: (page: number) => number;
}
@observer
export default class Page extends React.Component<IPageProps> {
@observable private _state: string = "N/A";
- @observable private _width: number = 0;
- @observable private _height: number = 0;
+ @observable private _width: number = this.props.size.width;
+ @observable private _height: number = this.props.size.height;
@observable private _page: Opt<Pdfjs.PDFPageProxy>;
@observable private _currPage: number = this.props.page + 1;
@observable private _marqueeX: number = 0;
@@ -50,9 +52,11 @@ export default class Page extends React.Component<IPageProps> {
private _textLayer: React.RefObject<HTMLDivElement>;
private _annotationLayer: React.RefObject<HTMLDivElement>;
private _marquee: React.RefObject<HTMLDivElement>;
- private _curly: React.RefObject<HTMLImageElement>;
+ // 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);
@@ -60,7 +64,7 @@ export default class Page extends React.Component<IPageProps> {
this._textLayer = React.createRef();
this._annotationLayer = React.createRef();
this._marquee = React.createRef();
- this._curly = React.createRef();
+ // this._curly = React.createRef();
}
componentDidMount = (): void => {
@@ -135,10 +139,10 @@ export default class Page extends React.Component<IPageProps> {
@action
highlight = (targetDoc?: Doc, color: string = "red") => {
// creates annotation documents for current highlights
- let annotationDoc = this.props.makeAnnotationDocuments(targetDoc, scale, color);
- let targetAnnotations = Cast(this.props.parent.Document.annotations, listSpec(Doc));
+ let annotationDoc = this.props.makeAnnotationDocuments(targetDoc, scale, color, false);
+ 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);
}
@@ -150,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): 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([this._textLayer.current], dragData, e.pageX, e.pageY, {
+ 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
});
@@ -177,6 +195,21 @@ export default class Page extends React.Component<IPageProps> {
e.stopPropagation();
}
+ createSnippet = (marquee: { left: number, top: number, width: number, height: number }): void => {
+ let doc = this.props.parent.Document;
+ let view = Doc.MakeAlias(doc);
+ let data = Doc.MakeDelegate(doc.proto!);
+ data.title = StrCast(data.title) + "_snippet";
+ view.proto = data;
+ view.nativeHeight = marquee.height;
+ view.height = (doc[WidthSym]() / NumCast(doc.nativeWidth)) * marquee.height;
+ view.nativeWidth = doc.nativeWidth;
+ view.startY = marquee.top + this.props.getScrollFromPage(this.props.page);
+ view.width = doc[WidthSym]();
+ let dragData = new DragManager.DocumentDragData([view], [undefined]);
+ DragManager.StartDocumentDrag([], dragData, 0, 0);
+ }
+
@action
onPointerDown = (e: React.PointerEvent): void => {
// if alt+left click, drag and annotate
@@ -191,6 +224,7 @@ export default class Page extends React.Component<IPageProps> {
else if (e.button === 0) {
PDFMenu.Instance.StartDrag = this.startDrag;
PDFMenu.Instance.Highlight = this.highlight;
+ PDFMenu.Instance.Snippet = this.createSnippet;
PDFMenu.Instance.Status = "pdf";
PDFMenu.Instance.fadeOut(true);
let target: any = e.target;
@@ -202,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";
@@ -226,12 +260,16 @@ 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) {
+ if (this._marquee.current /*&& this._curly.current*/) {
this._marquee.current.style.background = background;
- this._curly.current.style.opacity = opacity;
+ // this._curly.current.style.opacity = opacity;
this._rotate = transform;
}
}
@@ -244,33 +282,33 @@ export default class Page extends React.Component<IPageProps> {
}
getCurlyTransform = (): { background: string, opacity: string, transform: string } => {
- let background = "", opacity = "", transform = "";
- if (this._marquee.current && this._curly.current) {
- if (this._marqueeWidth > 100 && this._marqueeHeight > 100) {
- background = "red";
- opacity = "0";
- }
- else {
- background = "transparent";
- opacity = "1";
- }
+ // let background = "", opacity = "", transform = "";
+ // if (this._marquee.current && this._curly.current) {
+ // if (this._marqueeWidth > 100 && this._marqueeHeight > 100) {
+ // background = "red";
+ // opacity = "0";
+ // }
+ // else {
+ // background = "transparent";
+ // opacity = "1";
+ // }
- // split up for simplicity, could be done in a nested ternary. please do not. -syip2
- let ratio = this._marqueeWidth / this._marqueeHeight;
- if (ratio > 1.5) {
- // vertical
- transform = "rotate(90deg) scale(1, 5)";
- }
- else if (ratio < 0.5) {
- // horizontal
- transform = "scale(2, 1)";
- }
- else {
- // diagonal
- transform = "rotate(45deg) scale(1.5, 1.5)";
- }
- }
- return { background: background, opacity: opacity, transform: transform };
+ // // split up for simplicity, could be done in a nested ternary. please do not. -syip2
+ // let ratio = this._marqueeWidth / this._marqueeHeight;
+ // if (ratio > 1.5) {
+ // // vertical
+ // transform = "rotate(90deg) scale(1, 5)";
+ // }
+ // else if (ratio < 0.5) {
+ // // horizontal
+ // transform = "scale(2, 1)";
+ // }
+ // else {
+ // // diagonal
+ // transform = "rotate(45deg) scale(1.5, 1.5)";
+ // }
+ // }
+ return { background: "red", opacity: "0.5", transform: "" };
}
@action
@@ -280,31 +318,37 @@ export default class Page extends React.Component<IPageProps> {
if (this._marquee.current) {
let copy = document.createElement("div");
// make a copy of the marquee
- copy.style.left = this._marquee.current.style.left;
- copy.style.top = this._marquee.current.style.top;
- copy.style.width = this._marquee.current.style.width;
- copy.style.height = this._marquee.current.style.height;
+ let style = this._marquee.current.style;
+ copy.style.left = style.left;
+ copy.style.top = style.top;
+ copy.style.width = style.width;
+ copy.style.height = style.height;
// apply the appropriate background, opacity, and transform
let { background, opacity, transform } = this.getCurlyTransform();
copy.style.background = background;
// if curly bracing, add a curly brace
- if (opacity === "1" && this._curly.current) {
- copy.style.opacity = opacity;
- let img = this._curly.current.cloneNode();
- (img as any).style.opacity = opacity;
- (img as any).style.transform = transform;
- copy.appendChild(img);
- }
- else {
- copy.style.opacity = this._marquee.current.style.opacity;
- }
+ // if (opacity === "1" && this._curly.current) {
+ // copy.style.opacity = opacity;
+ // let img = this._curly.current.cloneNode();
+ // (img as any).style.opacity = opacity;
+ // (img as any).style.transform = transform;
+ // copy.appendChild(img);
+ // }
+ // else {
+ copy.style.border = style.border;
+ copy.style.opacity = style.opacity;
+ // }
copy.className = this._marquee.current.className;
this.props.createAnnotation(copy, this.props.page);
this._marquee.current.style.opacity = "0";
}
if (this._marqueeWidth > 10 || this._marqueeHeight > 10) {
+ if (!e.ctrlKey) {
+ PDFMenu.Instance.Status = "snippet";
+ PDFMenu.Instance.Marquee = { left: this._marqueeX, top: this._marqueeY, width: this._marqueeWidth, height: this._marqueeHeight };
+ }
PDFMenu.Instance.jumpTo(e.clientX, e.clientY);
}
@@ -320,7 +364,7 @@ export default class Page extends React.Component<IPageProps> {
if (PDFMenu.Instance.Highlighting) {
- this.highlight(undefined, "#f4f442");
+ this.highlight(undefined, "goldenrod");
}
else {
PDFMenu.Instance.StartDrag = this.startDrag;
@@ -381,8 +425,8 @@ export default class Page extends React.Component<IPageProps> {
</div>
<div className="pdfInkingLayer-cont" ref={this._annotationLayer} style={{ width: "100%", height: "100%", position: "relative", top: "-100%" }}>
<div className="pdfViewer-annotationBox" ref={this._marquee}
- style={{ left: `${this._marqueeX}px`, top: `${this._marqueeY}px`, width: `${this._marqueeWidth}px`, height: `${this._marqueeHeight}px`, background: "transparent" }}>
- <img ref={this._curly} src="https://static.thenounproject.com/png/331760-200.png" style={{ width: "100%", height: "100%", transform: `${this._rotate}` }} />
+ style={{ left: `${this._marqueeX}px`, top: `${this._marqueeY}px`, width: `${this._marqueeWidth}px`, height: `${this._marqueeHeight}px`, background: "red", border: `${this._marqueeWidth === 0 ? "" : "10px dashed black"}` }}>
+ {/* <img ref={this._curly} src="https://static.thenounproject.com/png/331760-200.png" style={{ width: "100%", height: "100%", transform: `${this._rotate}` }} /> */}
</div>
</div>
<div className="textlayer" ref={this._textLayer} style={{ "position": "relative", "top": `-${2 * this._height}px`, "height": `${this._height}px` }} />
diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx
new file mode 100644
index 000000000..329630875
--- /dev/null
+++ b/src/client/views/presentationview/PresentationElement.tsx
@@ -0,0 +1,818 @@
+import { observer } from "mobx-react";
+import React = require("react");
+import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc";
+import { NumCast, BoolCast, StrCast, Cast } from "../../../new_fields/Types";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { observable, action, computed, runInAction } from "mobx";
+import "./PresentationView.scss";
+import { Utils } from "../../../Utils";
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faFile as fileSolid, faFileDownload, faLocationArrow, faArrowUp, faSearch } from '@fortawesome/free-solid-svg-icons';
+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);
+library.add(faLocationArrow);
+library.add(fileRegular as any);
+library.add(faSearch);
+
+interface PresentationElementProps {
+ mainDocument: Doc;
+ document: Doc;
+ index: number;
+ deleteDocument(index: number): void;
+ gotoDocument(index: number, fromDoc: number): Promise<void>;
+ allListElements: Doc[];
+ groupMappings: Map<String, Doc[]>;
+ presStatus: boolean;
+ presButtonBackUp: Doc;
+ presGroupBackUp: Doc;
+ removeDocByRef(doc: Doc): boolean;
+ PresElementsMappings: Map<Doc, PresentationElement>;
+
+
+}
+
+//enum for the all kinds of buttons a doc in presentation can have
+export enum buttonIndex {
+ Show = 0,
+ Navigate = 1,
+ HideTillPressed = 2,
+ FadeAfter = 3,
+ HideAfter = 4,
+ Group = 5,
+
+}
+
+/**
+ * This class models the view a document added to presentation will have in the presentation.
+ * It involves some functionality for its buttons and options.
+ */
+@observer
+export 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.
+ */
+ @computed
+ get selected() {
+ return this.selectedButtons;
+ }
+
+ //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() {
+ if (this.presElRef.current) {
+ this.header = this.presElRef.current;
+ this.createListDropTarget(this.presElRef.current);
+ }
+ }
+
+ receiveButtonBackUp = async () => {
+
+ //get the list that stores docs that keep track of buttons
+ let castedList = Cast(this.props.presButtonBackUp.selectedButtonDocs, listSpec(Doc));
+ 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
+
+ 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);
+ this.backUpDoc = newDoc;
+ }
+
+ }
+
+ /**
+ * The function that is called to group docs together. It tries to group a doc
+ * that turned grouping option with the above document. If that doc is grouped with
+ * other documents. Those other documents will be grouped with doc's above document as well.
+ */
+ @action
+ onGroupClick = (document: Doc, index: number, buttonStatus: boolean) => {
+ let p = this.props;
+ if (index >= 1) {
+ //checking if options was turned true
+ if (buttonStatus) {
+ //getting the id of the above-doc and the doc
+ let aboveGuid = StrCast(p.allListElements[index - 1].presentId, null);
+ let docGuid = StrCast(document.presentId, null);
+ //the case where above-doc is already in group
+ if (p.groupMappings.has(aboveGuid)) {
+ let aboveArray = p.groupMappings.get(aboveGuid)!;
+ //case where doc is already in group
+ if (p.groupMappings.has(docGuid)) {
+ let docsArray = p.groupMappings.get(docGuid)!;
+ docsArray.forEach((doc: Doc) => {
+ if (!aboveArray.includes(doc)) {
+ aboveArray.push(doc);
+ }
+ doc.presentId = aboveGuid;
+ });
+ p.groupMappings.delete(docGuid);
+ //the case where doc was not in group
+ } else {
+ if (!aboveArray.includes(document)) {
+ aboveArray.push(document);
+
+ }
+
+ }
+ //the case where above-doc was not in group
+ } else {
+ let newAboveArray: Doc[] = [];
+ newAboveArray.push(p.allListElements[index - 1]);
+
+ //the case where doc is in group
+ if (p.groupMappings.has(docGuid)) {
+ let docsArray = p.groupMappings.get(docGuid)!;
+ docsArray.forEach((doc: Doc) => {
+ newAboveArray.push(doc);
+ doc.presentId = aboveGuid;
+ });
+ p.groupMappings.delete(docGuid);
+
+ //the case where doc is not in a group
+ } else {
+ newAboveArray.push(document);
+
+ }
+ p.groupMappings.set(aboveGuid, newAboveArray);
+
+ }
+ document.presentId = aboveGuid;
+
+ //when grouping is turned off
+ } else {
+ let curArray = p.groupMappings.get(StrCast(document.presentId, Utils.GenerateGuid()))!;
+ let targetIndex = curArray.indexOf(document);
+ let firstPart = curArray.slice(0, targetIndex);
+ let firstPartNewGuid = Utils.GenerateGuid();
+ firstPart.forEach((doc: Doc) => doc.presentId = firstPartNewGuid);
+ let secondPart = curArray.slice(targetIndex);
+ p.groupMappings.set(StrCast(p.allListElements[index - 1].presentId, Utils.GenerateGuid()), firstPart);
+ p.groupMappings.set(StrCast(document.presentId, Utils.GenerateGuid()), secondPart);
+
+
+ }
+
+ }
+ this.autoSaveGroupChanges();
+
+ }
+
+
+ /**
+ * This function is called at the end of each group update to update the group updates.
+ */
+ @action
+ autoSaveGroupChanges = () => {
+ let castedList: List<Doc> = new List<Doc>();
+ this.props.presGroupBackUp.groupDocs = castedList;
+ this.props.groupMappings.forEach((docArray: Doc[], id: String) => {
+ //create a new doc for each group
+ let newGroupDoc = new Doc();
+ castedList.push(newGroupDoc);
+ //store the id of the group in the doc
+ newGroupDoc.presentIdStore = id.toString();
+ //store the doc array which represents the group in the doc
+ newGroupDoc.grouping = new List(docArray);
+ });
+
+ }
+
+ /**
+ * Function that is called on click to change the group status of a docus, by turning the option on/off.
+ */
+ @action
+ changeGroupStatus = () => {
+ if (this.selectedButtons[buttonIndex.Group]) {
+ this.selectedButtons[buttonIndex.Group] = false;
+ } else {
+ this.selectedButtons[buttonIndex.Group] = true;
+ }
+ this.autoSaveButtonChange(buttonIndex.Group);
+
+ }
+
+ /**
+ * The function that is called on click to turn Hiding document till press option on/off.
+ * It also sets the beginning and end opacitys.
+ */
+ @action
+ onHideDocumentUntilPressClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ const current = NumCast(this.props.mainDocument.selectedDoc);
+ if (this.selectedButtons[buttonIndex.HideTillPressed]) {
+ this.selectedButtons[buttonIndex.HideTillPressed] = false;
+ if (this.props.index >= current) {
+ this.props.document.opacity = 1;
+ }
+ } else {
+ this.selectedButtons[buttonIndex.HideTillPressed] = true;
+ if (this.props.presStatus) {
+ if (this.props.index > current) {
+ this.props.document.opacity = 0;
+ }
+ }
+ }
+ this.autoSaveButtonChange(buttonIndex.HideTillPressed);
+ }
+
+ /**
+ * This function is called to get the updates for the changed buttons.
+ */
+ @action
+ autoSaveButtonChange = async (index: buttonIndex) => {
+ if (this.backUpDoc) {
+ this.backUpDoc.selectedButtons = new List(this.selectedButtons);
+ }
+ }
+
+ /**
+ * The function that is called on click to turn Hiding document after presented option on/off.
+ * It also makes sure that the option swithches from fade-after to this one, since both
+ * can't coexist.
+ */
+ @action
+ onHideDocumentAfterPresentedClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ const current = NumCast(this.props.mainDocument.selectedDoc);
+ if (this.selectedButtons[buttonIndex.HideAfter]) {
+ this.selectedButtons[buttonIndex.HideAfter] = false;
+ if (this.props.index <= current) {
+ this.props.document.opacity = 1;
+ }
+ } else {
+ if (this.selectedButtons[buttonIndex.FadeAfter]) {
+ this.selectedButtons[buttonIndex.FadeAfter] = false;
+ }
+ this.selectedButtons[buttonIndex.HideAfter] = true;
+ if (this.props.presStatus) {
+ if (this.props.index < current) {
+ this.props.document.opacity = 0;
+ }
+ }
+ }
+ this.autoSaveButtonChange(buttonIndex.HideAfter);
+
+ }
+
+ /**
+ * The function that is called on click to turn fading document after presented option on/off.
+ * It also makes sure that the option swithches from hide-after to this one, since both
+ * can't coexist.
+ */
+ @action
+ onFadeDocumentAfterPresentedClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ const current = NumCast(this.props.mainDocument.selectedDoc);
+ if (this.selectedButtons[buttonIndex.FadeAfter]) {
+ this.selectedButtons[buttonIndex.FadeAfter] = false;
+ if (this.props.index <= current) {
+ this.props.document.opacity = 1;
+ }
+ } else {
+ if (this.selectedButtons[buttonIndex.HideAfter]) {
+ this.selectedButtons[buttonIndex.HideAfter] = false;
+ }
+ this.selectedButtons[buttonIndex.FadeAfter] = true;
+ if (this.props.presStatus) {
+ if (this.props.index < current) {
+ this.props.document.opacity = 0.5;
+ }
+ }
+ }
+ this.autoSaveButtonChange(buttonIndex.FadeAfter);
+
+ }
+
+ /**
+ * The function that is called on click to turn navigation option of docs on/off.
+ */
+ @action
+ onNavigateDocumentClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ if (this.selectedButtons[buttonIndex.Navigate]) {
+ this.selectedButtons[buttonIndex.Navigate] = false;
+
+ } else {
+ if (this.selectedButtons[buttonIndex.Show]) {
+ this.selectedButtons[buttonIndex.Show] = false;
+ }
+ this.selectedButtons[buttonIndex.Navigate] = true;
+ const current = NumCast(this.props.mainDocument.selectedDoc);
+ if (current === this.props.index) {
+ this.props.gotoDocument(this.props.index, this.props.index);
+ }
+ }
+
+ this.autoSaveButtonChange(buttonIndex.Navigate);
+
+ }
+
+ /**
+ * The function that is called on click to turn zoom option of docs on/off.
+ */
+ @action
+ onZoomDocumentClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ if (this.selectedButtons[buttonIndex.Show]) {
+ this.selectedButtons[buttonIndex.Show] = false;
+ this.props.document.viewScale = 1;
+
+ } else {
+ if (this.selectedButtons[buttonIndex.Navigate]) {
+ this.selectedButtons[buttonIndex.Navigate] = false;
+ }
+ this.selectedButtons[buttonIndex.Show] = true;
+ const current = NumCast(this.props.mainDocument.selectedDoc);
+ if (current === this.props.index) {
+ this.props.gotoDocument(this.props.index, this.props.index);
+ }
+ }
+
+ this.autoSaveButtonChange(buttonIndex.Show);
+
+ }
+
+ /**
+ * 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;
+ let title = p.document.title;
+
+ //to get currently selected presentation doc
+ let selected = NumCast(p.mainDocument.selectedDoc, 0);
+
+ let className = " presentationView-item";
+ if (selected === p.index) {
+ //this doc is selected
+ className += " presentationView-selected";
+ }
+ 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}
+ ref={this.presElRef}
+ onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}
+ onPointerDown={onItemDown}
+ style={{
+ outlineColor: "maroon",
+ outlineStyle: "dashed",
+ 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" 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"} 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]);
+ }}> <FontAwesomeIcon icon={"arrow-up"} /> </button>
+
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/presentationview/PresentationList.tsx b/src/client/views/presentationview/PresentationList.tsx
new file mode 100644
index 000000000..2d63d41b5
--- /dev/null
+++ b/src/client/views/presentationview/PresentationList.tsx
@@ -0,0 +1,116 @@
+import { observer } from "mobx-react";
+import React = require("react");
+import { action } from "mobx";
+import "./PresentationView.scss";
+import { Utils } from "../../../Utils";
+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";
+
+
+
+
+interface PresListProps {
+ mainDocument: Doc;
+ deleteDocument(index: number): void;
+ gotoDocument(index: number, fromDoc: number): Promise<void>;
+ groupMappings: Map<String, Doc[]>;
+ PresElementsMappings: Map<Doc, PresentationElement>;
+ setChildrenDocs: (docList: Doc[]) => void;
+ presStatus: boolean;
+ presButtonBackUp: Doc;
+ presGroupBackUp: Doc;
+ removeDocByRef(doc: Doc): boolean;
+ clearElemMap(): void;
+
+}
+
+
+@observer
+/**
+ * Component that takes in a document prop and a boolean whether it's collapsed or not.
+ */
+export default class PresentationViewList extends React.Component<PresListProps> {
+
+ /**
+ * Method that initializes presentation ids for the
+ * docs that is in the presentation, when presentation list
+ * gets re-rendered. It makes sure to not assign ids to the
+ * docs that are in the group, so that mapping won't be disrupted.
+ */
+
+ @action
+ initializeGroupIds = async (docList: Doc[]) => {
+ docList.forEach(async (doc: Doc, index: number) => {
+ let docGuid = StrCast(doc.presentId, null);
+ //checking if part of group
+ let storedGuids: string[] = [];
+ let castedGroupDocs = await DocListCastAsync(this.props.presGroupBackUp.groupDocs);
+ //making sure the docs that were in groups, which were stored, to not get new guids.
+ if (castedGroupDocs !== undefined) {
+ castedGroupDocs.forEach((doc: Doc) => {
+ let storedGuid = StrCast(doc.presentIdStore, null);
+ if (storedGuid) {
+ storedGuids.push(storedGuid);
+ }
+
+ });
+ }
+ if (!this.props.groupMappings.has(docGuid) && !storedGuids.includes(docGuid)) {
+ doc.presentId = Utils.GenerateGuid();
+ }
+ });
+ }
+
+ /**
+ * Initially every document starts with a viewScale 1, which means
+ * that they will be displayed in a canvas with scale 1.
+ */
+ @action
+ initializeScaleViews = (docList: Doc[]) => {
+ docList.forEach((doc: Doc) => {
+ let curScale = NumCast(doc.viewScale, null);
+ if (curScale === undefined) {
+ doc.viewScale = 1;
+ }
+ });
+ }
+
+ render() {
+ const children = DocListCast(this.props.mainDocument.data);
+ 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 && 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.scss b/src/client/views/presentationview/PresentationView.scss
index fb4a851c4..2bb0ec8c8 100644
--- a/src/client/views/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;
@@ -47,20 +55,30 @@
padding-bottom: 3px;
font-size: 25px;
display: inline-block;
+ width: calc(100% - 160px);
}
.presentation-icon {
float: right;
}
+.presentation-interaction {
+ float: left;
+}
+
+.presentation-interaction-selected {
+ background: #505050;
+ float: left;
+}
+
.presentationView-name {
font-size: 15px;
display: inline-block;
}
.presentation-button {
- margin-right: 12.5%;
- margin-left: 12.5%;
+ margin-right: 2.5%;
+ margin-left: 2.5%;
width: 25%;
}
diff --git a/src/client/views/presentationview/PresentationView.tsx b/src/client/views/presentationview/PresentationView.tsx
new file mode 100644
index 000000000..f80840f96
--- /dev/null
+++ b/src/client/views/presentationview/PresentationView.tsx
@@ -0,0 +1,838 @@
+import { observer } from "mobx-react";
+import React = require("react");
+import { observable, action, runInAction, reaction, autorun } from "mobx";
+import "./PresentationView.scss";
+import { DocumentManager } from "../../util/DocumentManager";
+import { Utils } from "../../../Utils";
+import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc";
+import { listSpec } from "../../../new_fields/Schema";
+import { Cast, NumCast, FieldValue, PromiseValue, StrCast, BoolCast } from "../../../new_fields/Types";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { List } from "../../../new_fields/List";
+import PresentationElement, { buttonIndex } from "./PresentationElement";
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faArrowRight, faArrowLeft, faPlay, faStop, faPlus, faTimes, faMinus, faEdit } from '@fortawesome/free-solid-svg-icons';
+import { Docs } from "../../documents/Documents";
+import { undoBatch, UndoManager } from "../../util/UndoManager";
+import PresentationViewList from "./PresentationList";
+
+library.add(faArrowLeft);
+library.add(faArrowRight);
+library.add(faPlay);
+library.add(faStop);
+library.add(faPlus);
+library.add(faTimes);
+library.add(faMinus);
+library.add(faEdit);
+
+
+export interface PresViewProps {
+ Documents: List<Doc>;
+}
+
+@observer
+export class PresentationView extends React.Component<PresViewProps> {
+ public static Instance: PresentationView;
+
+ //Mapping from presentation ids to a list of doc that represent a group
+ @observable groupMappings: Map<String, Doc[]> = new Map();
+ //mapping from docs to their rendered component
+ @observable presElementsMappings: Map<Doc, PresentationElement> = new Map();
+ //variable that holds all the docs in the presentation
+ @observable childrenDocs: Doc[] = [];
+ //variable to hold if presentation is started
+ @observable presStatus: boolean = false;
+ //back-up so that presentation stays the way it's when refreshed
+ @observable presGroupBackUp: Doc = new Doc();
+ @observable presButtonBackUp: Doc = new Doc();
+
+ //Keeping track of the doc for the current presentation
+ @observable curPresentation: Doc = new Doc();
+ //Mapping of guids to presentations.
+ @observable presentationsMapping: Map<String, Doc> = new Map();
+ //Mapping of presentations to guid, so that select option values can be given.
+ @observable presentationsKeyMapping: Map<Doc, String> = new Map();
+ //Variable to keep track of guid of the current presentation
+ @observable currentSelectedPresValue: string | undefined;
+ //A flag to keep track if title input is open, which is used in rendering.
+ @observable PresTitleInputOpen: boolean = false;
+ //Variable that holds reference to title input, so that new presentations get titles assigned.
+ @observable titleInputElement: HTMLInputElement | undefined;
+ @observable PresTitleChangeOpen: boolean = false;
+
+ //initilize class variables
+ constructor(props: PresViewProps) {
+ super(props);
+ PresentationView.Instance = this;
+ }
+
+ //The first lifecycle function that gets called to set up the current presentation.
+ async componentWillMount() {
+ this.props.Documents.forEach(async (doc, index: number) => {
+
+ //For each presentation received from mainContainer, a mapping is created.
+ let curDoc: Doc = await doc;
+ let newGuid = Utils.GenerateGuid();
+ this.presentationsKeyMapping.set(curDoc, newGuid);
+ this.presentationsMapping.set(newGuid, curDoc);
+
+ //The Presentation at first index gets set as default start presentation
+ if (index === 0) {
+ runInAction(() => this.currentSelectedPresValue = newGuid);
+ runInAction(() => this.curPresentation = curDoc);
+ }
+ });
+ }
+
+ //Second lifecycle function that gets called when component mounts. It makes sure to
+ //get the back-up information from previous session for the current presentation.
+ async componentDidMount() {
+ let docAtZero = await this.props.Documents[0];
+ runInAction(() => this.curPresentation = docAtZero);
+
+ this.setPresentationBackUps();
+
+ }
+
+
+ /**
+ * The function that retrieves the backUps for the current Presentation if present,
+ * otherwise initializes.
+ */
+ setPresentationBackUps = async () => {
+ //getting both backUp documents
+
+ let castedGroupBackUp = Cast(this.curPresentation.presGroupBackUp, Doc);
+ let castedButtonBackUp = Cast(this.curPresentation.presButtonBackUp, Doc);
+ //if instantiated before
+ if (castedGroupBackUp instanceof Promise) {
+ castedGroupBackUp.then(doc => {
+ let toAssign = doc ? doc : new Doc();
+ this.curPresentation.presGroupBackUp = toAssign;
+ runInAction(() => this.presGroupBackUp = toAssign);
+ if (doc) {
+ if (toAssign[Id] === doc[Id]) {
+ this.retrieveGroupMappings();
+ }
+ }
+ });
+
+ //if never instantiated a store doc yet
+ } else if (castedGroupBackUp instanceof Doc) {
+ let castedDoc: Doc = await castedGroupBackUp;
+ runInAction(() => this.presGroupBackUp = castedDoc);
+ this.retrieveGroupMappings();
+ } else {
+ runInAction(() => {
+ let toAssign = new Doc();
+ this.presGroupBackUp = toAssign;
+ this.curPresentation.presGroupBackUp = toAssign;
+
+ });
+
+ }
+ //if instantiated before
+ if (castedButtonBackUp instanceof Promise) {
+ castedButtonBackUp.then(doc => {
+ let toAssign = doc ? doc : new Doc();
+ this.curPresentation.presButtonBackUp = toAssign;
+ runInAction(() => this.presButtonBackUp = toAssign);
+ });
+
+ //if never instantiated a store doc yet
+ } else if (castedButtonBackUp instanceof Doc) {
+ let castedDoc: Doc = await castedButtonBackUp;
+ runInAction(() => this.presButtonBackUp = castedDoc);
+
+ } else {
+ runInAction(() => {
+ let toAssign = new Doc();
+ this.presButtonBackUp = toAssign;
+ this.curPresentation.presButtonBackUp = toAssign;
+ });
+
+ }
+
+
+ //storing the presentation status,ie. whether it was stopped or playing
+ let presStatusBackUp = BoolCast(this.curPresentation.presStatus, null);
+ runInAction(() => this.presStatus = presStatusBackUp);
+ }
+
+ /**
+ * This is the function that is called to retrieve the groups that have been stored and
+ * push them to the groupMappings.
+ */
+ retrieveGroupMappings = async () => {
+ let castedGroupDocs = await DocListCastAsync(this.presGroupBackUp.groupDocs);
+ if (castedGroupDocs !== undefined) {
+ castedGroupDocs.forEach(async (groupDoc: Doc, index: number) => {
+ let castedGrouping = await DocListCastAsync(groupDoc.grouping);
+ let castedKey = StrCast(groupDoc.presentIdStore, null);
+ if (castedGrouping) {
+ castedGrouping.forEach((doc: Doc) => {
+ doc.presentId = castedKey;
+ });
+ }
+ if (castedGrouping !== undefined && castedKey !== undefined) {
+ this.groupMappings.set(castedKey, castedGrouping);
+ }
+ });
+ }
+ }
+
+ //observable means render is re-called every time variable is changed
+ @observable
+ collapsed: boolean = false;
+ closePresentation = action(() => this.curPresentation.width = 0);
+ next = async () => {
+ const current = NumCast(this.curPresentation.selectedDoc);
+ //asking to get document at current index
+ let docAtCurrentNext = await this.getDocAtIndex(current + 1);
+ if (docAtCurrentNext === undefined) {
+ return;
+ }
+ //asking for it's presentation id
+ let curNextPresId = StrCast(docAtCurrentNext.presentId);
+ let nextSelected = current + 1;
+
+ //if curDoc is in a group, selection slides until last one, if not it's next one
+ if (this.groupMappings.has(curNextPresId)) {
+ let currentsArray = this.groupMappings.get(StrCast(docAtCurrentNext.presentId))!;
+ nextSelected = current + currentsArray.length - currentsArray.indexOf(docAtCurrentNext);
+
+ //end of grup so go beyond
+ if (nextSelected === current) nextSelected = current + 1;
+ }
+
+ this.gotoDocument(nextSelected, current);
+
+ }
+ back = async () => {
+ const current = NumCast(this.curPresentation.selectedDoc);
+ //requesting for the doc at current index
+ let docAtCurrent = await this.getDocAtIndex(current);
+ if (docAtCurrent === undefined) {
+ return;
+ }
+
+ //asking for its presentation id.
+ let curPresId = StrCast(docAtCurrent.presentId);
+ let prevSelected = current - 1;
+ let zoomOut: boolean = false;
+
+ //checking if this presentation id is mapped to a group, if so chosing the first element in group
+ if (this.groupMappings.has(curPresId)) {
+ let currentsArray = this.groupMappings.get(StrCast(docAtCurrent.presentId))!;
+ prevSelected = current - currentsArray.length + (currentsArray.length - currentsArray.indexOf(docAtCurrent)) - 1;
+ //end of grup so go beyond
+ if (prevSelected === current) prevSelected = current - 1;
+
+ //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;
+ }
+ });
+
+ }
+
+ // if a group set that flag to zero or a single element
+ //If so making sure to zoom out, which goes back to state before zooming action
+ if (current > 0) {
+ if (zoomOut || this.presElementsMappings.get(docAtCurrent)!.selected[buttonIndex.Show]) {
+ let prevScale = NumCast(this.childrenDocs[prevSelected].viewScale, null);
+ let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[current]);
+ if (prevScale !== undefined) {
+ if (prevScale !== curScale) {
+ DocumentManager.Instance.zoomIntoScale(docAtCurrent, prevScale);
+ }
+ }
+ }
+ }
+ this.gotoDocument(prevSelected, current);
+
+ }
+
+ /**
+ * This is the method that checks for the actions that need to be performed
+ * after the document has been presented, which involves 3 button options:
+ * Hide Until Presented, Hide After Presented, Fade After Presented
+ */
+ showAfterPresented = (index: number) => {
+ this.presElementsMappings.forEach((presElem: PresentationElement, key: Doc) => {
+ let selectedButtons: boolean[] = presElem.selected;
+ //the order of cases is aligned based on priority
+ if (selectedButtons[buttonIndex.HideTillPressed]) {
+ if (this.childrenDocs.indexOf(key) <= index) {
+ key.opacity = 1;
+ }
+ }
+ if (selectedButtons[buttonIndex.HideAfter]) {
+ if (this.childrenDocs.indexOf(key) < index) {
+ key.opacity = 0;
+ }
+ }
+ if (selectedButtons[buttonIndex.FadeAfter]) {
+ if (this.childrenDocs.indexOf(key) < index) {
+ key.opacity = 0.5;
+ }
+ }
+ });
+ }
+
+ /**
+ * This is the method that checks for the actions that need to be performed
+ * before the document has been presented, which involves 3 button options:
+ * Hide Until Presented, Hide After Presented, Fade After Presented
+ */
+ hideIfNotPresented = (index: number) => {
+ this.presElementsMappings.forEach((presElem: PresentationElement, key: Doc) => {
+ let selectedButtons: boolean[] = presElem.selected;
+
+ //the order of cases is aligned based on priority
+
+ if (selectedButtons[buttonIndex.HideAfter]) {
+ if (this.childrenDocs.indexOf(key) >= index) {
+ key.opacity = 1;
+ }
+ }
+ if (selectedButtons[buttonIndex.FadeAfter]) {
+ if (this.childrenDocs.indexOf(key) >= index) {
+ key.opacity = 1;
+ }
+ }
+ if (selectedButtons[buttonIndex.HideTillPressed]) {
+ if (this.childrenDocs.indexOf(key) > index) {
+ key.opacity = 0;
+ }
+ }
+ });
+ }
+
+ /**
+ * This method makes sure that cursor navigates to the element that
+ * has the option open and last in the group. If not in the group, and it has
+ * te option open, navigates to that element.
+ */
+ navigateToElement = async (curDoc: Doc, fromDoc: number) => {
+ let docToJump: Doc = curDoc;
+ let curDocPresId = StrCast(curDoc.presentId, null);
+ let willZoom: boolean = false;
+
+ //checking if in group
+ if (curDocPresId !== undefined) {
+ if (this.groupMappings.has(curDocPresId)) {
+ let currentDocGroup = this.groupMappings.get(curDocPresId)!;
+ currentDocGroup.forEach((doc: Doc, index: number) => {
+ let selectedButtons: boolean[] = this.presElementsMappings.get(doc)!.selected;
+ if (selectedButtons[buttonIndex.Navigate]) {
+ docToJump = doc;
+ willZoom = false;
+ }
+ if (selectedButtons[buttonIndex.Show]) {
+ docToJump = doc;
+ willZoom = true;
+ }
+ });
+ }
+
+ }
+ //docToJump stayed same meaning, it was not in the group or was the last element in the group
+ if (docToJump === curDoc) {
+ //checking if curDoc has navigation open
+ let curDocButtons = this.presElementsMappings.get(curDoc)!.selected;
+ if (curDocButtons[buttonIndex.Navigate]) {
+ DocumentManager.Instance.jumpToDocument(curDoc, false);
+ } else if (curDocButtons[buttonIndex.Show]) {
+ let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[fromDoc]);
+ //awaiting jump so that new scale can be found, since jumping is async
+ await DocumentManager.Instance.jumpToDocument(curDoc, true);
+ let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc);
+ curDoc.viewScale = newScale;
+
+ //saving the scale user was on before zooming in
+ if (curScale !== 1) {
+ this.childrenDocs[fromDoc].viewScale = curScale;
+ }
+
+ }
+ return;
+ }
+ let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[fromDoc]);
+
+ //awaiting jump so that new scale can be found, since jumping is async
+ await DocumentManager.Instance.jumpToDocument(docToJump, willZoom);
+ let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc);
+ curDoc.viewScale = newScale;
+ //saving the scale that user was on
+ if (curScale !== 1) {
+ this.childrenDocs[fromDoc].viewScale = curScale;
+ }
+
+ }
+
+ /**
+ * Async function that supposedly return the doc that is located at given index.
+ */
+ getDocAtIndex = async (index: number) => {
+ const list = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
+ if (!list) {
+ return undefined;
+ }
+ if (index < 0 || index >= list.length) {
+ return undefined;
+ }
+
+ this.curPresentation.selectedDoc = index;
+ //awaiting async call to finish to get Doc instance
+ const doc = await list[index];
+ return doc;
+ }
+
+ /**
+ * The function that removes a doc from a presentation. It also makes sure to
+ * do necessary updates to backUps and mappings stored locally.
+ */
+ @action
+ public RemoveDoc = async (index: number) => {
+ const value = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
+ if (value) {
+ let removedDoc = await value.splice(index, 1)[0];
+
+ //removing the Presentation Element stored for it
+ this.presElementsMappings.delete(removedDoc);
+
+ let removedDocPresentId = StrCast(removedDoc.presentId);
+
+ //Removing it from local mapping of the groups
+ if (this.groupMappings.has(removedDocPresentId)) {
+ let removedDocsGroup = this.groupMappings.get(removedDocPresentId);
+ if (removedDocsGroup) {
+ removedDocsGroup.splice(removedDocsGroup.indexOf(removedDoc), 1);
+ if (removedDocsGroup.length === 0) {
+ this.groupMappings.delete(removedDocPresentId);
+ }
+ }
+ }
+
+ //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) {
+ 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
+ let castedGroupDocs = await DocListCastAsync(this.presGroupBackUp.groupDocs);
+ if (castedGroupDocs) {
+ castedGroupDocs.forEach(async (groupDoc: Doc, index: number) => {
+ let castedKey = StrCast(groupDoc.presentIdStore, null);
+ if (castedKey === removedDocPresentId) {
+ let castedGrouping = await DocListCastAsync(groupDoc.grouping);
+ if (castedGrouping) {
+ castedGrouping.splice(castedGrouping.indexOf(removedDoc), 1);
+ if (castedGrouping.length === 0) {
+ castedGroupDocs!.splice(castedGroupDocs!.indexOf(groupDoc), 1);
+ }
+ }
+ }
+
+ });
+
+ }
+
+
+ }
+ }
+
+ 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
+ public gotoDocument = async (index: number, fromDoc: number) => {
+ const list = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
+ if (!list) {
+ return;
+ }
+ if (index < 0 || index >= list.length) {
+ return;
+ }
+ this.curPresentation.selectedDoc = index;
+
+ if (!this.presStatus) {
+ this.presStatus = true;
+ this.startPresentation(index);
+ }
+
+ const doc = await list[index];
+ if (this.presStatus) {
+ this.navigateToElement(doc, fromDoc);
+ this.hideIfNotPresented(index);
+ this.showAfterPresented(index);
+ }
+
+ }
+
+ //Function that is called to resetGroupIds, so that documents get new groupIds at
+ //first load, when presentation is changed.
+ resetGroupIds = async () => {
+ let castedGroupDocs = await DocListCastAsync(this.presGroupBackUp.groupDocs);
+ if (castedGroupDocs !== undefined) {
+ castedGroupDocs.forEach(async (groupDoc: Doc, index: number) => {
+ let castedGrouping = await DocListCastAsync(groupDoc.grouping);
+ if (castedGrouping) {
+ castedGrouping.forEach((doc: Doc) => {
+ doc.presentId = Utils.GenerateGuid();
+ });
+ }
+ });
+ }
+ runInAction(() => this.groupMappings = new Map());
+ }
+
+ /**
+ * Adds a document to the presentation view
+ **/
+ @undoBatch
+ @action
+ public PinDoc(doc: Doc) {
+ //add this new doc to props.Document
+ const data = Cast(this.curPresentation.data, listSpec(Doc));
+ if (data) {
+ data.push(doc);
+ } else {
+ this.curPresentation.data = new List([doc]);
+ }
+
+ this.curPresentation.width = 400;
+ }
+
+ //Function that sets the store of the children docs.
+ @action
+ setChildrenDocs = (docList: Doc[]) => {
+ this.childrenDocs = docList;
+ }
+
+ //The function that is called to render the play or pause button depending on
+ //if presentation is running or not.
+ renderPlayPauseButton = () => {
+ if (this.presStatus) {
+ return <button title="Reset Presentation" className="presentation-button" onClick={this.startOrResetPres}><FontAwesomeIcon icon="stop" /></button>;
+ } else {
+ return <button title="Start Presentation From Start" className="presentation-button" onClick={this.startOrResetPres}><FontAwesomeIcon icon="play" /></button>;
+ }
+ }
+
+ //The function that starts or resets presentaton functionally, depending on status flag.
+ @action
+ startOrResetPres = () => {
+ if (this.presStatus) {
+ this.resetPresentation();
+ } else {
+ this.presStatus = true;
+ this.startPresentation(0);
+ const current = NumCast(this.curPresentation.selectedDoc);
+ this.gotoDocument(0, current);
+ }
+ this.curPresentation.presStatus = this.presStatus;
+ }
+
+ //The function that resets the presentation by removing every action done by it. It also
+ //stops the presentaton.
+ @action
+ resetPresentation = () => {
+ this.childrenDocs.forEach((doc: Doc) => {
+ doc.opacity = 1;
+ doc.viewScale = 1;
+ });
+ this.curPresentation.selectedDoc = 0;
+ this.presStatus = false;
+ this.curPresentation.presStatus = this.presStatus;
+ if (this.childrenDocs.length === 0) {
+ return;
+ }
+ DocumentManager.Instance.zoomIntoScale(this.childrenDocs[0], 1);
+ }
+
+
+ //The function that starts the presentation, also checking if actions should be applied
+ //directly at start.
+ startPresentation = (startIndex: number) => {
+ let selectedButtons: boolean[];
+ this.presElementsMappings.forEach((component: PresentationElement, doc: Doc) => {
+ selectedButtons = component.selected;
+ if (selectedButtons[buttonIndex.HideTillPressed]) {
+ if (this.childrenDocs.indexOf(doc) > startIndex) {
+ doc.opacity = 0;
+ }
+
+ }
+ if (selectedButtons[buttonIndex.HideAfter]) {
+ if (this.childrenDocs.indexOf(doc) < startIndex) {
+ doc.opacity = 0;
+ }
+ }
+ if (selectedButtons[buttonIndex.FadeAfter]) {
+ if (this.childrenDocs.indexOf(doc) < startIndex) {
+ doc.opacity = 0.5;
+ }
+ }
+
+ });
+
+ }
+
+ /**
+ * The function that is called to add a new presentation to the presentationView.
+ * It sets up te mappings and local copies of it. Resets the groupings and presentation.
+ * Makes the new presentation current selected, and retrieve the back-Ups if present.
+ */
+ @action
+ addNewPresentation = (presTitle: string) => {
+ //creating a new presentation doc
+ let newPresentationDoc = Docs.Create.TreeDocument([], { title: presTitle });
+ this.props.Documents.push(newPresentationDoc);
+
+ //setting that new doc as current
+ this.curPresentation = newPresentationDoc;
+
+ //storing the doc in local copies for easier access
+ let newGuid = Utils.GenerateGuid();
+ this.presentationsMapping.set(newGuid, newPresentationDoc);
+ this.presentationsKeyMapping.set(newPresentationDoc, newGuid);
+
+ //resetting the previous presentation's actions so that new presentation can be loaded.
+ this.resetGroupIds();
+ this.resetPresentation();
+ this.presElementsMappings = new Map();
+ this.currentSelectedPresValue = newGuid;
+ this.setPresentationBackUps();
+
+ }
+
+ /**
+ * The function that is called to change the current selected presentation.
+ * Changes the presentation, also resetting groupings and presentation in process.
+ * Plus retrieving the backUps for the newly selected presentation.
+ */
+ @action
+ getSelectedPresentation = (e: React.ChangeEvent<HTMLSelectElement>) => {
+ //get the guid of the selected presentation
+ let selectedGuid = e.target.value;
+ //set that as current presentation
+ this.curPresentation = this.presentationsMapping.get(selectedGuid)!;
+
+ //reset current Presentations local things so that new one can be loaded
+ this.resetGroupIds();
+ this.resetPresentation();
+ this.presElementsMappings = new Map();
+ this.currentSelectedPresValue = selectedGuid;
+ this.setPresentationBackUps();
+
+
+ }
+
+ /**
+ * The function that is called to render either select for presentations, or title inputting.
+ */
+ renderSelectOrPresSelection = () => {
+ let presentationList = DocListCast(this.props.Documents);
+ if (this.PresTitleInputOpen || this.PresTitleChangeOpen) {
+ return <input ref={(e) => this.titleInputElement = e!} type="text" className="presentationView-title" placeholder="Enter Name!" onKeyDown={this.submitPresentationTitle} />;
+ } else {
+ return <select value={this.currentSelectedPresValue} id="pres_selector" className="presentationView-title" onChange={this.getSelectedPresentation}>
+ {presentationList.map((doc: Doc, index: number) => {
+ let mappedGuid = this.presentationsKeyMapping.get(doc);
+ let docGuid: string = mappedGuid ? mappedGuid.toString() : "";
+ return <option key={docGuid} value={docGuid}>{StrCast(doc.title)}</option>;
+ })}
+ </select>;
+ }
+ }
+
+ /**
+ * The function that is called on enter press of title input. It gives the
+ * new presentation the title user entered. If nothing is entered, gives a default title.
+ */
+ @action
+ submitPresentationTitle = (e: React.KeyboardEvent) => {
+ if (e.keyCode === 13) {
+ let presTitle = this.titleInputElement!.value;
+ this.titleInputElement!.value = "";
+ if (this.PresTitleInputOpen) {
+ if (presTitle === "") {
+ presTitle = "Presentation";
+ }
+ this.PresTitleInputOpen = false;
+ this.addNewPresentation(presTitle);
+ } else if (this.PresTitleChangeOpen) {
+ this.PresTitleChangeOpen = false;
+ this.changePresentationTitle(presTitle);
+ }
+ }
+ }
+
+ /**
+ * The function that is called to remove a presentation from all its copies, and the main Container's
+ * list. Sets up the next presentation as current.
+ */
+ @action
+ removePresentation = async () => {
+ if (this.presentationsMapping.size !== 1) {
+ let presentationList = Cast(this.props.Documents, listSpec(Doc));
+ let batch = UndoManager.StartBatch("presRemoval");
+
+ //getting the presentation that will be removed
+ let removedDoc = this.presentationsMapping.get(this.currentSelectedPresValue!);
+ //that presentation is removed
+ presentationList!.splice(presentationList!.indexOf(removedDoc!), 1);
+
+ //its mappings are removed from local copies
+ this.presentationsKeyMapping.delete(removedDoc!);
+ this.presentationsMapping.delete(this.currentSelectedPresValue!);
+
+ //the next presentation is set as current
+ let remainingPresentations = this.presentationsMapping.values();
+ let nextDoc = remainingPresentations.next().value;
+ this.curPresentation = nextDoc;
+
+
+ //Storing these for being able to undo changes
+ let curGuid = this.currentSelectedPresValue!;
+ let curPresStatus = this.presStatus;
+
+ //resetting the groups and presentation actions so that next presentation gets loaded
+ this.resetGroupIds();
+ this.resetPresentation();
+ this.currentSelectedPresValue = this.presentationsKeyMapping.get(nextDoc)!.toString();
+ this.setPresentationBackUps();
+
+ //Storing for undo
+ let currentGroups = this.groupMappings;
+ let curPresElemMapping = this.presElementsMappings;
+
+ //Event to undo actions that are not related to doc directly, aka. local things
+ UndoManager.AddEvent({
+ undo: action(() => {
+ this.curPresentation = removedDoc!;
+ this.presentationsMapping.set(curGuid, removedDoc!);
+ this.presentationsKeyMapping.set(removedDoc!, curGuid);
+ this.currentSelectedPresValue = curGuid;
+
+ this.presStatus = curPresStatus;
+ this.groupMappings = currentGroups;
+ this.presElementsMappings = curPresElemMapping;
+ this.setPresentationBackUps();
+
+ }),
+ redo: action(() => {
+ this.curPresentation = nextDoc;
+ this.presStatus = false;
+ this.presentationsKeyMapping.delete(removedDoc!);
+ this.presentationsMapping.delete(curGuid);
+ this.currentSelectedPresValue = this.presentationsKeyMapping.get(nextDoc)!.toString();
+ this.setPresentationBackUps();
+
+ }),
+ });
+
+ batch.end();
+ }
+ }
+
+ /**
+ * The function that is called to change title of presentation to what user entered.
+ */
+ @undoBatch
+ changePresentationTitle = (newTitle: string) => {
+ if (newTitle === "") {
+ return;
+ }
+ this.curPresentation.title = newTitle;
+ }
+
+ addPressElem = (keyDoc: Doc, elem: PresentationElement) => {
+ this.presElementsMappings.set(keyDoc, elem);
+ }
+
+
+ render() {
+
+ let width = NumCast(this.curPresentation.width);
+
+ return (
+ <div className="presentationView-cont" style={{ width: width, overflow: "hidden" }}>
+ <div className="presentationView-heading">
+ {this.renderSelectOrPresSelection()}
+ <button title="Close Presentation" className='presentation-icon' onClick={this.closePresentation}><FontAwesomeIcon icon={"times"} /></button>
+ <button title="Add Presentation" className="presentation-icon" style={{ marginRight: 10 }} onClick={() => {
+ runInAction(() => { if (this.PresTitleChangeOpen) { this.PresTitleChangeOpen = false; } });
+ runInAction(() => this.PresTitleInputOpen ? this.PresTitleInputOpen = false : this.PresTitleInputOpen = true);
+ }}><FontAwesomeIcon icon={"plus"} /></button>
+ <button title="Remove Presentation" className='presentation-icon' style={{ marginRight: 10 }} onClick={this.removePresentation}><FontAwesomeIcon icon={"minus"} /></button>
+ <button title="Change Presentation Title" className="presentation-icon" style={{ marginRight: 10 }} onClick={() => {
+ runInAction(() => { if (this.PresTitleInputOpen) { this.PresTitleInputOpen = false; } });
+ runInAction(() => this.PresTitleChangeOpen ? this.PresTitleChangeOpen = false : this.PresTitleChangeOpen = true);
+ }}><FontAwesomeIcon icon={"edit"} /></button>
+ </div>
+ <div className="presentation-buttons">
+ <button title="Back" className="presentation-button" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></button>
+ {this.renderPlayPauseButton()}
+ <button title="Next" className="presentation-button" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></button>
+ </div>
+ <PresentationViewList
+ mainDocument={this.curPresentation}
+ deleteDocument={this.RemoveDoc}
+ gotoDocument={this.gotoDocument}
+ groupMappings={this.groupMappings}
+ 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/CheckBox.scss b/src/client/views/search/CheckBox.scss
new file mode 100644
index 000000000..af59d5fbf
--- /dev/null
+++ b/src/client/views/search/CheckBox.scss
@@ -0,0 +1,59 @@
+@import "../globalCssVariables";
+
+.checkboxfilter {
+ display: flex;
+ margin-top: 0px;
+ padding: 3px;
+
+ .outer {
+ display: flex;
+ position: relative;
+ justify-content: center;
+ align-items: center;
+ margin-top: 0px;
+
+ .check-container:hover~.check-box {
+ background-color: $intermediate-color;
+ }
+
+ .check-container {
+ width: 40px;
+ height: 40px;
+ position: absolute;
+ z-index: 1000;
+
+ .checkmark {
+ z-index: 1000;
+ position: absolute;
+ fill-opacity: 0;
+ stroke-width: 4px;
+ stroke: white;
+ }
+ }
+
+ .check-box {
+ z-index: 900;
+ position: relative;
+ height: 20px;
+ width: 20px;
+ overflow: visible;
+ background-color: transparent;
+ border-style: solid;
+ border-color: $alt-accent;
+ border-width: 2px;
+ -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;
+ }
+ }
+
+ .checkbox-title {
+ display: flex;
+ align-items: center;
+ text-transform: capitalize;
+ margin-left: 15px;
+ }
+
+}
+
diff --git a/src/client/views/search/CheckBox.tsx b/src/client/views/search/CheckBox.tsx
new file mode 100644
index 000000000..a9d48219a
--- /dev/null
+++ b/src/client/views/search/CheckBox.tsx
@@ -0,0 +1,131 @@
+import * as React from 'react';
+import { observer } from 'mobx-react';
+import { observable, action, runInAction, IReactionDisposer, reaction } from 'mobx';
+import "./CheckBox.scss";
+import * as anime from 'animejs';
+
+interface CheckBoxProps {
+ originalStatus: boolean;
+ updateStatus(newStatus: boolean): void;
+ title: string;
+ parent: any;
+ numCount: number;
+ default: boolean;
+}
+
+@observer
+export class CheckBox extends React.Component<CheckBoxProps>{
+ // true = checked, false = unchecked
+ @observable private _status: boolean;
+ @observable private uncheckTimeline: anime.AnimeTimelineInstance;
+ @observable private checkTimeline: anime.AnimeTimelineInstance;
+ @observable private checkRef: any;
+ @observable private _resetReaction?: IReactionDisposer;
+
+
+ constructor(props: CheckBoxProps) {
+ super(props);
+ this._status = this.props.originalStatus;
+ this.checkRef = React.createRef();
+
+ this.checkTimeline = anime.timeline({
+ loop: false,
+ autoplay: false,
+ direction: "normal",
+ }); this.uncheckTimeline = anime.timeline({
+ loop: false,
+ autoplay: false,
+ direction: "normal",
+ });
+ }
+
+ componentDidMount = () => {
+ this.uncheckTimeline.add({
+ targets: this.checkRef.current,
+ easing: "easeInOutQuad",
+ duration: 500,
+ opacity: 0,
+ });
+ this.checkTimeline.add({
+ targets: this.checkRef.current,
+ easing: "easeInOutQuad",
+ duration: 500,
+ strokeDashoffset: [anime.setDashoffset, 0],
+ opacity: 1
+ });
+
+ if (this.props.originalStatus) {
+ this.checkTimeline.play();
+ this.checkTimeline.restart();
+ }
+
+ this._resetReaction = reaction(
+ () => this.props.parent._resetBoolean,
+ () => {
+ if (this.props.parent._resetBoolean) {
+ runInAction(() => {
+ this.reset();
+ this.props.parent._resetCounter++;
+ if (this.props.parent._resetCounter === this.props.numCount) {
+ this.props.parent._resetCounter = 0;
+ this.props.parent._resetBoolean = false;
+ }
+ });
+ }
+ },
+ );
+ }
+
+ @action.bound
+ reset() {
+ if (this.props.default) {
+ if (!this._status) {
+ this._status = true;
+ this.checkTimeline.play();
+ this.checkTimeline.restart();
+ }
+ }
+ else {
+ if (this._status) {
+ this._status = false;
+ this.uncheckTimeline.play();
+ this.uncheckTimeline.restart();
+ }
+ }
+
+ this.props.updateStatus(this.props.default);
+ }
+
+ @action.bound
+ onClick = () => {
+ if (this._status) {
+ this.uncheckTimeline.play();
+ this.uncheckTimeline.restart();
+ }
+ else {
+ this.checkTimeline.play();
+ this.checkTimeline.restart();
+
+ }
+ this._status = !this._status;
+ this.props.updateStatus(this._status);
+
+ }
+
+ render() {
+ return (
+ <div className="checkboxfilter" onClick={this.onClick}>
+ <div className="outer">
+ <div className="check-container">
+ <svg viewBox="0 12 40 40">
+ <path ref={this.checkRef} className="checkmark" d="M14.1 27.2l7.1 7.2 16.7-18" />
+ </svg>
+ </div>
+ <div className="check-box" />
+ </div>
+ <div className="checkbox-title">{this.props.title}</div>
+ </div>
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/search/CollectionFilters.scss b/src/client/views/search/CollectionFilters.scss
new file mode 100644
index 000000000..b54cdcbd1
--- /dev/null
+++ b/src/client/views/search/CollectionFilters.scss
@@ -0,0 +1,20 @@
+@import "../globalCssVariables";
+
+.collection-filters {
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+
+ &.main {
+ width: 47%;
+ float: left;
+ }
+
+ &.optional {
+ width: 60%;
+ display: grid;
+ grid-template-columns: 50% 50%;
+ float: right;
+ opacity: 0;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/search/CollectionFilters.tsx b/src/client/views/search/CollectionFilters.tsx
new file mode 100644
index 000000000..48d0b2ddb
--- /dev/null
+++ b/src/client/views/search/CollectionFilters.tsx
@@ -0,0 +1,83 @@
+import * as React from 'react';
+import { observable, action } from 'mobx';
+import { CheckBox } from './CheckBox';
+import "./CollectionFilters.scss";
+import * as anime from 'animejs';
+
+interface CollectionFilterProps {
+ collectionStatus: boolean;
+ updateCollectionStatus(val: boolean): void;
+ collectionSelfStatus: boolean;
+ updateSelfCollectionStatus(val: boolean): void;
+ collectionParentStatus: boolean;
+ updateParentCollectionStatus(val: boolean): void;
+}
+
+export class CollectionFilters extends React.Component<CollectionFilterProps> {
+
+ static Instance: CollectionFilters;
+
+ @observable public _resetBoolean = false;
+ @observable public _resetCounter: number = 0;
+
+ @observable private _collectionsSelected = this.props.collectionStatus;
+ @observable private _timeline: anime.AnimeTimelineInstance;
+ @observable private _ref: any;
+
+ constructor(props: CollectionFilterProps) {
+ super(props);
+ CollectionFilters.Instance = this;
+ this._ref = React.createRef();
+
+ this._timeline = anime.timeline({
+ loop: false,
+ autoplay: false,
+ direction: "reverse",
+ });
+ }
+
+ componentDidMount = () => {
+ this._timeline.add({
+ targets: this._ref.current,
+ easing: "easeInOutQuad",
+ duration: 500,
+ opacity: 1,
+ });
+
+ if (this._collectionsSelected) {
+ this._timeline.play();
+ this._timeline.reverse();
+ }
+ }
+
+ @action.bound
+ resetCollectionFilters() { this._resetBoolean = true; }
+
+ @action.bound
+ updateColStat(val: boolean) {
+ this.props.updateCollectionStatus(val);
+
+ if (this._collectionsSelected !== val) {
+ this._timeline.play();
+ this._timeline.reverse();
+ }
+
+ this._collectionsSelected = val;
+ }
+
+ render() {
+ return (
+ <div>
+ <div className="collection-filters">
+ <div className="collection-filters main">
+ <CheckBox default={false} title={"limit to current collection"} parent={this} numCount={3} updateStatus={this.updateColStat} originalStatus={this.props.collectionStatus} />
+ </div>
+ <div className="collection-filters optional" ref={this._ref}>
+ <CheckBox default={true} title={"Search in self"} parent={this} numCount={3} updateStatus={this.props.updateSelfCollectionStatus} originalStatus={this.props.collectionSelfStatus} />
+ <CheckBox default={true} title={"Search in parent"} parent={this} numCount={3} updateStatus={this.props.updateParentCollectionStatus} originalStatus={this.props.collectionParentStatus} />
+ </div>
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/search/FieldFilters.scss b/src/client/views/search/FieldFilters.scss
new file mode 100644
index 000000000..ba0926140
--- /dev/null
+++ b/src/client/views/search/FieldFilters.scss
@@ -0,0 +1,5 @@
+.field-filters {
+ width: 100%;
+ display: grid;
+ grid-template-columns: 18% 20% 60%;
+} \ No newline at end of file
diff --git a/src/client/views/search/FieldFilters.tsx b/src/client/views/search/FieldFilters.tsx
new file mode 100644
index 000000000..7a33282d2
--- /dev/null
+++ b/src/client/views/search/FieldFilters.tsx
@@ -0,0 +1,41 @@
+import * as React from 'react';
+import { observable } from 'mobx';
+import { CheckBox } from './CheckBox';
+import { Keys } from './FilterBox';
+import "./FieldFilters.scss";
+
+export interface FieldFilterProps {
+ titleFieldStatus: boolean;
+ dataFieldStatus: boolean;
+ authorFieldStatus: boolean;
+ updateTitleStatus(stat: boolean): void;
+ updateAuthorStatus(stat: boolean): void;
+ updateDataStatus(stat: boolean): void;
+}
+
+export class FieldFilters extends React.Component<FieldFilterProps> {
+
+ static Instance: FieldFilters;
+
+ @observable public _resetBoolean = false;
+ @observable public _resetCounter: number = 0;
+
+ constructor(props: FieldFilterProps) {
+ super(props);
+ FieldFilters.Instance = this;
+ }
+
+ resetFieldFilters() {
+ this._resetBoolean = true;
+ }
+
+ render() {
+ return (
+ <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={false} numCount={3} parent={this} originalStatus={this.props.dataFieldStatus} updateStatus={this.props.updateDataStatus} title={"Deleted Docs"} />
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/search/FilterBox.scss b/src/client/views/search/FilterBox.scss
new file mode 100644
index 000000000..1eb8963d7
--- /dev/null
+++ b/src/client/views/search/FilterBox.scss
@@ -0,0 +1,108 @@
+@import "../globalCssVariables";
+@import "./NaviconButton.scss";
+
+.filter-form {
+ padding: 25px;
+ width: 600px;
+ background: $dark-color;
+ position: relative;
+ right: 1px;
+ color: $light-color;
+ flex-direction: column;
+ display: inline-block;
+ transform-origin: top;
+ overflow: auto;
+
+ .top-filter-header {
+
+ #header {
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ font-size: 25;
+ width: 80%;
+ }
+
+ .close-icon {
+ width: 20%;
+ opacity: .6;
+ position: relative;
+ display: inline-block;
+
+ .line {
+ display: block;
+ background: $alt-accent;
+ width: $width-line;
+ height: $height-line;
+ position: absolute;
+ right: 0;
+ border-radius: ($height-line / 2);
+
+ &.line-1 {
+ transform: rotate(45deg);
+ top: 45%;
+ }
+
+ &.line-2 {
+ transform: rotate(-45deg);
+ top: 45%;
+ }
+ }
+ }
+
+ .close-icon:hover {
+ opacity: 1;
+ }
+
+ }
+
+ .filter-options {
+
+ .filter-div {
+ margin-top: 10px;
+ margin-bottom: 10px;
+ display: inline-block;
+ width: 100%;
+ border-color: rgba(178, 206, 248, .2); // $darker-alt-accent
+ border-top-style: solid;
+
+ .filter-header {
+ display: flex;
+ align-items: center;
+ margin-bottom: 10px;
+
+ .filter-title {
+ font-size: 18;
+ text-transform: uppercase;
+ margin-top: 10px;
+ margin-bottom: 10px;
+ -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;
+ }
+ }
+
+ .filter-header:hover .filter-title {
+ transform: scale(1.05);
+ }
+
+ .filter-panel {
+ max-height: 0px;
+ width: 100%;
+ overflow: hidden;
+ opacity: 0;
+ transform-origin: top;
+ -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;
+ }
+ }
+ }
+
+ .filter-buttons {
+ border-color: rgba(178, 206, 248, .2); // $darker-alt-accent
+ border-top-style: solid;
+ padding-top: 10px;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/search/FilterBox.tsx b/src/client/views/search/FilterBox.tsx
new file mode 100644
index 000000000..706d1eb7f
--- /dev/null
+++ b/src/client/views/search/FilterBox.tsx
@@ -0,0 +1,394 @@
+import * as React from 'react';
+import { observer } from 'mobx-react';
+import { observable, action } from 'mobx';
+import "./SearchBox.scss";
+import { faTimes } from '@fortawesome/free-solid-svg-icons';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { Doc } from '../../../new_fields/Doc';
+import { Id } from '../../../new_fields/FieldSymbols';
+import { DocumentType } from '../../documents/Documents';
+import { Cast, StrCast } from '../../../new_fields/Types';
+import * as _ from "lodash";
+import { ToggleBar } from './ToggleBar';
+import { IconBar } from './IconBar';
+import { FieldFilters } from './FieldFilters';
+import { SelectionManager } from '../../util/SelectionManager';
+import { DocumentView } from '../nodes/DocumentView';
+import { CollectionFilters } from './CollectionFilters';
+import { NaviconButton } from './NaviconButton';
+import * as $ from 'jquery';
+import "./FilterBox.scss";
+import { SearchBox } from './SearchBox';
+
+library.add(faTimes);
+
+export enum Keys {
+ TITLE = "title",
+ AUTHOR = "author",
+ DATA = "data"
+}
+
+@observer
+export class FilterBox extends React.Component {
+
+ static Instance: FilterBox;
+ public _allIcons: string[] = [DocumentType.AUDIO, DocumentType.COL, DocumentType.HIST, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.TEXT, DocumentType.VID, DocumentType.WEB];
+
+ //if true, any keywords can be used. if false, all keywords are required.
+ @observable private _basicWordStatus: boolean = true;
+ @observable private _filterOpen: boolean = false;
+ @observable private _icons: string[] = this._allIcons;
+ @observable private _titleFieldStatus: boolean = true;
+ @observable private _authorFieldStatus: boolean = true;
+ @observable public _deletedDocsStatus: boolean = false;
+ @observable private _collectionStatus = false;
+ @observable private _collectionSelfStatus = true;
+ @observable private _collectionParentStatus = true;
+ @observable private _wordStatusOpen: boolean = false;
+ @observable private _typeOpen: boolean = false;
+ @observable private _colOpen: boolean = false;
+ @observable private _fieldOpen: boolean = false;
+ public _pointerTime: number = -1;
+
+ constructor(props: Readonly<{}>) {
+ super(props);
+ FilterBox.Instance = this;
+ }
+
+ componentDidMount = () => {
+ document.addEventListener("pointerdown", (e) => {
+ if (!e.defaultPrevented && e.timeStamp !== this._pointerTime) {
+ SearchBox.Instance.closeSearch();
+ }
+ });
+ }
+
+ setupAccordion() {
+ $('document').ready(function () {
+ const acc = document.getElementsByClassName('filter-header');
+ // tslint:disable-next-line: prefer-for-of
+ for (let i = 0; i < acc.length; i++) {
+ acc[i].addEventListener("click", function (this: HTMLElement) {
+ this.classList.toggle("active");
+
+ var panel = this.nextElementSibling as HTMLElement;
+ if (panel.style.maxHeight) {
+ panel.style.overflow = "hidden";
+ panel.style.maxHeight = null;
+ panel.style.opacity = "0";
+ } else {
+ setTimeout(() => {
+ panel.style.overflow = "visible";
+ }, 200);
+ setTimeout(() => {
+ panel.style.opacity = "1";
+ }, 50);
+ panel.style.maxHeight = panel.scrollHeight + "px";
+
+ }
+ });
+
+ let el = acc[i] as HTMLElement;
+ el.click();
+ }
+ });
+ }
+
+ @action.bound
+ minimizeAll() {
+ $('document').ready(function () {
+ var acc = document.getElementsByClassName('filter-header');
+
+ // tslint:disable-next-line: prefer-for-of
+ for (var i = 0; i < acc.length; i++) {
+ let classList = acc[i].classList;
+ if (classList.contains("active")) {
+ acc[i].classList.toggle("active");
+ var panel = acc[i].nextElementSibling as HTMLElement;
+ panel.style.overflow = "hidden";
+ panel.style.maxHeight = null;
+ }
+ }
+ });
+ }
+
+ @action.bound
+ resetFilters = () => {
+ ToggleBar.Instance.resetToggle();
+ IconBar.Instance.selectAll();
+ FieldFilters.Instance.resetFieldFilters();
+ CollectionFilters.Instance.resetCollectionFilters();
+ }
+
+ basicRequireWords(query: string): string {
+ let oldWords = query.split(" ");
+ let newWords: string[] = [];
+ oldWords.forEach(word => {
+ let newWrd = "+" + word;
+ newWords.push(newWrd);
+ });
+ query = newWords.join(" ");
+
+ return query;
+ }
+
+ basicFieldFilters(query: string, type: string): string {
+ let oldWords = query.split(" ");
+ let mod = "";
+
+ if (type === Keys.AUTHOR) {
+ mod = " author_t:";
+ } if (type === Keys.DATA) {
+ //TODO
+ } if (type === Keys.TITLE) {
+ mod = " title_t:";
+ }
+
+ let newWords: string[] = [];
+ oldWords.forEach(word => {
+ let newWrd = mod + word;
+ newWords.push(newWrd);
+ });
+
+ query = newWords.join(" ");
+
+ return query;
+ }
+
+ applyBasicFieldFilters(query: string) {
+ let finalQuery = "";
+
+ if (this._titleFieldStatus) {
+ finalQuery = finalQuery + this.basicFieldFilters(query, Keys.TITLE);
+ }
+ if (this._authorFieldStatus) {
+ finalQuery = finalQuery + this.basicFieldFilters(query, Keys.AUTHOR);
+ }
+ if (this._deletedDocsStatus) {
+ finalQuery = finalQuery + this.basicFieldFilters(query, Keys.DATA);
+ }
+ return finalQuery;
+ }
+
+ get fieldFiltersApplied() { return !(this._authorFieldStatus && this._titleFieldStatus); }
+
+ //TODO: basically all of this
+ //gets all of the collections of all the docviews that are selected
+ //if a collection is the only thing selected, search only in that collection (not its container)
+ getCurCollections(): Doc[] {
+ let selectedDocs: DocumentView[] = SelectionManager.SelectedDocuments();
+ let collections: Doc[] = [];
+
+ selectedDocs.forEach(async element => {
+ let layout: string = StrCast(element.props.Document.baseLayout);
+ //checks if selected view (element) is a collection. if it is, adds to list to search through
+ if (layout.indexOf("Collection") > -1) {
+ //makes sure collections aren't added more than once
+ if (!collections.includes(element.props.Document)) {
+ collections.push(element.props.Document);
+ }
+ }
+ //gets the selected doc's containing view
+ let containingView = element.props.ContainingCollectionView;
+ //makes sure collections aren't added more than once
+ if (containingView && !collections.includes(containingView.props.Document)) {
+ collections.push(containingView.props.Document);
+ }
+ });
+
+ return collections;
+ }
+
+ getFinalQuery(query: string): string {
+ //alters the query so it looks in the correct fields
+ //if this is true, then not all of the field boxes are checked
+ //TODO: data
+ if (this.fieldFiltersApplied) {
+ query = this.applyBasicFieldFilters(query);
+ query = query.replace(/\s+/g, ' ').trim();
+ }
+
+ //alters the query based on if all words or any words are required
+ //if this._wordstatus is false, all words are required and a + is added before each
+ if (!this._basicWordStatus) {
+ query = this.basicRequireWords(query);
+ query = query.replace(/\s+/g, ' ').trim();
+ }
+
+ //if should be searched in a specific collection
+ if (this._collectionStatus) {
+ query = this.addCollectionFilter(query);
+ query = query.replace(/\s+/g, ' ').trim();
+ }
+ return query;
+ }
+
+ addCollectionFilter(query: string): string {
+ let collections: Doc[] = this.getCurCollections();
+ let oldWords = query.split(" ");
+
+ let collectionString: string[] = [];
+ collections.forEach(doc => {
+ let proto = doc.proto;
+ let protoId = (proto || doc)[Id];
+ let colString: string = "{!join from=data_l to=id}id:" + protoId + " ";
+ collectionString.push(colString);
+ });
+
+ let finalColString = collectionString.join(" ");
+ finalColString = finalColString.trim();
+ 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)) {
+ finalDocs.push(doc);
+ }
+ });
+ return finalDocs;
+ }
+
+ @action.bound
+ openFilter = () => {
+ this._filterOpen = !this._filterOpen;
+ SearchBox.Instance.closeResults();
+ this.setupAccordion();
+ }
+
+ //if true, any keywords can be used. if false, all keywords are required.
+ @action.bound
+ handleWordQueryChange = () => { this._basicWordStatus = !this._basicWordStatus; }
+
+ @action.bound
+ getBasicWordStatus() { return this._basicWordStatus; }
+
+ @action.bound
+ updateIcon(newArray: string[]) { this._icons = newArray; }
+
+ @action.bound
+ getIcons(): string[] { return this._icons; }
+
+ stopProp = (e: React.PointerEvent) => {
+ e.stopPropagation();
+ this._pointerTime = e.timeStamp;
+ }
+
+ @action.bound
+ public closeFilter() {
+ this._filterOpen = false;
+ }
+
+ @action.bound
+ toggleFieldOpen() { this._fieldOpen = !this._fieldOpen; }
+
+ @action.bound
+ toggleColOpen() { this._colOpen = !this._colOpen; }
+
+ @action.bound
+ toggleTypeOpen() { this._typeOpen = !this._typeOpen; }
+
+ @action.bound
+ toggleWordStatusOpen() { this._wordStatusOpen = !this._wordStatusOpen; }
+
+ @action.bound
+ updateTitleStatus(newStat: boolean) { this._titleFieldStatus = newStat; }
+
+ @action.bound
+ updateAuthorStatus(newStat: boolean) { this._authorFieldStatus = newStat; }
+
+ @action.bound
+ updateDataStatus(newStat: boolean) { this._deletedDocsStatus = newStat; }
+
+ @action.bound
+ updateCollectionStatus(newStat: boolean) { this._collectionStatus = newStat; }
+
+ @action.bound
+ updateSelfCollectionStatus(newStat: boolean) { this._collectionSelfStatus = newStat; }
+
+ @action.bound
+ updateParentCollectionStatus(newStat: boolean) { this._collectionParentStatus = newStat; }
+
+ getCollectionStatus() { return this._collectionStatus; }
+ getSelfCollectionStatus() { return this._collectionSelfStatus; }
+ getParentCollectionStatus() { return this._collectionParentStatus; }
+ getTitleStatus() { return this._titleFieldStatus; }
+ getAuthorStatus() { return this._authorFieldStatus; }
+ getDataStatus() { return this._deletedDocsStatus; }
+
+ // 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} //id of collections prototype
+ render() {
+ return (
+ <div>
+ <div style={{ display: "flex", flexDirection: "row-reverse" }}>
+ <SearchBox />
+ </div>
+ {this._filterOpen ? (
+ <div className="filter-form" onPointerDown={this.stopProp} id="filter-form" style={this._filterOpen ? { display: "flex" } : { display: "none" }}>
+ <div className="top-filter-header" style={{ display: "flex", width: "100%" }}>
+ <div id="header">Filter Search Results</div>
+ <div className="close-icon" onClick={this.closeFilter}>
+ <span className="line line-1"></span>
+ <span className="line line-2"></span></div>
+ </div>
+ <div className="filter-options">
+ <div className="filter-div">
+ <div className="filter-header">
+ <div className='filter-title words'>Required words</div>
+ <div style={{ marginLeft: "auto" }}><NaviconButton onClick={this.toggleWordStatusOpen} /></div>
+ </div>
+ <div className="filter-panel" >
+ <ToggleBar handleChange={this.handleWordQueryChange} getStatus={this.getBasicWordStatus}
+ originalStatus={this._basicWordStatus} optionOne={"Include Any Keywords"} optionTwo={"Include All Keywords"} />
+ </div>
+ </div>
+ <div className="filter-div">
+ <div className="filter-header">
+ <div className="filter-title icon">Filter by type of node</div>
+ <div style={{ marginLeft: "auto" }}><NaviconButton onClick={this.toggleTypeOpen} /></div>
+ </div>
+ <div className="filter-panel"><IconBar /></div>
+ </div>
+ <div className="filter-div">
+ <div className="filter-header">
+ <div className='filter-title collection'>Search in current collections</div>
+ <div style={{ marginLeft: "auto" }}><NaviconButton onClick={this.toggleColOpen} /></div>
+ </div>
+ <div className="filter-panel"><CollectionFilters
+ updateCollectionStatus={this.updateCollectionStatus} updateParentCollectionStatus={this.updateParentCollectionStatus} updateSelfCollectionStatus={this.updateSelfCollectionStatus}
+ collectionStatus={this._collectionStatus} collectionParentStatus={this._collectionParentStatus} collectionSelfStatus={this._collectionSelfStatus} /></div>
+ </div>
+ <div className="filter-div">
+ <div className="filter-header">
+ <div className="filter-title field">Filter by Basic Keys</div>
+ <div style={{ marginLeft: "auto" }}><NaviconButton onClick={this.toggleFieldOpen} /></div>
+ </div>
+ <div className="filter-panel"><FieldFilters
+ titleFieldStatus={this._titleFieldStatus} dataFieldStatus={this._deletedDocsStatus} authorFieldStatus={this._authorFieldStatus}
+ updateAuthorStatus={this.updateAuthorStatus} updateDataStatus={this.updateDataStatus} updateTitleStatus={this.updateTitleStatus} /> </div>
+ </div>
+ </div>
+ <div className="filter-buttons" style={{ display: "flex", justifyContent: "space-around" }}>
+ <button className="minimize-filter" onClick={this.minimizeAll}>Minimize All</button>
+ <button className="advanced-filter" >Advanced Filters</button>
+ <button className="save-filter" >Save Filters</button>
+ <button className="reset-filter" onClick={this.resetFilters}>Reset Filters</button>
+ </div>
+ </div>
+ ) : undefined}
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/search/IconBar.scss b/src/client/views/search/IconBar.scss
new file mode 100644
index 000000000..e384722ce
--- /dev/null
+++ b/src/client/views/search/IconBar.scss
@@ -0,0 +1,12 @@
+@import "../globalCssVariables";
+
+.icon-bar {
+ display: flex;
+ justify-content: space-evenly;
+ align-items: center;
+ height: 40px;
+ width: 100%;
+ flex-wrap: wrap;
+ margin-bottom: 10px;
+}
+
diff --git a/src/client/views/search/IconBar.tsx b/src/client/views/search/IconBar.tsx
new file mode 100644
index 000000000..4712b0abc
--- /dev/null
+++ b/src/client/views/search/IconBar.tsx
@@ -0,0 +1,83 @@
+import * as React from 'react';
+import { observer } from 'mobx-react';
+import { observable, action } from 'mobx';
+// import "./SearchBox.scss";
+import "./IconBar.scss";
+import "./IconButton.scss";
+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';
+import * as _ from "lodash";
+import { IconButton } from './IconButton';
+import { FilterBox } from './FilterBox';
+
+library.add(faSearch);
+library.add(faObjectGroup);
+library.add(faImage);
+library.add(faStickyNote);
+library.add(faFilePdf);
+library.add(faFilm);
+library.add(faMusic);
+library.add(faLink);
+library.add(faChartBar);
+library.add(faGlobeAsia);
+library.add(faBan);
+
+@observer
+export class IconBar extends React.Component {
+
+ static Instance: IconBar;
+
+ @observable public _resetClicked: boolean = false;
+ @observable public _selectAllClicked: boolean = false;
+ @observable public _reset: number = 0;
+ @observable public _select: number = 0;
+
+ constructor(props: any) {
+ super(props);
+ IconBar.Instance = this;
+ }
+
+ @action.bound
+ getList(): string[] { return FilterBox.Instance.getIcons(); }
+
+ @action.bound
+ updateList(newList: string[]) { FilterBox.Instance.updateIcon(newList); }
+
+ @action.bound
+ resetSelf = () => {
+ this._resetClicked = true;
+ this.updateList([]);
+ }
+
+ @action.bound
+ selectAll = () => {
+ this._selectAllClicked = true;
+ this.updateList(FilterBox.Instance._allIcons);
+ }
+
+ render() {
+ return (
+ <div className="icon-bar">
+ <div className="type-outer">
+ <div className={"type-icon all"}
+ onClick={this.selectAll}>
+ <FontAwesomeIcon className="fontawesome-icon" icon={faCheckCircle} />
+ </div>
+ <div className="filter-description">Select All</div>
+ </div>
+ {FilterBox.Instance._allIcons.map((type: string) =>
+ <IconButton type={type} />
+ )}
+ <div className="type-outer">
+ <div className={"type-icon none"}
+ onClick={this.resetSelf}>
+ <FontAwesomeIcon className="fontawesome-icon" icon={faTimesCircle} />
+ </div>
+ <div className="filter-description">Clear</div>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/search/IconButton.scss b/src/client/views/search/IconButton.scss
new file mode 100644
index 000000000..94b294ba5
--- /dev/null
+++ b/src/client/views/search/IconButton.scss
@@ -0,0 +1,52 @@
+@import "../globalCssVariables";
+
+.type-outer {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 45px;
+ height: 60px;
+
+ .type-icon {
+ height: 45px;
+ width: 45px;
+ color: $light-color;
+ border-radius: 50%;
+ 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;
+ font-size: 2em;
+
+ .fontawesome-icon {
+ height: 24px;
+ width: 24px;
+ }
+ }
+
+ .filter-description {
+ text-transform: capitalize;
+ font-size: 10;
+ text-align: center;
+ height: 15px;
+ margin-top: 5px;
+ opacity: 0;
+ -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;
+ }
+
+ .type-icon:hover {
+ transform: scale(1.1);
+ background-color: $darker-alt-accent;
+ opacity: 1;
+
+ +.filter-description {
+ opacity: 1;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/client/views/search/IconButton.tsx b/src/client/views/search/IconButton.tsx
new file mode 100644
index 000000000..bfe2c7d0b
--- /dev/null
+++ b/src/client/views/search/IconButton.tsx
@@ -0,0 +1,192 @@
+import * as React from 'react';
+import { observer } from 'mobx-react';
+import { observable, action, runInAction, IReactionDisposer, reaction } from 'mobx';
+import "./SearchBox.scss";
+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 { DocumentType } from '../../documents/Documents';
+import '../globalCssVariables.scss';
+import * as _ from "lodash";
+import { IconBar } from './IconBar';
+import { props } from 'bluebird';
+import { FilterBox } from './FilterBox';
+import { Search } from '../../../server/Search';
+
+library.add(faSearch);
+library.add(faObjectGroup);
+library.add(faImage);
+library.add(faStickyNote);
+library.add(faFilePdf);
+library.add(faFilm);
+library.add(faMusic);
+library.add(faLink);
+library.add(faChartBar);
+library.add(faGlobeAsia);
+library.add(faBan);
+
+interface IconButtonProps {
+ type: string;
+}
+
+@observer
+export class IconButton extends React.Component<IconButtonProps>{
+
+ @observable private _isSelected: boolean = FilterBox.Instance.getIcons().indexOf(this.props.type) !== -1;
+ @observable private _hover = false;
+ private _resetReaction?: IReactionDisposer;
+ private _selectAllReaction?: IReactionDisposer;
+
+ static Instance: IconButton;
+ constructor(props: IconButtonProps) {
+ super(props);
+ IconButton.Instance = this;
+ }
+
+ componentDidMount = () => {
+ this._resetReaction = reaction(
+ () => IconBar.Instance._resetClicked,
+ () => {
+ if (IconBar.Instance._resetClicked) {
+ runInAction(() => {
+ this.reset();
+ IconBar.Instance._reset++;
+ if (IconBar.Instance._reset === 9) {
+ IconBar.Instance._reset = 0;
+ IconBar.Instance._resetClicked = false;
+ }
+ });
+ }
+ },
+ );
+ this._selectAllReaction = reaction(
+ () => IconBar.Instance._selectAllClicked,
+ () => {
+ if (IconBar.Instance._selectAllClicked) {
+ runInAction(() => {
+ this.select();
+ IconBar.Instance._select++;
+ if (IconBar.Instance._select === 9) {
+ IconBar.Instance._select = 0;
+ IconBar.Instance._selectAllClicked = false;
+ }
+ });
+ }
+ },
+ );
+ }
+
+ @action.bound
+ getIcon() {
+ switch (this.props.type) {
+ case (DocumentType.NONE):
+ return faBan;
+ case (DocumentType.AUDIO):
+ return faMusic;
+ case (DocumentType.COL):
+ return faObjectGroup;
+ case (DocumentType.HIST):
+ return faChartBar;
+ case (DocumentType.IMG):
+ return faImage;
+ case (DocumentType.LINK):
+ return faLink;
+ case (DocumentType.PDF):
+ return faFilePdf;
+ case (DocumentType.TEXT):
+ return faStickyNote;
+ case (DocumentType.VID):
+ return faVideo;
+ case (DocumentType.WEB):
+ return faGlobeAsia;
+ default:
+ return faCaretDown;
+ }
+ }
+
+ @action.bound
+ onClick = () => {
+ let newList: string[] = FilterBox.Instance.getIcons();
+
+ if (!this._isSelected) {
+ this._isSelected = true;
+ newList.push(this.props.type);
+ }
+ else {
+ this._isSelected = false;
+ _.pull(newList, this.props.type);
+ }
+
+ FilterBox.Instance.updateIcon(newList);
+ }
+
+ selected = {
+ opacity: 1,
+ backgroundColor: "#c2c2c5" //$alt-accent
+ };
+
+ notSelected = {
+ opacity: 0.6,
+ };
+
+ hoverStyle = {
+ opacity: 1,
+ backgroundColor: "rgb(178, 206, 248)" //$darker-alt-accent
+ };
+
+ @action.bound
+ public reset() { this._isSelected = false; }
+
+ @action.bound
+ public select() { this._isSelected = true; }
+
+ @action
+ onMouseLeave = () => { this._hover = false; }
+
+ @action
+ onMouseEnter = () => { this._hover = true; }
+
+ getFA = () => {
+ switch (this.props.type) {
+ case (DocumentType.NONE):
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faBan} />);
+ case (DocumentType.AUDIO):
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faMusic} />);
+ case (DocumentType.COL):
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faObjectGroup} />);
+ case (DocumentType.HIST):
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faChartBar} />);
+ case (DocumentType.IMG):
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faImage} />);
+ case (DocumentType.LINK):
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faLink} />);
+ case (DocumentType.PDF):
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faFilePdf} />);
+ case (DocumentType.TEXT):
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faStickyNote} />);
+ case (DocumentType.VID):
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faVideo} />);
+ case (DocumentType.WEB):
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faGlobeAsia} />);
+ default:
+ return (<FontAwesomeIcon className="fontawesome-icon" icon={faCaretDown} />);
+ }
+ }
+
+ render() {
+ return (
+ <div className="type-outer" id={this.props.type + "-filter"}
+ onMouseEnter={this.onMouseEnter}
+ onMouseLeave={this.onMouseLeave}
+ onClick={this.onClick}>
+ <div className="type-icon" id={this.props.type + "-icon"}
+ style={this._hover ? this.hoverStyle : this._isSelected ? this.selected : this.notSelected}
+ >
+ {this.getFA()}
+ </div>
+ <div className="filter-description">{this.props.type}</div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/search/NaviconButton.scss b/src/client/views/search/NaviconButton.scss
new file mode 100644
index 000000000..c23bab461
--- /dev/null
+++ b/src/client/views/search/NaviconButton.scss
@@ -0,0 +1,69 @@
+@import "../globalCssVariables";
+
+$height-icon: 15px;
+$width-line: 30px;
+$height-line: 4px;
+
+$transition-time: 0.4s;
+$rotation: 45deg;
+$translateY: ($height-icon / 2);
+$translateX: 0;
+
+#hamburger-icon {
+ width: $width-line;
+ height: $height-icon;
+ position: relative;
+ display: block;
+ transition: all $transition-time;
+ -webkit-transition: all $transition-time;
+ -moz-transition: all $transition-time;
+
+ .line {
+ display: block;
+ background: $alt-accent;
+ width: $width-line;
+ height: $height-line;
+ position: absolute;
+ left: 0;
+ border-radius: ($height-line / 2);
+ transition: all $transition-time;
+ -webkit-transition: all $transition-time;
+ -moz-transition: all $transition-time;
+
+ &.line-1 {
+ top: 0;
+ }
+
+ &.line-2 {
+ top: 50%;
+ }
+
+ &.line-3 {
+ top: 100%;
+ }
+ }
+}
+
+.filter-header.active {
+ .line-1 {
+ transform: translateY($translateY) translateX($translateX) rotate($rotation);
+ -webkit-transform: translateY($translateY) translateX($translateX) rotate($rotation);
+ -moz-transform: translateY($translateY) translateX($translateX) rotate($rotation);
+ }
+
+ .line-2 {
+ opacity: 0;
+ }
+
+ .line-3 {
+ transform: translateY($translateY * -1) translateX($translateX) rotate($rotation * -1);
+ -webkit-transform: translateY($translateY * -1) translateX($translateX) rotate($rotation * -1);
+ -moz-transform: translateY($translateY * -1) translateX($translateX) rotate($rotation * -1);
+ }
+}
+
+.filter-header:hover #hamburger-icon {
+ transform: scale(1.1);
+ -webkit-transform: scale(1.1);
+ -moz-transform: scale(1.1);
+} \ No newline at end of file
diff --git a/src/client/views/search/NaviconButton.tsx b/src/client/views/search/NaviconButton.tsx
new file mode 100644
index 000000000..3fa36b163
--- /dev/null
+++ b/src/client/views/search/NaviconButton.tsx
@@ -0,0 +1,35 @@
+import * as React from 'react';
+import { observer } from 'mobx-react';
+import "./NaviconButton.scss";
+import * as $ from 'jquery';
+import { observable } from 'mobx';
+
+export interface NaviconProps{
+ onClick(): void;
+}
+
+export class NaviconButton extends React.Component<NaviconProps> {
+
+ @observable private _ref: React.RefObject<HTMLAnchorElement> = React.createRef();
+
+ componentDidMount = () => {
+ let that = this;
+ if(this._ref.current){this._ref.current.addEventListener("click", function(e) {
+ e.preventDefault();
+ if(that._ref.current){
+ that._ref.current.classList.toggle('active');
+ return false;
+ }
+ });}
+ }
+
+ render() {
+ return (
+ <a id="hamburger-icon" href="#" ref = {this._ref} title="Menu">
+ <span className="line line-1"></span>
+ <span className="line line-2"></span>
+ <span className="line line-3"></span>
+ </a>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss
new file mode 100644
index 000000000..109b88ac9
--- /dev/null
+++ b/src/client/views/search/SearchBox.scss
@@ -0,0 +1,64 @@
+@import "../globalCssVariables";
+@import "./NaviconButton.scss";
+
+.searchBox-bar {
+ height: 32px;
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ padding-left: 2px;
+ padding-right: 2px;
+
+ .searchBox-barChild {
+
+ &.searchBox-collection {
+ flex: 0 1 auto;
+ margin-left: 2px;
+ margin-right: 2px
+ }
+
+ &.searchBox-input {
+ display: block;
+ width: 130px;
+ -webkit-transition: width 0.4s;
+ transition: width 0.4s;
+ align-self: stretch;
+ margin-left: 2px;
+ margin-right: 2px
+ }
+
+ .searchBox-input:focus {
+ width: 500px;
+ outline: 3px solid lightblue;
+ }
+
+ &.searchBox-filter {
+ align-self: stretch;
+ margin-left: 2px;
+ margin-right: 2px
+ }
+ }
+}
+
+.searchBox-results {
+ margin-right: 136px;
+ top: 300px;
+ display: flex;
+ flex-direction: column;
+ max-height: 560px;
+ overflow: hidden;
+ overflow-y: auto;
+
+ .no-result {
+ width: 500px;
+ background: $light-color-secondary;
+ border-color: $intermediate-color;
+ border-bottom-style: solid;
+ padding: 10px;
+ height: 50px;
+ 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
new file mode 100644
index 000000000..2214ac8af
--- /dev/null
+++ b/src/client/views/search/SearchBox.tsx
@@ -0,0 +1,340 @@
+import * as React from 'react';
+import { observer } from 'mobx-react';
+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, Cast } from '../../../new_fields/Types';
+import { Doc } from '../../../new_fields/Doc';
+import { SearchItem } from './SearchItem';
+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 _searchbarOpen: boolean = false;
+ @observable private _results: [Doc, string[]][] = [];
+ private _resultsSet = new Map<Doc, number>();
+ @observable private _openNoResults: boolean = false;
+ @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
+ getViews = async (doc: Doc) => {
+ const results = await SearchUtil.GetViewsOfDocument(doc);
+ let toReturn: Doc[] = [];
+ await runInAction(() => {
+ toReturn = results;
+ });
+ return toReturn;
+ }
+
+ @action.bound
+ onChange(e: React.ChangeEvent<HTMLInputElement>) {
+ this._searchString = e.target.value;
+
+ 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(); } };
+
+ 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
+ },
+ json: true,
+ });
+ return returnedUri;
+
+ } catch (e) {
+ console.log(e);
+ }
+ }
+
+ @action
+ submitSearch = async () => {
+ 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 === "") {
+ return;
+ }
+ else {
+ this._endIndex = 12;
+ this._maxSearchIndex = 0;
+ this._numTotalResults = -1;
+ await this.getResults(query);
+ }
+
+ runInAction(() => {
+ this._resultsOpen = true;
+ this._searchbarOpen = true;
+ this._openNoResults = true;
+ this.resultsScrolled();
+ });
+ }
+
+ 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) => {
+ 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 () => {
+ 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);
+ } 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}"` });
+
+ }
+
+ @action.bound
+ openSearch(e: React.PointerEvent) {
+ e.stopPropagation();
+ this._openNoResults = false;
+ FilterBox.Instance.closeFilter();
+ this._resultsOpen = true;
+ this._searchbarOpen = true;
+ FilterBox.Instance._pointerTime = e.timeStamp;
+ }
+
+ @action.bound
+ 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} 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._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" 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>
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/search/SearchItem.scss b/src/client/views/search/SearchItem.scss
new file mode 100644
index 000000000..273d49349
--- /dev/null
+++ b/src/client/views/search/SearchItem.scss
@@ -0,0 +1,215 @@
+@import "../globalCssVariables";
+
+.search-overview {
+ display: flex;
+ flex-direction: row-reverse;
+ justify-content: flex-end;
+ height: 70px;
+ z-index: 0;
+}
+
+.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 {
+ text-transform: uppercase;
+ text-align: left;
+ width: 100%;
+ font-weight: bold;
+ }
+ }
+
+ .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: 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-container.item:hover {
+ width: 70px;
+ }
+
+ .link-container.item:hover .link-count {
+ opacity: 0;
+ }
+
+ .link-container.item:hover .link-extended {
+ opacity: 1;
+ visibility: visible;
+ // display: inline;
+ }
+
+ .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;
+ }
+
+ .pdfBox-cont {
+ overflow: hidden;
+
+ img {
+ width: 100% !important;
+ height: auto !important;
+ }
+ }
+
+ .search-type:hover+.search-label {
+ opacity: 1;
+ }
+
+ .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;
+ }
+ }
+
+ .icon-live:hover {
+ height: 175px;
+
+ .pdfBox-cont {
+ img {
+ width: 100% !important;
+ }
+ }
+ }
+ }
+
+ .search-info:hover {
+ width: 60%;
+ }
+ }
+}
+
+.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-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;
+}
+
+.collection-item {
+ width: 35px;
+} \ No newline at end of file
diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx
new file mode 100644
index 000000000..a995140e2
--- /dev/null
+++ b/src/client/views/search/SearchItem.tsx
@@ -0,0 +1,269 @@
+import React = require("react");
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faCaretUp, faChartBar, faFilePdf, faFilm, faGlobeAsia, faImage, faLink, faMusic, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { action, computed, observable, runInAction } from "mobx";
+import { observer } from "mobx-react";
+import { Doc, DocListCast, HeightSym, WidthSym } from "../../../new_fields/Doc";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { emptyFunction, returnFalse, returnOne, Utils } from "../../../Utils";
+import { DocumentType } from "../../documents/Documents";
+import { DocumentManager } from "../../util/DocumentManager";
+import { SetupDrag, DragManager } from "../../util/DragManager";
+import { LinkManager } from "../../util/LinkManager";
+import { SearchUtil } from "../../util/SearchUtil";
+import { Transform } from "../../util/Transform";
+import { SEARCH_THUMBNAIL_SIZE } from "../../views/globalCssVariables.scss";
+import { CollectionViewType } from "../collections/CollectionBaseView";
+import { CollectionDockingView } from "../collections/CollectionDockingView";
+import { DocumentView } from "../nodes/DocumentView";
+import { SearchBox } from "./SearchBox";
+import "./SearchItem.scss";
+import "./SelectorContextMenu.scss";
+import { ContextMenu } from "../ContextMenu";
+import { faFile } from '@fortawesome/free-solid-svg-icons';
+
+export interface SearchItemProps {
+ doc: Doc;
+ highlighting: string[];
+}
+
+library.add(faCaretUp);
+library.add(faObjectGroup);
+library.add(faStickyNote);
+library.add(faFile);
+library.add(faFilePdf);
+library.add(faFilm);
+library.add(faMusic);
+library.add(faLink);
+library.add(faChartBar);
+library.add(faGlobeAsia);
+
+@observer
+export class SelectorContextMenu extends React.Component<SearchItemProps> {
+ @observable private _docs: { col: Doc, target: Doc }[] = [];
+ @observable private _otherDocs: { col: Doc, target: Doc }[] = [];
+
+ constructor(props: SearchItemProps) {
+ super(props);
+ this.fetchDocuments();
+ }
+
+ async fetchDocuments() {
+ let aliases = (await SearchUtil.GetViewsOfDocument(this.props.doc)).filter(doc => doc !== this.props.doc);
+ 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("", 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(() => {
+ this._docs = docs.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance.props.Document)).map(doc => ({ col: doc, target: this.props.doc }));
+ this._otherDocs = Array.from(map.entries()).filter(entry => !Doc.AreProtosEqual(entry[0], CollectionDockingView.Instance.props.Document)).map(([col, target]) => ({ col, target }));
+ });
+ }
+
+ getOnClick({ col, target }: { col: Doc, target: Doc }) {
+ return () => {
+ col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col;
+ if (NumCast(col.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
+ const newPanX = NumCast(target.x) + NumCast(target.width) / NumCast(target.zoomBasis, 1) / 2;
+ const newPanY = NumCast(target.y) + NumCast(target.height) / NumCast(target.zoomBasis, 1) / 2;
+ col.panX = newPanX;
+ col.panY = newPanY;
+ }
+ CollectionDockingView.Instance.AddRightSplit(col, undefined);
+ };
+ }
+ render() {
+ return (
+ <div className="parents">
+ <p className="contexts">Contexts:</p>
+ {[...this._docs, ...this._otherDocs].map(doc => {
+ let item = React.createRef<HTMLDivElement>();
+ return <div className="collection" key={doc.col[Id] + doc.target[Id]} ref={item}>
+ <div className="collection-item" onPointerDown={
+ SetupDrag(item, () => doc.col, undefined, undefined, undefined, undefined, () => SearchBox.Instance.closeSearch())}>
+ <FontAwesomeIcon icon={faStickyNote} />
+ </div>
+ <a className="title" onClick={this.getOnClick(doc)}>{doc.col.title}</a>
+ </div>;
+ })}
+ </div>
+ );
+ }
+}
+
+@observer
+export class SearchItem extends React.Component<SearchItemProps> {
+
+ @observable _selected: boolean = false;
+
+ 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);
+ // CollectionDockingView.Instance.AddRightSplit(this.props.doc, undefined);
+ }
+ @observable _useIcons = true;
+ @observable _displayDim = 50;
+
+ @computed
+ public get DocumentIcon() {
+ if (!this._useIcons) {
+ let returnXDimension = () => this._useIcons ? 50 : Number(SEARCH_THUMBNAIL_SIZE);
+ let returnYDimension = () => this._displayDim;
+ let scale = () => returnXDimension() / NumCast(this.props.doc.nativeWidth, returnXDimension());
+ return <div
+ onPointerDown={action(() => { this._useIcons = !this._useIcons; this._displayDim = this._useIcons ? 50 : Number(SEARCH_THUMBNAIL_SIZE); })}
+ onPointerEnter={action(() => this._displayDim = this._useIcons ? 50 : Number(SEARCH_THUMBNAIL_SIZE))}
+ onPointerLeave={action(() => this._displayDim = 50)} >
+ <DocumentView
+ fitToBox={StrCast(this.props.doc.type).indexOf(DocumentType.COL) !== -1}
+ Document={this.props.doc}
+ addDocument={returnFalse}
+ removeDocument={returnFalse}
+ ScreenToLocalTransform={Transform.Identity}
+ addDocTab={returnFalse}
+ renderDepth={1}
+ PanelWidth={returnXDimension}
+ PanelHeight={returnYDimension}
+ focus={emptyFunction}
+ selectOnLoad={false}
+ parentActive={returnFalse}
+ whenActiveChanged={returnFalse}
+ bringToFront={emptyFunction}
+ zoomToScale={emptyFunction}
+ getScale={returnOne}
+ ContainingCollectionView={undefined}
+ ContentScaling={scale}
+ />
+ </div>;
+ }
+
+ let layoutresult = StrCast(this.props.doc.type);
+ 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" />
+ </div>;
+ }
+
+ collectionRef = React.createRef<HTMLDivElement>();
+ startDocDrag = () => {
+ let doc = this.props.doc;
+ const isProto = Doc.GetT(doc, "isPrototype", "boolean", true);
+ if (isProto) {
+ return Doc.MakeDelegate(doc);
+ } else {
+ return Doc.MakeAlias(doc);
+ }
+ }
+
+ @computed
+ get linkCount() { return LinkManager.Instance.getAllRelatedLinks(this.props.doc).length; }
+
+ @computed
+ get linkString(): string {
+ let num = this.linkCount;
+ if (num === 1) {
+ return num.toString() + " link";
+ }
+ return num.toString() + " links";
+ }
+
+ @action
+ pointerDown = (e: React.PointerEvent) => { e.preventDefault(); e.button === 0 && SearchBox.Instance.openSearch(e); }
+
+ highlightDoc = (e: React.PointerEvent) => {
+ 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 = true);
+ doc2 && (doc2.libraryBrush = true);
+ }
+ } else {
+ let docViews: DocumentView[] = DocumentManager.Instance.getAllDocumentViews(this.props.doc);
+ docViews.forEach(element => {
+ element.props.Document.libraryBrush = true;
+ });
+ }
+ }
+
+ unHighlightDoc = (e: React.PointerEvent) => {
+ 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 = undefined);
+ doc2 && (doc2.libraryBrush = undefined);
+ }
+ } else {
+ let docViews: DocumentView[] = DocumentManager.Instance.getAllDocumentViews(this.props.doc);
+ docViews.forEach(element => {
+ element.props.Document.libraryBrush = undefined;
+ });
+ }
+ }
+
+ onContextMenu = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ ContextMenu.Instance.clearItems();
+ ContextMenu.Instance.addItem({
+ description: "Copy ID", event: () => {
+ Utils.CopyText(this.props.doc[Id]);
+ }
+ });
+ ContextMenu.Instance.displayMenu(e.clientX, e.clientY);
+ }
+
+ onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
+ e.stopPropagation();
+ const doc = Doc.IsPrototype(this.props.doc) ? Doc.MakeDelegate(this.props.doc) : this.props.doc;
+ DragManager.StartDocumentDrag([e.currentTarget], new DragManager.DocumentDragData([doc], []), e.clientX, e.clientY, {
+ handlers: { dragComplete: emptyFunction },
+ hideSource: false,
+ });
+ }
+
+ render() {
+ return (
+ <div className="search-overview" onPointerDown={this.pointerDown} onContextMenu={this.onContextMenu}>
+ <div className="search-item" onPointerEnter={this.highlightDoc} onPointerLeave={this.unHighlightDoc} id="result"
+ onClick={this.onClick} onPointerDown={this.pointerDown} >
+ <div className="main-search-info">
+ <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" title="Click to Preview">{this.DocumentIcon}</div>
+ <div className="search-label">{this.props.doc.type ? this.props.doc.type : "Other"}</div>
+ </div>
+ <div className="link-container item">
+ <div className="link-count">{this.linkCount}</div>
+ <div className="link-extended">{this.linkString}</div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div className="searchBox-instances">
+ <SelectorContextMenu {...this.props} />
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/search/SelectorContextMenu.scss b/src/client/views/search/SelectorContextMenu.scss
new file mode 100644
index 000000000..49f77b9bf
--- /dev/null
+++ b/src/client/views/search/SelectorContextMenu.scss
@@ -0,0 +1,15 @@
+@import "../globalCssVariables";
+
+.parents {
+ background: $lighter-alt-accent;
+ padding: 10px;
+
+ .contexts {
+ text-transform: uppercase;
+ }
+
+ .collection {
+ border-color: $darker-alt-accent;
+ border-bottom-style: solid;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/search/ToggleBar.scss b/src/client/views/search/ToggleBar.scss
new file mode 100644
index 000000000..633a194fe
--- /dev/null
+++ b/src/client/views/search/ToggleBar.scss
@@ -0,0 +1,36 @@
+@import "../globalCssVariables";
+
+.toggle-title {
+ display: flex;
+ align-items: center;
+ color: $light-color;
+ text-transform: uppercase;
+ flex-direction: row;
+ justify-content: space-around;
+ padding: 5px;
+
+ .toggle-option {
+ width: 50%;
+ text-align: 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;
+ }
+}
+
+.toggle-bar {
+ height: 50px;
+ background-color: $alt-accent;
+ border-radius: 10px;
+ padding: 5px;
+ display: flex;
+ align-items: center;
+
+ .toggle-button {
+ width: 275px;
+ height: 100%;
+ border-radius: 10px;
+ background-color: $light-color;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/search/ToggleBar.tsx b/src/client/views/search/ToggleBar.tsx
new file mode 100644
index 000000000..178578c5c
--- /dev/null
+++ b/src/client/views/search/ToggleBar.tsx
@@ -0,0 +1,86 @@
+import * as React from 'react';
+import { observer } from 'mobx-react';
+import { observable, action, runInAction, computed } from 'mobx';
+import "./SearchBox.scss";
+import "./ToggleBar.scss";
+import * as anime from 'animejs';
+
+export interface ToggleBarProps {
+ originalStatus: boolean;
+ optionOne: string;
+ optionTwo: string;
+ handleChange(): void;
+ getStatus(): boolean;
+}
+
+@observer
+export class ToggleBar extends React.Component<ToggleBarProps>{
+ static Instance: ToggleBar;
+
+ @observable private _forwardTimeline: anime.AnimeTimelineInstance;
+ @observable private _toggleButton: React.RefObject<HTMLDivElement>;
+ @observable private _originalStatus: boolean = this.props.originalStatus;
+
+ constructor(props: ToggleBarProps) {
+ super(props);
+ ToggleBar.Instance = this;
+ this._toggleButton = React.createRef();
+ this._forwardTimeline = anime.timeline({
+ loop: false,
+ autoplay: false,
+ direction: "reverse",
+ });
+ }
+
+ componentDidMount = () => {
+
+ let totalWidth = 265;
+
+ if (this._originalStatus) {
+ this._forwardTimeline.add({
+ targets: this._toggleButton.current,
+ translateX: totalWidth,
+ easing: "easeInOutQuad",
+ duration: 500
+ });
+ }
+ else {
+ this._forwardTimeline.add({
+ targets: this._toggleButton.current,
+ translateX: -totalWidth,
+ easing: "easeInOutQuad",
+ duration: 500
+ });
+ }
+ }
+
+ @action.bound
+ onclick() {
+ this._forwardTimeline.play();
+ this._forwardTimeline.reverse();
+ this.props.handleChange();
+ }
+
+ @action.bound
+ public resetToggle = () => {
+ if (!this.props.getStatus()) {
+ this._forwardTimeline.play();
+ this._forwardTimeline.reverse();
+ this.props.handleChange();
+ }
+ }
+
+ render() {
+ return (
+ <div>
+ <div className="toggle-title">
+ <div className="toggle-option" style={{ opacity: (this.props.getStatus() ? 1 : .4) }}>{this.props.optionOne}</div>
+ <div className="toggle-option" style={{ opacity: (this.props.getStatus() ? .4 : 1) }}>{this.props.optionTwo}</div>
+ </div>
+ <div className="toggle-bar" id="toggle-bar" onClick={this.onclick} style={{ flexDirection: (this._originalStatus ? "row" : "row-reverse") }}>
+ <div className="toggle-button" id="toggle-button" ref={this._toggleButton} />
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file