aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMohammad Amoush <mohammad_amoush@brown.edu>2019-07-16 18:03:12 -0400
committerMohammad Amoush <mohammad_amoush@brown.edu>2019-07-16 18:03:12 -0400
commit1cedadbdf01c392ca9910e3ca18f3875d9a86fed (patch)
tree602608ba06b997cd3144395640e404a01f666291 /src
parentf70b95879e87a6bb61aaae5de29747d9474623a7 (diff)
parentf18be9418b9237acd847eaf71adc034226c54695 (diff)
Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web into youtube-api-muhammed
Diffstat (limited to 'src')
-rw-r--r--src/.DS_Storebin6148 -> 6148 bytes
-rw-r--r--src/Utils.ts2
-rw-r--r--src/client/DocServer.ts339
-rw-r--r--src/client/documents/Documents.ts749
-rw-r--r--src/client/goldenLayout.js11
-rw-r--r--src/client/northstar/dash-nodes/HistogramBox.tsx5
-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.ts110
-rw-r--r--src/client/util/DragManager.ts181
-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/ProsemirrorCopy/prompt.js179
-rw-r--r--src/client/util/ProsemirrorExampleTransfer.ts (renamed from src/client/util/ProsemirrorKeymap.ts)10
-rw-r--r--src/client/util/RichTextSchema.tsx205
-rw-r--r--src/client/util/Scripting.ts55
-rw-r--r--src/client/util/SearchUtil.ts64
-rw-r--r--src/client/util/SelectionManager.ts79
-rw-r--r--src/client/util/SerializationHelper.ts24
-rw-r--r--src/client/util/TooltipTextMenu.scss94
-rw-r--r--src/client/util/TooltipTextMenu.tsx498
-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.d52
-rw-r--r--src/client/views/ContextMenu.scss49
-rw-r--r--src/client/views/ContextMenu.tsx188
-rw-r--r--src/client/views/ContextMenuItem.tsx60
-rw-r--r--src/client/views/DocumentDecorations.scss56
-rw-r--r--src/client/views/DocumentDecorations.tsx260
-rw-r--r--src/client/views/EditableView.tsx53
-rw-r--r--src/client/views/GlobalKeyHandler.ts178
-rw-r--r--src/client/views/InkingCanvas.tsx20
-rw-r--r--src/client/views/InkingControl.scss10
-rw-r--r--src/client/views/InkingControl.tsx83
-rw-r--r--src/client/views/InkingStroke.tsx4
-rw-r--r--src/client/views/Main.scss89
-rw-r--r--src/client/views/Main.tsx33
-rw-r--r--src/client/views/MainOverlayTextBox.scss17
-rw-r--r--src/client/views/MainOverlayTextBox.tsx87
-rw-r--r--src/client/views/MainView.tsx384
-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/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.tsx47
-rw-r--r--src/client/views/SearchItem.tsx73
-rw-r--r--src/client/views/TemplateMenu.tsx25
-rw-r--r--src/client/views/Templates.tsx59
-rw-r--r--src/client/views/_nodeModuleOverrides.scss19
-rw-r--r--src/client/views/collections/CollectionBaseView.scss12
-rw-r--r--src/client/views/collections/CollectionBaseView.tsx121
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx338
-rw-r--r--src/client/views/collections/CollectionPDFView.tsx89
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx196
-rw-r--r--src/client/views/collections/CollectionStackingView.scss55
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx322
-rw-r--r--src/client/views/collections/CollectionSubView.tsx186
-rw-r--r--src/client/views/collections/CollectionTreeView.scss119
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx621
-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.tsx28
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx128
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx12
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss46
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx286
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.scss2
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx129
-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.tsx175
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx56
-rw-r--r--src/client/views/nodes/DocumentView.scss1
-rw-r--r--src/client/views/nodes/DocumentView.tsx457
-rw-r--r--src/client/views/nodes/FieldView.tsx93
-rw-r--r--src/client/views/nodes/FormattedTextBox.scss2
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx339
-rw-r--r--src/client/views/nodes/IconBox.tsx6
-rw-r--r--src/client/views/nodes/ImageBox.tsx191
-rw-r--r--src/client/views/nodes/KeyValueBox.scss6
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx152
-rw-r--r--src/client/views/nodes/KeyValuePair.scss39
-rw-r--r--src/client/views/nodes/KeyValuePair.tsx78
-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.tsx119
-rw-r--r--src/client/views/nodes/PDFBox.scss113
-rw-r--r--src/client/views/nodes/PDFBox.tsx511
-rw-r--r--src/client/views/nodes/VideoBox.scss15
-rw-r--r--src/client/views/nodes/VideoBox.tsx240
-rw-r--r--src/client/views/nodes/WebBox.tsx44
-rw-r--r--src/client/views/pdf/Annotation.tsx153
-rw-r--r--src/client/views/pdf/PDFAnnotationLayer.tsx24
-rw-r--r--src/client/views/pdf/PDFMenu.scss36
-rw-r--r--src/client/views/pdf/PDFMenu.tsx269
-rw-r--r--src/client/views/pdf/PDFViewer.scss131
-rw-r--r--src/client/views/pdf/PDFViewer.tsx732
-rw-r--r--src/client/views/pdf/Page.tsx436
-rw-r--r--src/client/views/presentationview/PresentationElement.tsx12
-rw-r--r--src/client/views/presentationview/PresentationList.tsx19
-rw-r--r--src/client/views/presentationview/PresentationView.tsx17
-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.tsx331
-rw-r--r--src/client/views/search/SearchItem.scss211
-rw-r--r--src/client/views/search/SearchItem.tsx265
-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
-rw-r--r--src/debug/Repl.tsx11
-rw-r--r--src/debug/Test.tsx1
-rw-r--r--src/debug/Viewer.tsx24
-rw-r--r--src/fields/ScriptField.ts101
-rw-r--r--src/mobile/ImageUpload.tsx4
-rw-r--r--src/new_fields/DateField.ts6
-rw-r--r--src/new_fields/Doc.ts181
-rw-r--r--src/new_fields/List.ts5
-rw-r--r--src/new_fields/Proxy.ts4
-rw-r--r--src/new_fields/RichTextField.ts2
-rw-r--r--src/new_fields/Schema.ts42
-rw-r--r--src/new_fields/ScriptField.ts96
-rw-r--r--src/new_fields/Types.ts15
-rw-r--r--src/new_fields/URLField.ts13
-rw-r--r--src/new_fields/util.ts73
-rw-r--r--src/scraping/acm/.gitignore2
-rwxr-xr-xsrc/scraping/acm/chromedriverbin0 -> 10256192 bytes
-rw-r--r--src/scraping/acm/chromedriver.exebin0 -> 7477760 bytes
-rw-r--r--src/scraping/acm/citations.txt2
-rw-r--r--src/scraping/acm/debug.log38
-rw-r--r--src/scraping/acm/index.js279
-rw-r--r--src/scraping/acm/package.json17
-rw-r--r--src/scraping/acm/results.txt91
-rw-r--r--src/scraping/buxton/scraper.py386
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Bill_Notes_CyKey.docxbin0 -> 1675500 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Braun_T3.docxbin0 -> 1671968 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_CasioC801.docxbin0 -> 574664 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Casio_Mini.docxbin0 -> 581069 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_FingerWorks_Prototype.docxbin0 -> 585090 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Fingerworks_TouchStream.docxbin0 -> 1722555 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_FrogPad.docxbin0 -> 840173 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Gavilan_SC.docxbin0 -> 1695290 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Grandjean_Stenotype.docxbin0 -> 2094142 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Matias.docxbin0 -> 590407 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_MousePen.docxbin0 -> 505322 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_NewO.docxbin0 -> 2264571 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_OLPC.docxbin0 -> 6883659 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_PARCkbd.docxbin0 -> 631959 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Philco_Mystery_Control.docxbin0 -> 1994439 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_TASA_Kbd.docxbin0 -> 461199 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_The_Tap.docxbin0 -> 711321 bytes
-rw-r--r--src/server/GarbageCollector.ts150
-rw-r--r--src/server/Message.ts2
-rw-r--r--src/server/RouteStore.ts1
-rw-r--r--src/server/Search.ts48
-rw-r--r--src/server/authentication/controllers/user_controller.ts41
-rw-r--r--src/server/authentication/models/current_user_utils.ts71
-rw-r--r--src/server/authentication/models/user_model.ts4
-rw-r--r--src/server/database.ts22
-rw-r--r--src/server/index.ts258
-rw-r--r--src/server/updateSearch.ts34
189 files changed, 14726 insertions, 3882 deletions
diff --git a/src/.DS_Store b/src/.DS_Store
index d70e95c0a..071dafa1e 100644
--- a/src/.DS_Store
+++ b/src/.DS_Store
Binary files differ
diff --git a/src/Utils.ts b/src/Utils.ts
index 611c61135..e8a80bdc3 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -15,7 +15,7 @@ export class Utils {
return v5(seed, v5.URL);
}
- public static GetScreenTransform(ele: HTMLElement): { scale: number, translateX: number, translateY: number } {
+ public static GetScreenTransform(ele?: HTMLElement): { scale: number, translateX: number, translateY: number } {
if (!ele) {
return { scale: 1, translateX: 1, translateY: 1 };
}
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index 4fb8b8b7b..1e376e92f 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -1,35 +1,143 @@
import * as OpenSocket from 'socket.io-client';
-import { MessageStore, YoutubeQueryTypes } from "./../server/Message";
+import { MessageStore, Diff, YoutubeQueryTypes } 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 makeReadOnly() {
- _CreateField = emptyFunction;
- _UpdateField = emptyFunction;
- _respondToUpdate = emptyFunction;
- }
+ 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;
+ /**
+ * 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);
+ }
+ /**
+ * A convenience method. Prepends the full path (i.e. http://localhost:1050) to the
+ * requested extension
+ * @param extension the specified sub-path to append to the window origin
+ */
export function prepend(extension: string): string {
return window.location.origin + extension;
}
- export function DeleteDatabase() {
- Utils.Emit(_socket, MessageStore.DeleteAll, {});
+ function errorFunc(): never {
+ throw new Error("Can't use DocServer without calling init first");
+ }
+
+ 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; }
+
+ }
+
+ /**
+ * 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, {});
+ }
+
}
- export async function GetRefField(id: string): Promise<Opt<RefField>> {
+ // RETRIEVE DOCS FROM SERVER
+
+ /**
+ * Given a single Doc GUID, this utility function will asynchronously attempt to fetch the id's associated
+ * field, first looking in the RefField cache and then communicating with
+ * the server if the document has not been cached.
+ * @param id the id of the requested document
+ */
+ const _GetRefFieldImpl = (id: string): Promise<Opt<RefField>> => {
+ // an initial pass through the cache to determine whether the document needs to be fetched,
+ // is already in the process of being fetched or already exists in the
+ // cache
let cached = _cache[id];
if (cached === undefined) {
- const prom = Utils.EmitCallback(_socket, MessageStore.GetRefField, id).then(async fieldJson => {
+ // NOT CACHED => we'll have to send a request to the server
+
+ // synchronously, we emit a single callback to the server requesting the serialized (i.e. represented by a string)
+ // field for the given ids. This returns a promise, which, when resolved, indicates the the JSON serialized version of
+ // the field has been returned from the server
+ const getSerializedField = Utils.EmitCallback(_socket, MessageStore.GetRefField, id);
+
+ // when the serialized RefField has been received, go head and begin deserializing it into an object.
+ // Here, once deserialized, we also invoke .proto to 'load' the document's prototype, which ensures that all
+ // future .proto calls on the Doc won't have to go farther than the cache to get their actual value.
+ const deserializeField = getSerializedField.then(async fieldJson => {
+ // deserialize
const field = SerializationHelper.Deserialize(fieldJson);
+ // either way, overwrite or delete any promises cached at this id (that we inserted as flags
+ // to indicate that the field was in the process of being fetched). Now everything
+ // should be an actual value within or entirely absent from the cache.
if (field !== undefined) {
await field.proto;
_cache[id] = field;
@@ -38,13 +146,25 @@ 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 getYoutubeChannels() {
@@ -57,36 +177,83 @@ export namespace DocServer {
}
- 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 {
@@ -94,70 +261,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 fd4532807..49ce8760f 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -18,7 +18,6 @@ import { action } from "mobx";
import { ColumnAttributeModel } from "../northstar/core/attribute/AttributeModel";
import { AttributeTransformationModel } from "../northstar/core/attribute/AttributeTransformationModel";
import { AggregateFunction } from "../northstar/model/idea/idea";
-import { Template } from "../views/Templates";
import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss";
import { IconBox } from "../views/nodes/IconBox";
import { Field, Doc, Opt } from "../../new_fields/Doc";
@@ -26,24 +25,45 @@ import { OmitKeys } from "../../Utils";
import { ImageField, VideoField, AudioField, PdfField, WebField, YoutubeField } 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 { StrokeData, InkField } from "../../new_fields/InkField";
import { dropActionType } from "../util/DragManager";
import { DateField } from "../../new_fields/DateField";
import { UndoManager } from "../util/UndoManager";
import { RouteStore } from "../../server/RouteStore";
-import { createInstance } from "@react-pdf/renderer";
import { YoutubeBox } from "../apis/youtube/YoutubeBox";
-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",
+ YOUTUBE = "youtube"
+}
+
export interface DocumentOptions {
x?: number;
y?: number;
- ink?: InkField;
+ type?: string;
width?: number;
height?: number;
nativeWidth?: number;
@@ -53,7 +73,6 @@ export interface DocumentOptions {
panY?: number;
page?: number;
scale?: number;
- baseLayout?: string;
layout?: string;
templates?: List<string>;
viewType?: number;
@@ -62,348 +81,450 @@ 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) {
- 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 linkDoc = new Doc;
- linkDoc.proto!.title = "-link name-";
- linkDoc.proto!.linkDescription = "";
- linkDoc.proto!.linkTags = "Default";
-
- linkDoc.proto!.linkedTo = target;
- linkDoc.proto!.linkedToPage = target.curPage;
- linkDoc.proto!.linkedFrom = source;
- linkDoc.proto!.linkedFromPage = source.curPage;
-
- let linkedFrom = Cast(protoTarg.linkedFromDocs, listSpec(Doc));
- if (!linkedFrom) {
- protoTarg.linkedFromDocs = linkedFrom = new List<Doc>();
- }
- linkedFrom.push(linkDoc);
+class EmptyBox {
+ public static LayoutString() {
+ return "";
+ }
+}
+
+export namespace Docs {
+
+ export namespace Prototypes {
+
+ 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";
+
+ 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: {}
+ }],
+ [DocumentType.YOUTUBE, {
+ layout: { view: YoutubeBox }
+ }],
- let linkedTo = Cast(protoSrc.linkedToDocs, listSpec(Doc));
- if (!linkedTo) {
- protoSrc.linkedToDocs = linkedTo = new List<Doc>();
+ ]);
+
+ // All document prototypes are initialized with at least these values
+ const defaultOptions: DocumentOptions = { x: 0, y: 0, width: 300 };
+ const suffix = "Proto";
+
+ /**
+ * 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);
+ });
+ }
+
+ /**
+ * 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)!;
+ }
+
+ export function MainLinkDocument() {
+ return Prototypes.get(DocumentType.LINKDOC);
+ }
+
+ /**
+ * 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;
}
- linkedTo.push(linkDoc);
- return linkDoc;
- }, "make link");
+ 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 });
+ }
+
}
+ /**
+ * 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"];
-export namespace Docs {
- let textProto: Doc;
- let histoProto: Doc;
- let imageProto: Doc;
- let webProto: Doc;
- let collProto: Doc;
- let kvpProto: Doc;
- let videoProto: Doc;
- let audioProto: Doc;
- let pdfProto: Doc;
- let iconProto: Doc;
- let youtubeProto: Doc;
- const textProtoId = "textProto";
- const histoProtoId = "histoProto";
- const pdfProtoId = "pdfProto";
- const imageProtoId = "imageProto";
- const webProtoId = "webProto";
- const collProtoId = "collectionProto";
- const kvpProtoId = "kvpProto";
- const videoProtoId = "videoProto";
- const audioProtoId = "audioProto";
- const iconProtoId = "iconProto";
- const youtubeProtoId = "youtubeProto";
-
- export function initProtos(): Promise<void> {
- return DocServer.GetRefFields([textProtoId, histoProtoId, collProtoId, imageProtoId, webProtoId, kvpProtoId, videoProtoId, audioProtoId, pdfProtoId, iconProtoId, youtubeProtoId]).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();
- youtubeProto = fields[youtubeProtoId] as Doc || CreateYoutubePrototype();
- });
- }
+ /**
+ * 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);
- function setupPrototypeOptions(protoId: string, title: string, layout: string, options: DocumentOptions): Doc {
- return Doc.assign(new Doc(protoId, true), { ...options, title: title, layout: layout, baseLayout: 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);
- }
+ if (!("author" in protoProps)) {
+ protoProps.author = CurrentUserUtils.email;
+ }
- 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;
- }
+ if (!("creationDate" in protoProps)) {
+ protoProps.creationDate = new DateField;
+ }
- 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, height: 150, backgroundColor: "#f1efeb" });
- return textProto;
- }
- function CreatePdfPrototype(): Doc {
- let pdfProto = setupPrototypeOptions(pdfProtoId, "PDF_PROTO", CollectionPDFView.LayoutString("annotations"),
- { x: 0, y: 0, nativeWidth: 1200, width: 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 CreateYoutubePrototype(): Doc {
- let webProto = setupPrototypeOptions(youtubeProtoId, "YOUTUBE_PROTO", YoutubeBox.LayoutString(),
- { x: 0, y: 0, width: 300, height: 300 });
- return webProto;
- }
+ protoProps.isPrototype = true;
+ let dataDoc = MakeDataDelegate(proto, protoProps, data);
+ let viewDoc = Doc.MakeDelegate(dataDoc, delegId);
- function CreateCollectionPrototype(): Doc {
- let collProto = setupPrototypeOptions(collProtoId, "COLLECTION_PROTO", CollectionView.LayoutString("data"),
- { panX: 0, panY: 0, scale: 1, width: 500, height: 500 });
- return collProto;
- }
+ return Doc.assign(viewDoc, delegateProps);
+ }
- 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;
- }
+ /**
+ * 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);
+ }
- 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;
+ export function ImageDocument(url: string, options: DocumentOptions = {}) {
+ let inst = InstanceFromProto(Prototypes.get(DocumentType.IMG), new ImageField(new URL(url)), { title: path.basename(url), ...options });
+ requestImageSize(window.origin + RouteStore.corsProxy + "/" + url)
+ .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.proto!.height = NumCast(inst.proto!.width) * aspect;
+ })
+ .catch((err: any) => console.log(err));
+ return inst;
}
- if (!("creationDate" in protoProps)) {
- protoProps.creationDate = new DateField;
+
+ export function VideoDocument(url: string, options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.VID), new VideoField(new URL(url)), options);
}
- protoProps.isPrototype = true;
- return SetDelegateOptions(SetInstanceOptions(proto, protoProps, data), delegateProps, delegId);
- }
+ export function YoutubeDocument(url: string, options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.YOUTUBE), new YoutubeField(new URL(url)), options);
+ }
- 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.proto!.height = NumCast(inst.proto!.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 YoutubeDocument(url: string, options: DocumentOptions = {}) {
- return CreateInstance(youtubeProto, new YoutubeField(new URL(url)), options);
- }
+ export function AudioDocument(url: string, options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(new URL(url)), options);
+ }
- 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);
- }
+ export function HistogramDocument(histoOp: HistogramOperation, options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.HIST), new HistogramField(histoOp), options);
+ }
- export function HistogramDocument(histoOp: HistogramOperation, options: DocumentOptions = {}) {
- return CreateInstance(histoProto, new HistogramField(histoOp), options);
- }
- export function TextDocument(options: DocumentOptions = {}) {
- return CreateInstance(textProto, "", options);
- }
- export function IconDocument(icon: string, options: DocumentOptions = {}) {
- return CreateInstance(iconProto, new IconField(icon), options);
- }
- export function PdfDocument(url: string, options: DocumentOptions = {}) {
- return CreateInstance(pdfProto, new PdfField(new URL(url)), options);
- }
+ 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 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;
+ 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;
}
- 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! }));
+ 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 schemaDoc;
+ ]
+ };
+ return DockDocument(configs.map(c => c.doc), JSON.stringify(layoutConfig), options, id);
}
- 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 namespace Get {
+
+ 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;
}
- 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 namespace DocUtils {
- // 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 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;
- 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>
- `);
- }
+ 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;
- this template requires an additional style setting on the collectionView-cont to make the layout relative
-
-.collectionView-cont {
- position: relative;
- width: 100%;
- height: 100%;
-}
- */
- 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>
- `);
+ 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..28c009645 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
*
diff --git a/src/client/northstar/dash-nodes/HistogramBox.tsx b/src/client/northstar/dash-nodes/HistogramBox.tsx
index d7732ee86..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 ?
@@ -125,8 +125,7 @@ export class HistogramBox extends React.Component<FieldViewProps> {
let mapped = brushingDocs.map((brush, i) => {
brush.backgroundColor = StyleConstants.BRUSH_COLORS[i % StyleConstants.BRUSH_COLORS.length];
let brushed = DocListCast(brush.brushingDocs);
- if (!brushed.length)
- return null;
+ if (!brushed.length) return null;
return { l: brush, b: brushed[0][Id] === proto[Id] ? brushed[1] : brushed[0] };
});
runInAction(() => this.HistoOp.BrushLinks.splice(0, this.HistoOp.BrushLinks.length, ...mapped.filter(m => m) as { l: Doc, b: Doc }[]));
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 a613625cb..bb1345044 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,7 +1,7 @@
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 { CollectionDockingView } from '../views/collections/CollectionDockingView';
@@ -9,6 +9,8 @@ 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,14 +32,37 @@ 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;
let passes = preferredCollection ? [preferredCollection, undefined] : [undefined];
- for (let i = 0; i < passes.length; i++) {
+ for (let pass of passes) {
DocumentManager.Instance.DocumentViews.map(view => {
- if (view.props.Document[Id] === id && (!passes[i] || view.props.ContainingCollectionView === preferredCollection)) {
+ if (view.props.Document[Id] === id && (!pass || view.props.ContainingCollectionView === preferredCollection)) {
toReturn = view;
return;
}
@@ -45,7 +70,7 @@ export class DocumentManager {
if (!toReturn) {
DocumentManager.Instance.DocumentViews.map(view => {
let doc = view.props.Document.proto;
- if (doc && doc[Id] === id && (!passes[i] || view.props.ContainingCollectionView === preferredCollection)) {
+ if (doc && doc[Id] === id && (!pass || view.props.ContainingCollectionView === preferredCollection)) {
toReturn = view;
}
});
@@ -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, willZoom: boolean, forceDockFunc: boolean = false, dockFunc?: (doc: Doc) => void, linkPage?: number): 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) {
@@ -123,6 +136,7 @@ export class DocumentManager {
const curPage = NumCast(contextDoc.curPage, page);
if (page !== curPage) contextDoc.curPage = page;
}
+
let docView: DocumentView | null;
// using forceDockFunc as a flag for splitting linked to doc to the right...can change later if needed
if (!forceDockFunc && (docView = DocumentManager.Instance.getDocumentView(doc))) {
@@ -131,18 +145,34 @@ export class DocumentManager {
docView.props.focus(docView.props.Document, willZoom);
} else {
if (!contextDoc) {
- const actualDoc = Doc.MakeAlias(docDelegate);
- actualDoc.libraryBrush = true;
- if (linkPage !== undefined) actualDoc.curPage = linkPage;
- (dockFunc || CollectionDockingView.Instance.AddRightSplit)(actualDoc);
+ if (docContext) {
+ let targetContextView: DocumentView | null;
+ if (!forceDockFunc && docContext && (targetContextView = DocumentManager.Instance.getDocumentView(docContext))) {
+ docContext.panTransformType = "Ease";
+ targetContextView.props.focus(docDelegate, willZoom);
+ } else {
+ (dockFunc || CollectionDockingView.Instance.AddRightSplit)(docContext, undefined);
+ setTimeout(() => {
+ 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, undefined);
+ }
} else {
let contextView: DocumentView | null;
docDelegate.libraryBrush = true;
if (!forceDockFunc && (contextView = DocumentManager.Instance.getDocumentView(contextDoc))) {
contextDoc.panTransformType = "Ease";
- contextView.props.focus(contextDoc, willZoom);
+ contextView.props.focus(docDelegate, willZoom);
} else {
- (dockFunc || CollectionDockingView.Instance.AddRightSplit)(contextDoc);
+ (dockFunc || CollectionDockingView.Instance.AddRightSplit)(contextDoc, undefined);
+ setTimeout(() => {
+ this.jumpToDocument(docDelegate, willZoom, forceDockFunc, dockFunc, linkPage);
+ }, 10);
}
}
}
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 1e84a0db0..323908302 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -1,62 +1,109 @@
import { action, runInAction } from "mobx";
-import { Doc, DocListCastAsync } from "../../new_fields/Doc";
+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 { 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) {
+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),
},
@@ -65,6 +112,7 @@ export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: n
}
}
+
export namespace DragManager {
export function Root() {
const root = document.getElementById("root");
@@ -86,6 +134,10 @@ export namespace DragManager {
handlers: DragHandlers;
hideSource: boolean | (() => boolean);
+
+ dragHasStarted?: () => void;
+
+ withoutShiftDrag?: boolean;
}
export interface DragDropDisposer {
@@ -137,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;
@@ -153,17 +207,63 @@ export namespace DragManager {
[id: string]: any;
}
+ export class AnnotationDragData {
+ constructor(dragDoc: Doc, annotationDoc: Doc, dropDoc: Doc) {
+ this.dragDocument = dragDoc;
+ this.dropDocument = dropDoc;
+ this.annotationDocument = annotationDoc;
+ this.xOffset = this.yOffset = 0;
+ }
+ dragDocument: Doc;
+ annotationDocument: Doc;
+ dropDocument: Doc;
+ xOffset: number;
+ yOffset: number;
+ dropAction: dropActionType;
+ userDropAction: dropActionType;
+ }
+
export let StartDragFunctions: (() => void)[] = [];
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) {
+ StartDrag(eles, dragData, downX, downY, options);
}
export class LinkDragData {
@@ -178,27 +278,44 @@ export namespace DragManager {
[id: string]: any;
}
+ export class EmbedDragData {
+ constructor(embeddableSourceDoc: Doc) {
+ this.embeddableSourceDoc = embeddableSourceDoc;
+ this.urlField = embeddableSourceDoc.data instanceof URLField ? embeddableSourceDoc.data : undefined;
+ }
+ embeddableSourceDoc: Doc;
+ urlField?: URLField;
+ [id: string]: any;
+ }
+
export function StartLinkDrag(ele: HTMLElement, dragData: LinkDragData, downX: number, downY: number, options?: DragOptions) {
StartDrag([ele], dragData, downX, downY, options);
}
+ export function StartEmbedDrag(ele: HTMLElement, dragData: EmbedDragData, downX: number, downY: number, options?: DragOptions) {
+ StartDrag([ele], dragData, downX, downY, options);
+ }
+
export let AbortDrag: () => void = emptyFunction;
function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: { [id: string]: any }) => void) {
+ eles = eles.filter(e => e);
if (!dragDiv) {
dragDiv = document.createElement("div");
dragDiv.className = "dragManager-dragDiv";
dragDiv.style.pointerEvents = "none";
DragManager.Root().appendChild(dragDiv);
}
-
+ SelectionManager.SetIsDragging(true);
let scaleXs: number[] = [];
let scaleYs: number[] = [];
let xs: number[] = [];
let ys: number[] = [];
const docs: Doc[] =
- dragData instanceof DocumentDragData ? dragData.draggedDocuments : [];
+ 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;
@@ -213,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";
@@ -241,6 +360,16 @@ export namespace DragManager {
// pdfBox.replaceChild(img, pdfBox.children[0])
// }
// }
+ let set = dragElement.getElementsByTagName('*');
+ if (dragElement.hasAttribute("style")) (dragElement as any).style.pointerEvents = "none";
+ // tslint:disable-next-line: prefer-for-of
+ for (let i = 0; i < set.length; i++) {
+ if (set[i].hasAttribute("style")) {
+ let s = set[i];
+ (s as any).style.pointerEvents = "none";
+ }
+ }
+
dragDiv.appendChild(dragElement);
return dragElement;
@@ -259,19 +388,18 @@ export namespace DragManager {
let lastX = downX;
let lastY = downY;
const moveHandler = (e: PointerEvent) => {
- e.stopPropagation();
- e.preventDefault();
+ e.preventDefault(); // required or dragging text menu link item ends up dragging the link button as native drag/drop
if (dragData instanceof DocumentDragData) {
dragData.userDropAction = e.ctrlKey || e.altKey ? "alias" : undefined;
}
- 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;
@@ -284,6 +412,7 @@ export namespace DragManager {
};
let hideDragElements = () => {
+ SelectionManager.SetIsDragging(false);
dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement));
eles.map(ele => (ele.hidden = false));
};
@@ -309,7 +438,7 @@ export namespace DragManager {
}
function dispatchDrag(dragEles: HTMLElement[], e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions, finishDrag?: (dragData: { [index: string]: any }) => void) {
- let removed = dragEles.map(dragEle => {
+ let removed = dragData.dontHideOnDrop ? [] : dragEles.map(dragEle => {
// let parent = dragEle.parentElement;
// if (parent) parent.removeChild(dragEle);
let ret = [dragEle, dragEle.style.width, dragEle.style.height];
@@ -335,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..cbf5b3fc8 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 = DocServer.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..c096e9ceb
--- /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(DocServer.prepend(RouteStore.upload), {
+ method: 'POST',
+ body: formData
+ }).then(async (res: Response) => {
+ (await res.json()).map(action((file: any) => {
+ let docPromise = Docs.Get.DocumentFromType(type, DocServer.prepend(file), { nativeWidth: 300, width: 300, title: dropFileName });
+ 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/ProsemirrorCopy/prompt.js b/src/client/util/ProsemirrorCopy/prompt.js
new file mode 100644
index 000000000..b9068195f
--- /dev/null
+++ b/src/client/util/ProsemirrorCopy/prompt.js
@@ -0,0 +1,179 @@
+const prefix = "ProseMirror-prompt"
+
+export function openPrompt(options) {
+ let wrapper = document.body.appendChild(document.createElement("div"))
+ wrapper.className = prefix
+ wrapper.style.zIndex = 1000;
+ wrapper.style.width = 250;
+ wrapper.style.textAlign = "center";
+
+ let mouseOutside = e => { if (!wrapper.contains(e.target)) close() }
+ setTimeout(() => window.addEventListener("mousedown", mouseOutside), 50)
+ let close = () => {
+ window.removeEventListener("mousedown", mouseOutside)
+ if (wrapper.parentNode) wrapper.parentNode.removeChild(wrapper)
+ }
+
+ let domFields = []
+ for (let name in options.fields) domFields.push(options.fields[name].render())
+
+ let submitButton = document.createElement("button")
+ submitButton.type = "submit"
+ submitButton.className = prefix + "-submit"
+ submitButton.textContent = "OK"
+ let cancelButton = document.createElement("button")
+ cancelButton.type = "button"
+ cancelButton.className = prefix + "-cancel"
+ cancelButton.textContent = "Cancel"
+ cancelButton.addEventListener("click", close)
+
+ let form = wrapper.appendChild(document.createElement("form"))
+ let title = document.createElement("h5")
+ title.style.marginBottom = 15
+ title.style.marginTop = 10
+ if (options.title) form.appendChild(title).textContent = options.title
+ domFields.forEach(field => {
+ form.appendChild(document.createElement("div")).appendChild(field)
+ })
+ let b = document.createElement("div");
+ b.style.marginTop = 15;
+ let buttons = form.appendChild(b)
+ // buttons.className = prefix + "-buttons"
+ buttons.appendChild(submitButton)
+ buttons.appendChild(document.createTextNode(" "))
+ buttons.appendChild(cancelButton)
+
+ let box = wrapper.getBoundingClientRect()
+ wrapper.style.top = options.flyout_top + "px"
+ wrapper.style.left = options.flyout_left + "px"
+
+ let submit = () => {
+ let params = getValues(options.fields, domFields)
+ if (params) {
+ close()
+ options.callback(params)
+ }
+ }
+
+ form.addEventListener("submit", e => {
+ e.preventDefault()
+ submit()
+ })
+
+ form.addEventListener("keydown", e => {
+ if (e.keyCode == 27) {
+ e.preventDefault()
+ close()
+ } else if (e.keyCode == 13 && !(e.ctrlKey || e.metaKey || e.shiftKey)) {
+ e.preventDefault()
+ submit()
+ } else if (e.keyCode == 9) {
+ window.setTimeout(() => {
+ if (!wrapper.contains(document.activeElement)) close()
+ }, 500)
+ }
+ })
+
+ let input = form.elements[0]
+ if (input) input.focus()
+}
+
+function getValues(fields, domFields) {
+ let result = Object.create(null), i = 0
+ for (let name in fields) {
+ let field = fields[name], dom = domFields[i++]
+ let value = field.read(dom), bad = field.validate(value)
+ if (bad) {
+ reportInvalid(dom, bad)
+ return null
+ }
+ result[name] = field.clean(value)
+ }
+ return result
+}
+
+function reportInvalid(dom, message) {
+ // FIXME this is awful and needs a lot more work
+ let parent = dom.parentNode
+ let msg = parent.appendChild(document.createElement("div"))
+ msg.style.left = (dom.offsetLeft + dom.offsetWidth + 2) + "px"
+ msg.style.top = (dom.offsetTop - 5) + "px"
+ msg.className = "ProseMirror-invalid"
+ msg.textContent = message
+ setTimeout(() => parent.removeChild(msg), 1500)
+}
+
+// ::- The type of field that `FieldPrompt` expects to be passed to it.
+export class Field {
+ // :: (Object)
+ // Create a field with the given options. Options support by all
+ // field types are:
+ //
+ // **`value`**`: ?any`
+ // : The starting value for the field.
+ //
+ // **`label`**`: string`
+ // : The label for the field.
+ //
+ // **`required`**`: ?bool`
+ // : Whether the field is required.
+ //
+ // **`validate`**`: ?(any) → ?string`
+ // : A function to validate the given value. Should return an
+ // error message if it is not valid.
+ constructor(options) { this.options = options }
+
+ // render:: (state: EditorState, props: Object) → dom.Node
+ // Render the field to the DOM. Should be implemented by all subclasses.
+
+ // :: (dom.Node) → any
+ // Read the field's value from its DOM node.
+ read(dom) { return dom.value }
+
+ // :: (any) → ?string
+ // A field-type-specific validation function.
+ validateType(_value) { }
+
+ validate(value) {
+ if (!value && this.options.required)
+ return "Required field"
+ return this.validateType(value) || (this.options.validate && this.options.validate(value))
+ }
+
+ clean(value) {
+ return this.options.clean ? this.options.clean(value) : value
+ }
+}
+
+// ::- A field class for single-line text fields.
+export class TextField extends Field {
+ render() {
+ let input = document.createElement("input")
+ input.type = "text"
+ input.placeholder = this.options.label
+ input.value = this.options.value || ""
+ input.autocomplete = "off"
+ input.style.marginBottom = 4
+ input.style.border = "1px solid black"
+ input.style.padding = "4px 4px"
+ return input
+ }
+}
+
+
+// ::- A field class for dropdown fields based on a plain `<select>`
+// tag. Expects an option `options`, which should be an array of
+// `{value: string, label: string}` objects, or a function taking a
+// `ProseMirror` instance and returning such an array.
+export class SelectField extends Field {
+ render() {
+ let select = document.createElement("select")
+ this.options.options.forEach(o => {
+ let opt = select.appendChild(document.createElement("option"))
+ opt.value = o.value
+ opt.selected = o.value == this.options.value
+ opt.label = o.label
+ })
+ return select
+ }
+}
diff --git a/src/client/util/ProsemirrorKeymap.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index 00d086b97..fa9e2e5af 100644
--- a/src/client/util/ProsemirrorKeymap.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -1,4 +1,4 @@
-import { Schema } from "prosemirror-model";
+import { Schema, NodeType } from "prosemirror-model";
import {
wrapIn, setBlockType, chainCommands, toggleMark, exitCode,
joinUp, joinDown, lift, selectParentNode
@@ -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;
@@ -50,7 +52,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
}
if (type = schema.nodes.bullet_list) {
- bind("Ctrl-b", wrapInList(type));
+ bind("Ctrl-.", wrapInList(type));
}
if (type = schema.nodes.ordered_list) {
bind("Ctrl-n", wrapInList(type));
@@ -96,5 +98,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
});
}
+ bind("Mod-s", TooltipTextMenu.insertStar);
+
return keys;
-} \ No newline at end of file
+}
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 3e3e98206..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 } from "prosemirror-model";
-import { joinUp, lift, setBlockType, toggleMark, wrapIn } from 'prosemirror-commands';
+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, } from "prosemirror-state";
+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];
@@ -26,6 +27,14 @@ export const nodes: { [index: string]: NodeSpec } = {
toDOM() { return pDOM; }
},
+ // starmine: {
+ // inline: true,
+ // attrs: { oldtext: { default: "" } },
+ // group: "inline",
+ // toDOM() { return ["star", "㊉"]; },
+ // parseDOM: [{ tag: "star" }]
+ // },
+
// :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks.
blockquote: {
content: "block+",
@@ -77,6 +86,30 @@ export const nodes: { [index: string]: NodeSpec } = {
group: "inline"
},
+ star: {
+ inline: true,
+ attrs: {
+ visibility: { default: false },
+ text: { default: undefined },
+ textslice: { default: undefined },
+ textlen: { default: 0 }
+
+ },
+ group: "inline",
+ toDOM(node) {
+ const attrs = { style: `width: 40px` };
+ return ["span", { ...node.attrs, ...attrs }];
+ },
+ // parseDOM: [{
+ // tag: "star", getAttrs(dom: any) {
+ // return {
+ // visibility: dom.getAttribute("visibility"),
+ // oldtext: dom.getAttribute("oldtext"),
+ // oldtextlen: dom.getAttribute("oldtextlen"),
+ // }
+ // }
+ // }]
+ },
// :: NodeSpec An inline image (`<img>`) node. Supports `src`,
// `alt`, and `href` attributes. The latter two default to the empty
// string.
@@ -107,6 +140,32 @@ export const nodes: { [index: string]: NodeSpec } = {
}
},
+ video: {
+ inline: true,
+ attrs: {
+ src: {},
+ width: { default: "100px" },
+ alt: { default: null },
+ title: { default: null }
+ },
+ group: "inline",
+ draggable: true,
+ parseDOM: [{
+ tag: "video[src]", getAttrs(dom: any) {
+ return {
+ src: dom.getAttribute("src"),
+ title: dom.getAttribute("title"),
+ alt: dom.getAttribute("alt"),
+ width: Math.min(100, Number(dom.getAttribute("width"))),
+ };
+ }
+ }],
+ toDOM(node) {
+ const attrs = { style: `width: ${node.attrs.width}` };
+ return ["video", { ...node.attrs, ...attrs }];
+ }
+ },
+
// :: NodeSpec A hard line break, represented in the DOM as `<br>`.
hard_break: {
inline: true,
@@ -156,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]; }
@@ -222,6 +282,15 @@ export const marks: { [index: string]: MarkSpec } = {
toDOM: () => ['sup']
},
+ highlight: {
+ parseDOM: [{ style: 'background: #d9dbdd' }],
+ toDOM() {
+ return ['span', {
+ style: 'color: blue'
+ }];
+ }
+ },
+
// :: MarkSpec Code font mark. Represented as a `<code>` element.
code: {
@@ -280,7 +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;' }],
@@ -310,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', {
@@ -407,6 +512,86 @@ export class ImageResizeView {
this._handle.style.display = "none";
}
}
+
+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 = node.attrs.visibility ? "㊀" : "㊉";
+ this._collapsed.style.opacity = "0.5";
+ 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) {
+ if (node.attrs.visibility) {
+ // node.attrs.visibility = !node.attrs.visibility;
+ let y = getPos();
+ 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;
+ let y = getPos();
+ 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)));
+ 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();
+ };
+ (this as any).dom = this._collapsed;
+
+ }
+ 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() {
+ }
+}
// :: Schema
// This schema rougly corresponds to the document schema used by
// [CommonMark](http://commonmark.org/), minus the list elements,
@@ -415,4 +600,14 @@ export class ImageResizeView {
//
// To reuse elements from this schema, extend or read from its
// `spec.nodes` and `spec.marks` [properties](#model.Schema.spec).
-export const schema = new Schema({ nodes, marks }); \ No newline at end of file
+export const schema = new Schema({ nodes, marks });
+
+const fromJson = schema.nodeFromJSON;
+
+schema.nodeFromJSON = (json: any) => {
+ let node = fromJson(json);
+ if (json.type === "star") {
+ 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 beaf5cb03..62c2cfe85 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -7,11 +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';
export interface ScriptSucccess {
success: true;
@@ -39,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];
- 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 => {
@@ -63,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 };
}
};
@@ -132,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 {
@@ -167,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..806746496 100644
--- a/src/client/util/SearchUtil.ts
+++ b/src/client/util/SearchUtil.ts
@@ -4,23 +4,65 @@ import { Doc } from '../../new_fields/Doc';
import { Id } from '../../new_fields/FieldSymbols';
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 interface IdSearchResult {
+ ids: string[];
+ numFound: number;
+ }
+
+ export interface DocSearchResult {
+ docs: Doc[];
+ numFound: number;
+ }
+
+ export function Search(query: string, filterQuery: string | undefined, returnDocs: true, start?: number, count?: number): Promise<DocSearchResult>;
+ export function Search(query: string, filterQuery: string | undefined, returnDocs: false, start?: number, count?: number): Promise<IdSearchResult>;
+ export async function Search(query: string, filterQuery: string | undefined, returnDocs: boolean, start?: number, rows?: number) {
+ query = query || "*"; //If we just have a filter query, search for * as the query
+ const result: IdSearchResult = JSON.parse(await rp.get(DocServer.prepend("/search"), {
+ qs: { query, filterQuery, start, rows },
}));
if (!returnDocs) {
- return ids;
+ return result;
}
+ const { ids, numFound } = 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 };
}
- 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("", `proto_i:"${protoId}"`, returnDocs)).docs;
+ } else {
+ return (await Search("", `proto_i:"${protoId}"`, returnDocs)).ids;
+ }
// return Search(`{!join from=id to=proto_i}id:${protoId}`, true);
}
+
+ export async function GetViewsOfDocument(doc: Doc): Promise<Doc[]> {
+ const results = await Search("", `proto_i:"${doc[Id]}"`, true);
+ return results.docs;
+ }
+
+ export async function GetContextsOfDocument(doc: Doc): Promise<{ contexts: Doc[], aliasContexts: Doc[] }> {
+ const docContexts = (await Search("", `data_l:"${doc[Id]}"`, true)).docs;
+ const aliases = await GetAliasesOfDocument(doc, false);
+ const aliasContexts = (await Promise.all(aliases.map(doc => Search("", `data_l:"${doc}"`, true))));
+ const contexts = { contexts: docContexts, aliasContexts: [] as Doc[] };
+ aliasContexts.forEach(result => contexts.aliasContexts.push(...result.docs));
+ return contexts;
+ }
+
+ export async function GetContextIdsOfDocument(doc: Doc): Promise<{ contexts: string[], aliasContexts: string[] }> {
+ const docContexts = (await Search("", `data_l:"${doc[Id]}"`, false)).ids;
+ const aliases = await GetAliasesOfDocument(doc, false);
+ const aliasContexts = (await Promise.all(aliases.map(doc => Search("", `data_l:"${doc}"`, false))));
+ 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 8c92c2023..9efef888d 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -1,49 +1,65 @@
-import { observable, action } 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
- SelectedDocuments: Array<DocumentView> = [];
+
+ @observable IsDragging: boolean = false;
+ @observable SelectedDocuments: Array<DocumentView> = [];
+
@action
- SelectDoc(doc: DocumentView, ctrlPressed: boolean): void {
+ SelectDoc(docView: DocumentView, ctrlPressed: boolean): void {
// if doc is not in SelectedDocuments, add it
- if (!ctrlPressed) {
- this.DeselectAll();
- }
+ if (manager.SelectedDocuments.indexOf(docView) === -1) {
+ if (!ctrlPressed) {
+ this.DeselectAll();
+ }
- if (manager.SelectedDocuments.indexOf(doc) === -1) {
- manager.SelectedDocuments.push(doc);
- doc.props.whenActiveChanged(true);
+ manager.SelectedDocuments.push(docView);
+ // console.log(manager.SelectedDocuments);
+ docView.props.whenActiveChanged(true);
+ }
+ }
+ @action
+ DeselectDoc(docView: DocumentView): void {
+ let ind = manager.SelectedDocuments.indexOf(docView);
+ if (ind !== -1) {
+ manager.SelectedDocuments.splice(ind, 1);
+ docView.props.whenActiveChanged(false);
}
}
-
@action
DeselectAll(): void {
manager.SelectedDocuments.map(dv => dv.props.whenActiveChanged(false));
manager.SelectedDocuments = [];
FormattedTextBox.InputBoxOverlay = undefined;
}
- @action
- ReselectAll() {
- let sdocs = manager.SelectedDocuments.map(d => d);
- manager.SelectedDocuments = [];
- return sdocs;
- }
- @action
- ReselectAll2(sdocs: DocumentView[]) {
- sdocs.map(s => SelectionManager.SelectDoc(s, true));
- }
}
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 SelectDoc(doc: DocumentView, ctrlPressed: boolean): void {
- manager.SelectDoc(doc, ctrlPressed);
+ export function DeselectDoc(docView: DocumentView): void {
+ manager.DeselectDoc(docView);
+ }
+ export function SelectDoc(docView: DocumentView, ctrlPressed: boolean): void {
+ manager.SelectDoc(docView, ctrlPressed);
}
export function IsSelected(doc: DocumentView): boolean {
@@ -62,14 +78,13 @@ export namespace SelectionManager {
if (found) manager.SelectDoc(found, false);
}
- export function ReselectAll() {
- let sdocs = manager.ReselectAll();
- setTimeout(() => manager.ReselectAll2(sdocs), 0);
- }
+ export function SetIsDragging(dragging: boolean) { runInAction(() => manager.IsDragging = dragging); }
+ export function GetIsDragging() { return manager.IsDragging; }
+
export function SelectedDocuments(): Array<DocumentView> {
- return manager.SelectedDocuments;
+ return manager.SelectedDocuments.slice();
}
- export function ViewsSortedVertically(): DocumentView[] {
+ export function ViewsSortedHorizontally(): DocumentView[] {
let sorted = SelectionManager.SelectedDocuments().slice().sort((doc1, doc2) => {
if (NumCast(doc1.props.Document.x) > NumCast(doc2.props.Document.x)) return 1;
if (NumCast(doc1.props.Document.x) < NumCast(doc2.props.Document.x)) return -1;
@@ -77,7 +92,7 @@ export namespace SelectionManager {
});
return sorted;
}
- export function ViewsSortedHorizontally(): DocumentView[] {
+ export function ViewsSortedVertically(): DocumentView[] {
let sorted = SelectionManager.SelectedDocuments().slice().sort((doc1, doc2) => {
if (NumCast(doc1.props.Document.y) > NumCast(doc2.props.Document.y)) return 1;
if (NumCast(doc1.props.Document.y) < NumCast(doc2.props.Document.y)) return -1;
diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts
index 7ded85e43..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,20 +39,29 @@ 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)) {
throw Error(`type '${obj.__type}' not registered. Make sure you register it using a @Deserializable decorator`);
}
- const value = deserialize(serializationTypes[obj.__type], obj);
+ const type = serializationTypes[obj.__type];
+ const value = deserialize(type.ctor, obj);
+ if (type.afterDeserialize) {
+ type.afterDeserialize(value);
+ }
serializing -= 1;
return value;
}
}
-let serializationTypes: { [name: string]: any } = {};
+let serializationTypes: { [name: string]: { ctor: { new(): any }, afterDeserialize?: (obj: any) => void } } = {};
let reverseMap: { [ctor: string]: string } = {};
export interface DeserializableOpts {
@@ -59,9 +69,9 @@ export interface DeserializableOpts {
withFields(fields: string[]): Function;
}
-export function Deserializable(name: string): DeserializableOpts;
+export function Deserializable(name: string, afterDeserialize?: (obj: any) => void): DeserializableOpts;
export function Deserializable(constructor: { new(...args: any[]): any }): void;
-export function Deserializable(constructor: { new(...args: any[]): any } | string): DeserializableOpts | void {
+export function Deserializable(constructor: { new(...args: any[]): any } | string, afterDeserialize?: (obj: any) => void): DeserializableOpts | void {
function addToMap(name: string, ctor: { new(...args: any[]): any }) {
const schema = getDefaultModelSchema(ctor) as any;
if (schema.targetClass !== ctor) {
@@ -69,7 +79,7 @@ export function Deserializable(constructor: { new(...args: any[]): any } | strin
setDefaultModelSchema(ctor, newSchema);
}
if (!(name in serializationTypes)) {
- serializationTypes[name] = ctor;
+ serializationTypes[name] = { ctor, afterDeserialize };
reverseMap[ctor.name] = name;
} else {
throw new Error(`Name ${name} has already been registered as deserializable`);
diff --git a/src/client/util/TooltipTextMenu.scss b/src/client/util/TooltipTextMenu.scss
index 437da0d63..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 {
@@ -36,7 +37,7 @@
position: relative;
padding-right: 15px;
margin: 3px;
- background: #333333;
+ background: white;
border-radius: 3px;
text-align: center;
}
@@ -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,8 +67,10 @@
}
.ProseMirror-menu-dropdown-menu {
- z-index: 15;
+ z-index: 100000;
min-width: 6em;
+ background: white;
+ position: absolute;
}
.linking {
@@ -79,10 +81,11 @@
cursor: pointer;
padding: 2px 8px 2px 4px;
width: auto;
+ z-index: 100000;
}
.ProseMirror-menu-dropdown-item:hover {
- background: #2e2b2b;
+ background: white;
}
.ProseMirror-menu-submenu-wrap {
@@ -132,7 +135,7 @@
position: relative;
min-height: 1em;
color: white;
- padding: 1px 6px;
+ padding: 10px 10px;
top: 0; left: 0; right: 0;
border-bottom: 1px solid silver;
background:$dark-color;
@@ -155,7 +158,7 @@
}
.ProseMirror-icon svg {
- fill: currentColor;
+ fill:white;
height: 1em;
}
@@ -184,7 +187,7 @@
position: fixed;
border-radius: 3px;
z-index: 11;
- box-shadow: -.5px 2px 5px rgba(0, 0, 0, .2);
+ box-shadow: -.5px 2px 5px white(255, 255, 255, 0.2);
}
.ProseMirror-prompt h5 {
@@ -196,7 +199,7 @@
.ProseMirror-prompt input[type="text"],
.ProseMirror-prompt textarea {
- background: #eee;
+ background: white;
border: none;
outline: none;
}
@@ -232,16 +235,20 @@
}
.tooltipMenu {
- position: absolute;
- z-index: 200;
- background: $dark-color;
+ position: relative;
+ z-index: 2000;
+ background: #121721;
border: 1px solid silver;
- border-radius: 4px;
- padding: 2px 10px;
- margin-bottom: 7px;
- -webkit-transform: translateX(-50%);
- transform: translateX(-50%);
+ border-radius: 15px;
+ //height: 60px;
+ //padding: 2px 10px;
+ //margin-top: 100px;
+ //-webkit-transform: translateX(-50%);
+ //transform: translateX(-50%);
+ transform: translateY(-85px);
pointer-events: all;
+ height: 30px;
+ width:550px;
.ProseMirror-example-setup-style hr {
padding: 2px 10px;
border: none;
@@ -257,34 +264,34 @@
}
}
-.tooltipMenu:before {
- content: "";
- height: 0; width: 0;
- position: absolute;
- left: 50%;
- margin-left: -5px;
- bottom: -6px;
- border: 5px solid transparent;
- border-bottom-width: 0;
- border-top-color: silver;
- }
- .tooltipMenu:after {
- content: "";
- height: 0; width: 0;
- position: absolute;
- left: 50%;
- margin-left: -5px;
- bottom: -4.5px;
- border: 5px solid transparent;
- border-bottom-width: 0;
- border-top-color: $dark-color;
- }
+// .tooltipMenu:before {
+// content: "";
+// height: 0; width: 0;
+// position: absolute;
+// left: 50%;
+// margin-left: -5px;
+// bottom: -6px;
+// border: 5px solid transparent;
+// border-bottom-width: 0;
+// border-top-color: silver;
+// }
+// .tooltipMenu:after {
+// content: "";
+// height: 0; width: 0;
+// position: absolute;
+// left: 50%;
+// margin-left: -5px;
+// bottom: -4.5px;
+// border: 5px solid transparent;
+// border-bottom-width: 0;
+// border-top-color: $dark-color;
+// }
.menuicon {
display: inline-block;
- border-right: 1px solid rgba(0, 0, 0, 0.2);
+ border-right: 1px solid white(0, 0, 0, 0.2);
//color: rgb(19, 18, 18);
- color: white;
+ color: rgb(226, 21, 21);
line-height: 1;
padding: 0px 2px;
margin: 1px;
@@ -302,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 34785446b..f4579fc51 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -1,51 +1,38 @@
-import { action, IReactionDisposer, reaction } from "mobx";
-import { Dropdown, DropdownSubmenu, MenuItem, MenuItemSpec, renderGrouped, icons, } from "prosemirror-menu"; //no import css
-import { baseKeymap, lift } 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,
-} 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";
-import { View } from "@react-pdf/renderer";
+const { openPrompt, TextField } = require("./ProsemirrorCopy/prompt.js");
import { DragManager } from "./DragManager";
import { Doc, Opt, Field } from "../../new_fields/Doc";
-import { Utils } from "../northstar/utils/Utils";
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";
-
-const SVG = "http://www.w3.org/2000/svg";
+import { FormattedTextBoxProps } from "../views/nodes/FormattedTextBox";
//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 linkEditor?: HTMLDivElement;
private linkText?: HTMLDivElement;
private linkDrag?: HTMLImageElement;
@@ -54,26 +41,35 @@ 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);
//add the buttons to the tooltip
let items = [
- { command: toggleMark(schema.marks.strong), dom: this.icon("B", "strong") },
- { command: toggleMark(schema.marks.em), dom: this.icon("i", "em") },
- { command: toggleMark(schema.marks.underline), dom: this.icon("U", "underline") },
- { command: toggleMark(schema.marks.strikethrough), dom: this.icon("S", "strikethrough") },
- { command: toggleMark(schema.marks.superscript), dom: this.icon("s", "superscript") },
- { command: toggleMark(schema.marks.subscript), dom: this.icon("s", "subscript") },
+ { command: toggleMark(schema.marks.strong), dom: this.icon("B", "strong", "Bold") },
+ { command: toggleMark(schema.marks.em), dom: this.icon("i", "em", "Italic") },
+ { command: toggleMark(schema.marks.underline), dom: this.icon("U", "underline", "Underline") },
+ { 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: 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") },
@@ -86,10 +82,14 @@ export class TooltipTextMenu {
dom.addEventListener("pointerdown", e => {
e.preventDefault();
view.focus();
- command(view.state, view.dispatch, view);
+ if (dom.contains(e.target as Node)) {
+ e.stopPropagation();
+ command(view.state, view.dispatch, view);
+ }
});
});
+ this.updateLinkMenu();
//list of font styles
this.fontStylesToName = new Map();
@@ -108,10 +108,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
@@ -120,7 +124,21 @@ export class TooltipTextMenu {
this.listTypeToIcon.set(schema.nodes.ordered_list, "1)");
this.listTypes = Array.from(this.listTypeToIcon.keys());
+ // this.tooltip.appendChild(this.createLink().render(this.view).dom);
+
+ this.tooltip.appendChild(this.createStar().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
@@ -131,15 +149,18 @@ export class TooltipTextMenu {
//font SIZES
let fontSizeBtns: MenuItem[] = [];
this.fontSizeToNum.forEach((number, mark) => {
- fontSizeBtns.push(this.dropdownMarkBtn(String(number), "width: 50px;", mark, this.view, this.changeToMarkInGroup, this.fontSizes));
+ 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:white; min-width: 60px; padding-left: 5px; margin-right: 0;"
+ 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
@@ -150,22 +171,26 @@ export class TooltipTextMenu {
//font STYLES
let fontBtns: MenuItem[] = [];
this.fontStylesToName.forEach((name, mark) => {
- fontBtns.push(this.dropdownMarkBtn(name, "font-family: " + name + ", sans-serif; width: 125px;", mark, this.view, this.changeToMarkInGroup, this.fontStyles));
+ 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:white; width: 125px; margin-left: -3px; padding-left: 2px;"
+ 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.style.color = "white";
+ this.linkEditor.className = "ProseMirror-icon menuicon";
+ this.linkEditor.style.color = "black";
this.linkText = document.createElement("div");
this.linkText.style.cssFloat = "left";
this.linkText.style.marginRight = "5px";
@@ -178,8 +203,8 @@ export class TooltipTextMenu {
this.linkText.onpointerdown = (e: PointerEvent) => { e.stopPropagation(); };
let linkBtn = document.createElement("div");
linkBtn.textContent = ">>";
- linkBtn.style.width = "20px";
- linkBtn.style.height = "20px";
+ linkBtn.style.width = "10px";
+ linkBtn.style.height = "10px";
linkBtn.style.color = "white";
linkBtn.style.cssFloat = "left";
linkBtn.onpointerdown = (e: PointerEvent) => {
@@ -194,7 +219,7 @@ export class TooltipTextMenu {
if (DocumentManager.Instance.getDocumentView(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);
}
}));
}
@@ -205,28 +230,32 @@ 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.color = "white";
+ 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;
+ 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;
+ linkDoc instanceof Doc && this.makeLink(DocServer.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);
}
@@ -236,39 +265,89 @@ 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");
}
+ 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.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); }
//Make a dropdown of all list types
let toAdd: MenuItem[] = [];
this.listTypeToIcon.forEach((icon, type) => {
- toAdd.push(this.dropdownNodeBtn(icon, "width: 40px;", type, this.view, this.listTypes, this.changeToNodeType));
+ toAdd.push(this.dropdownNodeBtn(icon, "color: black; width: 40px;", type, this.view, this.listTypes, this.changeToNodeType));
});
//option to remove the list formatting
- toAdd.push(this.dropdownNodeBtn("X", "width: 40px;", undefined, this.view, this.listTypes, this.changeToNodeType));
+ toAdd.push(this.dropdownNodeBtn("X", "color: black; width: 40px;", undefined, this.view, this.listTypes, this.changeToNodeType));
listTypeBtn = (new Dropdown(toAdd, {
label: label,
- css: "color:white; width: 40px;"
+ css: "color:black; width: 40px;"
}) as MenuItem).render(this.view).dom;
//add this new button and return it
@@ -277,8 +356,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;
@@ -290,25 +369,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
@@ -325,13 +413,114 @@ 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({
+ title: "Add or remove link",
+ label: "Add or remove link",
+ execEvent: "",
+ icon: icons.link,
+ css: "color:white;",
+ class: "menuicon",
+ enable(state) { return !state.selection.empty; },
+ run: (state, dispatch, view) => {
+ // to remove link
+ let curLink = "";
+ if (this.markActive(state, markType)) {
+
+ 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({
+ value: curLink,
+ label: "Link Target",
+ required: true
+ }),
+ title: new TextField({ label: "Title" })
+ },
+ callback(attrs: any) {
+ toggleMark(markType, attrs)(view.state, view.dispatch);
+ view.focus();
+ },
+ flyout_top: 0,
+ flyout_left: 0
+ });
+ }
+ });
+ }
+
//makes a button for the drop down FOR NODE TYPES
//css is the style you want applied to the button
dropdownNodeBtn(label: string, css: string, nodeType: NodeType | undefined, view: EditorView, groupNodes: NodeType[], changeToNodeInGroup: (nodeType: NodeType<any> | undefined, view: EditorView, groupNodes: NodeType[]) => any) {
@@ -341,18 +530,24 @@ export class TooltipTextMenu {
execEvent: "",
class: "menuicon",
css: css,
- enable(state) { return true; },
+ enable() { return true; },
run() {
changeToNodeInGroup(nodeType, view, groupNodes);
}
});
}
+ markActive = function (state: EditorState<any>, type: MarkType<Schema<string, string>>) {
+ let { from, $from, to, empty } = state.selection;
+ if (empty) return type.isInSet(state.storedMarks || $from.marks());
+ else return state.doc.rangeHasMark(from, to, type);
+ };
+
// Helper function to create menu icons
- icon(text: string, name: string) {
+ icon(text: string, name: string, title: string = name) {
let span = document.createElement("span");
span.className = name + " menuicon";
- span.title = name;
+ span.title = title;
span.textContent = text;
span.style.color = "white";
return span;
@@ -395,6 +590,20 @@ export class TooltipTextMenu {
};
}
+ getMarksInSelection(state: EditorState<any>, targets: MarkType<any>[]) {
+ let found: Mark<any>[] = [];
+ let { from, to } = state.selection as TextSelection;
+ state.doc.nodesBetween(from, to, (node) => {
+ let marks = node.marks;
+ if (marks) {
+ marks.forEach(m => {
+ if (targets.includes(m.type)) found.push(m);
+ });
+ }
+ });
+ return found;
+ }
+
//updates the tooltip menu when the selection changes
update(view: EditorView, lastState: EditorState | undefined) {
let state = view.state;
@@ -404,76 +613,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;
}
- // 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";
//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 557f6f574..1f95af00c 100644
--- a/src/client/util/type_decls.d
+++ b/src/client/util/type_decls.d
@@ -141,17 +141,12 @@ declare abstract class RefField {
readonly [Id]: FieldId;
constructor();
- // protected [HandleUpdate]?(diff: any): void;
-
- // abstract [ToScriptString](): string;
}
-declare abstract class ObjectField {
- protected [OnUpdate](diff?: any): void;
- private [Parent]?: RefField | ObjectField;
- // abstract [Copy](): ObjectField;
+declare type FieldId = string;
- // abstract [ToScriptString](): string;
+declare abstract class ObjectField {
+ abstract [Copy](): ObjectField;
}
declare abstract class URLField extends ObjectField {
@@ -161,32 +156,53 @@ declare abstract class URLField extends ObjectField {
constructor(url: URL);
}
-declare class AudioField extends URLField { }
-declare class VideoField extends URLField { }
-declare class ImageField extends URLField { }
-declare class WebField extends URLField { }
-declare class PdfField extends URLField { }
+declare class AudioField extends URLField { [Copy](): ObjectField; }
+declare class VideoField extends URLField { [Copy](): ObjectField; }
+declare class ImageField extends URLField { [Copy](): ObjectField; }
+declare class WebField extends URLField { [Copy](): ObjectField; }
+declare class PdfField extends URLField { [Copy](): ObjectField; }
-declare type FieldId = string;
+declare const ComputedField: any;
+declare const CompileScript: any;
+// @ts-ignore
+declare type Extract<T, U> = T extends U ? T : never;
declare type Field = number | string | boolean | ObjectField | RefField;
+declare type FieldWaiting<T extends RefField = RefField> = T extends undefined ? never : Promise<T | undefined>;
+declare type FieldResult<T extends Field = Field> = Opt<T> | FieldWaiting<Extract<T, RefField>>;
declare type Opt<T> = T | undefined;
declare class Doc extends RefField {
constructor();
- [key: string]: Field | undefined;
+ [key: string]: FieldResult;
// [ToScriptString](): string;
}
declare class ListImpl<T extends Field> extends ObjectField {
constructor(fields?: T[]);
[index: number]: T | (T extends RefField ? Promise<T> : never);
- // [ToScriptString](): string;
- // [Copy](): ObjectField;
+ [Copy](): ObjectField;
}
// @ts-ignore
declare const console: any;
-declare const Documents: any;
+interface DocumentOptions { }
+
+declare const Docs: {
+ ImageDocument(url: string, options?: DocumentOptions): Doc;
+ VideoDocument(url: string, options?: DocumentOptions): Doc;
+ // HistogramDocument(url:string, options?:DocumentOptions);
+ TextDocument(options?: DocumentOptions): Doc;
+ PdfDocument(url: string, options?: DocumentOptions): Doc;
+ WebDocument(url: string, options?: DocumentOptions): Doc;
+ HtmlDocument(html: string, options?: DocumentOptions): Doc;
+ KVPDocument(document: Doc, options?: DocumentOptions): Doc;
+ FreeformDocument(documents: Doc[], options?: DocumentOptions): Doc;
+ SchemaDocument(columns: string[], documents: Doc[], options?: DocumentOptions): Doc;
+ 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.scss b/src/client/views/ContextMenu.scss
index 7e066d53a..254163b53 100644
--- a/src/client/views/ContextMenu.scss
+++ b/src/client/views/ContextMenu.scss
@@ -8,19 +8,19 @@
flex-direction: column;
}
-.contextMenu-item:first-child {
- background: $intermediate-color;
- color: $light-color;
-}
+// .contextMenu-item:first-child {
+// background: $intermediate-color;
+// color: $light-color;
+// }
-.contextMenu-item:first-child::placeholder {
- color: $light-color;
-}
+// .contextMenu-item:first-child::placeholder {
+// color: $light-color;
+// }
-.contextMenu-item:first-child:hover {
- background: $intermediate-color;
- color: $light-color;
-}
+// .contextMenu-item:first-child:hover {
+// background: $intermediate-color;
+// color: $light-color;
+// }
.contextMenu-subMenu-cont {
position: absolute;
@@ -53,6 +53,33 @@
font-size: 20px;
}
+.contextMenu-itemSelected {
+ background: rgb(136, 136, 136)
+}
+
+.contextMenu-group {
+ // width: 11vw; //10vw
+ height: 30px; //2vh
+ background: rgb(200, 200, 200);
+ display: flex; //comment out to allow search icon to be inline with search text
+ justify-content: left;
+ align-items: center;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ transition: all .1s;
+ border-width: .11px;
+ border-style: none;
+ border-color: $intermediate-color; // rgb(187, 186, 186);
+ border-bottom-style: solid;
+ // padding: 10px 0px 10px 0px;
+ white-space: nowrap;
+ font-size: 20px;
+}
+
.contextMenu-item:hover {
transition: all 0.1s;
background: $lighter-alt-accent;
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index da374455e..c163c56a0 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -1,11 +1,12 @@
import React = require("react");
-import { ContextMenuItem, ContextMenuProps } from "./ContextMenuItem";
-import { observable, action } from "mobx";
-import { observer } from "mobx-react"
-import "./ContextMenu.scss"
+import { ContextMenuItem, ContextMenuProps, OriginalMenuProps } from "./ContextMenuItem";
+import { observable, action, computed } from "mobx";
+import { observer } from "mobx-react";
+import "./ContextMenu.scss";
import { library } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSearch, faCircle } from '@fortawesome/free-solid-svg-icons';
+import Measure from "react-measure";
library.add(faSearch);
library.add(faCircle);
@@ -14,29 +15,27 @@ library.add(faCircle);
export class ContextMenu extends React.Component {
static Instance: ContextMenu;
- @observable private _items: Array<ContextMenuProps> = [{ description: "test", event: (e: React.MouseEvent) => e.preventDefault(), icon: "smile" }];
+ @observable private _items: Array<ContextMenuProps> = [];
@observable private _pageX: number = 0;
@observable private _pageY: number = 0;
- @observable private _display: string = "none";
+ @observable private _display: boolean = false;
@observable private _searchString: string = "";
// afaik displaymenu can be called before all the items are added to the menu, so can't determine in displayMenu what the height of the menu will be
@observable private _yRelativeToTop: boolean = true;
+ @observable selectedIndex = -1;
-
- private ref: React.RefObject<HTMLDivElement>;
+ @observable private _width: number = 0;
+ @observable private _height: number = 0;
constructor(props: Readonly<{}>) {
super(props);
- this.ref = React.createRef();
-
ContextMenu.Instance = this;
}
@action
clearItems() {
this._items = [];
- this._display = "none";
}
@action
@@ -50,63 +49,174 @@ export class ContextMenu extends React.Component {
return this._items;
}
+ static readonly buffer = 20;
+ get pageX() {
+ const x = this._pageX;
+ if (x < 0) {
+ return 0;
+ }
+ const width = this._width;
+ if (x + width > window.innerWidth - ContextMenu.buffer) {
+ return window.innerWidth - ContextMenu.buffer - width;
+ }
+ return x;
+ }
+
+ get pageY() {
+ const y = this._pageY;
+ if (y < 0) {
+ return 0;
+ }
+ const height = this._height;
+ if (y + height > window.innerHeight - ContextMenu.buffer) {
+ return window.innerHeight - ContextMenu.buffer - height;
+ }
+ return y;
+ }
+
@action
displayMenu(x: number, y: number) {
//maxX and maxY will change if the UI/font size changes, but will work for any amount
//of items added to the menu
- let maxX = window.innerWidth - 150;
- let maxY = window.innerHeight - ((this._items.length + 1/*for search box*/) * 34 + 30);
- this._pageX = x > maxX ? maxX : x;
- this._pageY = y > maxY ? maxY : y;
+ this._pageX = x;
+ this._pageY = y;
this._searchString = "";
- this._display = "flex";
+ this._display = true;
}
- intersects = (x: number, y: number): boolean => {
- if (this.ref.current && this._display !== "none") {
- let menuSize = { width: this.ref.current.getBoundingClientRect().width, height: this.ref.current.getBoundingClientRect().height };
-
- let upperLeft = { x: this._pageX, y: this._yRelativeToTop ? this._pageY : window.innerHeight - (this._pageY + menuSize.height) };
- let bottomRight = { x: this._pageX + menuSize.width, y: this._yRelativeToTop ? this._pageY + menuSize.height : window.innerHeight - this._pageY };
+ @action
+ closeMenu = () => {
+ this.clearItems();
+ this._display = false;
+ }
- if (x >= upperLeft.x && x <= bottomRight.x) {
- if (y >= upperLeft.y && y <= bottomRight.y) {
- return true;
+ @computed get filteredItems(): (OriginalMenuProps | string[])[] {
+ const searchString = this._searchString.toLowerCase().split(" ");
+ const matches = (descriptions: string[]): boolean => {
+ return searchString.every(s => descriptions.some(desc => desc.toLowerCase().includes(s)));
+ };
+ const flattenItems = (items: ContextMenuProps[], groupFunc: (groupName: any) => string[]) => {
+ let eles: (OriginalMenuProps | string[])[] = [];
+
+ const leaves: OriginalMenuProps[] = [];
+ for (const item of items) {
+ const description = item.description;
+ const path = groupFunc(description);
+ if ("subitems" in item) {
+ const children = flattenItems(item.subitems, name => [...groupFunc(description), name]);
+ if (children.length || matches(path)) {
+ eles.push(path);
+ eles = eles.concat(children);
+ }
+ } else {
+ if (!matches(path)) {
+ continue;
+ }
+ leaves.push(item);
}
}
- }
- return false;
+
+ eles = [...leaves, ...eles];
+
+ return eles;
+ };
+ return flattenItems(this._items, name => [name]);
}
- @action
- closeMenu = () => {
- this.clearItems();
+ @computed get flatItems(): OriginalMenuProps[] {
+ return this.filteredItems.filter(item => !Array.isArray(item)) as OriginalMenuProps[];
}
- render() {
- let style = this._yRelativeToTop ? { left: this._pageX, top: this._pageY, display: this._display } :
- { left: this._pageX, bottom: this._pageY, display: this._display };
+ @computed get filteredViews() {
+ const createGroupHeader = (contents: any) => {
+ return (
+ <div className="contextMenu-group">
+ <div className="contextMenu-description">{contents}</div>
+ </div>
+ );
+ };
+ const createItem = (item: ContextMenuProps, selected: boolean) => <ContextMenuItem {...item} key={item.description} closeMenu={this.closeMenu} selected={selected} />;
+ let itemIndex = 0;
+ return this.filteredItems.map(value => {
+ if (Array.isArray(value)) {
+ return createGroupHeader(value.join(" -> "));
+ } else {
+ return createItem(value, itemIndex++ === this.selectedIndex);
+ }
+ });
+ }
+ @computed get menuItems() {
+ if (!this._searchString) {
+ return this._items.map(item => <ContextMenuItem {...item} key={item.description} closeMenu={this.closeMenu} />);
+ }
+ return this.filteredViews;
+ }
- return (
- <div className="contextMenu-cont" style={style} ref={this.ref}>
+ render() {
+ if (!this._display) {
+ return null;
+ }
+ let style = this._yRelativeToTop ? { left: this.pageX, top: this.pageY } :
+ { left: this.pageX, bottom: this.pageY };
+
+ const contents = (
+ <>
<span>
<span className="icon-background">
<FontAwesomeIcon icon="search" size="lg" />
</span>
- <input className="contextMenu-item contextMenu-description" type="text" placeholder="Search . . ." value={this._searchString} onChange={this.onChange} />
+ <input className="contextMenu-item contextMenu-description" type="text" placeholder="Search . . ." value={this._searchString} onKeyDown={this.onKeyDown} onChange={this.onChange} autoFocus />
</span>
- {this._items.filter(prop => prop.description.toLowerCase().indexOf(this._searchString.toLowerCase()) !== -1).
- map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this.closeMenu} />)}
- </div>
+ {this.menuItems}
+ </>
);
+ return (
+ <Measure offset onResize={action((r: any) => { this._width = r.offset.width; this._height = r.offset.height; })}>
+ {({ measureRef }) => (
+ <div className="contextMenu-cont" style={style} ref={measureRef}>
+ {contents}
+ </div>
+ )
+ }
+ </Measure>
+ );
+ }
+
+ @action
+ onKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "ArrowDown") {
+ if (this.selectedIndex < this.flatItems.length - 1) {
+ this.selectedIndex++;
+ }
+ e.preventDefault();
+ } else if (e.key === "ArrowUp") {
+ if (this.selectedIndex > 0) {
+ this.selectedIndex--;
+ }
+ e.preventDefault();
+ } else if (e.key === "Enter") {
+ const item = this.flatItems[this.selectedIndex];
+ item.event();
+ this.closeMenu();
+ }
}
@action
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this._searchString = e.target.value;
+ if (!this._searchString) {
+ this.selectedIndex = -1;
+ }
+ else {
+ if (this.selectedIndex === -1) {
+ this.selectedIndex = 0;
+ } else {
+ this.selectedIndex = Math.min(this.flatItems.length - 1, this.selectedIndex);
+ }
+ }
}
} \ No newline at end of file
diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index fcda0db89..9bbb97d7e 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -1,12 +1,15 @@
import React = require("react");
import { observable, action } from "mobx";
import { observer } from "mobx-react";
-import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
+import { faAngleRight } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+library.add(faAngleRight);
+
export interface OriginalMenuProps {
description: string;
- event: (e: React.MouseEvent<HTMLDivElement>) => void;
+ event: () => void;
icon?: IconProp; //maybe should be optional (icon?)
closeMenu?: () => void;
}
@@ -14,16 +17,14 @@ export interface OriginalMenuProps {
export interface SubmenuProps {
description: string;
subitems: ContextMenuProps[];
+ icon?: IconProp; //maybe should be optional (icon?)
closeMenu?: () => void;
}
-export interface ContextMenuItemProps {
- type: ContextMenuProps | SubmenuProps;
-}
export type ContextMenuProps = OriginalMenuProps | SubmenuProps;
@observer
-export class ContextMenuItem extends React.Component<ContextMenuProps> {
+export class ContextMenuItem extends React.Component<ContextMenuProps & { selected?: boolean }> {
@observable private _items: Array<ContextMenuProps> = [];
@observable private overItem = false;
@@ -36,33 +37,64 @@ export class ContextMenuItem extends React.Component<ContextMenuProps> {
handleEvent = (e: React.MouseEvent<HTMLDivElement>) => {
if ("event" in this.props) {
- this.props.event(e);
+ this.props.event();
this.props.closeMenu && this.props.closeMenu();
}
}
+ currentTimeout?: any;
+ static readonly timeout = 300;
+ onPointerEnter = () => {
+ if (this.currentTimeout) {
+ clearTimeout(this.currentTimeout);
+ this.currentTimeout = undefined;
+ }
+ if (this.overItem) {
+ return;
+ }
+ this.currentTimeout = setTimeout(action(() => this.overItem = true), ContextMenuItem.timeout);
+ }
+
+ onPointerLeave = () => {
+ if (this.currentTimeout) {
+ clearTimeout(this.currentTimeout);
+ this.currentTimeout = undefined;
+ }
+ if (!this.overItem) {
+ return;
+ }
+ this.currentTimeout = setTimeout(action(() => this.overItem = false), ContextMenuItem.timeout);
+ }
+
render() {
if ("event" in this.props) {
return (
- <div className="contextMenu-item" onClick={this.handleEvent}>
- <span className="icon-background">
- {this.props.icon ? <FontAwesomeIcon icon={this.props.icon} size="sm" /> : <FontAwesomeIcon icon="circle" size="sm" />}
- </span>
+ <div className={"contextMenu-item" + (this.props.selected ? " contextMenu-itemSelected" : "")} onClick={this.handleEvent}>
+ {this.props.icon ? (
+ <span className="icon-background">
+ <FontAwesomeIcon icon={this.props.icon} size="sm" />
+ </span>
+ ) : null}
<div className="contextMenu-description">
{this.props.description}
</div>
</div>
);
- }
- else {
+ } else if ("subitems" in this.props) {
let submenu = !this.overItem ? (null) :
<div className="contextMenu-subMenu-cont" style={{ marginLeft: "100.5%", left: "0px" }}>
{this._items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} />)}
</div>;
return (
- <div className="contextMenu-item" onMouseEnter={action(() => { this.overItem = true; })} onMouseLeave={action(() => this.overItem = false)}>
+ <div className={"contextMenu-item" + (this.props.selected ? " contextMenu-itemSelected" : "")} onMouseEnter={this.onPointerEnter} onMouseLeave={this.onPointerLeave}>
+ {this.props.icon ? (
+ <span className="icon-background">
+ <FontAwesomeIcon icon={this.props.icon} size="sm" />
+ </span>
+ ) : null}
<div className="contextMenu-description">
{this.props.description}
+ <FontAwesomeIcon icon={faAngleRight} size="lg" style={{ position: "absolute", right: "5px" }} />
</div>
{submenu}
</div>
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 da9b1253e..398974cb6 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,35 +1,39 @@
-import { action, computed, observable, runInAction, untracked, reaction } from "mobx";
+import { library } from '@fortawesome/fontawesome-svg-core';
+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 { 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';
import { DocumentView, PositionDocument } from "./nodes/DocumentView";
+import { FieldView } from "./nodes/FieldView";
+import { FormattedTextBox } from "./nodes/FormattedTextBox";
+import { IconBox } from "./nodes/IconBox";
import { LinkMenu } from "./nodes/LinkMenu";
import { TemplateMenu } from "./TemplateMenu";
-import React = require("react");
import { Template, Templates } from "./Templates";
-import { CompileScript } from "../util/Scripting";
-import { IconBox } from "./nodes/IconBox";
-import { Cast, FieldValue, NumCast, StrCast } from "../../new_fields/Types";
-import { Doc, FieldResult } from "../../new_fields/Doc";
-import { listSpec } from "../../new_fields/Schema";
-import { Docs } from "../documents/Documents";
-import { List } from "../../new_fields/List";
+import React = require("react");
+import { RichTextField } from '../../new_fields/RichTextField';
+import { LinkManager } from '../util/LinkManager';
+import { ObjectField } from '../../new_fields/ObjectField';
+import { MetadataEntryMenu } from './MetadataEntryMenu';
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
-import { faLink } from '@fortawesome/free-solid-svg-icons';
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss";
-import { CollectionView } from "./collections/CollectionView";
-import { DocumentManager } from "../util/DocumentManager";
-import { FormattedTextBox } from "./nodes/FormattedTextBox";
-import { FieldView } from "./nodes/FieldView";
library.add(faLink);
+library.add(faTag);
@observer
export class DocumentDecorations extends React.Component<{}, { value: string }> {
@@ -42,9 +46,13 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
private _titleHeight = 20;
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 = "";
@@ -72,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) {
@@ -121,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();
@@ -140,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
@@ -148,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;
@@ -274,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;
@@ -309,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();
@@ -316,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);
@@ -330,12 +376,27 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
document.removeEventListener("pointerup", this.onLinkerButtonUp);
document.addEventListener("pointerup", this.onLinkerButtonUp);
}
+
+ onEmbedButtonDown = (e: React.PointerEvent): void => {
+ e.stopPropagation();
+ document.removeEventListener("pointermove", this.onEmbedButtonMoved);
+ document.addEventListener("pointermove", this.onEmbedButtonMoved);
+ document.removeEventListener("pointerup", this.onEmbedButtonUp);
+ document.addEventListener("pointerup", this.onEmbedButtonUp);
+ }
+
onLinkerButtonUp = (e: PointerEvent): void => {
document.removeEventListener("pointermove", this.onLinkerButtonMoved);
document.removeEventListener("pointerup", this.onLinkerButtonUp);
e.stopPropagation();
}
+ onEmbedButtonUp = (e: PointerEvent): void => {
+ document.removeEventListener("pointermove", this.onEmbedButtonMoved);
+ document.removeEventListener("pointerup", this.onEmbedButtonUp);
+ e.stopPropagation();
+ }
+
@action
onLinkerButtonMoved = (e: PointerEvent): void => {
if (this._linkerButton.current !== null) {
@@ -355,6 +416,25 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
e.stopPropagation();
}
+ @action
+ onEmbedButtonMoved = (e: PointerEvent): void => {
+ if (this._embedButton.current !== null) {
+ document.removeEventListener("pointermove", this.onEmbedButtonMoved);
+ document.removeEventListener("pointerup", this.onEmbedButtonUp);
+
+ let dragDocView = SelectionManager.SelectedDocuments()[0];
+ let dragData = new DragManager.EmbedDragData(dragDocView.props.Document);
+
+ DragManager.StartEmbedDrag(dragDocView.ContentDiv!, dragData, e.x, e.y, {
+ handlers: {
+ dragComplete: action(emptyFunction),
+ },
+ hideSource: false
+ });
+ }
+ e.stopPropagation();
+ }
+
onLinkButtonDown = (e: React.PointerEvent): void => {
e.stopPropagation();
document.removeEventListener("pointermove", this.onLinkButtonMoved);
@@ -373,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();
@@ -429,31 +508,47 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
runInAction(() => FormattedTextBox.InputBoxOverlay = undefined);
SelectionManager.SelectedDocuments().forEach(element => {
- const rect = element.ContentDiv ? element.ContentDiv.getBoundingClientRect() : new DOMRect();
-
- if (rect.width !== 0 && (dX != 0 || dY != 0 || dW != 0 || dH != 0)) {
+ if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) {
let doc = PositionDocument(element.props.Document);
- let docHeightBefore = doc.height;
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 = width / rect.width;
+ 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);
doc.y = (doc.y || 0) + dY * (actualdH - height);
+ let proto = Doc.GetProto(element.props.Document);
+ let fixedAspect = e.ctrlKey || (!BoolCast(proto.ignoreAspect, false) && nwidth && nheight);
+ if (fixedAspect && (!nwidth || !nheight)) {
+ proto.nativeWidth = nwidth = doc.width || 0;
+ proto.nativeHeight = nheight = doc.height || 0;
+ proto.ignoreAspect = true;
+ }
if (nwidth > 0 && nheight > 0) {
if (Math.abs(dW) > Math.abs(dH)) {
- 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 = actualdH;
+ Doc.SetInPlace(element.props.Document, "nativeHeight", (doc.height || 0) / doc.width * (doc.nativeWidth || 0), true);
}
else {
- 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 = actualdW;
+ Doc.SetInPlace(element.props.Document, "nativeWidth", (doc.width || 0) / doc.height * (doc.nativeHeight || 0), true);
}
} else {
- doc.width = zoomBasis * actualdW;
- if (docHeightBefore === doc.height) doc.height = zoomBasis * actualdH;
+ dW && (doc.width = actualdW);
+ dH && (doc.height = actualdH);
+ Doc.SetInPlace(element.props.Document, "autoHeight", undefined, true);
}
}
});
@@ -464,10 +559,10 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
e.stopPropagation();
this._resizing = "";
this.Interacting = false;
- SelectionManager.ReselectAll();
if (e.button === 0) {
e.preventDefault();
this._isPointerDown = false;
+ this._resizeUndo && this._resizeUndo.end();
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
}
@@ -496,6 +591,60 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
// e.stopPropagation();
// }
+ considerEmbed = () => {
+ let thisDoc = SelectionManager.SelectedDocuments()[0].props.Document;
+ let canEmbed = thisDoc.data && thisDoc.data instanceof URLField;
+ if (!canEmbed) return (null);
+ return (
+ <div className="linkButtonWrapper">
+ <div title="Drag Embed" className="linkButton-linker" ref={this._embedButton} onPointerDown={this.onEmbedButtonDown}>
+ <FontAwesomeIcon className="documentdecorations-icon" icon="image" size="sm" />
+ </div>
+ </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>
+ );
+ }
+
render() {
var bounds = this.Bounds;
let seldoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined;
@@ -510,9 +659,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}
@@ -523,11 +670,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
let templates: Map<Template, boolean> = new Map();
Array.from(Object.values(Templates.TemplateList)).map(template => {
- let sorted = SelectionManager.ViewsSortedVertically().slice().sort((doc1, doc2) => {
- if (NumCast(doc1.props.Document.x) > NumCast(doc2.props.Document.x)) return 1;
- if (NumCast(doc1.props.Document.x) < NumCast(doc2.props.Document.x)) return -1;
- return 0;
- });
+ let sorted = SelectionManager.ViewsSortedVertically(); // slice().sort((doc1, doc2) => {
+ // if (NumCast(doc1.props.Document.y) > NumCast(doc2.props.Document.x)) return 1;
+ // if (NumCast(doc1.props.Document.x) < NumCast(doc2.props.Document.x)) return -1;
+ // return 0;
+ // });
let docTemps = sorted.reduce((res: string[], doc: DocumentView, i) => {
let temps = doc.props.Document.templates;
if (temps instanceof List) {
@@ -548,6 +695,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",
@@ -555,7 +713,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={{
@@ -570,7 +728,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>
@@ -580,16 +738,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 c946d68e1..989fb1be9 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -14,17 +14,23 @@ 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;
+ OnTab?(): void;
+
/**
* The contents to render when not editing
*/
contents: any;
+ fontStyle?: string;
+ fontSize?: number;
height?: number;
display?: string;
oneLine?: boolean;
+ editing?: boolean;
+ onClick?: (e: React.MouseEvent) => boolean;
}
/**
@@ -34,40 +40,59 @@ export interface EditableProps {
*/
@observer
export class EditableView extends React.Component<EditableProps> {
- @observable
- editing: boolean = false;
+ @observable _editing: boolean = false;
+
+ constructor(props: EditableProps) {
+ super(props);
+ this._editing = this.props.editing ? true : false;
+ }
@action
onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
- if (e.key === "Enter") {
+ if (e.key === "Tab") {
+ this.props.OnTab && this.props.OnTab();
+ } else if (e.key === "Enter") {
if (!e.ctrlKey) {
- if (this.props.SetValue(e.currentTarget.value)) {
- this.editing = false;
+ if (this.props.SetValue(e.currentTarget.value, e.shiftKey)) {
+ this._editing = false;
}
} else if (this.props.OnFillDown) {
this.props.OnFillDown(e.currentTarget.value);
- this.editing = false;
+ this._editing = false;
}
} else if (e.key === "Escape") {
- this.editing = false;
+ this._editing = false;
}
}
@action
onClick = (e: React.MouseEvent) => {
- this.editing = true;
+ if (!this.props.onClick || !this.props.onClick(e)) {
+ this._editing = true;
+ }
+ e.stopPropagation();
+ }
+
+ stopPropagation(e: React.SyntheticEvent) {
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)}
- style={{ display: this.props.display }} />;
+ 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, fontSize: this.props.fontSize }} />;
} else {
return (
- <div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`} style={{ display: this.props.display, height: "auto", maxHeight: `${this.props.height}` }}
+ <div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`}
+ style={{ display: this.props.display, height: "auto", maxHeight: `${this.props.height}` }}
onClick={this.onClick} >
- <span>{this.props.contents}</span>
+ <span style={{ fontStyle: this.props.fontStyle }}>{this.props.contents}</span>
</div>
);
}
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 42ab08001..37a6bbab7 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.SetOnPrototype(this.props.Document, "ink", new InkField(value));
+ this.props.AnnotationDocument[this.props.inkFieldKey] = new InkField(value);
}
@action
@@ -74,7 +76,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
e.stopPropagation();
e.preventDefault();
- this.previousState = this.inkData;
+ this.previousState = new Map(this.inkData);
if (InkingControl.Instance.selectedTool !== InkTool.Eraser) {
// start the new line, saves a uuid to represent the field of the stroke
@@ -106,10 +108,10 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
const batch = UndoManager.StartBatch("One ink stroke");
const oldState = this.previousState || new Map;
this.previousState = undefined;
- const newState = this.inkData;
+ const newState = new Map(this.inkData);
UndoManager.AddEvent({
undo: () => this.inkData = oldState,
- redo: () => this.inkData = newState,
+ redo: () => this.inkData = newState
});
batch.end();
}
@@ -134,9 +136,13 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
return { x, y };
}
- @undoBatch
@action
removeLine = (id: string): void => {
+ if (!this.previousState) {
+ this.previousState = new Map(this.inkData);
+ document.addEventListener("pointermove", this.onPointerMove, true);
+ document.addEventListener("pointerup", this.onPointerUp, true);
+ }
let data = this.inkData;
data.delete(id);
this.inkData = data;
diff --git a/src/client/views/InkingControl.scss b/src/client/views/InkingControl.scss
index ba4ec41af..465e14d07 100644
--- a/src/client/views/InkingControl.scss
+++ b/src/client/views/InkingControl.scss
@@ -1,8 +1,6 @@
@import "globalCssVariables";
.inking-control {
- position: absolute;
- left: 70px;
- bottom: 70px;
+ bottom: 20px;
margin: 0;
padding: 0;
display: flex;
@@ -63,10 +61,9 @@
margin-top: 4px;
}
.ink-panel {
- margin: 6px 12px 6px 0;
- height: 30px;
+ height: 24px;
vertical-align: middle;
- line-height: 36px;
+ line-height: 28px;
padding: 0 10px;
color: $intermediate-color;
&:first {
@@ -114,7 +111,6 @@
border-radius: 11px;
width: 22px;
height: 22px;
- margin-top: 6px;
cursor: pointer;
text-align: center; // span {
// color: $light-color;
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index d456f531f..c7f7bdb66 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -1,15 +1,17 @@
-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";
import { library } from '@fortawesome/fontawesome-svg-core';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPen, faHighlighter, faEraser, faBan } from '@fortawesome/free-solid-svg-icons';
import { SelectionManager } from "../util/SelectionManager";
import { InkTool } from "../../new_fields/InkField";
import { Doc } from "../../new_fields/Doc";
+import { undoBatch, UndoManager } from "../util/UndoManager";
+import { StrCast } from "../../new_fields/Types";
+import { FormattedTextBox } from "./nodes/FormattedTextBox";
+import { MainOverlayTextBox } from "./MainOverlayTextBox";
library.add(faPen, faHighlighter, faEraser, faBan);
@@ -19,8 +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 private _colorPickerDisplay = false;
+ @observable public _open: boolean = false;
constructor(props: Readonly<{}>) {
super(props);
@@ -31,13 +32,37 @@ export class InkingControl extends React.Component {
switchTool = (tool: InkTool): void => {
this._selectedTool = tool;
}
+ decimalToHexString(number: number) {
+ if (number < 0) {
+ number = 0xFFFFFFFF + number + 1;
+ }
- @action
- switchColor = (color: ColorResult): void => {
- this._selectedColor = color.hex;
- SelectionManager.SelectedDocuments().forEach(doc => Doc.GetProto(doc.props.Document).backgroundColor = color.hex);
+ return number.toString(16).toUpperCase();
}
+ @undoBatch
+ switchColor = action((color: ColorResult): void => {
+ this._selectedColor = color.hex + (color.rgb.a !== undefined ? this.decimalToHexString(Math.round(color.rgb.a * 255)) : "ff");
+ if (InkingControl.Instance.selectedTool === InkTool.None) {
+ if (MainOverlayTextBox.Instance.SetColor(color.hex)) return;
+ let selected = SelectionManager.SelectedDocuments();
+ let oldColors = selected.map(view => {
+ let targetDoc = view.props.Document.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 => {
this._selectedWidth = width;
@@ -53,50 +78,24 @@ export class InkingControl extends React.Component {
return this._selectedColor;
}
+ @action
+ updateSelectedColor(value: string) {
+ this._selectedColor = value;
+ }
+
@computed
get selectedWidth() {
return this._selectedWidth;
}
- selected = (tool: InkTool) => {
- if (this._selectedTool === tool) {
- return { color: "#61aaa3" };
- }
- return {};
- }
-
@action
toggleDisplay = () => {
this._open = !this._open;
+ this.switchTool(this._open ? InkTool.Pen : InkTool.None);
}
-
-
- @action
- toggleColorPicker = () => {
- this._colorPickerDisplay = !this._colorPickerDisplay;
- }
-
render() {
return (
<ul className="inking-control" style={this._open ? { display: "flex" } : { display: "none" }}>
- <li className="ink-tools ink-panel">
- <div className="ink-tool-buttons">
- <button onClick={() => this.switchTool(InkTool.Pen)} style={this.selected(InkTool.Pen)}><FontAwesomeIcon icon="pen" size="lg" title="Pen" /></button>
- <button onClick={() => this.switchTool(InkTool.Highlighter)} style={this.selected(InkTool.Highlighter)}><FontAwesomeIcon icon="highlighter" size="lg" title="Highlighter" /></button>
- <button onClick={() => this.switchTool(InkTool.Eraser)} style={this.selected(InkTool.Eraser)}><FontAwesomeIcon icon="eraser" size="lg" title="Eraser" /></button>
- <button onClick={() => this.switchTool(InkTool.None)} style={this.selected(InkTool.None)}><FontAwesomeIcon icon="ban" size="lg" title="Pointer" /></button>
- </div>
- </li>
- <li className="ink-color ink-panel">
- <label>COLOR: </label>
- <div className="ink-color-display" style={{ backgroundColor: this._selectedColor }}
- onClick={() => this.toggleColorPicker()}>
- {/* {this._colorPickerDisplay ? <span>&#9660;</span> : <span>&#9650;</span>} */}
- </div>
- <div className="ink-color-picker" style={this._colorPickerDisplay ? { display: "block" } : { display: "none" }}>
- <CirclePicker onChange={this.switchColor} circleSize={22} width={"220"} />
- </div>
- </li>
<li className="ink-size ink-panel">
<label htmlFor="stroke-width">SIZE: </label>
<input type="text" min="1" max="100" value={this._selectedWidth} name="stroke-width"
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 37b1f5899..b8d428d31 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -28,6 +28,8 @@ export class InkingStroke extends React.Component<StrokeProps> {
deleteStroke = (e: React.PointerEvent): void => {
if (InkingControl.Instance.selectedTool === InkTool.Eraser && e.buttons === 1) {
this.props.deleteCallback(this.props.id);
+ e.stopPropagation();
+ e.preventDefault();
}
}
@@ -50,7 +52,7 @@ export class InkingStroke extends React.Component<StrokeProps> {
render() {
let pathStyle = this.createStyle();
let pathData = this.parseData(this.props.line);
- let pathlength = this.props.count; // bcz: this is needed to force reactions to the line data changes
+ let pathlength = this.props.count; // bcz: this is needed to force reactions to the line's data changes
let marker = this.props.tool === InkTool.Highlighter ? "-marker" : "";
let pointerEvents: any = InkingControl.Instance.selectedTool === InkTool.Eraser ? "all" : "none";
diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss
index 57a53c999..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,22 +111,42 @@ button:hover {
//toolbar stuff
#toolbar {
position: absolute;
- bottom: 62px;
- left: 24px;
+ right: 8px;
+ top: 5px;
.toolbar-button {
display: block;
margin-bottom: 10px;
}
}
+.toolbar-color-picker {
+ background-color: $light-color;
+ border-radius: 5px;
+ padding: 12px;
+ position: absolute;
+ bottom: 36px;
+ left: -3px;
+ box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw;
+}
+.toolbar-color-button {
+ border-radius: 11px;
+ width: 22px;
+ height: 22px;
+ cursor: pointer;
+ text-align: center; // span {
+ // color: $light-color;
+ // font-size: 8px;
+ // user-select: none;
+ // }
+}
// add nodes menu. Note that the + button is actually an input label, not an actual button.
#add-nodes-menu {
position: absolute;
bottom: 22px;
- left: 24px;
+ left: 250px;
- label {
+ > label {
background: $dark-color;
color: $light-color;
display: inline-block;
@@ -155,15 +168,15 @@ button:hover {
transform: scale(1.15);
}
- input {
+ > input {
display: none;
}
- input:not(:checked)~#add-options-content {
+ > input:not(:checked)~#add-options-content {
display: none;
}
- input:checked~label {
+ > input:checked~label {
transform: rotate(45deg);
transition: transform 0.5s;
cursor: pointer;
@@ -180,7 +193,7 @@ button:hover {
position: absolute;
top: 0;
left: 0;
- overflow: scroll;
+ overflow: auto;
z-index: 1;
}
@@ -190,6 +203,7 @@ button:hover {
position: absolute;
top: 0;
left: 0;
+ overflow: hidden;
}
#add-options-content {
@@ -207,8 +221,43 @@ ul#add-options-list {
list-style: none;
padding: 5 0 0 0;
- li {
+ > li {
display: inline-block;
padding: 0;
}
+}
+
+.mainView-libraryFlyout {
+ height: 100%;
+ position: absolute;
+ display: flex;
+ flex-direction:column;
+}
+
+.mainView-libraryHandle {
+ width: 20px;
+ height: 40px;
+ top: 50%;
+ border: 1px solid black;
+ border-radius: 5px;
+ position: absolute;
+ z-index: 1;
+}
+.svg-inline--fa {
+ vertical-align: unset;
+}
+.mainView-workspace {
+ height:200px;
+ position:relative;
+ display:flex;
+}
+.mainView-library {
+ height:75%;
+ position:relative;
+ display:flex;
+}
+.mainView-recentlyClosed {
+ height:25%;
+ position:relative;
+ display:flex;
} \ No newline at end of file
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 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 d1224febe..126efd11c 100644
--- a/src/client/views/MainOverlayTextBox.tsx
+++ b/src/client/views/MainOverlayTextBox.tsx
@@ -1,14 +1,15 @@
-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';
-import { emptyFunction, returnTrue, returnZero } from '../../Utils';
+import { Doc } from '../../new_fields/Doc';
+import { BoolCast } from '../../new_fields/Types';
+import { emptyFunction, returnTrue, returnZero, Utils } from '../../Utils';
import { DragManager } from '../util/DragManager';
import { Transform } from '../util/Transform';
-import "normalize.css";
+import { CollectionDockingView } from './collections/CollectionDockingView';
import "./MainOverlayTextBox.scss";
import { FormattedTextBox } from './nodes/FormattedTextBox';
-import { CollectionDockingView } from './collections/CollectionDockingView';
-import { Doc } from '../../new_fields/Doc';
interface MainOverlayTextBoxProps {
}
@@ -17,11 +18,28 @@ interface MainOverlayTextBoxProps {
export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> {
public static Instance: MainOverlayTextBox;
@observable _textXf: () => Transform = () => Transform.Identity();
- private _textFieldKey: string = "data";
+ public TextFieldKey: string = "data";
private _textColor: string | null = null;
private _textHideOnLeave?: boolean;
private _textTargetDiv: HTMLDivElement | undefined;
private _textProxyDiv: React.RefObject<HTMLDivElement>;
+ private _textBottom: boolean | undefined;
+ private _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);
@@ -29,21 +47,38 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
MainOverlayTextBox.Instance = this;
reaction(() => FormattedTextBox.InputBoxOverlay,
(box?: FormattedTextBox) => {
- if (box) this.setTextDoc(box.props.fieldKey, box.CurrentDiv, box.props.ScreenToLocalTransform);
- else this.setTextDoc();
+ this._textBox = box;
+ if (box) {
+ this.TextDoc = box.props.Document;
+ this.TextDataDoc = box.props.DataDoc;
+ let xf = () => {
+ box.props.ScreenToLocalTransform();
+ let sxf = Utils.GetScreenTransform(box ? box.CurrentDiv : undefined);
+ return new Transform(-sxf.translateX, -sxf.translateY, 1 / sxf.scale);
+ };
+ this.setTextDoc(box.props.fieldKey, box.CurrentDiv, xf, BoolCast(box.props.Document.autoHeight, false) || box.props.height === "min-content");
+ }
+ else {
+ this.TextDoc = undefined;
+ this.TextDataDoc = undefined;
+ this.setTextDoc();
+ }
});
}
@action
- private setTextDoc(textFieldKey?: string, div?: HTMLDivElement, tx?: () => Transform) {
+ private setTextDoc(textFieldKey?: string, div?: HTMLDivElement, tx?: () => Transform, autoHeight?: boolean) {
if (this._textTargetDiv) {
this._textTargetDiv.style.color = this._textColor;
}
- this._textFieldKey = textFieldKey!;
- this._textXf = tx ? tx : () => Transform.Identity();
+ this._textAutoHeight = autoHeight;
+ this.TextFieldKey = textFieldKey!;
+ let txf = tx ? tx : () => Transform.Identity();
+ this._textXf = txf;
this._textTargetDiv = div;
this._textHideOnLeave = FormattedTextBox.InputBoxOverlay && FormattedTextBox.InputBoxOverlay.props.hideOnLeave;
if (div) {
+ this._textBottom = div.parentElement && div.parentElement.style.bottom ? true : false;
this._textColor = (getComputedStyle(div) as any).color;
div.style.color = "transparent";
}
@@ -64,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;
@@ -84,21 +119,31 @@ 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.TextDataDoc;
if (FormattedTextBox.InputBoxOverlay && this._textTargetDiv) {
let textRect = this._textTargetDiv.getBoundingClientRect();
let s = this._textXf().Scale;
- return <div className="mainOverlayTextBox-textInput" style={{ transform: `translate(${textRect.left}px, ${textRect.top}px) scale(${1 / s},${1 / s})`, width: "auto", height: "auto" }} >
- <div className="mainOverlayTextBox-textInput" onPointerDown={this.textBoxDown} ref={this._textProxyDiv} onScroll={this.textScroll}
- style={{ width: `${textRect.width * s}px`, height: `${textRect.height * s}px` }}>
- <FormattedTextBox 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} />
+ let location = this._textBottom ? textRect.bottom : textRect.top;
+ let hgt = this._textAutoHeight || this._textBottom ? "auto" : this._textTargetDiv.clientHeight;
+ return <div ref={this._setouterdiv} className="mainOverlayTextBox-unscaled_div" style={{ transform: `translate(${textRect.left}px, ${location}px)` }} >
+ <div className="mainOverlayTextBox-textInput" style={{ transform: `scale(${1 / s},${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 4868fa41c..5a2e0c6c3 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,72 +1,104 @@
import { IconName, library } from '@fortawesome/fontawesome-svg-core';
-import { faFilePdf, faFilm, faFont, faGlobeAsia, faImage, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faTree, faUndoAlt, faBell, faPlay } 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 "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 } 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/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 { OverlayView } from './OverlayView';
+import PDFMenu from './pdf/PDFMenu';
+import { PresentationView } from './presentationview/PresentationView';
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 { List } from '../../new_fields/List';
-
+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 = new List<Doc>([Docs.TreeDocument([], { title: "Presentation" })]);
+ doc.presentationView = new List<Doc>([Docs.Create.TreeDocument([], { title: "Presentation" })]);
}
CurrentUserUtils.UserDocument.activeWorkspace = doc;
}
}
+ componentWillMount() {
+ var tag = document.createElement('script');
+
+ tag.src = "https://www.youtube.com/iframe_api";
+ var firstScriptTag = document.getElementsByTagName('script')[0];
+ firstScriptTag.parentNode!.insertBefore(tag, firstScriptTag);
+ window.removeEventListener("keydown", KeyManager.Instance.handle);
+ window.addEventListener("keydown", KeyManager.Instance.handle);
+
+ 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) {
@@ -78,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);
@@ -90,6 +124,14 @@ export class MainView extends React.Component {
library.add(faMusic);
library.add(faTree);
library.add(faPlay);
+ 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();
}
@@ -98,18 +140,12 @@ 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();
- }
- }, 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.clearItems();
+ ContextMenu.Instance.closeMenu();
}
}), true);
}
@@ -130,13 +166,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(() => {
@@ -147,29 +191,41 @@ 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();
+ console.log("Drop");
}
@action
@@ -183,123 +239,205 @@ 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}
- zoomToScale={emptyFunction}
- getScale={returnOne}
- />;
let castRes = mainCont ? FieldValue(Cast(mainCont.presentationView, listSpec(Doc))) : undefined;
- console.log("GETTING mainContent()");
- console.log(castRes instanceof Promise);
- console.log(castRes);
return <Measure offset onResize={this.onResize}>
{({ measureRef }) =>
- <div ref={measureRef} id="mainContent-div">
- {content}
+ <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) {
+ return { color: "#61aaa3", fontSize: "50%" };
+ }
+ return { fontSize: "50%" };
+ }
+
+ onColorClick = (e: React.MouseEvent) => {
+ let target = (e.nativeEvent as any).path[0];
+ let parent = (e.nativeEvent as any).path[1];
+ if (target.localName === "input" || parent.localName === "span") {
+ e.stopPropagation();
+ }
+ }
+
+
+ @observable private _colorPickerDisplay = false;
/* for the expandable add nodes menu. Not included with the miscbuttons because once it expands it expands the whole div with it, making canvas interactions limited. */
nodesMenu() {
let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg";
- let pdfurl = "http://www.adobe.com/support/products/enterprise/knowledgecenter/media/c27211_sample_explain.pdf";
- let weburl = "https://cs.brown.edu/courses/cs166/";
- let audiourl = "http://techslides.com/demos/samples/sample.mp3";
- let videourl = "http://techslides.com/demos/sample-videos/small.mp4";
- let youtubeurl = "https://www.youtube.com/embed/TqcApsGRzWw";
- let addTextNode = action(() => Docs.TextDocument({ borderRounding: -1, width: 200, height: 200, title: "a text note" }));
- let addColNode = action(() => Docs.FreeformDocument([], { width: this.pwidth * .7, height: this.pheight, title: "a freeform collection" }));
- let addSchemaNode = action(() => Docs.SchemaDocument(["title"], [], { width: 200, height: 200, title: "a schema collection" }));
- let addTreeNode = action(() => CurrentUserUtils.UserDocument);
+ // 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 addVideoNode = action(() => Docs.VideoDocument(videourl, { width: 200, title: "video node" }));
- let addPDFNode = action(() => Docs.PdfDocument(pdfurl, { width: 200, height: 200, title: "a pdf doc" }));
- let addImageNode = action(() => Docs.ImageDocument(imgurl, { width: 200, title: "an image of a cat" }));
- let addWebNode = action(() => Docs.WebDocument(weburl, { width: 200, height: 200, title: "a sample web page" }));
- let addAudioNode = action(() => Docs.AudioDocument(audiourl, { width: 200, height: 200, title: "audio node" }));
+ 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.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 youtubeurl = "https://www.youtube.com/embed/TqcApsGRzWw";
let addYoutubeSearcher = action(() => Docs.YoutubeDocument(youtubeurl, { width: 200, height: 200, title: "youtube node" }));
let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc][] = [
- [React.createRef<HTMLDivElement>(), "font", "Add Textbox", addTextNode],
- [React.createRef<HTMLDivElement>(), "image", "Add Image", addImageNode],
- [React.createRef<HTMLDivElement>(), "file-pdf", "Add PDF", addPDFNode],
- [React.createRef<HTMLDivElement>(), "film", "Add Video", addVideoNode],
- [React.createRef<HTMLDivElement>(), "music", "Add Audio", addAudioNode],
- [React.createRef<HTMLDivElement>(), "globe-asia", "Add Web Clipping", addWebNode],
[React.createRef<HTMLDivElement>(), "object-group", "Add Collection", addColNode],
- [React.createRef<HTMLDivElement>(), "tree", "Add Tree", addTreeNode],
- [React.createRef<HTMLDivElement>(), "table", "Add Schema", addSchemaNode],
+ // [React.createRef<HTMLDivElement>(), "clone", "Add Docking Frame", addDockingNode],
+ [React.createRef<HTMLDivElement>(), "cloud-upload-alt", "Import Directory", addImportCollectionNode],
[React.createRef<HTMLDivElement>(), "play", "Add Youtube Searcher", addYoutubeSearcher]
];
+ if (!ClientUtils.RELEASE) btns.unshift([React.createRef<HTMLDivElement>(), "cat", "Add Cat Image", addImageNode]);
- return < div id="add-nodes-menu" >
- <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" 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)} 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>
</div >;
}
+
+
+ @action
+ toggleColorPicker = (close = false) => {
+ this._colorPickerDisplay = close ? false : !this._colorPickerDisplay;
+ }
+
/* @TODO this should really be moved into a moveable toolbar component, but for now let's put it here to meet the deadline */
@computed
get miscButtons() {
- 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 [
- <button className="clear-db-button" key="clear-db" onClick={e => e.shiftKey && e.altKey && DocServer.DeleteDatabase()}>Clear Database</button>,
- <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>
- <button className="toolbar-button round-button" title="Search" onClick={this.toggleSearch}><FontAwesomeIcon icon="search" size="sm" /></button>
- <button className="toolbar-button round-button" title="Undo" onClick={() => UndoManager.Undo()}><FontAwesomeIcon icon="undo-alt" size="sm" /></button>
- <button className="toolbar-button round-button" title="Redo" onClick={() => UndoManager.Redo()}><FontAwesomeIcon icon="redo-alt" size="sm" /></button>
- <button className="toolbar-button round-button" title="Ink" onClick={() => InkingControl.Instance.toggleDisplay()}><FontAwesomeIcon icon="pen-nib" size="sm" /></button>
- </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(DocServer.prepend(RouteStore.logout))}>Log Out</button></div>
];
}
@@ -310,6 +448,7 @@ export class MainView extends React.Component {
this.isSearchVisible = !this.isSearchVisible;
}
+
render() {
return (
<div id="main-div">
@@ -319,9 +458,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/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..8fb43021a 100644
--- a/src/client/views/SearchBox.tsx
+++ b/src/client/views/SearchBox.tsx
@@ -1,27 +1,18 @@
-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";
library.add(faSearch);
library.add(faObjectGroup);
@@ -72,22 +63,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 +141,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:
diff --git a/src/client/views/SearchItem.tsx b/src/client/views/SearchItem.tsx
deleted file mode 100644
index 101d893de..000000000
--- a/src/client/views/SearchItem.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import React = require("react");
-import { Doc } from "../../new_fields/Doc";
-import { DocumentManager } from "../util/DocumentManager";
-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 { Cast } from "../../new_fields/Types";
-import { FieldView, FieldViewProps } from './nodes/FieldView';
-import { computed } from "mobx";
-import { IconField } from "../../new_fields/IconField";
-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, false);
- }
-
- //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 e5b679e24..1b32f0ddd 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -1,12 +1,12 @@
-import { observable, computed, action, trace } from "mobx";
-import React = require("react");
+import { action, observable } from "mobx";
import { observer } from "mobx-react";
+import { Doc } from "../../new_fields/Doc";
+import { List } from "../../new_fields/List";
import './DocumentDecorations.scss';
-import { Template } from "./Templates";
import { DocumentView } from "./nodes/DocumentView";
-import { List } from "../../new_fields/List";
-import { Doc } from "../../new_fields/Doc";
-import { NumCast } from "../../new_fields/Types";
+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;
@@ -40,13 +40,14 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
super(props);
}
+ @undoBatch
@action
toggleTemplate = (event: React.ChangeEvent<HTMLInputElement>, template: Template): void => {
if (event.target.checked) {
if (template.Name === "Bullet") {
let topDocView = this.props.docs[0];
topDocView.addTemplate(template);
- topDocView.props.Document.subBulletDocs = new List<Doc>(this.props.docs.filter(v => v !== topDocView).map(v => v.props.Document.proto!));
+ topDocView.props.Document.subBulletDocs = new List<Doc>(this.props.docs.filter(v => v !== topDocView).map(v => v.props.Document));
} else {
this.props.docs.map(d => d.addTemplate(template));
}
@@ -63,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;
@@ -80,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 0cd367bcb..236704fa2 100644
--- a/src/client/views/Templates.tsx
+++ b/src/client/views/Templates.tsx
@@ -41,48 +41,59 @@ export namespace Templates {
export const Caption = new Template("Caption", TemplatePosition.OutterBottom,
`<div>
- <div style="height:100%; width:100%;position:absolute;">{layout}</div>
+ <div style="height:100%; width:100%;">{layout}</div>
<div style="bottom: 0; font-size:14px; width:100%; position:absolute">
- <FormattedTextBox {...props} fieldKey={"caption"} hideOnLeave={"true"} />
+ <FormattedTextBox {...props} height="min-content" fieldKey={"caption"} hideOnLeave={"true"} />
</div>
</div>` );
- export const TitleOverlay = new Template("TitleOverlay", TemplatePosition.InnerTop,
+ export const Title = new Template("Title", TemplatePosition.InnerTop,
`<div>
- <div style="height:100%; width:100%;position:absolute;">{layout}</div>
- <div style="height:25px; width:100%; position:absolute; top: 0; 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);">
+ <div style="width:100%;overflow:auto">{layout}</div>
+ </div>
</div>` );
- export const Title = new Template("Title", TemplatePosition.InnerTop,
- `<div>
- <div style="height:calc(100% - 25px); margin-top: 25px; width:100%;position:absolute;">{layout}</div>
- <div style="height:25px; width:100%; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white; ">
- <span style="text-align:center;width:100%;font-size:20px;position:absolute;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">{props.Document.title}</span>
+ export const Header = new Template("Header", TemplatePosition.InnerTop,
+ `<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>
- </div>` );
+ <div style="width:100%;height:100%;overflow:auto;">{layout}</div>
+ </div > ` );
export const Bullet = new Template("Bullet", TemplatePosition.InnerTop,
- `<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"/>
- </div>
- </div>`
+ `< 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="/assets/downarrow.png" width="15px" height="15px" />
+ </div>
+ </div > `
);
export function ImageOverlay(width: number, height: number, field: string = "thumbnail") {
- return (`<div>
- <div style="height:100%; width:100%; position:absolute;">{layout}</div>
- <div style="height:auto; width:${width}px; bottom:0; right:0; background:rgba(0,0,0,0.25); position:absolute;overflow:hidden;">
- <ImageBox id="isExpander" {...props} style="width:100%; height=auto;" PanelWidth={${width}} fieldKey={"${field}"} />
+ return (`< div >
+ <div style="height:100%; width:100%; position:absolute;">{layout}</div>
+ <div style="height:auto; width:${width}px; bottom:0; right:0; background:rgba(0,0,0,0.25); position:absolute;overflow:hidden;">
+ <ImageBox id="isExpander" {...props} style="width:100%; height=auto;" PanelWidth={${width}} fieldKey={"${field}"} />
</div>
- </div>`);
+ </div > `);
}
- export const TemplateList: Template[] = [Title, TitleOverlay, Caption, Bullet];
+ 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) {
if (a.Position < b.Position) { return -1; }
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
new file mode 100644
index 000000000..34bcb705e
--- /dev/null
+++ b/src/client/views/collections/CollectionBaseView.scss
@@ -0,0 +1,12 @@
+@import "../globalCssVariables";
+#collectionBaseView {
+ border-width: 0;
+ 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 6639879e1..eba69b448 100644
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -1,14 +1,16 @@
import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { ContextMenu } from '../ContextMenu';
-import { FieldViewProps } from '../nodes/FieldView';
-import { Cast, FieldValue, PromiseValue, NumCast } from '../../../new_fields/Types';
-import { Doc, FieldResult, Opt, DocListCast } from '../../../new_fields/Doc';
-import { listSpec } from '../../../new_fields/Schema';
+import { Doc } from '../../../new_fields/Doc';
+import { Id } from '../../../new_fields/FieldSymbols';
import { List } from '../../../new_fields/List';
+import { listSpec } from '../../../new_fields/Schema';
+import { BoolCast, Cast, NumCast, PromiseValue, StrCast } from '../../../new_fields/Types';
+import { DocumentManager } from '../../util/DocumentManager';
import { SelectionManager } from '../../util/SelectionManager';
-import { Id } from '../../../new_fields/FieldSymbols';
+import { ContextMenu } from '../ContextMenu';
+import { FieldViewProps } from '../nodes/FieldView';
+import './CollectionBaseView.scss';
export enum CollectionViewType {
Invalid,
@@ -34,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;
@@ -58,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?
@@ -71,83 +74,42 @@ 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);
- Doc.SetOnPrototype(doc, "page", curPage);
+ let self = this;
+ var curPage = NumCast(this.props.Document.curPage, -1);
+ Doc.GetProto(doc).page = curPage;
if (curPage >= 0) {
- Doc.SetOnPrototype(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;
}
@action.bound
removeDocument(doc: Doc): boolean {
- SelectionManager.DeselectAll();
- const props = this.props;
+ let docView = DocumentManager.Instance.getDocumentView(doc, this.props.ContainingCollectionView);
+ docView && SelectionManager.DeselectDoc(docView);
//TODO This won't create the field if it doesn't already exist
- const value = Cast(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;
- }
- });
-
- //initial
- //
+ 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,11 +123,13 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
@action.bound
moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean {
- if (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)) {
- SelectionManager.DeselectAll();
return addDocument(doc);
}
return false;
@@ -181,12 +145,13 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
};
const viewtype = this.collectionViewType;
return (
- <div className={this.props.className || "collectionView-cont"}
- style={{ borderRadius: "inherit", pointerEvents: "all" }}
+ <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 81f574a6c..a193ff677 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -1,33 +1,40 @@
import 'golden-layout/src/css/goldenlayout-base.css';
import 'golden-layout/src/css/goldenlayout-dark-theme.css';
-import { action, observable, reaction, Lambda } 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";
import * as GoldenLayout from "../../../client/goldenLayout";
-import { Doc, Field, Opt, DocListCast } from "../../../new_fields/Doc";
+import { Doc, DocListCast, Field, Opt } from "../../../new_fields/Doc";
+import { Id } from '../../../new_fields/FieldSymbols';
import { FieldId } from "../../../new_fields/RefField";
import { listSpec } from "../../../new_fields/Schema";
-import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { Cast, NumCast, StrCast, BoolCast } from "../../../new_fields/Types";
import { emptyFunction, returnTrue, Utils, returnOne } from "../../../Utils";
import { DocServer } from "../../DocServer";
+import { DocumentManager } from '../../util/DocumentManager';
import { DragLinksAsDocuments, DragManager } from "../../util/DragManager";
+import { SelectionManager } from '../../util/SelectionManager';
import { Transform } from '../../util/Transform';
import { undoBatch, UndoManager } from "../../util/UndoManager";
import { DocumentView } from "../nodes/DocumentView";
+import { CollectionViewType } from './CollectionBaseView';
import "./CollectionDockingView.scss";
import { SubCollectionViewProps } from "./CollectionSubView";
-import React = require("react");
import { ParentDocSelector } from './ParentDocumentSelector';
-import { DocumentManager } from '../../util/DocumentManager';
-import { CollectionViewType } from './CollectionBaseView';
-import { Id } from '../../../new_fields/FieldSymbols';
+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',
@@ -35,6 +42,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
width: width,
props: {
documentId: document[Id],
+ dataDocumentId: dataDoc ? dataDoc[Id] : ""
//collectionDockingView: CollectionDockingView.Instance
}
};
@@ -44,37 +52,75 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
private _containerRef = React.createRef<HTMLDivElement>();
private _flush: boolean = false;
private _ignoreStateChange = "";
+ private _isPointerDown = false;
+ private _maximizedSrc: Opt<DocumentView>;
constructor(props: SubCollectionViewProps) {
super(props);
- CollectionDockingView.Instance = this;
+ if (props.addDocTab === emptyFunction) CollectionDockingView.Instance = this;
+ //Why is this here?
(window as any).React = React;
(window as any).ReactDOM = ReactDOM;
}
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 => {
@@ -120,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);
@@ -157,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();
@@ -213,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 });
@@ -222,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);
@@ -242,18 +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 => {
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();
@@ -264,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
@@ -313,6 +346,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
}
itemDropped = () => {
+ CollectionDockingView.Instance._ignoreStateChange = JSON.stringify(CollectionDockingView.Instance._goldenLayout.toConfig());
this.stateChanged();
}
@@ -328,24 +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;
- 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
@@ -357,8 +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());
});
}
@@ -373,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);
}
@@ -397,9 +471,18 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
}
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>
);
}
@@ -407,6 +490,8 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
interface DockedFrameProps {
documentId: FieldId;
+ dataDocumentId: FieldId;
+ glContainer: any;
//collectionDockingView: CollectionDockingView
}
@observer
@@ -415,19 +500,52 @@ 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)
+ if (this._document && this._document.excludeFromLibrary && parent.parent && parent.parent.contentItems.length > 1) {
return parent.parent.contentItems[1];
+ }
return parent;
}
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 = () => NumCast(this._document!.nativeHeight, this._panelHeight);
+ nativeHeight = () => {
+ let nh = NumCast(this._document!.nativeHeight, this._panelHeight);
+ let res = BoolCast(this._document!.ignoreAspect) ? this._panelHeight : nh;
+ return res;
+ }
contentScaling = () => {
const nativeH = this.nativeHeight();
const nativeW = this.nativeWidth();
@@ -448,6 +566,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
let docHeight = NumCast(this._document!.height);
if (NumCast(this._document!.nativeWidth) || !docWidth || !this._panelWidth || !this._panelHeight) return 1;
if (StrCast(this._document!.layout).indexOf("Collection") === -1 ||
+ !BoolCast(this._document!.fitToContents, false) ||
NumCast(this._document!.viewType) !== CollectionViewType.Freeform) return 1;
let scaling = Math.max(1, this._panelWidth / docWidth * docHeight > this._panelHeight ?
this._panelHeight / docHeight : this._panelWidth / docWidth);
@@ -455,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) => {
- if (location === "onRight") {
- CollectionDockingView.Instance.AddRightSplit(doc);
+ addDocTab = (doc: Doc, dataDoc: Doc | undefined, location: string) => {
+ if (doc.dockingConfig) {
+ MainView.Instance.openWorkspace(doc);
+ } else if (location === "onRight") {
+ 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}
- zoomToScale={emptyFunction}
- getScale={returnOne} />
+ 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 5e51437a4..8ab360984 100644
--- a/src/client/views/collections/CollectionPDFView.tsx
+++ b/src/client/views/collections/CollectionPDFView.tsx
@@ -1,60 +1,74 @@
-import { action, observable } from "mobx";
+import { action, IReactionDisposer, observable, reaction } from "mobx";
import { observer } from "mobx-react";
+import { WidthSym } from "../../../new_fields/Doc";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { NumCast } from "../../../new_fields/Types";
+import { emptyFunction } from "../../../Utils";
import { ContextMenu } from "../ContextMenu";
+import { FieldView, FieldViewProps } from "../nodes/FieldView";
+import { CollectionBaseView, CollectionRenderProps, CollectionViewType } from "./CollectionBaseView";
+import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";
import "./CollectionPDFView.scss";
import React = require("react");
-import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";
-import { FieldView, FieldViewProps } from "../nodes/FieldView";
-import { CollectionRenderProps, CollectionBaseView, CollectionViewType } from "./CollectionBaseView";
-import { emptyFunction } from "../../../Utils";
-import { NumCast } from "../../../new_fields/Types";
-import { Id } from "../../../new_fields/FieldSymbols";
+import { PDFBox } from "../nodes/PDFBox";
@observer
export class CollectionPDFView extends React.Component<FieldViewProps> {
+ private _pdfBox?: PDFBox;
+ private _reactionDisposer?: IReactionDisposer;
+ private _buttonTray: React.RefObject<HTMLDivElement>;
+
+ constructor(props: FieldViewProps) {
+ super(props);
- public static LayoutString(fieldKey: string = "data") {
- return FieldView.LayoutString(CollectionPDFView, fieldKey);
+ this._buttonTray = React.createRef();
+ }
+
+ componentDidMount() {
+ 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]());
+ },
+ { fireImmediately: true }
+ );
+ }
+
+ componentWillUnmount() {
+ this._reactionDisposer && this._reactionDisposer();
+ }
+
+ public static LayoutString(fieldKey: string = "data", fieldExt: string = "annotations") {
+ return FieldView.LayoutString(CollectionPDFView, fieldKey, fieldExt);
}
@observable _inThumb = false;
- private set curPage(value: number) { this.props.Document.curPage = value; }
+ private set curPage(value: number) { this._pdfBox && this._pdfBox.GotoPage(value); }
private get curPage() { return NumCast(this.props.Document.curPage, -1); }
private get numPages() { return NumCast(this.props.Document.numPages); }
- @action onPageBack = () => this.curPage > 1 ? (this.props.Document.curPage = this.curPage - 1) : -1;
- @action onPageForward = () => this.curPage < this.numPages ? (this.props.Document.curPage = this.curPage + 1) : -1;
+ @action onPageBack = () => this._pdfBox && this._pdfBox.BackPage();
+ @action onPageForward = () => this._pdfBox && this._pdfBox.ForwardPage();
- @action
- onThumbDown = (e: React.PointerEvent) => {
- document.addEventListener("pointermove", this.onThumbMove, false);
- document.addEventListener("pointerup", this.onThumbUp, false);
- e.stopPropagation();
- this._inThumb = true;
- }
- @action
- onThumbMove = (e: PointerEvent) => {
- let pso = (e.clientY - (e as any).target.parentElement.getBoundingClientRect().top) / (e as any).target.parentElement.getBoundingClientRect().height;
- this.curPage = Math.trunc(Math.min(this.numPages, pso * this.numPages + 1));
- e.stopPropagation();
- }
- @action
- onThumbUp = (e: PointerEvent) => {
- this._inThumb = false;
- document.removeEventListener("pointermove", this.onThumbMove);
- document.removeEventListener("pointerup", this.onThumbUp);
- }
nativeWidth = () => NumCast(this.props.Document.nativeWidth);
nativeHeight = () => NumCast(this.props.Document.nativeHeight);
private get uIButtons() {
let ratio = (this.curPage - 1) / this.numPages * 100;
return (
- <div className="collectionPdfView-buttonTray" key="tray" style={{ height: "100%" }}>
+ <div className="collectionPdfView-buttonTray" ref={this._buttonTray} key="tray" style={{ height: "100%" }}>
<button className="collectionPdfView-backward" onClick={this.onPageBack}>{"<"}</button>
<button className="collectionPdfView-forward" onClick={this.onPageForward}>{">"}</button>
- <div className="collectionPdfView-slider" onPointerDown={this.onThumbDown} style={{ top: 60, left: -20, width: 50, height: `calc(100% - 80px)` }} >
+ {/* <div className="collectionPdfView-slider" onPointerDown={this.onThumbDown} style={{ top: 60, left: -20, width: 50, height: `calc(100% - 80px)` }} >
<div className="collectionPdfView-thumb" onPointerDown={this.onThumbDown} style={{ top: `${ratio}%`, width: 50, height: 50 }} />
- </div>
+ </div> */}
</div>
);
}
@@ -65,12 +79,15 @@ export class CollectionPDFView extends React.Component<FieldViewProps> {
}
}
+ setPdfBox = (pdfBox: PDFBox) => { this._pdfBox = pdfBox; };
+
+
private subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => {
let props = { ...this.props, ...renderProps };
return (
<>
- <CollectionFreeFormView {...props} CollectionView={this} />
- {this.props.isSelected() ? this.uIButtons : (null)}
+ <CollectionFreeFormView {...props} setPdfBox={this.setPdfBox} CollectionView={this} />
+ {renderProps.active() ? this.uIButtons : (null)}
</>
);
}
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 715faafd0..f72b1aa07 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -2,36 +2,35 @@ import React = require("react");
import { library } from '@fortawesome/fontawesome-svg-core';
import { faCog, faPlus } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable, untracked, runInAction, trace } from "mobx";
+import { action, computed, observable, trace, untracked } from "mobx";
import { observer } from "mobx-react";
import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table";
-import { MAX_ROW_HEIGHT } from '../../views/globalCssVariables.scss';
import "react-table/react-table.css";
import { emptyFunction, returnFalse, returnZero, returnOne } from "../../../Utils";
-import { SetupDrag } from "../../util/DragManager";
+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, BoolCast } from "../../../new_fields/Types";
+import { Docs } from "../../documents/Documents";
+import { Gateway } from "../../northstar/manager/Gateway";
+import { SetupDrag, DragManager } from "../../util/DragManager";
import { CompileScript } from "../../util/Scripting";
import { Transform } from "../../util/Transform";
-import { COLLECTION_BORDER_WIDTH } from "../../views/globalCssVariables.scss";
+import { COLLECTION_BORDER_WIDTH, MAX_ROW_HEIGHT } from '../../views/globalCssVariables.scss';
+import { ContextMenu } from "../ContextMenu";
import { anchorPoints, Flyout } from "../DocumentDecorations";
import '../DocumentDecorations.scss';
import { EditableView } from "../EditableView";
import { DocumentView } from "../nodes/DocumentView";
import { FieldView, FieldViewProps } from "../nodes/FieldView";
+import { CollectionPDFView } from "./CollectionPDFView";
import "./CollectionSchemaView.scss";
import { CollectionSubView } from "./CollectionSubView";
-import { Opt, Field, Doc, DocListCastAsync, DocListCast } from "../../../new_fields/Doc";
-import { Cast, FieldValue, NumCast, StrCast, BoolCast } from "../../../new_fields/Types";
-import { listSpec } from "../../../new_fields/Schema";
-import { List } from "../../../new_fields/List";
-import { Id } from "../../../new_fields/FieldSymbols";
-import { Gateway } from "../../northstar/manager/Gateway";
-import { Docs } from "../../documents/Documents";
-import { ContextMenu } from "../ContextMenu";
-import { CollectionView } from "./CollectionView";
-import { CollectionPDFView } from "./CollectionPDFView";
import { CollectionVideoView } from "./CollectionVideoView";
-import { SelectionManager } from "../../util/SelectionManager";
+import { CollectionView } from "./CollectionView";
import { undoBatch } from "../../util/UndoManager";
+import { timesSeries } from "async";
library.add(faCog);
@@ -90,8 +89,9 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
let columnDocs = DocListCast(schemaDoc.data);
if (columnDocs) {
let ddoc = columnDocs.find(doc => doc.title === columnName);
- if (ddoc)
+ if (ddoc) {
return ddoc;
+ }
}
}
return this.props.Document;
@@ -100,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,
@@ -116,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;
@@ -232,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();
}
}
@@ -261,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;
@@ -282,13 +284,13 @@ 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(
- - this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth, - this.borderWidth);
+ - this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth, - this.borderWidth)
get documentKeysCheckList() {
@@ -331,13 +333,12 @@ 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}
column={{ ...ReactTableDefaults.column, Cell: this.renderCell, }}
getTrProps={this.getTrProps}
- />
+ />;
}
@computed
@@ -346,23 +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}
- 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) => {
@@ -370,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}>
@@ -384,21 +399,29 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
}
interface CollectionSchemaPreviewProps {
Document?: Doc;
+ DataDocument?: Doc;
+ childDocs?: Doc[];
+ renderDepth: number;
+ fitToBox?: boolean;
width: () => number;
height: () => number;
- CollectionView: CollectionView | CollectionPDFView | CollectionVideoView;
+ showOverlays?: (doc: Doc) => { title?: string, caption?: string };
+ CollectionView?: CollectionView | CollectionPDFView | CollectionVideoView;
getTransform: () => Transform;
addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
+ moveDocument: (document: Doc, target: Doc, addDoc: ((doc: Doc) => boolean)) => boolean;
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,49 +431,78 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre
}
return wscale;
}
- private PanelWidth = () => this.nativeWidth * this.contentScaling();
- private PanelHeight = () => this.nativeHeight * this.contentScaling();
- private getTransform = () => this.props.getTransform().translate(-this.centeringOffset, 0).scale(1 / this.contentScaling())
- get centeringOffset() { return (this.props.width() - this.nativeWidth * this.contentScaling()) / 2; }
- @action
- onPreviewScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- this.props.setPreviewScript(e.currentTarget.value);
+ 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
- public collapseToPoint = (scrpt: number[], expandedDocs: Doc[] | undefined): void => {
- SelectionManager.DeselectAll();
- if (expandedDocs) {
- let isMinimized: boolean | undefined;
- expandedDocs.map(d => Doc.GetProto(d)).map(maximizedDoc => {
- if (isMinimized === undefined) {
- isMinimized = BoolCast(maximizedDoc.isMinimized, false);
- }
- maximizedDoc.isMinimized = !isMinimized;
+ 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.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() {
- trace();
- console.log(this.props.Document);
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)` }} />;
- return (<div className="collectionSchemaView-previewRegion" style={{ width: this.props.width() }}>
+ <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)` }}>
- <DocumentView Document={this.props.Document} isTopMost={false} selectOnLoad={false}
- addDocument={this.props.addDocument} removeDocument={this.props.removeDocument}
+ <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}
- collapseToPoint={this.collapseToPoint}
zoomToScale={emptyFunction}
getScale={returnOne}
/>
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index 4d84aaaa9..7e886304d 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -1,19 +1,6 @@
@import "../globalCssVariables";
-
.collectionStackingView {
- top: 0;
- left: 0;
- display: flex;
- flex-direction: column;
- width: 100%;
- position: absolute;
overflow-y: auto;
- 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;
.collectionStackingView-docView-container {
width: 45%;
@@ -29,13 +16,26 @@
align-items: center;
}
- .collectionStackingView-masonrySingle, .collectionStackingView-masonryGrid{
+ .collectionStackingView-masonrySingle, .collectionStackingView-masonryGrid {
width:100%;
height:100%;
position: absolute;
- }
- .collectionStackingView-masonryGrid {
display:grid;
+ top: 0;
+ 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 {
@@ -48,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 e1453c658..fe01103d6 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -1,40 +1,40 @@
import React = require("react");
-import { action, computed, IReactionDisposer, reaction } from "mobx";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { action, computed, IReactionDisposer, reaction, untracked } 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 { SelectionManager } from "../../util/SelectionManager";
-import { undoBatch } from "../../util/UndoManager";
-import { DocumentView } from "../nodes/DocumentView";
+import { BoolCast, NumCast, Cast } from "../../../new_fields/Types";
+import { emptyFunction, Utils } from "../../../Utils";
+import { ContextMenu } from "../ContextMenu";
import { CollectionSchemaPreview } from "./CollectionSchemaView";
import "./CollectionStackingView.scss";
import { CollectionSubView } from "./CollectionSubView";
+import { undoBatch } from "../../util/UndoManager";
+import { DragManager } from "../../util/DragManager";
+import { DocumentType } from "../../documents/Documents";
+import { Transform } from "../../util/Transform";
@observer
export class CollectionStackingView extends CollectionSubView(doc => doc) {
_masonryGridRef: HTMLDivElement | null = null;
+ _draggerRef = React.createRef<HTMLDivElement>();
_heightDisposer?: IReactionDisposer;
- get gridGap() { return 10; }
- get gridSize() { return 20; }
- get singleColumn() { return BoolCast(this.props.Document.singleColumn, true); }
- get columnWidth() { return this.singleColumn ? this.props.PanelWidth() - 4 * this.gridGap : NumCast(this.props.Document.columnWidth, 250); }
+ _gridSize = 1;
+ _docXfs: any[] = [];
+ @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); }
componentDidMount() {
- this._heightDisposer = reaction(() => [this.childDocs.map(d => [d.height, d.width, d.zoomBasis, d.nativeHeight, d.nativeWidth, d.isMinimized]), this.columnWidth, this.props.PanelHeight()],
- () => {
- if (this.singleColumn) {
- this.props.Document.height = this.childDocs.filter(d => !d.isMinimized).reduce((height, d) => {
- let hgt = d[HeightSym]();
- let wid = d[WidthSym]();
- let nw = NumCast(d.nativeWidth);
- let nh = NumCast(d.nativeHeight);
- if (nw && nh) hgt = nh / nw * Math.min(this.columnWidth, wid);
- return height + hgt + 2 * this.gridGap;
- }, this.gridGap * 2);
- }
- }, { fireImmediately: true });
+ this._heightDisposer = reaction(() => [this.yMargin, this.gridGap, this.columnWidth, this.childDocs.map(d => [d.height, d.width, d.zoomBasis, d.nativeHeight, d.nativeWidth, d.isMinimized])],
+ () => this.singleColumn &&
+ (this.props.Document.height = this.filteredChildren.reduce((height, d, i) =>
+ height + this.getDocHeight(d) + (i === this.filteredChildren.length - 1 ? this.yMargin : this.gridGap), this.yMargin))
+ , { fireImmediately: true });
}
componentWillUnmount() {
if (this._heightDisposer) this._heightDisposer();
@@ -42,138 +42,200 @@ 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;
+ return this.props.removeDocument(doc) && addDocument(doc);
}
- getDocTransform(doc: Doc, dref: HTMLDivElement) {
- let { scale, translateX, translateY } = Utils.GetScreenTransform(dref);
+ createRef = (ele: HTMLDivElement | null) => {
+ this._masonryGridRef = ele;
+ this.createDropTarget(ele!);
+ }
+
+ 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);
}
- createRef = (ele: HTMLDivElement | null) => {
- this._masonryGridRef = ele;
- this.createDropTarget(ele!);
+ 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() {
+ 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();
+ 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 => {
+ document.removeEventListener("pointermove", this.onDividerMove);
+ document.removeEventListener('pointerup', this.onDividerUp);
+ }
+
+ @computed get columnDragger() {
+ return <div className="collectionStackingView-columnDragger" onPointerDown={this.columnDividerDown} ref={this._draggerRef} style={{ left: `${this.columnWidth + this.xMargin}px` }} >
+ <FontAwesomeIcon icon={"caret-down"} />
+ </div>;
+ }
+ onContextMenu = (e: React.MouseEvent): void => {
+ if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
+ ContextMenu.Instance.addItem({
+ description: "Toggle multi-column",
+ event: () => this.props.Document.singleColumn = !BoolCast(this.props.Document.singleColumn, true), icon: "file-pdf"
+ });
+ }
+ }
+
@undoBatch
@action
- public collapseToPoint = (scrpt: number[], expandedDocs: Doc[] | undefined): void => {
- SelectionManager.DeselectAll();
- if (expandedDocs) {
- let isMinimized: boolean | undefined;
- expandedDocs.map(d => Doc.GetProto(d)).map(maximizedDoc => {
- if (isMinimized === undefined) {
- isMinimized = BoolCast(maximizedDoc.isMinimized, false);
+ 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;
}
- maximizedDoc.isMinimized = !isMinimized;
});
}
+ 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;
}
-
- @computed
- get singleColumnChildren() {
- return this.childDocs.filter(d => !d.isMinimized).map((d, i) => {
- let dref = React.createRef<HTMLDivElement>();
- let script = undefined;
- let colWidth = () => d.nativeWidth ? Math.min(d[WidthSym](), this.columnWidth) : this.columnWidth;
- let margin = colWidth() < this.columnWidth ? "auto" : undefined;
- let rowHeight = () => {
- let hgt = d[HeightSym]();
- let nw = NumCast(d.nativeWidth);
- let nh = NumCast(d.nativeHeight);
- if (nw && nh) hgt = nh / nw * colWidth();
- return hgt;
+ @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;
}
- let dxf = () => this.getDocTransform(d, dref.current!).scale(this.columnWidth / d[WidthSym]());
- return <div className="collectionStackingView-masonryDoc"
- key={d[Id]}
- ref={dref}
- style={{ marginTop: `${i ? 2 * this.gridGap : 0}px`, width: colWidth(), height: rowHeight(), marginLeft: margin, marginRight: margin }} >
- <CollectionSchemaPreview
- Document={d}
- width={colWidth}
- height={rowHeight}
- getTransform={dxf}
- CollectionView={this.props.CollectionView}
- addDocument={this.props.addDocument}
- removeDocument={this.props.removeDocument}
- active={this.props.active}
- whenActiveChanged={this.props.whenActiveChanged}
- addDocTab={this.props.addDocTab}
- setPreviewScript={emptyFunction}
- previewScript={script}>
- </CollectionSchemaPreview>
- </div>;
});
- }
- @computed
- get children() {
- return this.childDocs.filter(d => !d.isMinimized).map(d => {
- let dref = React.createRef<HTMLDivElement>();
- let dxf = () => this.getDocTransform(d, dref.current!);
- let colSpan = Math.ceil(Math.min(d[WidthSym](), this.columnWidth + this.gridGap) / (this.gridSize + this.gridGap));
- let rowSpan = Math.ceil((this.columnWidth / d[WidthSym]() * d[HeightSym]() + 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.
+ 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);
+ }
}
- return (<div className="collectionStackingView-masonryDoc"
- key={d[Id]}
- ref={dref}
- style={{
- width: NumCast(d.nativeWidth, d[WidthSym]()),
- height: NumCast(d.nativeHeight, d[HeightSym]()),
- transformOrigin: "top left",
- gridRowEnd: `span ${rowSpan}`,
- gridColumnEnd: `span ${colSpan}`,
- transform: `scale(${this.columnWidth / NumCast(d.nativeWidth, d[WidthSym]())}, ${this.columnWidth / NumCast(d.nativeWidth, d[WidthSym]())})`
- }} >
- <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={d[WidthSym]}
- PanelHeight={d[HeightSym]}
- selectOnLoad={false}
- parentActive={this.props.active}
- addDocTab={this.props.addDocTab}
- bringToFront={emptyFunction}
- whenActiveChanged={this.props.whenActiveChanged}
- collapseToPoint={this.collapseToPoint}
- zoomToScale={emptyFunction}
- getScale={returnOne}
- />
- </div>);
- })
+ });
}
render() {
- let leftMargin = 2 * this.gridGap;
- let topMargin = 2 * this.gridGap;
- let itemCols = Math.ceil(this.columnWidth / (this.gridSize + this.gridGap));
- let cells = Math.floor((this.props.PanelWidth() - leftMargin) / (itemCols * (this.gridSize + this.gridGap)));
+ 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" style={{ height: "100%" }}
- ref={this.createRef} 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: `${topMargin}px 0px 0px ${leftMargin}px`,
- width: this.singleColumn ? "100%" : `${cells * itemCols * (this.gridSize + this.gridGap) + leftMargin}`,
+ padding: this.singleColumn ? `${this.yMargin}px ${this.xMargin}px ${this.yMargin}px ${this.xMargin}px` : `${this.yMargin}px ${this.xMargin}px`,
+ margin: "auto",
+ width: this.singleColumn ? undefined : `${cols * (this.columnWidth + this.gridGap) + 2 * this.xMargin - this.gridGap}px`,
height: "100%",
- overflow: "hidden",
- marginRight: "auto",
position: "relative",
gridGap: this.gridGap,
- gridTemplateColumns: this.singleColumn ? undefined : `repeat(auto-fill, minmax(${this.gridSize}px,1fr))`,
- gridAutoRows: this.singleColumn ? undefined : `${this.gridSize}px`
+ gridTemplateColumns: this.singleColumn ? undefined : templatecols,
+ 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 fe9e12640..8e8d5708b 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -1,22 +1,25 @@
-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 } from "../../../new_fields/Types";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
+import { RouteStore } from "../../../server/RouteStore";
+import { DocServer } from "../../DocServer";
+import { Docs, DocumentOptions } from "../../documents/Documents";
+import { DragManager } from "../../util/DragManager";
+import { undoBatch, UndoManager } from "../../util/UndoManager";
+import { DocComponent } from "../DocComponent";
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 { 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";
+import { CollectionView } from "./CollectionView";
+import React = require("react");
+import { MainView } from "../MainView";
export interface CollectionViewProps extends FieldViewProps {
addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
@@ -34,9 +37,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
class CollectionSubView extends DocComponent<SubCollectionViewProps, T>(schemaCtor) {
private dropDisposer?: DragManager.DragDropDisposer;
protected createDropTarget = (ele: HTMLDivElement) => {
- if (this.dropDisposer) {
- this.dropDisposer();
- }
+ this.dropDisposer && this.dropDisposer();
if (ele) {
this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
}
@@ -45,10 +46,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.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 DocListCast(this.props.Document[this.props.fieldKey]);
+ return Cast(this.extensionDoc[this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey], listSpec(Doc));
}
@action
@@ -59,10 +69,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] === "collectionProto") {
+ 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>();
@@ -80,79 +97,29 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
@action
protected drop(e: Event, de: DragManager.DropEvent): boolean {
if (de.data instanceof DragManager.DocumentDragData) {
- if (de.data.dropAction || de.data.userDropAction) {
- ["width", "height", "curPage"].map(key =>
- de.data.draggedDocuments.map((draggedDocument: Doc, i: number) =>
- PromiseValue(Cast(draggedDocument[key], "number")).then(f => f && (de.data.droppedDocuments[i][key] = f))));
- }
let added = false;
if (de.data.dropAction || de.data.userDropAction) {
- added = de.data.droppedDocuments.reduce((added: boolean, d) => {
- let moved = this.props.addDocument(d);
- return moved || added;
- }, false);
+ added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || 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);
- return moved || added;
- }, false);
+ added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false);
}
e.stopPropagation();
return added;
}
- 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 };
+ else if (de.data instanceof DragManager.AnnotationDragData) {
+ e.stopPropagation();
+ return this.props.addDocument(de.data.dropDocument);
}
- return ctor ? ctor(path, options) : undefined;
+ return false;
}
@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;
@@ -166,14 +133,54 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
e.stopPropagation();
e.preventDefault();
- if (html && html.indexOf("<img") !== 0 && !html.startsWith("<a")) {
- let htmlDoc = Docs.HtmlDocument(html, { ...options, width: 300, height: 300, documentText: text });
- this.props.addDocument(htmlDoc, false);
+ if (html && FormattedTextBox.IsFragment(html)) {
+ let href = FormattedTextBox.GetHref(html);
+ if (href) {
+ let docid = FormattedTextBox.GetDocFromUrl(href);
+ if (docid) { // prosemirror text containing link to dash document
+ DocServer.GetRefField(docid).then(f => {
+ if (f instanceof Doc) {
+ if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView
+ (f instanceof Doc) && this.props.addDocument(f, false);
+ }
+ });
+ } else {
+ this.props.addDocument && this.props.addDocument(Docs.Create.WebDocument(href, options));
+ }
+ } else if (text) {
+ this.props.addDocument && this.props.addDocument(Docs.Create.TextDocument({ ...options, width: 100, height: 25, documentText: "@@@" + text }), false);
+ }
return;
}
+ if (html && !html.startsWith("<a")) {
+ 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];
+ DocServer.GetRefField(docid).then(f => {
+ if (f instanceof Doc) {
+ if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView
+ (f instanceof Doc) && this.props.addDocument(f, false);
+ }
+ });
+ } else {
+ let htmlDoc = Docs.Create.HtmlDocument(html, { ...options, width: 300, height: 300, documentText: text });
+ this.props.addDocument(htmlDoc, false);
+ }
+ return;
+ }
+ }
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 }));
return;
}
@@ -190,7 +197,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
.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));
}
});
@@ -211,10 +218,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 = DocServer.prepend(file);
+ Docs.Get.DocumentFromType(type, path, full).then(doc => doc && this.props.addDocument(doc));
}));
});
promises.push(prom);
@@ -222,7 +228,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 bb3be0a73..5205f4313 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -4,59 +4,33 @@
border-width: $COLLECTION_BORDER_WIDTH;
border-color: transparent;
border-style: solid;
- border-radius: $border-radius;
+ border-radius: inherit;
box-sizing: border-box;
height: 100%;
- padding: 20px;
+ padding-top: 20px;
padding-left: 10px;
padding-right: 0px;
background: $light-color-secondary;
font-size: 13px;
- overflow: scroll;
+ overflow: auto;
ul {
list-style: none;
padding-left: 20px;
}
- li {
- margin: 5px 0;
- }
-
.no-indent {
padding-left: 0;
}
.bullet {
- float: left;
position: relative;
width: 15px;
- display: block;
color: $intermediate-color;
- margin-top: 3px;
+ margin-top: 4px;
transform: scale(1.3, 1.3);
}
-
- .docContainer {
- margin-left: 10px;
- display: block;
- // width:100%;//width: max-content;
- }
-
- .docContainer:hover {
- .treeViewItem-openRight {
- display: inline-block;
- // display: inline;
- svg {
- display:block;
- padding:0px;
- margin: 0px;
- }
- }
- }
-
-
.editableView-container {
font-weight: bold;
}
@@ -69,31 +43,72 @@
display: inline;
}
- .treeViewItem-openRight {
- margin-left: 5px;
- display: none;
- }
-
- .docContainer:hover {
- .delete-button {
- display: inline;
- // width: auto;
- }
- }
-
- .coll-title {
- width: max-content;
+ .editableView-input, .editableView-container-editing {
display: block;
+ text-overflow: ellipsis;
font-size: 24px;
+ white-space: nowrap;
}
-
- .collection-child {
- margin-top: 10px;
- margin-bottom: 10px;
- }
-
+}
+.collectionTreeView-keyHeader {
+ font-style: italic;
+ font-size: 8pt;
+ margin-left: 3px;
+ display:none;
+ background: lightgray;
+}
+
+.collectionTreeView-subtitle {
+ font-style: italic;
+ font-size: 8pt;
+ color: $intermediate-color;
+}
+
+.docContainer {
+ position: relative;
+ text-overflow: ellipsis;
+ white-space: pre-wrap;
+ overflow: hidden;
+ // width:100%;//width: max-content;
+
+}
+.treeViewItem-openRight {
+ display: none;
+}
+
+.treeViewItem-border {
+ display:inherit;
+ border-left: dashed 1px #00000042;
+}
+
+.treeViewItem-header:hover {
.collectionTreeView-keyHeader {
- font-style: italic;
- font-size: 8pt;
+ display:inherit;
+ }
+ .treeViewItem-openRight {
+ display: inline-block;
+ height:13px;
+ margin-top:2px;
+ margin-left: 5px;
+ // display: inline;
+ svg {
+ display:block;
+ padding:0px;
+ margin: 0px;
+ }
}
+}
+
+.treeViewItem-header {
+ border: transparent 1px solid;
+ display:flex;
+}
+.treeViewItem-header-above {
+ border-top: black 1px solid;
+}
+.treeViewItem-header-below {
+ border-bottom: black 1px solid;
+}
+.treeViewItem-header-inside {
+ border: black 1px solid;
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index c80bd8fce..c212cc97c 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -1,251 +1,562 @@
-import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
-import { faCaretDown, faCaretRight, faTrashAlt, faAngleRight } from '@fortawesome/free-solid-svg-icons';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faAngleRight, faCamera, faExpand, faTrash, faBell, faCaretDown, faCaretRight, faCaretSquareDown, faCaretSquareRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, observable, trace } from "mobx";
+import { action, computed, observable, trace, untracked } from "mobx";
import { observer } from "mobx-react";
-import { DragManager, SetupDrag, dropActionType } from "../../util/DragManager";
-import { EditableView } from "../EditableView";
-import { CollectionSubView } from "./CollectionSubView";
-import "./CollectionTreeView.scss";
-import React = require("react");
-import { Document, listSpec } from '../../../new_fields/Schema';
-import { Cast, StrCast, BoolCast, FieldValue, NumCast } from '../../../new_fields/Types';
-import { Doc, DocListCast } from '../../../new_fields/Doc';
+import { Doc, DocListCast, HeightSym, WidthSym, Opt } from '../../../new_fields/Doc';
import { Id } from '../../../new_fields/FieldSymbols';
-import { ContextMenu } from '../ContextMenu';
-import { undoBatch } from '../../util/UndoManager';
-import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
-import { CollectionDockingView } from './CollectionDockingView';
+import { List } from '../../../new_fields/List';
+import { Document, listSpec } from '../../../new_fields/Schema';
+import { BoolCast, Cast, NumCast, StrCast } from '../../../new_fields/Types';
+import { emptyFunction, Utils } from '../../../Utils';
+import { Docs, DocUtils, DocumentType } from '../../documents/Documents';
import { DocumentManager } from '../../util/DocumentManager';
-import { Docs } from '../../documents/Documents';
+import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager";
+import { SelectionManager } from '../../util/SelectionManager';
+import { Transform } from '../../util/Transform';
+import { undoBatch } from '../../util/UndoManager';
+import { ContextMenu } from '../ContextMenu';
+import { EditableView } from "../EditableView";
import { MainView } from '../MainView';
+import { Templates } from '../Templates';
import { CollectionViewType } from './CollectionBaseView';
+import { CollectionDockingView } from './CollectionDockingView';
+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;
- deleteDoc: (doc: Doc) => void;
+ dataDoc?: Doc;
+ containingCollection: Doc;
+ renderDepth: number;
+ deleteDoc: (doc: Doc) => boolean;
moveDocument: DragManager.MoveFunction;
dropAction: "alias" | "copy" | undefined;
- addDocTab: (doc: Doc, where: string) => void;
-}
-
-export enum BulletType {
- Collapsed,
- Collapsible,
- List
+ addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void;
+ panelWidth: () => number;
+ panelHeight: () => number;
+ addDocument: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean;
+ indentDocument?: () => void;
+ ScreenToLocalTransform: () => Transform;
+ outerXf: () => { translateX: number, translateY: number };
+ treeViewId: string;
+ parentKey: string;
+ active: () => boolean;
}
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);
@observer
/**
* Component that takes in a document prop and a boolean whether it's collapsed or not.
*/
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 = "";
+ @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;
- @undoBatch delete = () => this.props.deleteDoc(this.props.document);
-
- @undoBatch openRight = async () => {
- if (this.props.document.dockingConfig) {
- MainView.Instance.openWorkspace(this.props.document);
- } else {
- this.props.addDocTab(this.props.document, "openRight");
+ @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";
}
- get children() {
- return Cast(this.props.document.data, listSpec(Doc), []); // bcz: needed? .filter(doc => FieldValue(doc));
+ @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) {
+ this._treedropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.treeDrop.bind(this) } });
+ }
}
- onPointerDown = (e: React.PointerEvent) => {
+ @undoBatch delete = () => this.props.deleteDoc(this.resolvedDataDoc);
+ @undoBatch openRight = async () => this.props.addDocTab(this.props.document, undefined, "onRight");
+
+ 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()) {
+ this._header!.current!.className = "treeViewItem-header";
+ document.addEventListener("pointermove", this.onDragMove, true);
+ }
+ }
+ onPointerLeave = (e: React.PointerEvent): void => {
+ 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 = 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);
+ let before = x[1] < bounds[1];
+ let inside = x[0] > bounds[0] + 75 || (!before && !this._collapsed);
+ this._header!.current!.className = "treeViewItem-header";
+ if (inside) this._header!.current!.className += " treeViewItem-header-inside";
+ else if (before) this._header!.current!.className += " treeViewItem-header-above";
+ else if (!before) this._header!.current!.className += " treeViewItem-header-below";
e.stopPropagation();
}
@action
- remove = (document: Document, key: string) => {
- let children = Cast(this.props.document[key], listSpec(Doc), []);
- if (children) {
+ remove = (document: Document, key: string): boolean => {
+ let children = Cast(this.resolvedDataDoc[key], listSpec(Doc), []);
+ if (children.indexOf(document) !== -1) {
children.splice(children.indexOf(document), 1);
+ return true;
}
+ return false;
}
@action
- move: DragManager.MoveFunction = (document, target, addDoc) => {
- if (this.props.document === target) {
- return true;
- }
- //TODO This should check if it was removed
- this.remove(document, "data");
- return addDoc(document);
+ move: DragManager.MoveFunction = (doc: Doc, target: Doc, addDoc) => {
+ return this.props.document !== target && this.props.deleteDoc(doc) && addDoc(doc);
}
+ @action
+ indent = () => this.props.addDocument(this.props.document) && this.delete()
- renderBullet(type: BulletType) {
- let onClicked = action(() => this._collapsed = !this._collapsed);
- let bullet: IconProp | undefined = undefined;
- switch (type) {
- case BulletType.Collapsed: bullet = "caret-right"; break;
- case BulletType.Collapsible: bullet = "caret-down"; break;
- }
- return <div className="bullet" onClick={onClicked}>{bullet ? <FontAwesomeIcon icon={bullet} /> : ""} </div>;
+ renderBullet() {
+ let docList = Cast(this.resolvedDataDoc[this.fieldKey], listSpec(Doc));
+ let doc = Cast(this.resolvedDataDoc[this.fieldKey], Doc);
+ let isDoc = doc instanceof Doc || docList;
+ let c;
+ return <div className="bullet" onClick={action(() => this._collapsed = !this._collapsed)} style={{ color: StrCast(this.props.document.color, "black"), opacity: 0.4 }}>
+ {<FontAwesomeIcon icon={this._collapsed ? (isDoc ? "caret-square-right" : "caret-right") : (isDoc ? "caret-square-down" : "caret-down")} />}
+ </div>;
}
- @action
- onMouseEnter = () => {
- this._isOver = true;
- }
- @observable _isOver: boolean = false;
- @action
- onMouseLeave = () => {
- this._isOver = false;
+ static loadId = "";
+ editableView = (key: string, style?: string) => (<EditableView
+ oneLine={true}
+ display={"inline"}
+ editing={this.resolvedDataDoc[Id] === TreeView.loadId}
+ contents={StrCast(this.props.document[key])}
+ height={36}
+ fontStyle={style}
+ fontSize={12}
+ GetValue={() => StrCast(this.props.document[key])}
+ SetValue={(value: string) => (Doc.GetProto(this.resolvedDataDoc)[key] = value) ? true : true}
+ OnFillDown={(value: string) => {
+ Doc.GetProto(this.resolvedDataDoc)[key] = value;
+ let doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
+ TreeView.loadId = doc[Id];
+ return this.props.addDocument(doc);
+ }}
+ OnTab={() => this.props.indentDocument && this.props.indentDocument()}
+ />)
+
+ @computed get keyList() {
+ let keys = Array.from(Object.keys(this.resolvedDataDoc));
+ if (this.resolvedDataDoc.proto instanceof Doc) {
+ keys.push(...Array.from(Object.keys(this.resolvedDataDoc.proto)));
+ }
+ let 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, 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.props.moveDocument, this.props.dropAction);
- let editableView = (titleString: string) =>
- (<EditableView
- oneLine={!this._isOver ? true : false}
- display={"inline-block"}
- contents={titleString}
- height={36}
- GetValue={() => StrCast(this.props.document.title)}
- SetValue={(value: string) => {
- let target = this.props.document.proto ? this.props.document.proto : this.props.document;
- target.title = value;
- return true;
- }}
- />);
- let dataDocs = CollectionDockingView.Instance ? Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc), []) : [];
- let openRight = dataDocs && dataDocs.indexOf(this.props.document) !== -1 ? (null) : (
+ let onItemDown = SetupDrag(reference, () => this.resolvedDataDoc, this.move, this.props.dropAction, this.props.treeViewId, true);
+
+ let headerElements = (
+ <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}
+ </span>);
+ 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" />
- {/* <FontAwesomeIcon icon="angle-right" size="lg" /> */}
</div>);
- return (
- <div className="docContainer" ref={reference} onPointerDown={onItemDown} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}
- style={{ background: BoolCast(this.props.document.protoBrush, false) ? "#06123232" : BoolCast(this.props.document.libraryBrush, false) ? "#06121212" : "0" }}
- onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
- {editableView(StrCast(this.props.document.title))}
- {openRight}
+ return <>
+ <div className="docContainer" id={`docContainer-${this.props.parentKey}`} ref={reference} onPointerDown={onItemDown}
+ style={{
+ 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"
+ }}
+ >
+ {this.editableView("title")}
{/* {<div className="delete-button" onClick={this.delete}><FontAwesomeIcon icon="trash-alt" size="xs" /></div>} */}
- </div >);
+ </div >
+ {headerElements}
+ {openRight}
+ </>;
}
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, false)) });
+ 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.displayMenu(e.pageX - 15, e.pageY - 15);
+ 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();
}
}
- onPointerEnter = (e: React.PointerEvent): void => { this.props.document.libraryBrush = true; };
- onPointerLeave = (e: React.PointerEvent): void => { this.props.document.libraryBrush = false; };
-
- render() {
- let bulletType = BulletType.List;
- let contentElement: (JSX.Element | null)[] = [];
- 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);
+ @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();
}
- keys.map(key => {
- let docList = DocListCast(this.props.document[key]);
- let doc = Cast(this.props.document[key], Doc);
- if (doc instanceof Doc || docList.length) {
- if (!this._collapsed) {
- bulletType = BulletType.Collapsible;
- let spacing = (key === "data") ? 0 : -10;
- contentElement.push(<ul key={key + "more"}>
- {(key === "data") ? (null) :
- <span className="collectionTreeView-keyHeader" style={{ display: "block", marginTop: "7px" }} key={key}>{key}</span>}
- <div style={{ display: "block", marginTop: `${spacing}px` }}>
- {TreeView.GetChildElements(doc instanceof Doc ? [doc] : docList, key !== "data", (doc: Doc) => this.remove(doc, key), this.move, this.props.dropAction, this.props.addDocTab)}
- </div>
- </ul >);
- } else {
- bulletType = BulletType.Collapsed;
+ if (de.data instanceof DragManager.DocumentDragData) {
+ e.stopPropagation();
+ if (de.data.draggedDocuments[0] === this.props.document) return true;
+ let addDoc = (doc: Doc) => this.props.addDocument(doc, this.resolvedDataDoc, before);
+ if (inside) {
+ let docList = Cast(this.resolvedDataDoc.data, listSpec(Doc));
+ if (docList !== undefined) {
+ addDoc = (doc: Doc) => { docList && docList.push(doc); return true; };
}
}
+ let movedDocs = (de.data.options === this.props.treeViewId ? de.data.draggedDocuments : de.data.droppedDocuments);
+ return (de.data.dropAction || de.data.userDropAction) ?
+ de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.resolvedDataDoc, before) || added, false)
+ : (de.data.moveDocument) ?
+ movedDocs.reduce((added: boolean, d) => de.data.moveDocument(d, this.resolvedDataDoc, addDoc) || added, false)
+ : de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.resolvedDataDoc, before), false);
+ }
+ return false;
+ }
+
+ docTransform = () => {
+ let { scale, translateX, translateY } = Utils.GetScreenTransform(this._dref.current!);
+ let outerXf = this.props.outerXf();
+ let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY);
+ let finalXf = this.props.ScreenToLocalTransform().translate(offset[0], offset[1]);
+ 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 <div className="treeViewItem-container"
- onContextMenu={this.onWorkspaceContextMenu}>
+ 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.resolvedDataDoc[this._chosenKey], listSpec(Doc));
+ let remDoc = (doc: Doc) => this.remove(doc, this._chosenKey);
+ let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.resolvedDataDoc, this._chosenKey, doc, addBefore, before);
+ let doc = Cast(this.resolvedDataDoc[this._chosenKey], Doc);
+
+ if (!this._collapsed) {
+ if (!this.props.document.embed) {
+ contentElement = <ul key={this._chosenKey + "more"}>
+ {this._chosenKey === "links" ? this.renderLinks() :
+ TreeView.GetChildElements(doc instanceof Doc ? [doc] : DocListCast(docList), this.props.treeViewId, this.props.document, this.props.dataDoc, this._chosenKey, addDoc, remDoc, this.move,
+ this.props.dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth)}
+ </ul >;
+ } else {
+ let layoutDoc = Doc.expandTemplateLayout(this.props.document, this.props.dataDoc);
+ contentElement = <div ref={this._dref} style={{ display: "inline-block", height: this.docHeight() }} key={this.props.document[Id] + this.props.document.title}>
+ <CollectionSchemaPreview
+ Document={layoutDoc}
+ DataDocument={this.resolvedDataDoc}
+ renderDepth={this.props.renderDepth}
+ fitToBox={this.boundsOfCollectionDocument !== undefined}
+ width={this.docWidth}
+ height={this.docHeight}
+ getTransform={this.docTransform}
+ CollectionView={undefined}
+ addDocument={emptyFunction as any}
+ moveDocument={this.props.moveDocument}
+ removeDocument={emptyFunction as any}
+ active={this.props.active}
+ whenActiveChanged={emptyFunction as any}
+ addDocTab={this.props.addDocTab}
+ setPreviewScript={emptyFunction}>
+ </CollectionSchemaPreview>
+ </div>;
+ }
+ }
+ return <div className="treeViewItem-container" ref={this.createTreeDropTarget} onContextMenu={this.onWorkspaceContextMenu}>
<li className="collection-child">
- {this.renderBullet(bulletType)}
- {this.renderTitle()}
- {contentElement}
+ <div className="treeViewItem-header" ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
+ {this.renderBullet()}
+ {this.renderTitle()}
+ </div>
+ <div className="treeViewItem-border">
+ {contentElement}
+ </div>
</li>
</div>;
}
- public static GetChildElements(docs: Doc[], allowMinimized: boolean, remove: ((doc: Doc) => void), move: DragManager.MoveFunction, dropAction: dropActionType, addDocTab: (doc: Doc, where: string) => void) {
- return docs.filter(child => !child.excludeFromLibrary && (allowMinimized || !child.isMinimized)).map(child =>
- <TreeView document={child} key={child[Id]} deleteDoc={remove} moveDocument={move} dropAction={dropAction} addDocTab={addDocTab} />);
+ 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, 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);
+ 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];
+ 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] + "child " + i}
+ indentDocument={indent}
+ renderDepth={renderDepth}
+ deleteDoc={remove}
+ addDocument={addDocument}
+ panelWidth={rowWidth}
+ panelHeight={rowHeight}
+ moveDocument={move}
+ dropAction={dropAction}
+ addDocTab={addDocTab}
+ ScreenToLocalTransform={screenToLocalXf}
+ outerXf={outerXf}
+ parentKey={key}
+ active={active} />;
+ });
}
}
@observer
export class CollectionTreeView extends CollectionSubView(Document) {
+ private treedropDisposer?: DragManager.DragDropDisposer;
+ private _mainEle?: HTMLDivElement;
+
+ protected createTreeDropTarget = (ele: HTMLDivElement) => {
+ this.treedropDisposer && this.treedropDisposer();
+ if (this._mainEle = ele) {
+ this.treedropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
+ }
+ }
+
+ componentWillUnmount() {
+ this.treedropDisposer && this.treedropDisposer();
+ }
+
@action
- remove = (document: Document) => {
- let children = Cast(this.props.Document.data, listSpec(Doc), []);
- if (children) {
+ remove = (document: Document): boolean => {
+ let children = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []);
+ if (children.indexOf(document) !== -1) {
children.splice(children.indexOf(document), 1);
+ return true;
}
+ return false;
}
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);
}
}
- render() {
- let dropAction = StrCast(this.props.Document.dropAction, "alias") as dropActionType;
- if (!this.childDocs) {
- return (null);
+
+ @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);
}
- let childElements = TreeView.GetChildElements(this.childDocs, false, this.remove, this.props.moveDocument, dropAction, this.props.addDocTab);
+ }
+ @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 >;
+ }
- return (
+
+ render() {
+ let dropAction = StrCast(this.props.Document.dropAction) as dropActionType;
+ let addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before);
+ let moveDoc = (d: Doc, target: Doc, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc);
+ return !this.childDocs ? (null) : (
<div id="body" className="collectionTreeView-dropTarget"
- style={{ borderRadius: "inherit" }}
+ style={{ overflow: "auto", background: StrCast(this.props.Document.backgroundColor, "lightgray") }}
onContextMenu={this.onContextMenu}
- onWheel={(e: React.WheelEvent) => this.props.isSelected() && e.stopPropagation()}
- onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}>
- <div className="coll-title">
- <EditableView
- contents={this.props.Document.title}
- display={"inline"}
- height={72}
- GetValue={() => StrCast(this.props.Document.title)}
- SetValue={(value: string) => {
- let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document;
- target.title = value;
- return true;
- }} />
- </div>
- <ul className="no-indent">
- {childElements}
+ onWheel={(e: React.WheelEvent) => (e.target as any).scrollHeight > (e.target as any).clientHeight && e.stopPropagation()}
+ onDrop={this.onTreeDrop}
+ ref={this.createTreeDropTarget}>
+ <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.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..a97aa4f36 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("", `data_l:"${this.props.Document[Id]}"`, true);
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("", `data_l:"${doc[Id]}"`, true).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 7af4f1682..b546d1b78 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -1,11 +1,10 @@
import { observer } from "mobx-react";
-import { Utils } from "../../../../Utils";
+import { Doc, HeightSym, WidthSym } from "../../../../new_fields/Doc";
+import { BoolCast, NumCast, StrCast } from "../../../../new_fields/Types";
+import { InkingControl } from "../../InkingControl";
import "./CollectionFreeFormLinkView.scss";
import React = require("react");
import v5 = require("uuid/v5");
-import { StrCast, NumCast, BoolCast } from "../../../../new_fields/Types";
-import { Doc, WidthSym, HeightSym } from "../../../../new_fields/Doc";
-import { InkingControl } from "../../InkingControl";
export interface CollectionFreeFormLinkViewProps {
A: Doc;
@@ -26,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);
@@ -45,12 +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 = "";
- this.props.LinkDocs.map(l => text += StrCast(l.title) + ", ");
- text = text.substr(0, text.length - 2);
+ // 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 a43c5f241..2d94f1b8e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -1,75 +1,72 @@
-import { computed, IReactionDisposer, reaction, trace } from "mobx";
+import { computed, IReactionDisposer, reaction } from "mobx";
import { observer } from "mobx-react";
-import { Utils } from "../../../../Utils";
+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 { Cast, FieldValue, NumCast, StrCast } from "../../../../new_fields/Types";
import { DocumentManager } from "../../../util/DocumentManager";
import { DocumentView } from "../../nodes/DocumentView";
import { CollectionViewProps } from "../CollectionSubView";
import "./CollectionFreeFormLinksView.scss";
import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView";
import React = require("react");
-import { Doc, DocListCastAsync, DocListCast } from "../../../../new_fields/Doc";
-import { Cast, FieldValue, NumCast, StrCast } from "../../../../new_fields/Types";
-import { listSpec } from "../../../../new_fields/Schema";
-import { List } from "../../../../new_fields/List";
-import { Id } from "../../../../new_fields/FieldSymbols";
@observer
export class CollectionFreeFormLinksView extends React.Component<CollectionViewProps> {
_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) {
@@ -98,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 => {
@@ -110,25 +108,21 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
}
return match || found;
}, false)) {
- console.log("A" + possiblePair.a[Id] + " B" + possiblePair.b[Id] + " L" + connection.l[Id]);
- drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] })
+ drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] });
}
});
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/CollectionFreeFormRemoteCursors.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
index 2838b7905..3193f5624 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
@@ -1,15 +1,13 @@
-import { computed } from "mobx";
import { observer } from "mobx-react";
+import * as mobxUtils from 'mobx-utils';
+import CursorField from "../../../../new_fields/CursorField";
+import { listSpec } from "../../../../new_fields/Schema";
+import { Cast } from "../../../../new_fields/Types";
+import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
import { CollectionViewProps } from "../CollectionSubView";
import "./CollectionFreeFormView.scss";
import React = require("react");
import v5 = require("uuid/v5");
-import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
-import CursorField from "../../../../new_fields/CursorField";
-import { List } from "../../../../new_fields/List";
-import { Cast } from "../../../../new_fields/Types";
-import { listSpec } from "../../../../new_fields/Schema";
-import * as mobxUtils from 'mobx-utils';
@observer
export class CollectionFreeFormRemoteCursors extends React.Component<CollectionViewProps> {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index e10ba9d7e..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,55 +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 {
- 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 419d95b5f..19e280444 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,16 +1,25 @@
-import { action, computed, trace } from "mobx";
+import { action, computed } from "mobx";
import { observer } from "mobx-react";
-import { emptyFunction, returnFalse, returnOne } from "../../../../Utils";
+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 { 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";
@@ -18,18 +27,18 @@ import "./CollectionFreeFormView.scss";
import { MarqueeView } from "./MarqueeView";
import React = require("react");
import v5 = require("uuid/v5");
-import { createSchema, makeInterface, listSpec } from "../../../../new_fields/Schema";
-import { Doc, WidthSym, HeightSym } from "../../../../new_fields/Doc";
-import { FieldValue, Cast, NumCast, BoolCast } from "../../../../new_fields/Types";
-import { pageSchema } from "../../nodes/ImageBox";
-import { InkField, StrokeData } from "../../../../new_fields/InkField";
-import { HistoryUtil } from "../../../util/History";
-import { Id } from "../../../../new_fields/FieldSymbols";
+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]>;
@@ -43,13 +52,22 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
private get _pwidth() { return this.props.PanelWidth(); }
private get _pheight() { return this.props.PanelHeight(); }
+ @computed get contentBounds() {
+ let bounds = this.props.fitToBox && !NumCast(this.nativeWidth) ? Doc.ComputeContentBounds(DocListCast(this.props.Document.data)) : undefined;
+ return {
+ panX: bounds ? (bounds.x + bounds.r) / 2 : this.Document.panX || 0,
+ panY: bounds ? (bounds.y + bounds.b) / 2 : this.Document.panY || 0,
+ scale: bounds ? Math.min(this.props.PanelHeight() / (bounds.b - bounds.y), this.props.PanelWidth() / (bounds.r - bounds.x)) : this.Document.scale || 1
+ };
+ }
+
@computed get nativeWidth() { return this.Document.nativeWidth || 0; }
@computed get nativeHeight() { return this.Document.nativeHeight || 0; }
- public get isAnnotationOverlay() { return this.props.fieldKey && 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());
@@ -77,34 +95,51 @@ 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) => {
- if (super.drop(e, de) && 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 dropX = NumCast(de.data.droppedDocuments[0].x);
- let dropY = NumCast(de.data.droppedDocuments[0].y);
- de.data.droppedDocuments.forEach(d => {
- d.x = x + NumCast(d.x) - dropX;
- d.y = y + NumCast(d.y) - dropY;
- if (!NumCast(d.width)) {
- d.width = 300;
- }
- if (!NumCast(d.height)) {
- let nw = NumCast(d.nativeWidth);
- let nh = NumCast(d.nativeHeight);
- d.height = nw && nh ? nh / nw * NumCast(d.width) : 300;
- }
- this.bringToFront(d);
- });
- SelectionManager.ReselectAll();
+ if (super.drop(e, de)) {
+ if (de.data instanceof DragManager.DocumentDragData) {
+ if (de.data.droppedDocuments.length) {
+ let dragDoc = de.data.droppedDocuments[0];
+ let [xp, yp] = this.getTransform().transformPoint(de.x, de.y);
+ 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 => {
+ d.x = x + NumCast(d.x) - dropX;
+ d.y = y + NumCast(d.y) - dropY;
+ if (!NumCast(d.width)) {
+ d.width = 300;
+ }
+ if (!NumCast(d.height)) {
+ let nw = NumCast(d.nativeWidth);
+ let nh = NumCast(d.nativeHeight);
+ d.height = nw && nh ? nh / nw * NumCast(d.width) : 300;
+ }
+ this.bringToFront(d);
+ });
+ }
+ }
+ else if (de.data instanceof DragManager.AnnotationDragData) {
+ if (de.data.dropDocument) {
+ let dragDoc = de.data.dropDocument;
+ let [xp, yp] = this.getTransform().transformPoint(de.x, de.y);
+ 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;
+ dragDoc.y = y + NumCast(dragDoc.y) - dropY;
+ this.bringToFront(dragDoc);
+ }
}
- return true;
}
return false;
}
@@ -134,19 +169,20 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
let docs = this.childDocs || [];
let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
if (!this.isAnnotationOverlay) {
+ PDFMenu.Instance.fadeOut(true);
let minx = docs.length ? NumCast(docs[0].x) : 0;
- let maxx = docs.length ? NumCast(docs[0].width) / 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);
@@ -174,14 +210,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();
@@ -200,7 +240,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();
}
@@ -217,12 +257,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.panX = panX;
+ // this.props.Document.panY = panY;
+ if (this.props.Document.scrollY) {
+ this.props.Document.scrollY = panY;
+ }
}
@action
@@ -249,6 +295,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
const panY = this.Document.panY;
const id = this.Document[Id];
const state = HistoryUtil.getState();
+ state.initializers = state.initializers || {};
// TODO This technically isn't correct if type !== "doc", as
// currently nothing is done, but we should probably push a new state
if (state.type === "doc" && panX !== undefined && panY !== undefined) {
@@ -265,10 +312,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
}
SelectionManager.DeselectAll();
- const newPanX = NumCast(doc.x) + NumCast(doc.width) / NumCast(doc.zoomBasis, 1) / 2;
- const newPanY = NumCast(doc.y) + NumCast(doc.height) / NumCast(doc.zoomBasis, 1) / 2;
+ const newPanX = NumCast(doc.x) + NumCast(doc.width) / 2;
+ const newPanY = NumCast(doc.y) + NumCast(doc.height) / 2;
const newState = HistoryUtil.getState();
- newState.initializers[id] = { panX: newPanX, panY: newPanY };
+ (newState.initializers || (newState.initializers = {}))[id] = { panX: newPanX, panY: newPanY };
HistoryUtil.pushState(newState);
this.setPan(newPanX, newPanY);
@@ -308,17 +355,44 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
- 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,
@@ -331,16 +405,36 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
};
}
+ 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) {
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;
@@ -356,32 +450,91 @@ 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>
<CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />
</CollectionFreeFormViewPannableContents>
</MarqueeView>
- <CollectionFreeFormOverlayView {...this.getDocumentViewProps(this.props.Document)} {...this.props} />
+ <CollectionFreeFormOverlayView {...this.props} {...this.getDocumentViewProps(this.props.Document)} />
</div>
);
}
@@ -391,7 +544,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;
@@ -401,8 +554,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 29734fa19..b765517a2 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -1,27 +1,24 @@
import * as htmlToImage from "html-to-image";
-import { action, computed, observable, trace } from "mobx";
+import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
+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";
+import { Cast, NumCast } from "../../../../new_fields/Types";
+import { Utils } from "../../../../Utils";
+import { DocServer } from "../../../DocServer";
import { Docs } from "../../../documents/Documents";
import { SelectionManager } from "../../../util/SelectionManager";
import { Transform } from "../../../util/Transform";
-import { undoBatch, UndoManager } from "../../../util/UndoManager";
+import { undoBatch } from "../../../util/UndoManager";
import { InkingCanvas } from "../../InkingCanvas";
import { PreviewCursor } from "../../PreviewCursor";
+import { Templates } from "../../Templates";
+import { CollectionViewType } from "../CollectionBaseView";
import { CollectionFreeFormView } from "./CollectionFreeFormView";
import "./MarqueeView.scss";
import React = require("react");
-import { Utils } from "../../../../Utils";
-import { Doc } from "../../../../new_fields/Doc";
-import { NumCast, Cast } from "../../../../new_fields/Types";
-import { InkField, StrokeData } from "../../../../new_fields/InkField";
-import { List } from "../../../../new_fields/List";
-import { ImageField } from "../../../../new_fields/URLField";
-import { Template, Templates } from "../../Templates";
-import { SearchBox } from "../../SearchBox";
-import { DocServer } from "../../../DocServer";
-import { Id } from "../../../../new_fields/FieldSymbols";
-import { CollectionView } from "../CollectionView";
-import { CollectionViewType } from "../CollectionBaseView";
interface MarqueeViewProps {
getContainerTransform: () => Transform;
@@ -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,14 @@ 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 {
- let newBox = Docs.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" });
+ } else if (!e.ctrlKey) {
+ let newBox = Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" });
+ newBox.proto!.autoHeight = true;
this.props.addLiveTextDocument(newBox);
}
e.stopPropagation();
@@ -136,7 +134,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
doc.width = 200;
docList.push(doc);
}
- let newCol = Docs.SchemaDocument([...(groupAttr ? ["_group"] : []), ...columns.filter(c => c)], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 });
+ let newCol = Docs.Create.SchemaDocument([...(groupAttr ? ["_group"] : []), ...columns.filter(c => c)], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 });
this.props.addDocument(newCol, false);
}
@@ -147,8 +145,8 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
this._downY = this._lastY = e.pageY;
this._commandExecuted = false;
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);
@@ -182,14 +180,19 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
@action
onPointerUp = (e: PointerEvent): void => {
+ if (!this.props.container.props.active()) this.props.selectDocuments([this.props.container.props.Document]);
+ // console.log("pointer up!");
if (this._visible) {
+ // console.log("visible");
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]);
}
+ //console.log("invisible");
this.cleanupInteractions(true);
+
if (e.altKey) {
e.preventDefault();
}
@@ -221,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) => {
@@ -232,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();
@@ -256,23 +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",
- scale: zoomBasis,
- width: bounds.width * zoomBasis,
- height: bounds.height * zoomBasis,
- ink: inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined,
- title: e.key === "s" || e.key === "S" ? "-summary-" : e.key === "p" ? "-summary-" : "a nested collection",
+ width: bounds.width,
+ height: bounds.height,
+ title: e.key === "s" || e.key === "S" ? "-summary-" : "a nested collection",
});
- newCollection.zoomBasis = zoomBasis;
+ newCollection.data_ink = inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined;
this.marqueeInkDelete(inkData);
if (e.key === "s") {
@@ -283,51 +292,37 @@ 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);
// });
} else if (e.key === "S") {
- await htmlToImage.toPng(this._mainCont.current!, { width: bounds.width * zoomBasis, height: bounds.height * zoomBasis, quality: 0.2 }).then((dataUrl) => {
- selected.map(d => {
- this.props.removeDocument(d);
- d.x = NumCast(d.x) - bounds.left - bounds.width / 2;
- d.y = NumCast(d.y) - bounds.top - bounds.height / 2;
- d.page = -1;
- return d;
- });
- let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
- SearchBox.convertDataUri(dataUrl, "icon" + summary[Id] + "_image").then((returnedFilename) => {
- if (returnedFilename) {
- let url = DocServer.prepend(returnedFilename);
- let imageSummary = Docs.ImageDocument(url, {
- x: bounds.left, y: bounds.top + 100 / zoomBasis,
- width: 150, height: bounds.height / bounds.width * 150, title: "-summary image-"
- });
- summary.imageSummary = imageSummary;
- this.props.addDocument(imageSummary, false);
- }
- })
- newCollection.proto!.summaryDoc = summary;
- selected = [newCollection];
- newCollection.x = bounds.left + bounds.width;
- //this.props.addDocument(newCollection, false);
- summary.proto!.summarizedDocs = new List<Doc>(selected);
- summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight"
-
- this.props.addLiveTextDocument(summary);
+ selected.map(d => {
+ this.props.removeDocument(d);
+ d.x = NumCast(d.x) - bounds.left - bounds.width / 2;
+ d.y = NumCast(d.y) - bounds.top - bounds.height / 2;
+ d.page = -1;
+ return d;
});
+ 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;
+ //this.props.addDocument(newCollection, false);
+ summary.proto!.summarizedDocs = new List<Doc>(selected);
+ summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight"
+
+ this.props.addLiveTextDocument(summary);
}
else {
this.props.addDocument(newCollection, false);
- SelectionManager.DeselectAll();
this.props.selectDocuments([newCollection]);
}
this.cleanupInteractions(false);
@@ -363,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 499b83c0f..b09538d1a 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,19 +1,19 @@
-import { computed, IReactionDisposer, reaction, action } from "mobx";
+import { computed } from "mobx";
import { observer } from "mobx-react";
-import { Doc } from "../../../new_fields/Doc";
-import { List } from "../../../new_fields/List";
-import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema";
-import { BoolCast, Cast, FieldValue, NumCast } from "../../../new_fields/Types";
-import { OmitKeys } from "../../../Utils";
+import { createSchema, makeInterface } from "../../../new_fields/Schema";
+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 { UndoManager } from "../../util/UndoManager";
-import { SelectionManager } from "../../util/SelectionManager";
+import { Doc } from "../../../new_fields/Doc";
export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
+ x?: number;
+ y?: number;
+ width?: number;
+ height?: number;
}
const schema = createSchema({
@@ -27,20 +27,14 @@ const FreeformDocument = makeInterface(schema, positionSchema);
@observer
export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, FreeformDocument>(FreeformDocument) {
- private _mainCont = React.createRef<HTMLDivElement>();
- _bringToFrontDisposer?: IReactionDisposer;
-
- @computed get transform() {
- return `scale(${this.props.ContentScaling()}, ${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) scale(${this.zoom}, ${this.zoom}) `;
- }
-
- @computed get X() { return FieldValue(this.Document.x, 0); }
- @computed get Y() { return FieldValue(this.Document.y, 0); }
+ @computed get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) scale(${this.zoom}) `; }
+ @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;
@@ -54,135 +48,60 @@ 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)
-
- @computed
- get docView() {
- return <DocumentView {...OmitKeys(this.props, ['zoomFade']).omit}
- ContentScaling={this.contentScaling}
- ScreenToLocalTransform={this.getTransform}
- PanelWidth={this.panelWidth}
- PanelHeight={this.panelHeight}
- collapseToPoint={this.collapseToPoint}
- />;
- }
-
- componentDidMount() {
- this._bringToFrontDisposer = reaction(() => this.props.Document.isIconAnimating, (values) => {
- this.props.bringToFront(this.props.Document);
- if (values instanceof List) {
- let scrpt = this.props.ScreenToLocalTransform().transformPoint(values[0], values[1]);
- this.animateBetweenIcon(true, scrpt, [this.Document.x || 0, this.Document.y || 0],
- this.Document.width || 0, this.Document.height || 0, values[2], values[3] ? true : false);
- }
- }, { fireImmediately: true });
- }
-
- componentWillUnmount() {
- if (this._bringToFrontDisposer) this._bringToFrontDisposer();
- }
-
- static _undoBatch?: UndoManager.Batch = undefined;
- @action
- public collapseToPoint = async (scrpt: number[], expandedDocs: Doc[] | undefined): Promise<void> => {
- SelectionManager.DeselectAll();
- if (expandedDocs) {
- if (!CollectionFreeFormDocumentView._undoBatch) {
- CollectionFreeFormDocumentView._undoBatch = UndoManager.StartBatch("iconAnimating");
- }
- let isMinimized: boolean | undefined;
- expandedDocs.map(d => Doc.GetProto(d)).map(maximizedDoc => {
- let iconAnimating = Cast(maximizedDoc.isIconAnimating, List);
- if (!iconAnimating || (Date.now() - iconAnimating[2] > 1000)) {
- if (isMinimized === undefined) {
- isMinimized = BoolCast(maximizedDoc.isMinimized, false);
- }
- maximizedDoc.willMaximize = isMinimized;
- maximizedDoc.isMinimized = false;
- maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], Date.now(), isMinimized ? 1 : 0]);
- }
+ .scale(1 / this.contentScaling()).scale(1 / this.zoom / this.scaleToOverridingWidth)
+
+ animateBetweenIcon = (icon: number[], stime: number, maximizing: boolean) => {
+ this.props.bringToFront(this.props.Document);
+ let targetPos = [this.Document.x || 0, this.Document.y || 0];
+ let iconPos = this.props.ScreenToLocalTransform().transformPoint(icon[0], icon[1]);
+ DocumentView.animateBetweenIconFunc(this.props.Document,
+ this.Document.width || 0, this.Document.height || 0, stime, maximizing, (progress: number) => {
+ let pval = maximizing ?
+ [iconPos[0] + (targetPos[0] - iconPos[0]) * progress, iconPos[1] + (targetPos[1] - iconPos[1]) * progress] :
+ [targetPos[0] + (iconPos[0] - targetPos[0]) * progress, targetPos[1] + (iconPos[1] - targetPos[1]) * progress];
+ this.Document.x = progress === 1 ? targetPos[0] : pval[0];
+ this.Document.y = progress === 1 ? targetPos[1] : pval[1];
});
- setTimeout(() => {
- CollectionFreeFormDocumentView._undoBatch && CollectionFreeFormDocumentView._undoBatch.end();
- CollectionFreeFormDocumentView._undoBatch = undefined;
- }, 500);
- }
- }
-
- animateBetweenIcon(first: boolean, icon: number[], targ: number[], width: number, height: number, stime: number, maximizing: boolean) {
-
- setTimeout(() => {
- let now = Date.now();
- let progress = Math.min(1, (now - stime) / 200);
- let pval = maximizing ?
- [icon[0] + (targ[0] - icon[0]) * progress, icon[1] + (targ[1] - icon[1]) * progress] :
- [targ[0] + (icon[0] - targ[0]) * progress, targ[1] + (icon[1] - targ[1]) * progress];
- this.props.Document.width = maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress;
- this.props.Document.height = maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress;
- this.props.Document.x = pval[0];
- this.props.Document.y = pval[1];
- if (first) {
- this.props.Document.proto!.willMaximize = false;
- }
- if (now < stime + 200) {
- this.animateBetweenIcon(false, icon, targ, width, height, stime, maximizing);
- }
- else {
- if (!maximizing) {
- this.props.Document.proto!.isMinimized = true;
- this.props.Document.x = targ[0];
- this.props.Document.y = targ[1];
- this.props.Document.width = width;
- this.props.Document.height = height;
- }
- this.props.Document.proto!.isIconAnimating = undefined;
- }
- },
- 2);
}
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() {
- let maximizedDoc = FieldValue(Cast(this.props.Document.maximizedDocs, listSpec(Doc)));
- let zoomFade = 1;
- //var zoom = doc.GetNumber(KeyStore.ZoomBasis, 1);
- // let transform = this.getTransform().scale(this.contentScaling()).inverse();
- // var [sptX, sptY] = transform.transformPoint(0, 0);
- // let [bptX, bptY] = transform.transformPoint(this.props.PanelWidth(), this.props.PanelHeight());
- // let w = bptX - sptX;
- //zoomFade = area < 100 || area > 800 ? Math.max(0, Math.min(1, 2 - 5 * (zoom < this.scale ? this.scale / zoom : zoom / this.scale))) : 1;
- const screenWidth = Math.min(50 * NumCast(this.props.Document.nativeWidth, 0), 1800);
- let fadeUp = .75 * screenWidth;
- let fadeDown = (maximizedDoc ? .0075 : .075) * screenWidth;
- // zoomFade = w < fadeDown /* || w > fadeUp */ ? Math.max(0.1, Math.min(1, 2 - (w < fadeDown ? Math.sqrt(Math.sqrt(fadeDown / w)) : w / fadeUp))) : 1;
-
return (
- <div className="collectionFreeFormDocumentView-container" ref={this._mainCont}
+ <div className="collectionFreeFormDocumentView-container"
style={{
- opacity: zoomFade,
- borderRadius: `${this.borderRounding()}px`,
transformOrigin: "left top",
+ position: "absolute",
+ backgroundColor: "transparent",
+ borderRadius: this.borderRounding(),
transform: this.transform,
- pointerEvents: (zoomFade < 0.09 ? "none" : "all"),
+ transition: StrCast(this.props.Document.transition),
width: this.width,
height: this.height,
- position: "absolute",
zIndex: this.Document.zIndex || 0,
- backgroundColor: "transparent"
}} >
- {this.docView}
+ <DocumentView {...this.props}
+ ContentScaling={this.contentScaling}
+ ScreenToLocalTransform={this.getTransform}
+ PanelWidth={this.panelWidth}
+ PanelHeight={this.panelHeight}
+ animateBetweenIcon={this.animateBetweenIcon}
+ />
</div>
);
}
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index d242d8fad..fa8d5dca3 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -24,6 +24,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'>;
@@ -48,11 +51,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 {
@@ -60,8 +63,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> {
@@ -72,39 +90,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, YoutubeBox }}
+ components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, YoutubeBox }}
bindings={this.CreateBindings()}
jsx={this.finalLayout}
showWarnings={true}
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 7c72fb6e6..3a4b46b7e 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -4,6 +4,7 @@
position: inherit;
top: 0;
left:0;
+ pointer-events: all;
// background: $light-color; //overflow: hidden;
transform-origin: left top;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index fb7657b68..970ef24d8 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,16 +1,16 @@
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faAlignCenter, faCaretSquareRight, faCompressArrowsAlt, faExpandArrowsAlt, faLayerGroup, faSquare, faTrash, faConciergeBell, faFolder, faMapPin, faLink, faFingerprint, faCrosshairs, faDesktop } from '@fortawesome/free-solid-svg-icons';
-import { action, computed, IReactionDisposer, reaction } from "mobx";
+import * as fa from '@fortawesome/free-solid-svg-icons';
+import { action, computed, IReactionDisposer, reaction, trace, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocListCastAsync } from "../../../new_fields/Doc";
import { List } from "../../../new_fields/List";
import { ObjectField } from "../../../new_fields/ObjectField";
-import { createSchema, makeInterface } from "../../../new_fields/Schema";
-import { BoolCast, Cast, FieldValue, StrCast, NumCast } from "../../../new_fields/Types";
+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";
@@ -24,48 +24,62 @@ import { CollectionView } from "../collections/CollectionView";
import { ContextMenu } from "../ContextMenu";
import { DocComponent } from "../DocComponent";
import { PresentationView } from "../presentationview/PresentationView";
-import { Template } from "./../Templates";
+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(faTrash);
-library.add(faExpandArrowsAlt);
-library.add(faCompressArrowsAlt);
-library.add(faLayerGroup);
-library.add(faAlignCenter);
-library.add(faCaretSquareRight);
-library.add(faSquare);
-library.add(faConciergeBell);
-library.add(faFolder);
-library.add(faMapPin);
-library.add(faLink);
-library.add(faFingerprint);
-library.add(faCrosshairs);
-library.add(faDesktop);
-
-const linkSchema = createSchema({
- title: "string",
- linkDescription: "string",
- linkTags: "string",
- linkedTo: Doc,
- linkedFrom: Doc
-});
+library.add(fa.faTrash);
+library.add(fa.faShare);
+library.add(fa.faExpandArrowsAlt);
+library.add(fa.faCompressArrowsAlt);
+library.add(fa.faLayerGroup);
+library.add(fa.faExternalLinkAlt);
+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);
+library.add(fa.faFingerprint);
+library.add(fa.faCrosshairs);
+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);
+// 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;
@@ -74,10 +88,11 @@ export interface DocumentViewProps {
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;
}
const schema = createSchema({
@@ -85,7 +100,8 @@ const schema = createSchema({
nativeWidth: "number",
nativeHeight: "number",
backgroundColor: "string",
- opacity: "number"
+ opacity: "number",
+ hidden: "boolean"
});
export const positionSchema = createSchema({
@@ -115,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) {
@@ -126,6 +142,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
set templates(templates: List<string>) { this.props.Document.templates = templates; }
screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect();
+ constructor(props: DocumentViewProps) {
+ super(props);
+ }
+
+ _animateToIconDisposer?: IReactionDisposer;
_reactionDisposer?: IReactionDisposer;
@action
componentDidMount() {
@@ -147,8 +168,35 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
this.props.Document.proto!.title = "-" + sumDoc.title + ".expanded";
}
}, { fireImmediately: true });
+ this._animateToIconDisposer = reaction(() => this.props.Document.isIconAnimating, (values) =>
+ (values instanceof List) && this.animateBetweenIcon(values, values[2], values[3] ? true : false)
+ , { fireImmediately: true });
DocumentManager.Instance.DocumentViews.push(this);
}
+
+ animateBetweenIcon = (iconPos: number[], startTime: number, maximizing: boolean) => {
+ this.props.animateBetweenIcon ? this.props.animateBetweenIcon(iconPos, startTime, maximizing) :
+ DocumentView.animateBetweenIconFunc(this.props.Document, this.Document[WidthSym](), this.Document[HeightSym](), startTime, maximizing);
+ }
+
+ public static animateBetweenIconFunc = (doc: Doc, width: number, height: number, stime: number, maximizing: boolean, cb?: (progress: number) => void) => {
+ setTimeout(() => {
+ let now = Date.now();
+ let progress = now < stime + 200 ? Math.min(1, (now - stime) / 200) : 1;
+ doc.width = progress === 1 ? width : maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress;
+ doc.height = progress === 1 ? height : maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress;
+ cb && cb(progress);
+ if (now < stime + 200) {
+ DocumentView.animateBetweenIconFunc(doc, width, height, stime, maximizing, cb);
+ }
+ else {
+ Doc.GetProto(doc).isMinimized = !maximizing;
+ Doc.GetProto(doc).isIconAnimating = undefined;
+ }
+ Doc.GetProto(doc).willMaximize = false;
+ },
+ 2);
+ }
@action
componentDidUpdate() {
if (this._dropDisposer) {
@@ -163,6 +211,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@action
componentWillUnmount() {
if (this._reactionDisposer) this._reactionDisposer();
+ if (this._animateToIconDisposer) this._animateToIconDisposer();
if (this._dropDisposer) this._dropDisposer();
DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1);
}
@@ -171,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;
@@ -192,9 +251,36 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
toggleMinimized = async () => {
let minimizedDoc = await Cast(this.props.Document.minimizedDoc, Doc);
if (minimizedDoc) {
- let scrpt = this.props.ScreenToLocalTransform().inverse().transformPoint(
+ let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(
NumCast(minimizedDoc.x) - NumCast(this.Document.x), NumCast(minimizedDoc.y) - NumCast(this.Document.y));
- this.props.collapseToPoint && this.props.collapseToPoint(scrpt, await DocListCastAsync(minimizedDoc.maximizedDocs));
+ this.collapseTargetsToPoint(scrpt, await DocListCastAsync(minimizedDoc.maximizedDocs));
+ }
+ }
+
+ static _undoBatch?: UndoManager.Batch = undefined;
+ @action
+ 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(maximizedDoc => {
+ let iconAnimating = Cast(maximizedDoc.isIconAnimating, List);
+ if (!iconAnimating || (Date.now() - iconAnimating[2] > 1000)) {
+ if (isMinimized === undefined) {
+ isMinimized = BoolCast(maximizedDoc.isMinimized, false);
+ }
+ maximizedDoc.willMaximize = isMinimized;
+ maximizedDoc.isMinimized = false;
+ maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], Date.now(), isMinimized ? 1 : 0]);
+ }
+ });
+ setTimeout(() => {
+ DocumentView._undoBatch && DocumentView._undoBatch.end();
+ DocumentView._undoBatch = undefined;
+ }, 500);
}
}
@@ -202,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 &&
@@ -217,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;
@@ -228,8 +315,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
let expandedProtoDocs = expandedDocs.map(doc => Doc.GetProto(doc));
let maxLocation = StrCast(this.props.Document.maximizeLocation, "inPlace");
let getDispDoc = (target: Doc) => Object.getOwnPropertyNames(target).indexOf("isPrototype") === -1 ? target : Doc.MakeDelegate(target);
- if (altKey) {
- maxLocation = this.props.Document.maximizeLocation = (maxLocation === "inPlace" || !maxLocation ? "inTab" : "inPlace");
+ if (altKey || ctrlKey) {
+ maxLocation = this.props.Document.maximizeLocation = (ctrlKey ? maxLocation : (maxLocation === "inPlace" || !maxLocation ? "inTab" : "inPlace"));
if (!maxLocation || maxLocation === "inPlace") {
let hadView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedProtoDocs[0], this.props.ContainingCollectionView);
let wasMinimized = !hadView && expandedDocs.reduce((min, d) => !min && !BoolCast(d.IsMinimized, false), false);
@@ -246,24 +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().inverse().transformPoint(NumCast(this.Document.width) / 2, NumCast(this.Document.height) / 2);
- this.props.collapseToPoint && this.props.collapseToPoint(scrpt, expandedProtoDocs);
+ 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]];
-
- 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];
+ 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]];
+
+ // @TODO: shouldn't always follow target context
+ let linkedFwdContextDocs = [first.length ? await (first[0].targetContext) as Doc : undefined, undefined];
+
+ let linkedFwdPage = [first.length ? NumCast(first[0].linkedToPage, undefined) : undefined, undefined];
+
if (!linkedFwdDocs.some(l => l instanceof Promise)) {
- let maxLocation = StrCast(linkedFwdDocs[altKey ? 1 : 0].maximizeLocation, "inTab");
- DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, document => this.props.addDocTab(document, maxLocation), linkedFwdPage[altKey ? 1 : 0]);
+ 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, false, document => this.props.addDocTab(document, undefined, maxLocation), linkedFwdPage[altKey ? 1 : 0], targetContext);
}
}
}
@@ -273,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 (!e.altKey && !this.topMost && e.buttons === 1) {
+ 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);
}
}
@@ -304,32 +393,56 @@ 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);
- if (StrCast(doc.layout).indexOf("Formatted") !== -1) { // only need to freeze the dimensions of text boxes since they don't have a native width and height naturally
- if (doc.isButton && !doc.nativeWidth) {
+ if (doc.isButton) {
+ if (!doc.nativeWidth) {
doc.nativeWidth = this.props.Document[WidthSym]();
doc.nativeHeight = this.props.Document[HeightSym]();
- } else {
- doc.nativeWidth = doc.nativeHeight = undefined;
}
+ } else {
+ 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;
+ e.stopPropagation();
if (de.mods === "AltKey") {
const protoDest = destDoc.proto;
const protoSrc = sourceDoc.proto;
@@ -340,10 +453,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
dst.nativeHeight = src.nativeHeight;
}
else {
- DocUtils.MakeLink(sourceDoc, destDoc);
+ // 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);
+ let linkDoc = DocUtils.MakeLink(sourceDoc, destDoc, this.props.ContainingCollectionView ? this.props.ContainingCollectionView.props.Document : undefined);
de.data.droppedDocuments.push(destDoc);
+ de.data.linkDocument = linkDoc;
}
- e.stopPropagation();
}
}
@@ -375,29 +490,33 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
this.templates = this.templates;
}
+ @action
+ clearTemplates = () => {
+ this.templates.length = 0;
+ this.templates = this.templates;
+ }
- freezeNativeDimensions = (e: React.MouseEvent): void => {
- if (NumCast(this.props.Document.nativeWidth)) {
- let proto = Doc.GetProto(this.props.Document);
- let nw = proto.nativeWidth;
- let nh = proto.nativeHeight;
- proto.nativeWidth = proto.nativeHeight = undefined;
- this.props.Document.width = this.props.Document.frozenWidth;
- this.props.Document.height = this.props.Document.frozenHeight;
- }
- else {
- let scale = this.props.ScreenToLocalTransform().Scale * NumCast(this.props.Document.zoomBasis, 1);
- let scr = this.screenRect();
- let proto = Doc.GetProto(this.props.Document);
- this.props.Document.frozenWidth = this.props.Document.width;
- this.props.Document.frozenHeight = this.props.Document.height;
- this.props.Document.height = proto.nativeHeight = scr.height * scale;
- this.props.Document.width = proto.nativeWidth = scr.width * scale;
+ @undoBatch
+ @action
+ freezeNativeDimensions = (): void => {
+ let proto = Doc.GetProto(this.props.Document);
+ if (proto.ignoreAspect === undefined && !proto.nativeWidth) {
+ proto.nativeWidth = this.props.PanelWidth();
+ proto.nativeHeight = this.props.PanelHeight();
+ proto.ignoreAspect = true;
}
+ proto.ignoreAspect = !BoolCast(proto.ignoreAspect, false);
}
+ @undoBatch
@action
- onContextMenu = (e: React.MouseEvent): void => {
+ toggleLockPosition = (): void => {
+ this.props.Document.lockedPosition = BoolCast(this.props.Document.lockedPosition) ? undefined : true;
+ }
+
+ @action
+ onContextMenu = async (e: React.MouseEvent): Promise<void> => {
+ e.persist();
e.stopPropagation();
if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3 ||
e.isDefaultPrevented()) {
@@ -409,72 +528,150 @@ 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 });
- cm.addItem({ description: NumCast(this.props.Document.nativeWidth) ? "Unfreeze" : "Freeze", event: this.freezeNativeDimensions, icon: "edit" });
+ 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.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, false), icon: "crosshairs" });
cm.addItem({ description: "Copy URL", event: () => Utils.CopyText(DocServer.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" });
- if (!this.topMost) {
- // DocumentViews should stop propagation of this event
- e.stopPropagation();
- }
- ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
- if (!SelectionManager.IsSelected(this)) {
- SelectionManager.SelectDoc(this, false);
+ type User = { email: string, userDocumentId: string };
+ let usersMenu: ContextMenuProps[] = [];
+ try {
+ let stuff = await rp.get(DocServer.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) {
+ // DocumentViews should stop propagation of this event
+ e.stopPropagation();
+ }
+ ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
+ if (!SelectionManager.IsSelected(this)) {
+ SelectionManager.SelectDoc(this, false);
+ }
+ });
}
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);
- select = (ctrlPressed: boolean) => SelectionManager.SelectDoc(this, ctrlPressed);
+ @action select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); };
@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} layoutKey={"layout"} />); }
+ @computed get contents() {
+ return (<DocumentContentsView {...this.props} isSelected={this.isSelected} select={this.select} selectOnLoad={this.props.selectOnLoad} layoutKey={"layout"} DataDoc={this.dataDoc} />);
+ }
render() {
- var scaling = this.props.ContentScaling();
- var nativeHeight = this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%";
+ if (this.Document.hidden) {
+ return null;
+ }
+ 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: NumCast(this.props.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" }}>
+ {!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 style={{ width: "100%", height: showTextTitle ? "calc(100% - 25px)" : "100%", display: "inline-block", position: showTextTitle ? "relative" : "absolute" }}>
+ {this.contents}
+ </div>
+ </div>
+ }
</div>
);
}
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 3047d55a3..ea6730cd0 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -1,25 +1,23 @@
import React = require("react");
+import { computed } from "mobx";
import { observer } from "mobx-react";
-import { computed, observable } from "mobx";
-import { FormattedTextBox } from "./FormattedTextBox";
-import { ImageBox } from "./ImageBox";
-import { VideoBox } from "./VideoBox";
-import { AudioBox } from "./AudioBox";
-import { DocumentContentsView } from "./DocumentContentsView";
+import { DateField } from "../../../new_fields/DateField";
+import { Doc, FieldResult, Opt } from "../../../new_fields/Doc";
+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 { Transform } from "../../util/Transform";
-import { returnFalse, emptyFunction, returnOne } from "../../../Utils";
-import { CollectionView } from "../collections/CollectionView";
import { CollectionPDFView } from "../collections/CollectionPDFView";
import { CollectionVideoView } from "../collections/CollectionVideoView";
+import { CollectionView } from "../collections/CollectionView";
+import { AudioBox } from "./AudioBox";
+import { FormattedTextBox } from "./FormattedTextBox";
import { IconBox } from "./IconBox";
-import { Opt, Doc, FieldResult } from "../../../new_fields/Doc";
-import { List } from "../../../new_fields/List";
-import { ImageField, VideoField, AudioField } from "../../../new_fields/URLField";
-import { IconField } from "../../../new_fields/IconField";
-import { RichTextField } from "../../../new_fields/RichTextField";
-import { DateField } from "../../../new_fields/DateField";
-import { NumCast } from "../../../new_fields/Types";
-
+import { ImageBox } from "./ImageBox";
+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 { NumCast } from "../../../new_fields/Types";
//
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;
@@ -45,12 +47,13 @@ export interface FieldViewProps {
PanelWidth: () => number;
PanelHeight: () => number;
setVideoBox?: (player: VideoBox) => void;
+ setPdfBox?: (player: PDFBox) => void;
}
@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
@@ -70,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} />;
@@ -84,34 +87,32 @@ export class FieldView extends React.Component<FieldViewProps> {
return <p>{field.date.toLocaleString()}</p>;
}
else if (field instanceof Doc) {
- let returnHundred = () => 100;
- return (
- <DocumentContentsView Document={field}
- addDocument={undefined}
- addDocTab={this.props.addDocTab}
- removeDocument={undefined}
- ScreenToLocalTransform={Transform.Identity}
- ContentScaling={returnOne}
- PanelWidth={returnHundred}
- PanelHeight={returnHundred}
- isTopMost={true} //TODO Why is this top most?
- selectOnLoad={false}
- focus={emptyFunction}
- isSelected={this.props.isSelected}
- select={returnFalse}
- layoutKey={"layout"}
- ContainingCollectionView={this.props.ContainingCollectionView}
- parentActive={this.props.active}
- whenActiveChanged={this.props.whenActiveChanged}
- bringToFront={emptyFunction}
- zoomToScale={emptyFunction}
- getScale={returnOne}
- />
- );
+ return <p><b>{field.title + " : id= " + field[Id]}</b></p>;
+ // let returnHundred = () => 100;
+ // return (
+ // <DocumentContentsView Document={field}
+ // addDocument={undefined}
+ // addDocTab={this.props.addDocTab}
+ // removeDocument={undefined}
+ // ScreenToLocalTransform={Transform.Identity}
+ // ContentScaling={returnOne}
+ // PanelWidth={returnHundred}
+ // PanelHeight={returnHundred}
+ // renderDepth={0} //TODO Why is renderDepth reset?
+ // selectOnLoad={false}
+ // focus={emptyFunction}
+ // isSelected={this.props.isSelected}
+ // select={returnFalse}
+ // layoutKey={"layout"}
+ // ContainingCollectionView={this.props.ContainingCollectionView}
+ // parentActive={this.props.active}
+ // whenActiveChanged={this.props.whenActiveChanged}
+ // bringToFront={emptyFunction} />
+ // );
}
else if (field instanceof List) {
return (<div>
- {field.map(f => f instanceof Doc ? f.title : f.toString()).join(", ")}
+ {field.map(f => f instanceof Doc ? f.title : (f && f.toString && f.toString())).join(", ")}
</div>);
}
// bcz: this belongs here, but it doesn't render well so taking it out for now
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 5c635cc0c..45e7171d2 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -1,58 +1,53 @@
import { library } from '@fortawesome/fontawesome-svg-core';
import { faEdit, faSmile } from '@fortawesome/free-solid-svg-icons';
-import { action, IReactionDisposer, observable, reaction } 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";
import { keymap } from "prosemirror-keymap";
+import { NodeType } from 'prosemirror-model';
import { EditorState, Plugin, Transaction } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
-import { Doc, Field, Opt, WidthSym, HeightSym } from "../../../new_fields/Doc";
+import { Doc, Opt } from "../../../new_fields/Doc";
+import { Id, Copy } from '../../../new_fields/FieldSymbols';
+import { List } from '../../../new_fields/List';
import { RichTextField } from "../../../new_fields/RichTextField";
-import { createSchema, makeInterface } from "../../../new_fields/Schema";
-import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema";
+import { BoolCast, Cast, NumCast, StrCast, DateCast } from "../../../new_fields/Types";
import { DocServer } from "../../DocServer";
-import { DocumentManager } from "../../util/DocumentManager";
+import { Docs } from '../../documents/Documents';
+import { DocumentManager } from '../../util/DocumentManager';
import { DragManager } from "../../util/DragManager";
-import buildKeymap from "../../util/ProsemirrorKeymap";
+import buildKeymap from "../../util/ProsemirrorExampleTransfer";
import { inpRules } from "../../util/RichTextRules";
-import { ImageResizeView, schema } from "../../util/RichTextSchema";
+import { ImageResizeView, schema, SummarizedView } from "../../util/RichTextSchema";
import { SelectionManager } from "../../util/SelectionManager";
import { TooltipLinkingMenu } from "../../util/TooltipLinkingMenu";
import { TooltipTextMenu } from "../../util/TooltipTextMenu";
import { undoBatch, UndoManager } from "../../util/UndoManager";
import { ContextMenu } from "../../views/ContextMenu";
-import { CollectionDockingView } from "../collections/CollectionDockingView";
+import { ContextMenuProps } from '../ContextMenuItem';
import { DocComponent } from "../DocComponent";
import { InkingControl } from "../InkingControl";
+import { Templates } from '../Templates';
import { FieldView, FieldViewProps } from "./FieldView";
import "./FormattedTextBox.scss";
import React = require("react");
-import { DocUtils } from '../../documents/Documents';
+import { DateField } from '../../../new_fields/DateField';
+import { thisExpression } from 'babel-types';
library.add(faEdit);
library.add(faSmile);
// FormattedTextBox: Displays an editable plain text node that maps to a specified Key of a Document
//
-// HTML Markup: <FormattedTextBox Doc={Document's ID} FieldKey={Key's name}
-//
-// In Code, the node's HTML is specified in the document's parameterized structure as:
-// document.SetField(KeyStore.Layout, "<FormattedTextBox doc={doc} fieldKey={<KEYNAME>Key} />");
-// and the node's binding to the specified document KEYNAME as:
-// document.SetField(KeyStore.LayoutKeys, new ListField([KeyStore.<KEYNAME>]));
-// The Jsx parser at run time will bind:
-// 'fieldKey' property to the Key stored in LayoutKeys
-// and 'doc' property to the document that is being rendered
-//
-// When rendered() by React, this extracts the TextController from the Document stored at the
-// specified Key and assigns it to an HTML input node. When changes are made to this node,
-// this will edit the document and assign the new value to that field.
-//]
export interface FormattedTextBoxProps {
isOverlay?: boolean;
hideOnLeave?: boolean;
+ height?: string;
+ color?: string;
+ outer_div?: (domminus: HTMLElement) => void;
}
const richTextSchema = createSchema({
@@ -68,68 +63,138 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
return FieldView.LayoutString(FormattedTextBox, fieldStr);
}
private _ref: React.RefObject<HTMLDivElement>;
- private _proseRef: React.RefObject<HTMLDivElement>;
+ private _outerdiv?: (dominus: HTMLElement) => void;
+ private _proseRef?: HTMLDivElement;
private _editorView: Opt<EditorView>;
- private _gotDown: boolean = false;
- private _dropDisposer?: DragManager.DragDropDisposer;
+ private _toolTipTextMenu: TooltipTextMenu | undefined = undefined;
+ private _applyingChange: boolean = false;
+ private _linkClicked = "";
private _reactionDisposer: Opt<IReactionDisposer>;
- private _inputReactionDisposer: Opt<IReactionDisposer>;
+ private _textReactionDisposer: Opt<IReactionDisposer>;
private _proxyReactionDisposer: Opt<IReactionDisposer>;
+ private dropDisposer?: DragManager.DragDropDisposer;
public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
+ @observable _entered = false;
@observable public static InputBoxOverlay?: FormattedTextBox = undefined;
public static InputBoxOverlayScroll: number = 0;
+ public static IsFragment(html: string) {
+ return html.indexOf("data-pm-slice") !== -1;
+ }
+ public static GetHref(html: string): string {
+ let parser = new DOMParser();
+ let parsedHtml = parser.parseFromString(html, 'text/html');
+ if (parsedHtml.body.childNodes.length === 1 && parsedHtml.body.childNodes[0].childNodes.length === 1 &&
+ (parsedHtml.body.childNodes[0].childNodes[0] as any).href) {
+ return (parsedHtml.body.childNodes[0].childNodes[0] as any).href;
+ }
+ return "";
+ }
+ public static GetDocFromUrl(url: string) {
+ if (url.startsWith(document.location.origin)) {
+ 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();
- this._proseRef = React.createRef();
if (this.props.isOverlay) {
DragManager.StartDragFunctions.push(() => FormattedTextBox.InputBoxOverlay = undefined);
}
}
- _applyingChange: boolean = false;
+ @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); }
- _lastState: any = undefined;
dispatchTransaction = (tx: Transaction) => {
if (this._editorView) {
- const state = this._lastState = this._editorView.state.apply(tx);
+ 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 ? "..." : "");
}
}
}
+ protected createDropTarget = (ele: HTMLDivElement) => {
+ this._proseRef = ele;
+ if (this.dropDisposer) {
+ this.dropDisposer();
+ }
+ if (ele) {
+ this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
+ }
+ }
+
@undoBatch
@action
drop = async (e: Event, de: DragManager.DropEvent) => {
- if (de.data instanceof DragManager.LinkDragData) {
- let sourceDoc = de.data.linkSourceDocument;
- let destDoc = this.props.Document;
-
- DocUtils.MakeLink(sourceDoc, destDoc);
- de.data.droppedDocuments.push(destDoc);
+ // We're dealing with a link to a document
+ if (de.data instanceof DragManager.EmbedDragData && de.data.urlField) {
+ // We're dealing with an internal document drop
+ let url = de.data.urlField.url.href;
+ let model: NodeType = (url.includes(".mov") || url.includes(".mp4")) ? schema.nodes.video : schema.nodes.image;
+ this._editorView!.dispatch(this._editorView!.state.tr.insert(0, model.create({ src: url })));
e.stopPropagation();
+ } else {
+ if (de.data instanceof DragManager.DocumentDragData) {
+ 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;
+ // });
+ // }
+ }
}
}
componentDidMount() {
- if (this._ref.current) {
- this._dropDisposer = DragManager.MakeDropTarget(this._ref.current, {
- handlers: { drop: this.drop.bind(this) }
- });
- }
const config = {
schema,
inpRules, //these currently don't do anything, but could eventually be helpful
@@ -151,97 +216,118 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
]
};
- if (this.props.isOverlay) {
- this._inputReactionDisposer = reaction(() => FormattedTextBox.InputBoxOverlay,
- () => {
- if (this._editorView) {
- this._editorView.destroy();
- }
- this.setupEditor(config, this.props.Document);// MainOverlayTextBox.Instance.TextDoc); // bcz: not sure why, but the order of events is such that this.props.Document hasn't updated yet, so without forcing the editor to the MainOverlayTextBox, it will display the previously focused textbox
- }
- );
- } else {
+ if (!this.props.isOverlay) {
this._proxyReactionDisposer = reaction(() => this.props.isSelected(),
() => {
if (this.props.isSelected()) {
FormattedTextBox.InputBoxOverlay = this;
FormattedTextBox.InputBoxOverlayScroll = this._ref.current!.scrollTop;
}
- });
+ }, { fireImmediately: true });
}
-
this._reactionDisposer = reaction(
() => {
- const field = this.props.Document ? Cast(this.props.Document[this.props.fieldKey], RichTextField) : undefined;
- return field ? field.Data : 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 => 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._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) {
- let field = doc ? Cast(doc[this.props.fieldKey], RichTextField) : undefined;
- if (this._proseRef.current) {
- this._editorView = new EditorView(this._proseRef.current, {
+ 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 (!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, {
state: field && field.Data ? EditorState.fromJSON(config, JSON.parse(field.Data)) : EditorState.create(config),
dispatchTransaction: this.dispatchTransaction,
nodeViews: {
- image(node, view, getPos) { return new ImageResizeView(node, view, getPos); }
+ image(node, view, getPos) { return new ImageResizeView(node, view, getPos); },
+ star(node, view, getPos) { return new SummarizedView(node, view, getPos); }
}
});
- let text = StrCast(this.props.Document.documentText);
- if (text.startsWith("@@@")) {
- this.props.Document.proto!.documentText = undefined;
- this._editorView.dispatch(this._editorView.state.tr.insertText(text.replace("@@@", "")));
+ if (startup) {
+ Doc.GetProto(doc).documentText = undefined;
+ this._editorView.dispatch(this._editorView.state.tr.insertText(startup));
}
}
if (this.props.selectOnLoad) {
- this.props.select(false);
- this._editorView!.focus();
+ 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._inputReactionDisposer) {
- this._inputReactionDisposer();
- }
- if (this._proxyReactionDisposer) {
- this._proxyReactionDisposer();
- }
- if (this._dropDisposer) {
- this._dropDisposer();
- }
+ 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 = (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) {
- let docid = href.replace(DocServer.prepend("/doc/"), "").split("?")[0];
- DocServer.GetRefField(docid).then(f => {
- (f instanceof Doc) && DocumentManager.Instance.jumpToDocument(f, ctrlKey, document => this.props.addDocTab(document, "inTab"))
- });
+ this._linkClicked = href.replace(DocServer.prepend("/doc/"), "").split("?")[0];
+ if (this._linkClicked) {
+ DocServer.GetRefField(this._linkClicked).then(async linkDoc => {
+ if (linkDoc instanceof Doc) {
+ let proto = Doc.GetProto(linkDoc);
+ let targetContext = await Cast(proto.targetContext, Doc);
+ 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.Create.WebDocument(href, { x: NumCast(this.props.Document.x, 0) + NumCast(this.props.Document.width, 0), y: NumCast(this.props.Document.y) });
+ this.props.addDocument && this.props.addDocument(webDoc);
+ this._linkClicked = webDoc[Id];
}
e.stopPropagation();
e.preventDefault();
@@ -249,13 +335,12 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
if (e.button === 2 || (e.button === 0 && e.ctrlKey)) {
- this._gotDown = true;
e.preventDefault();
}
}
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();
@@ -273,13 +358,19 @@ 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();
}
}
onClick = (e: React.MouseEvent): void => {
- this._proseRef.current!.focus();
+ this._proseRef!.focus();
+ if (this._linkClicked) {
+ this._linkClicked = "";
+ e.preventDefault();
+ e.stopPropagation();
+ }
}
onMouseDown = (e: React.MouseEvent): void => {
if (!this.props.isSelected()) { // preventing default allows the onClick to be generated instead of being swallowed by the text box itself
@@ -295,9 +386,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
return self._toolTipTextMenu = new TooltipTextMenu(_editorView, myprops);
}
});
+ //this.props.Document.tooltip = self._toolTipTextMenu;
}
- _toolTipTextMenu: TooltipTextMenu | undefined = undefined;
tooltipLinkingMenuPlugin() {
let myprops = this.props;
return new Plugin({
@@ -322,19 +413,31 @@ 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.dataDoc.nativeHeight, 0);
+ let dh = NumCast(this.props.Document.height, 0);
+ let sh = scrBounds.height;
+ this.props.Document.height = nh ? dh / nh * sh : sh;
+ this.dataDoc.nativeHeight = nh ? sh : undefined;
+ }
}
- @observable
- _entered = false;
@action
onPointerEnter = (e: React.PointerEvent) => {
this._entered = true;
@@ -343,33 +446,45 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
onPointerLeave = (e: React.PointerEvent) => {
this._entered = false;
}
+
+ specificContextMenu = (e: React.MouseEvent): void => {
+ let subitems: ContextMenuProps[] = [];
+ subitems.push({
+ description: BoolCast(this.props.Document.autoHeight, false) ? "Manual Height" : "Auto Height",
+ event: action(() => Doc.GetProto(this.props.Document).autoHeight = !BoolCast(this.props.Document.autoHeight, false)), icon: "expand-arrows-alt"
+ });
+ 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.hideOnLeave ? "white" : "initial",
+ color: this.props.color ? this.props.color : this.props.hideOnLeave ? "white" : "inherit",
pointerEvents: interactive ? "all" : "none",
+ fontSize: "13px"
}}
- // onKeyDown={this.onKeyPress}
- onKeyPress={this.onKeyPress}
+ onKeyDown={this.onKeyPress}
onFocus={this.onFocused}
onClick={this.onClick}
+ onContextMenu={this.specificContextMenu}
onBlur={this.onBlur}
onPointerUp={this.onPointerUp}
onPointerDown={this.onPointerDown}
onMouseDown={this.onMouseDown}
- onContextMenu={this.specificContextMenu}
// tfs: do we need this event handler
onWheel={this.onPointerWheel}
onPointerEnter={this.onPointerEnter}
onPointerLeave={this.onPointerLeave}
>
- <div className={`formattedTextBox-inner${rounded}`} style={{ whiteSpace: "pre-wrap", pointerEvents: this.props.Document.isButton && !this.props.isSelected() ? "none" : "all" }} ref={this._proseRef} />
+ <div className={`formattedTextBox-inner${rounded}`} ref={this.createDropTarget} style={{ whiteSpace: "pre-wrap", pointerEvents: this.props.Document.isButton && !this.props.isSelected() ? "none" : "all" }} />
</div>
);
}
diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx
index 00021bc78..d6ab2a34a 100644
--- a/src/client/views/nodes/IconBox.tsx
+++ b/src/client/views/nodes/IconBox.tsx
@@ -37,14 +37,14 @@ export class IconBox extends React.Component<FieldViewProps> {
return <FontAwesomeIcon icon={button} className="documentView-minimizedIcon" />;
}
- setLabelField = (e: React.MouseEvent): void => {
+ setLabelField = (): void => {
this.props.Document.hideLabel = !BoolCast(this.props.Document.hideLabel);
}
- setUseOwnTitleField = (e: React.MouseEvent): void => {
+ setUseOwnTitleField = (): void => {
this.props.Document.useOwnTitle = !BoolCast(this.props.Document.useTargetTitle);
}
- specificContextMenu = (e: React.MouseEvent): void => {
+ specificContextMenu = (): void => {
ContextMenu.Instance.addItem({
description: BoolCast(this.props.Document.hideLabel) ? "Show label with icon" : "Remove label from icon",
event: this.setLabelField
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 4c2b73b70..4c5ad7a7d 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,25 +1,29 @@
-import { action, observable, trace } from 'mobx';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faImage } from '@fortawesome/free-solid-svg-icons';
+import { action, observable, computed } 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, DocListCast } from '../../../new_fields/Doc';
+import { List } from '../../../new_fields/List';
+import { createSchema, listSpec, makeInterface } from '../../../new_fields/Schema';
+import { Cast, FieldValue, NumCast, StrCast, BoolCast } from '../../../new_fields/Types';
+import { ImageField } from '../../../new_fields/URLField';
import { Utils } from '../../../Utils';
import { DragManager } from '../../util/DragManager';
import { undoBatch } from '../../util/UndoManager';
import { ContextMenu } from "../../views/ContextMenu";
+import { ContextMenuProps } from '../ContextMenuItem';
+import { DocComponent } from '../DocComponent';
+import { InkingControl } from '../InkingControl';
+import { positionSchema } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
import "./ImageBox.scss";
import React = require("react");
-import { createSchema, makeInterface, listSpec } from '../../../new_fields/Schema';
-import { DocComponent } from '../DocComponent';
-import { positionSchema } from './DocumentView';
-import { FieldValue, Cast, StrCast, PromiseValue, NumCast } from '../../../new_fields/Types';
-import { ImageField } from '../../../new_fields/URLField';
-import { List } from '../../../new_fields/List';
-import { InkingControl } from '../InkingControl';
-import { Doc, WidthSym, HeightSym } from '../../../new_fields/Doc';
-import { faImage } from '@fortawesome/free-solid-svg-icons';
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { ContextMenuItemProps, ContextMenuProps } from '../ContextMenuItem';
+import { RouteStore } from '../../../server/RouteStore';
+import { Docs } from '../../documents/Documents';
+import { DocServer } from '../../DocServer';
+var requestImageSize = require('../../util/request-image-size');
var path = require('path');
@@ -27,25 +31,35 @@ library.add(faImage);
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 +75,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();
}
}));
@@ -86,9 +106,9 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
}
onPointerDown = (e: React.PointerEvent): void => {
- if (e.shiftKey && e.ctrlKey)
-
- e.stopPropagation();
+ if (e.shiftKey && e.ctrlKey) {
+ e.stopPropagation(); // allows default system drag drop of images with shift+ctrl only
+ }
// if (Date.now() - this._lastTap < 300) {
// if (e.buttons === 1) {
// this._downX = e.clientX;
@@ -109,39 +129,81 @@ 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(DocServer.prepend(RouteStore.upload), {
+ method: 'POST',
+ body: formData
+ });
+ const files = await res.json();
+ const url = DocServer.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);
+ }
+ };
+ recorder.start();
+ setTimeout(() => {
+ recorder.stop();
+
+ 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(() => {
- this.props.Document.rotation = (NumCast(this.props.Document.rotation) + 90) % 360;
+ let proto = Doc.GetProto(this.props.Document);
let nw = this.props.Document.nativeWidth;
- this.props.Document.nativeWidth = this.props.Document.nativeHeight;
- this.props.Document.nativeHeight = nw;
+ let nh = this.props.Document.nativeHeight;
let w = this.props.Document.width;
- this.props.Document.width = this.props.Document.height;
+ let h = this.props.Document.height;
+ proto.rotation = (NumCast(this.props.Document.rotation) + 90) % 360;
+ proto.nativeWidth = nh;
+ proto.nativeHeight = nw;
+ this.props.Document.width = h;
this.props.Document.height = w;
}), icon: "expand-arrows-alt"
});
@@ -151,7 +213,6 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
@action
onDotDown(index: number) {
- this._photoIndex = index;
this.Document.curPage = index;
}
@@ -161,7 +222,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>
);
}
@@ -172,7 +233,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;
@@ -185,10 +247,31 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
}
@action onError = () => {
let timeout = this._curSuffix === "_s" ? this._smallRetryCount : this._curSuffix === "_m" ? this._mediumRetryCount : this._largeRetryCount;
- if (timeout < 10)
+ if (timeout < 10) {
setTimeout(this.retryPath, Math.min(10000, timeout * 5));
+ }
}
_curSuffix = "_m";
+
+ resize(srcpath: string, layoutdoc: Doc) {
+ requestImageSize(window.origin + RouteStore.corsProxy + "/" + 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);
+ });
+ }
+
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;
@@ -198,29 +281,37 @@ 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"];
// 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 shift = (rotation % 180) ? (this.props.Document[HeightSym]() - this.props.Document[WidthSym]() / aspect) / 2 : 0;
+ 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}`}
+ <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") }}
+ // style={{ objectFit: (this.Document.curPage === 0 ? undefined : "contain") }}
width={nativeWidth}
ref={this._imgRef}
onError={this.onError} />
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 849f17aa4..c9dd9a64e 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -2,17 +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 } 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 { 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 = "";
@@ -27,28 +46,53 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
@action
onEnterKey = (e: React.KeyboardEvent): void => {
if (e.key === 'Enter') {
- if (this._keyInput && this._valueInput) {
- let doc = this.fieldDocToLayout;
- if (!doc) {
- return;
+ if (this._keyInput && this._valueInput && this.fieldDocToLayout) {
+ if (KeyValueBox.SetField(this.fieldDocToLayout, this._keyInput, this._valueInput)) {
+ this._keyInput = "";
+ this._valueInput = "";
}
- let realDoc = doc;
-
- let script = CompileScript(this._valueInput, { addReturn: true });
- if (!script.compiled) {
- return;
- }
- let res = script.run();
- if (!res.success) return;
- const field = res.result;
- if (Field.IsField(field)) {
- realDoc[this._keyInput] = field;
- }
- this._keyInput = "";
- this._valueInput = "";
}
}
}
+ public static CompileKVPScript(value: string): KVPScript | undefined {
+ let eq = value.startsWith("=");
+ value = eq ? value.substr(1) : value;
+ const dubEq = value.startsWith(":=") ? "computed" : value.startsWith(";=") ? "script" : false;
+ value = dubEq ? value.substr(2) : value;
+ let options: ScriptOptions = { addReturn: true, params: { this: "Doc" } };
+ if (dubEq) options.typecheck = false;
+ let script = CompileScript(value, options);
+ if (!script.compiled) {
+ return undefined;
+ }
+ return { script, type: dubEq, onDelegate: eq };
+ }
+
+ public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript): boolean {
+ const { script, type, onDelegate } = kvpScript;
+ const target = onDelegate ? doc : Doc.GetProto(doc);
+ let field: Field;
+ if (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)) {
+ 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()) {
@@ -78,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;
}
@@ -123,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)` }}>
@@ -133,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()}
@@ -143,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 228d07018..209782242 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -2,7 +2,7 @@ import { action, observable } from 'mobx';
import { observer } from "mobx-react";
import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
import { emptyFunction, returnFalse, returnZero, returnTrue } from '../../../Utils';
-import { CompileScript } from "../../util/Scripting";
+import { CompileScript, CompiledScript, ScriptOptions } from "../../util/Scripting";
import { Transform } from '../../util/Transform';
import { EditableView } from "../EditableView";
import { FieldView, FieldViewProps } from './FieldView';
@@ -11,6 +11,8 @@ import "./KeyValuePair.scss";
import React = require("react");
import { Doc, Opt, Field } from '../../../new_fields/Doc';
import { FieldValue } from '../../../new_fields/Types';
+import { KeyValueBox } from './KeyValueBox';
+import { DragManager, SetupDrag } from '../../util/DragManager';
// Represents one row in a key value plane
@@ -22,15 +24,31 @@ 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;
+ }
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,
@@ -38,15 +56,19 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
focus: emptyFunction,
PanelWidth: returnZero,
PanelHeight: returnZero,
- addDocTab: emptyFunction
+ 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 keyStyle = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? "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;
}
@@ -54,33 +76,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 }}>{props.fieldKey}</div>
</div>
</td>
<td className="keyValuePair-td-value" style={{ width: `${100 - this.props.keyWidth}%` }}>
- <EditableView contents={contents} height={36} GetValue={() => {
-
- let field = FieldValue(props.Document[props.fieldKey]);
- if (Field.IsField(field)) {
- return Field.toScriptString(field);
- }
- return "";
- }}
- SetValue={(value: string) => {
- let script = CompileScript(value, { addReturn: true });
- if (!script.compiled) {
- return false;
- }
- let res = script.run();
- if (!res.success) return false;
- const field = res.result;
- if (Field.IsField(field, true)) {
- props.Document[props.fieldKey] = field;
- return true;
- }
- return false;
- }}>
- </EditableView></td>
+ <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>
</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..6a18a4e7b
--- /dev/null
+++ b/src/client/views/nodes/LinkMenuItem.tsx
@@ -0,0 +1,119 @@
+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();
+ let jumpToDoc = this.props.destinationDoc;
+ let pdfDoc = FieldValue(Cast(this.props.destinationDoc, Doc));
+ if (pdfDoc) {
+ jumpToDoc = pdfDoc;
+ }
+ if (DocumentManager.Instance.getDocumentView(jumpToDoc)) {
+ let self = this;
+ 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 {
+ CollectionDockingView.Instance.AddRightSplit(jumpToDoc, 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 449408a61..e7655d598 100644
--- a/src/client/views/nodes/PDFBox.scss
+++ b/src/client/views/nodes/PDFBox.scss
@@ -2,40 +2,139 @@
transform-origin: left top;
position: absolute;
top: 0;
- left:0;
+ left: 0;
}
+
.react-pdf__Page__textContent span {
user-select: text;
}
+
.react-pdf__Document {
position: absolute;
}
+
.pdfBox-buttonTray {
- position:absolute;
+ position: absolute;
top: 0;
- left:0;
+ left: 0;
z-index: 25;
pointer-events: all;
}
+
.pdfBox-thumbnail {
position: absolute;
width: 100%;
}
+
.pdfButton {
pointer-events: all;
width: 100px;
- height:100px;
+ height: 100px;
+}
+
+.pdfBox-cont,
+.pdfBox-cont-interactive {
+ display: flex;
+ flex-direction: row;
+ height: 100%;
+ overflow-y: scroll;
+ overflow-x: hidden;
}
+
.pdfBox-cont {
- pointer-events: none ;
- span {
- pointer-events: none !important;
+ pointer-events: none;
+
+ .textlayer {
+ pointer-events: none;
+
+ span {
+ pointer-events: none !important;
+ }
+ }
+
+ .page-cont {
+ pointer-events: none;
}
}
+
.pdfBox-cont-interactive {
pointer-events: all;
+ display: flex;
+ flex-direction: row;
+
+ .textlayer {
+ span {
+ pointer-events: all !important;
+ user-select: text;
+ }
+ }
}
+
.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 aa29a7170..5a5e6e6dd 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -1,403 +1,258 @@
-import * as htmlToImage from "html-to-image";
-import { action, computed, IReactionDisposer, observable, reaction, runInAction, trace } from 'mobx';
+import { action, IReactionDisposer, observable, reaction, trace, untracked, computed } from 'mobx';
import { observer } from "mobx-react";
import 'react-image-lightbox/style.css';
-import Measure from "react-measure";
-//@ts-ignore
-import { Document, Page } from "react-pdf";
-import 'react-pdf/dist/Page/AnnotationLayer.css';
-import { Id } from "../../../new_fields/FieldSymbols";
+import { WidthSym, Doc } from "../../../new_fields/Doc";
import { makeInterface } from "../../../new_fields/Schema";
-import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
-import { ImageField, PdfField } from "../../../new_fields/URLField";
+import { Cast, NumCast, BoolCast } from "../../../new_fields/Types";
+import { PdfField } from "../../../new_fields/URLField";
+//@ts-ignore
+// import { Document, Page } from "react-pdf";
+// import 'react-pdf/dist/Page/AnnotationLayer.css';
import { RouteStore } from "../../../server/RouteStore";
-import { Utils } from '../../../Utils';
-import { DocServer } from "../../DocServer";
import { DocComponent } from "../DocComponent";
import { InkingControl } from "../InkingControl";
-import { SearchBox } from "../SearchBox";
+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";
-var path = require('path');
import React = require("react");
-import { ContextMenu } from "../ContextMenu";
-
-/** ALSO LOOK AT: Annotation.tsx, Sticky.tsx
- * This method renders PDF and puts all kinds of functionalities such as annotation, highlighting,
- * area selection (I call it stickies), embedded ink node for directly annotating using a pen or
- * mouse, and pagination.
- *
- *
- * HOW TO USE:
- * AREA selection:
- * 1) Click on Area button.
- * 2) click on any part of the PDF, and drag to get desired sized area shape
- * 3) You can write on the area (hence the reason why it's called sticky)
- * 4) to make another area, you need to click on area button AGAIN.
- *
- * HIGHLIGHT: (Buggy. No multiline/multidiv text highlighting for now...)
- * 1) just click and drag on a text
- * 2) click highlight
- * 3) for annotation, just pull your cursor over to that text
- * 4) another method: click on highlight first and then drag on your desired text
- * 5) To make another highlight, you need to reclick on the button
- *
- * written by: Andrew Kim
- */
+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) {
public static LayoutString() { return FieldView.LayoutString(PDFBox); }
- private _mainDiv = React.createRef<HTMLDivElement>();
- private renderHeight = 2400;
-
- @observable private _renderAsSvg = true;
@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>;
- @observable private _perPageInfo: Object[] = []; //stores pageInfo
- @observable private _pageInfo: any = { area: [], divs: [], anno: [] }; //divs is array of objects linked to anno
+ constructor(props: FieldViewProps) {
+ super(props);
- @observable private _currAnno: any = [];
- @observable private _interactive: boolean = false;
- @observable private _loaded: boolean = false;
-
- @computed private get curPage() { return NumCast(this.Document.curPage, 1); }
- @computed private get thumbnailPage() { return NumCast(this.props.Document.thumbnailPage, -1); }
-
- componentDidMount() {
- let wasSelected = this.props.isSelected();
+ this._mainCont = React.createRef();
this._reactionDisposer = reaction(
- () => [this.props.isSelected(), this.curPage],
+ () => this.props.Document.scrollY,
() => {
- if (this.curPage > 0 && !this.props.isTopMost && this.curPage !== this.thumbnailPage && wasSelected && !this.props.isSelected()) {
- this.saveThumbnail();
+ if (this._mainCont.current) {
+ this._mainCont.current && this._mainCont.current.scrollTo({ top: NumCast(this.Document.scrollY), behavior: "auto" });
}
- wasSelected = this._interactive = this.props.isSelected();
- },
- { fireImmediately: true });
+ }
+ );
+
+ this._keyRef = React.createRef();
+ this._valueRef = React.createRef();
+ this._scriptRef = React.createRef();
+ }
+ componentDidMount() {
+ if (this.props.setPdfBox) this.props.setPdfBox(this);
}
componentWillUnmount() {
- if (this._reactionDisposer) this._reactionDisposer();
+ this._reactionDisposer && this._reactionDisposer();
}
- /**
- * highlighting helper function
- */
- makeEditableAndHighlight = (colour: string) => {
- var range, sel = window.getSelection();
- if (sel && sel.rangeCount && sel.getRangeAt) {
- range = sel.getRangeAt(0);
+ public GetPage() {
+ 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.dataDoc.pdfHeight)) + 1;
+ cp = cp - 1;
+ if (cp > 0) {
+ this.props.Document.curPage = cp;
+ this.props.Document.scrollY = (cp - 1) * NumCast(this.dataDoc.pdfHeight);
}
- document.designMode = "on";
- if (!document.execCommand("HiliteColor", false, colour)) {
- document.execCommand("HiliteColor", false, colour);
+ }
+ 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.dataDoc.pdfHeight);
}
+ }
- if (range && sel) {
- sel.removeAllRanges();
- sel.addRange(range);
-
- let obj: Object = { parentDivs: [], spans: [] };
- //@ts-ignore
- if (range.commonAncestorContainer.className === 'react-pdf__Page__textContent') { //multiline highlighting case
- obj = this.highlightNodes(range.commonAncestorContainer.childNodes);
- } else { //single line highlighting case
- let parentDiv = range.commonAncestorContainer.parentElement;
- if (parentDiv) {
- if (parentDiv.className === 'react-pdf__Page__textContent') { //when highlight is overwritten
- obj = this.highlightNodes(parentDiv.childNodes);
- } else {
- parentDiv.childNodes.forEach((child) => {
- if (child.nodeName === 'SPAN') {
- //@ts-ignore
- obj.parentDivs.push(parentDiv);
- //@ts-ignore
- child.id = "highlighted";
- //@ts-ignore
- obj.spans.push(child);
- // child.addEventListener("mouseover", this.onEnter); //adds mouseover annotation handler
- }
- });
- }
- }
- }
- this._pageInfo.divs.push(obj);
-
+ public ForwardPage() {
+ 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.dataDoc.pdfHeight);
}
- document.designMode = "off";
}
- highlightNodes = (nodes: NodeListOf<ChildNode>) => {
- let temp = { parentDivs: [], spans: [] };
- nodes.forEach((div) => {
- div.childNodes.forEach((child) => {
- if (child.nodeName === 'SPAN') {
- //@ts-ignore
- temp.parentDivs.push(div);
- //@ts-ignore
- child.id = "highlighted";
- //@ts-ignore
- temp.spans.push(child);
- // child.addEventListener("mouseover", this.onEnter); //adds mouseover annotation handler
- }
- });
-
- });
- return temp;
+ private newKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this._keyValue = e.currentTarget.value;
}
- /**
- * when the cursor enters the highlight, it pops out annotation. ONLY WORKS FOR SINGLE DIV LINES
- */
- @action
- onEnter = (e: any) => {
- let span: HTMLSpanElement = e.toElement;
- let index: any;
- this._pageInfo.divs.forEach((obj: any) => {
- obj.spans.forEach((element: any) => {
- if (element === span && !index) {
- index = this._pageInfo.divs.indexOf(obj);
- }
- });
- });
-
- if (this._pageInfo.anno.length >= index + 1) {
- if (this._currAnno.length === 0) {
- this._currAnno.push(this._pageInfo.anno[index]);
- }
- } else {
- if (this._currAnno.length === 0) { //if there are no current annotation
- let div = span.offsetParent;
- //@ts-ignore
- let divX = div.style.left;
- //@ts-ignore
- let divY = div.style.top;
- //slicing "px" from the end
- divX = divX.slice(0, divX.length - 2); //gets X of the DIV element (parent of Span)
- divY = divY.slice(0, divY.length - 2); //gets Y of the DIV element (parent of Span)
- let annotation = <Annotation key={Utils.GenerateGuid()} Span={span} X={divX} Y={divY - 300} Highlights={this._pageInfo.divs} Annotations={this._pageInfo.anno} CurrAnno={this._currAnno} />;
- this._pageInfo.anno.push(annotation);
- this._currAnno.push(annotation);
- }
- }
-
+ private newValueChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this._valueValue = e.currentTarget.value;
}
- /**
- * highlight function for highlighting actual text. This works fine.
- */
- highlight = (color: string) => {
- if (window.getSelection()) {
- try {
- if (!document.execCommand("hiliteColor", false, color)) {
- this.makeEditableAndHighlight(color);
- }
- } catch (ex) {
- this.makeEditableAndHighlight(color);
- }
- }
+ @action
+ private newScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this._scriptValue = e.currentTarget.value;
}
- /**
- * controls the area highlighting (stickies) Kinda temporary
- */
- onPointerDown = (e: React.PointerEvent) => {
- if (this.props.isSelected() && !InkingControl.Instance.selectedTool && e.buttons === 1) {
- if (e.altKey) {
- this._alt = true;
- } else {
- if (e.metaKey) {
- e.stopPropagation();
- }
- }
- document.removeEventListener("pointerup", this.onPointerUp);
- document.addEventListener("pointerup", this.onPointerUp);
+ 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}`;
}
- if (this.props.isSelected() && e.buttons === 2) {
- runInAction(() => this._alt = true);
- document.removeEventListener("pointerup", this.onPointerUp);
- document.addEventListener("pointerup", this.onPointerUp);
+ else {
+ scriptText = "return true";
}
- }
-
- /**
- * controls area highlighting and partially highlighting. Kinda temporary
- */
- @action
- onPointerUp = (e: PointerEvent) => {
- this._alt = false;
- document.removeEventListener("pointerup", this.onPointerUp);
- if (this.props.isSelected()) {
- this.highlight("rgba(76, 175, 80, 0.3)"); //highlights to this default color.
+ let script = CompileScript(scriptText, { params: { this: Doc.name } });
+ if (script.compiled) {
+ this.props.Document.filterScript = new ScriptField(script);
}
- this._interactive = true;
}
-
@action
- saveThumbnail = () => {
- this.props.Document.thumbnailPage = FieldValue(this.Document.curPage, -1);
- this._renderAsSvg = false;
- setTimeout(() => {
- runInAction(() => this._smallRetryCount = this._mediumRetryCount = this._largeRetryCount = 0);
- let nwidth = FieldValue(this.Document.nativeWidth, 0);
- let nheight = FieldValue(this.Document.nativeHeight, 0);
- htmlToImage.toPng(this._mainDiv.current!, { width: nwidth, height: nheight, quality: 0.8 })
- .then(action((dataUrl: string) => {
- SearchBox.convertDataUri(dataUrl, "icon" + this.Document[Id] + "_" + this.curPage).then((returnedFilename) => {
- if (returnedFilename) {
- let url = DocServer.prepend(returnedFilename);
- this.props.Document.thumbnail = new ImageField(new URL(url));
- }
- runInAction(() => this._renderAsSvg = true);
- })
- }))
- .catch(function (error: any) {
- console.error('oops, something went wrong!', error);
- });
- }, 1250);
+ private toggleFlyout = () => {
+ this._flyout = !this._flyout;
}
@action
- onLoaded = (page: any) => {
- // bcz: the number of pages should really be set when the document is imported.
- this.props.Document.numPages = page._transport.numPages;
- if (this._perPageInfo.length === 0) { //Makes sure it only runs once
- this._perPageInfo = [...Array(page._transport.numPages)];
+ 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 = "";
}
- this._loaded = true;
+ if (this._scriptRef.current) {
+ this._scriptRef.current.value = "";
+ }
+ this.applyFilter();
}
- @action
- setScaling = (r: any) => {
- // bcz: the nativeHeight should really be set when the document is imported.
- // also, the native dimensions could be different for different pages of the canvas
- // so this design is flawed.
- var nativeWidth = FieldValue(this.Document.nativeWidth, 0);
- if (!FieldValue(this.Document.nativeHeight, 0)) {
- var nativeHeight = nativeWidth * r.offset.height / r.offset.width;
- this.props.Document.height = nativeHeight / nativeWidth * FieldValue(this.Document.width, 0);
- this.props.Document.nativeHeight = nativeHeight;
+ scrollTo(y: number) {
+ if (this._mainCont.current) {
+ this._mainCont.current.scrollTo({ top: y, behavior: "auto" });
}
}
- @computed
- get pdfPage() {
- return <Page height={this.renderHeight} renderTextLayer={false} pageNumber={this.curPage} onLoadSuccess={this.onLoaded} />;
- }
- @computed
- get pdfContent() {
- let pdfUrl = Cast(this.props.Document[this.props.fieldKey], PdfField);
- if (!pdfUrl) {
- return <p>No pdf url to render</p>;
- }
- let pdfpage = this.pdfPage;
- let body = this.Document.nativeHeight ?
- pdfpage :
- <Measure offset onResize={this.setScaling}>
- {({ measureRef }) =>
- <div className="pdfBox-page" ref={measureRef}>
- {pdfpage}
+
+ 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>
- }
- </Measure>;
- let xf = (this.Document.nativeHeight || 0) / this.renderHeight;
- return <div className="pdfBox-contentContainer" key="container" style={{ transform: `scale(${xf}, ${xf})` }}>
- <Document file={window.origin + RouteStore.corsProxy + `/${pdfUrl.url}`} renderMode={this._renderAsSvg || this.props.isTopMost ? "svg" : "canvas"}>
- {body}
- </Document>
- </div >;
+ </div>
+ );
}
- @computed
- get pdfRenderer() {
- let pdfUrl = Cast(this.props.Document[this.props.fieldKey], PdfField);
- let proxy = this.imageProxyRenderer;
- if ((!this._interactive && proxy && (!this.props.ContainingCollectionView || !this.props.ContainingCollectionView.props.isTopMost)) || !pdfUrl) {
- return proxy;
+ loaded = (nw: number, nh: number, np: number) => {
+ if (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);
+ doc.nativeWidth = nw;
+ if (doc.nativeHeight) doc.nativeHeight = nw * oldaspect;
+ else doc.nativeHeight = nh;
+ let ccv = this.props.ContainingCollectionView;
+ if (ccv) {
+ ccv.props.Document.pdfHeight = nh;
+ }
+ doc.height = nh * (doc[WidthSym]() / nw);
}
- return [
- proxy,
- this._pageInfo.area.filter(() => this._pageInfo.area).map((element: any) => element),
- this._currAnno.map((element: any) => element),
- this.pdfContent
- ];
}
- choosePath(url: URL) {
- if (url.protocol === "data" || url.href.indexOf(window.location.origin) === -1)
- return url.href;
- let ext = path.extname(url.href);
- return url.href.replace(ext, this._curSuffix + ext);
- }
- @observable _smallRetryCount = 1;
- @observable _mediumRetryCount = 1;
- @observable _largeRetryCount = 1;
- @action retryPath = () => {
- if (this._curSuffix === "_s") this._smallRetryCount++;
- if (this._curSuffix === "_m") this._mediumRetryCount++;
- if (this._curSuffix === "_l") this._largeRetryCount++;
- }
- @action onError = () => {
- let timeout = this._curSuffix === "_s" ? this._smallRetryCount : this._curSuffix === "_m" ? this._mediumRetryCount : this._largeRetryCount;
- if (timeout < 10)
- setTimeout(this.retryPath, Math.min(10000, timeout * 5));
+ @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;
+ }
+ }
}
- _curSuffix = "_m";
- @computed
- get imageProxyRenderer() {
- let thumbField = this.props.Document.thumbnail;
- if (thumbField && this._renderAsSvg && NumCast(this.props.Document.thumbnailPage, 0) === this.Document.curPage) {
- // let transform = this.props.ScreenToLocalTransform().inverse();
- let pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50;
- // var [sptX, sptY] = transform.transformPoint(0, 0);
- // let [bptX, bptY] = transform.transformPoint(pw, this.props.PanelHeight());
- // let w = bptX - sptX;
-
- let path = thumbField instanceof ImageField ? thumbField.url.href : "http://cs.brown.edu/people/bcz/prairie.jpg";
- // this._curSuffix = "";
- // if (w > 20) {
- let field = thumbField;
- // if (w < 100 && this._smallRetryCount < 10) this._curSuffix = "_s";
- // else if (w < 400 && this._mediumRetryCount < 10) this._curSuffix = "_m";
- // else if (this._largeRetryCount < 10) this._curSuffix = "_l";
- if (field instanceof ImageField) path = this.choosePath(field.url);
- // }
- return <img className="pdfBox-thumbnail" key={path + (this._mediumRetryCount).toString()} src={path} onError={this.onError} />;
- }
- return (null);
- }
- @action onKeyDown = (e: React.KeyboardEvent) => e.key === "Alt" && (this._alt = true);
- @action onKeyUp = (e: React.KeyboardEvent) => e.key === "Alt" && (this._alt = false);
- onContextMenu = (e: React.MouseEvent): void => {
- let field = Cast(this.Document[this.props.fieldKey], PdfField);
- if (field) {
- let url = field.url.href;
- ContextMenu.Instance.addItem({
- description: "Copy path", event: () => {
- Utils.CopyText(url);
- }, icon: "expand-arrows-alt"
- });
- }
+ @computed get fieldExtensionDoc() {
+ return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, "true");
}
render() {
- let classname = "pdfBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !this._alt ? "-interactive" : "");
+ // uses mozilla pdf as default
+ 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 className={classname} tabIndex={0} ref={this._mainDiv} onPointerDown={this.onPointerDown} onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} onContextMenu={this.onContextMenu} >
- {this.pdfRenderer}
- </div >
+ <div className={classname}
+ onScroll={this.onScroll}
+ style={{
+ marginTop: `${NumCast(this.props.ContainingCollectionView!.props.Document.panY)}px`
+ }}
+ ref={this._mainCont}
+ onWheel={(e: React.WheelEvent) => {
+ e.stopPropagation();
+ }}>
+ <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 35ecf12f6..9806b10b5 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -1,19 +1,24 @@
import React = require("react");
+import { action, computed, IReactionDisposer, observable, reaction, runInAction, untracked } from "mobx";
import { observer } from "mobx-react";
-import { FieldView, FieldViewProps } from './FieldView';
-import * as rp from "request-promise";
-import "./VideoBox.scss";
-import { action, IReactionDisposer, reaction, observable } from "mobx";
-import { DocComponent } from "../DocComponent";
-import { positionSchema } from "./DocumentView";
+import * as rp from 'request-promise';
+import { InkTool } from "../../../new_fields/InkField";
import { makeInterface } from "../../../new_fields/Schema";
-import { pageSchema } from "./ImageBox";
import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
import { VideoField } from "../../../new_fields/URLField";
-import "./VideoBox.scss";
import { RouteStore } from "../../../server/RouteStore";
+import { Utils } from "../../../Utils";
import { DocServer } from "../../DocServer";
-import { actionFieldDecorator } from "mobx/lib/internal";
+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";
+import "./VideoBox.scss";
type VideoDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>;
const VideoDocument = makeInterface(positionSchema, pageSchema);
@@ -21,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;
@@ -42,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 Seek(time: number) {
+ this._youtubePlayer && this._youtubePlayer.seekTo(Math.round(time), true);
}
- @action public Pause() {
+ @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 || Math.abs(nativeWidth / nativeHeight - youtubeaspect) > 0.05) {
+ 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
@@ -87,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 = DocServer.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 = DocServer.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()}>
<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="640" height="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 2239a8e38..f0a9ec6d8 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -1,18 +1,50 @@
-import "./WebBox.scss";
-import React = require("react");
-import { FieldViewProps, FieldView } from './FieldView';
+import { observer } from "mobx-react";
import { HtmlField } from "../../../new_fields/HtmlField";
import { WebField } from "../../../new_fields/URLField";
-import { observer } from "mobx-react";
-import { computed, reaction, IReactionDisposer } from 'mobx';
import { DocumentDecorations } from "../DocumentDecorations";
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;
@@ -47,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..104241237
--- /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 - (NumCast(this.props.parent.props.parent.Document.pdfHeight) / 2));
+ }
+ }
+ );
+ }
+
+ 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/PDFAnnotationLayer.tsx b/src/client/views/pdf/PDFAnnotationLayer.tsx
new file mode 100644
index 000000000..1f49e0d2f
--- /dev/null
+++ b/src/client/views/pdf/PDFAnnotationLayer.tsx
@@ -0,0 +1,24 @@
+import React = require("react");
+import { observer } from "mobx-react";
+
+interface IAnnotationProps {
+
+}
+
+@observer
+export class PDFAnnotationLayer extends React.Component {
+ onPointerDown = (e: React.PointerEvent) => {
+ if (e.ctrlKey) {
+ console.log("annotating");
+ e.stopPropagation();
+ }
+ }
+
+ render() {
+ return (
+ <div className="pdfAnnotationLayer-cont" style={{ width: "100%", height: "100%", position: "relative", top: "-200%" }} onPointerDown={this.onPointerDown}>
+
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/pdf/PDFMenu.scss b/src/client/views/pdf/PDFMenu.scss
new file mode 100644
index 000000000..b06d19c53
--- /dev/null
+++ b/src/client/views/pdf/PDFMenu.scss
@@ -0,0 +1,36 @@
+.pdfMenu-cont {
+ position: absolute;
+ z-index: 10000;
+ height: 35px;
+ background: #323232;
+ box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
+ border-radius: 0px 6px 6px 6px;
+ overflow: hidden;
+ display: flex;
+
+ .pdfMenu-button {
+ background-color: transparent;
+ width: 35px;
+ height: 35px;
+ }
+
+ .pdfMenu-button:hover {
+ background-color: #121212;
+ }
+
+ .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
new file mode 100644
index 000000000..27c2a8f1a
--- /dev/null
+++ b/src/client/views/pdf/PDFMenu.tsx
@@ -0,0 +1,269 @@
+import React = require("react");
+import "./PDFMenu.scss";
+import { observable, action, runInAction } from "mobx";
+import { observer } from "mobx-react";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { emptyFunction, returnFalse } from "../../../Utils";
+import { Doc } from "../../../new_fields/Doc";
+import { handleBackspace } from "../nodes/PDFBox";
+
+@observer
+export default class PDFMenu extends React.Component {
+ static Instance: PDFMenu;
+
+ @observable private _top: number = -300;
+ @observable private _left: number = -300;
+ @observable private _opacity: number = 1;
+ @observable private _transition: string = "opacity 0.5s";
+ @observable private _transitionDelay: string = "";
+
+
+ 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" | "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> = 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;
+ }
+
+ pointerDown = (e: React.PointerEvent) => {
+ document.removeEventListener("pointermove", this.pointerMove);
+ document.addEventListener("pointermove", this.pointerMove);
+ document.removeEventListener("pointerup", this.pointerUp);
+ document.addEventListener("pointerup", this.pointerUp);
+
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ pointerMove = (e: PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+
+ if (this._dragging) {
+ return;
+ }
+
+ this.StartDrag(e, this._commentCont.current!);
+ this._dragging = true;
+ }
+
+ pointerUp = (e: PointerEvent) => {
+ this._dragging = false;
+ document.removeEventListener("pointermove", this.pointerMove);
+ document.removeEventListener("pointerup", this.pointerUp);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ @action
+ jumpTo = (x: number, y: number, forceJump: boolean = false) => {
+ if (!this.Pinned || forceJump) {
+ this._transition = this._transitionDelay = "";
+ this._opacity = 1;
+ this._left = x;
+ this._top = y;
+ }
+ }
+
+ @action
+ fadeOut = (forceOut: boolean) => {
+ if (!this.Pinned) {
+ if (this._opacity === 0.2) {
+ this._transition = "opacity 0.1s";
+ this._transitionDelay = "";
+ this._opacity = 0;
+ this._left = this._top = -300;
+ }
+
+ if (forceOut) {
+ this._transition = "";
+ this._transitionDelay = "";
+ this._opacity = 0;
+ this._left = this._top = -300;
+ }
+ }
+ }
+
+ @action
+ pointerLeave = (e: React.PointerEvent) => {
+ if (!this.Pinned) {
+ this._transition = "opacity 0.5s";
+ this._transitionDelay = "1s";
+ this._opacity = 0.2;
+ setTimeout(() => this.fadeOut(false), 3000);
+ }
+ }
+
+ @action
+ pointerEntered = (e: React.PointerEvent) => {
+ this._transition = "opacity 0.1s";
+ this._transitionDelay = "";
+ this._opacity = 1;
+ }
+
+ @action
+ togglePin = (e: React.MouseEvent) => {
+ this.Pinned = !this.Pinned;
+ if (!this.Pinned) {
+ this.Highlighting = false;
+ }
+ }
+
+ @action
+ dragging = (e: PointerEvent) => {
+ this._left = e.pageX - this._offsetX;
+ this._top = e.pageY - this._offsetY;
+
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ dragEnd = (e: PointerEvent) => {
+ document.removeEventListener("pointermove", this.dragging);
+ document.removeEventListener("pointerup", this.dragEnd);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ dragStart = (e: React.PointerEvent) => {
+ document.removeEventListener("pointermove", this.dragging);
+ document.addEventListener("pointermove", this.dragging);
+ document.removeEventListener("pointerup", this.dragEnd);
+ document.addEventListener("pointerup", this.dragEnd);
+
+ this._offsetX = this._mainCont.current!.getBoundingClientRect().width - e.nativeEvent.offsetX;
+ this._offsetY = e.nativeEvent.offsetY;
+
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ @action
+ highlightClicked = (e: React.MouseEvent) => {
+ if (!this.Pinned) {
+ this.Highlight(undefined, "#f4f442");
+ }
+ else {
+ this.Highlighting = !this.Highlighting;
+ this.Highlight(undefined, "#f4f442");
+ }
+ }
+
+ deleteClicked = (e: React.PointerEvent) => {
+ this.Delete();
+ }
+
+ handleContextMenu = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ 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" || 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" 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 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}
+ <div className="pdfMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} />
+ </div >
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss
new file mode 100644
index 000000000..0fde764d0
--- /dev/null
+++ b/src/client/views/pdf/PDFViewer.scss
@@ -0,0 +1,131 @@
+
+.textLayer {
+ div {
+ user-select: text;
+ }
+}
+.viewer-button-cont {
+ position: absolute;
+ display: flex;
+ justify-content: space-evenly;
+ align-items: center;
+}
+
+.viewer-previousPage,
+.viewer-nextPage {
+ background: grey;
+ font-weight: bold;
+ opacity: 0.5;
+ padding: 0 10px;
+ border-radius: 5px;
+}
+
+.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;
+ background-color: red;
+ opacity: 0.1;
+}
+
+.pdfViewer-annotationLayer {
+ position: absolute;
+ top: 0;
+}
+
+
+
+.pdfViewer-pinAnnotation {
+ background-color: red;
+ position: absolute;
+ border-radius: 100%;
+} \ No newline at end of file
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
new file mode 100644
index 000000000..c560e581c
--- /dev/null
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -0,0 +1,732 @@
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
+import { observer } from "mobx-react";
+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 { Id } from "../../../new_fields/FieldSymbols";
+import { List } from "../../../new_fields/List";
+import { BoolCast, Cast, NumCast, StrCast, FieldValue } from "../../../new_fields/Types";
+import { emptyFunction } from "../../../Utils";
+import { DocServer } from "../../DocServer";
+import { Docs, DocUtils, DocumentOptions } from "../../documents/Documents";
+import { DocumentManager } from "../../util/DocumentManager";
+import { DragManager } from "../../util/DragManager";
+import { DocumentView } from "../nodes/DocumentView";
+import { PDFBox, handleBackspace } from "../nodes/PDFBox";
+import Page from "./Page";
+import "./PDFViewer.scss";
+import React = require("react");
+import PDFMenu from "./PDFMenu";
+import { UndoManager } from "../../util/UndoManager";
+import { CompileScript, CompiledScript, CompileResult } from "../../util/Scripting";
+import { 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 {
+ url: string;
+ loaded: (nw: number, nh: number, np: number) => void;
+ scrollY: number;
+ parent: PDFBox;
+}
+
+/**
+ * Wrapper that loads the PDF and cascades the pdf down
+ */
+@observer
+export class PDFViewer extends React.Component<IPDFViewerProps> {
+ @observable _pdf: Opt<Pdfjs.PDFDocumentProxy>;
+ private _mainDiv = React.createRef<HTMLDivElement>();
+
+ @action
+ componentDidMount() {
+ Pdfjs.getDocument(this.props.url).promise.then(pdf => runInAction(() => this._pdf = pdf));
+ }
+
+ render() {
+ return (
+ <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>
+ );
+ }
+}
+
+interface IViewerProps {
+ pdf: Pdfjs.PDFDocumentProxy;
+ loaded: (nw: number, nh: number, np: number) => void;
+ scrollY: number;
+ parent: PDFBox;
+ mainCont: React.RefObject<HTMLDivElement>;
+ url: string;
+}
+
+/**
+ * Handles rendering and virtualization of the pdf
+ */
+@observer
+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
+ @observable private _isPage: string[] = [];
+ @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 _activeReactionDisposer?: IReactionDisposer;
+ private _viewer: React.RefObject<HTMLDivElement>;
+ private _mainCont: React.RefObject<HTMLDivElement>;
+ private _pdfViewer: any;
+ // private _textContent: Pdfjs.TextContent[] = [];
+ private _pdfFindController: any;
+ private _searchString: string = "";
+ private _rendered: boolean = false;
+ private _pageIndex: number = -1;
+ private _matchIndex: number = 0;
+
+ constructor(props: IViewerProps) {
+ super(props);
+
+ 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) {
+ this.renderPages();
+ }
+ }
+
+ @action
+ componentDidMount = () => {
+ this._reactionDisposer = reaction(
+
+ () => [this.props.parent.props.active(), this.startIndex, this._pageSizes.length ? this.endIndex : 0],
+ async () => {
+ await this.initialLoad();
+ this.renderPages();
+ }, { fireImmediately: true });
+
+ this._annotationReactionDisposer = reaction(
+ () => {
+ return this.props.parent && this.props.parent.fieldExtensionDoc && DocListCast(this.props.parent.fieldExtensionDoc.annotations);
+ },
+ (annotations: Doc[]) => {
+ annotations && annotations.length && this.renderAnnotations(annotations, true);
+ },
+ { fireImmediately: true });
+
+ this._activeReactionDisposer = reaction(
+ () => this.props.parent.props.active(),
+ () => {
+ runInAction(() => {
+ if (!this.props.parent.props.active()) {
+ this._searching = false;
+ this._pdfFindController = null;
+ if (this._viewer.current) {
+ let cns = this._viewer.current.childNodes;
+ for (let i = cns.length - 1; i >= 0; i--) {
+ cns.item(i).remove();
+ }
+ }
+ }
+ });
+ }
+ );
+
+ if (this.props.parent.props.ContainingCollectionView) {
+ this._filterReactionDisposer = reaction(
+ () => 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) {
+ this.props.parent.scrollTo(y);
+ }
+
+ @action
+ initialLoad = async () => {
+ 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++) {
+ 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);
+
+ 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 = 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;
+ 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;
+ annoDoc.target = sourceDoc;
+ annoDoc.group = mainAnnoDoc;
+ annoDoc.color = color;
+ annoDoc.type = AnnotationTypes.Region;
+ annoDocs.push(annoDoc);
+ anno.remove();
+ }
+ });
+
+ 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;
+ }
+
+ drop = async (e: Event, de: DragManager.DropEvent) => {
+ if (de.data instanceof DragManager.LinkDragData) {
+ let sourceDoc = de.data.linkSourceDocument;
+ let destDoc = this.makeAnnotationDocument(sourceDoc, 1, "red");
+ de.data.droppedDocuments.push(destDoc);
+ let targetAnnotations = DocListCast(this.props.parent.fieldExtensionDoc.annotations);
+ if (targetAnnotations) {
+ targetAnnotations.push(destDoc);
+ this.props.parent.fieldExtensionDoc.annotations = new List<Doc>(targetAnnotations);
+ }
+ else {
+ this.props.parent.fieldExtensionDoc.annotations = new List<Doc>([destDoc]);
+ }
+ e.stopPropagation();
+ }
+ }
+ /**
+ * Called by the Page class when it gets rendered, initializes the lists and
+ * puts a placeholder with all of the correct page sizes when all of the pages have been loaded.
+ */
+ @action
+ 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") {
+ this._isPage[page] = "none";
+ this._visibleElements[page] = (
+ <div key={`${this.props.url}-placeholder-${page + 1}`} className="pdfviewer-placeholder"
+ style={{ width: this._pageSizes[page].width, height: this._pageSizes[page].height }} />
+ );
+ }
+ }
+
+ @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}
+ key={`${this.props.url}-rendered-${page + 1}`}
+ name={`${this.props.pdf.fingerprint + `-page${page + 1}`}`}
+ pageLoaded={this.pageLoaded}
+ parent={this.props.parent}
+ makePin={emptyFunction}
+ renderAnnotations={this.renderAnnotations}
+ createAnnotation={this.createAnnotation}
+ sendAnnotations={this.receiveAnnotations}
+ makeAnnotationDocuments={this.makeAnnotationDocument}
+ getScrollFromPage={this.getScrollFromPage}
+ {...this.props} />
+ );
+ }
+ }
+
+ // change the address to be the file address of the PNG version of each page
+ // file address of the pdf
+ @action
+ getPageImage = async (page: number) => {
+ let handleError = () => this.getRenderedPage(page);
+ if (this._isPage[page] !== "image") {
+ this._isPage[page] = "image";
+ const address = this.props.url;
+ try {
+ let res = JSON.parse(await rp.get(DocServer.prepend(`/thumbnail${address.substring("files/".length, address.length - ".pdf".length)}-${page + 1}.PNG`)));
+ 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) {
+
+ }
+ }
+ }
+
+ @computed get scrollY(): number { return this.props.scrollY; }
+
+ // startIndex: where to start rendering pages
+ @computed get startIndex(): number { return Math.max(0, this.getPageFromScroll(this.scrollY) - this._pageBuffer); }
+
+ // endIndex: where to end rendering pages
+ @computed get endIndex(): number {
+ return Math.min(this.props.pdf.numPages - 1, this.getPageFromScroll(this.scrollY + this._pageSizes[0].height) + this._pageBuffer);
+ }
+
+ @action
+ renderPages = () => {
+ for (let i = 0; i < this.props.pdf.numPages; i++) {
+ if (i < this.startIndex || i > this.endIndex) {
+ this.getPlaceholderPage(i); // pages outside of the pdf use empty stand-in divs
+ } else {
+ if (this.props.parent.props.active()) {
+ this.getRenderedPage(i);
+ } else {
+ this.getPageImage(i);
+ }
+ }
+ }
+ }
+
+ @action
+ private renderAnnotations = (annotations: Doc[], removeOldAnnotations: boolean): void => {
+ if (removeOldAnnotations) {
+ this._annotations = annotations;
+ }
+ else {
+ this._annotations.push(...annotations);
+ this._annotations = new Array<Doc>(...this._annotations);
+ }
+ }
+
+ @action
+ receiveAnnotations = (annotations: HTMLDivElement[], page: number) => {
+ if (page === -1) {
+ this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove()));
+ this._savedAnnotations.keys().forEach(k => this._savedAnnotations.setValue(k, annotations));
+ }
+ else {
+ this._savedAnnotations.setValue(page, annotations);
+ }
+ }
+
+ sendAnnotations = (page: number): HTMLDivElement[] | undefined => {
+ return this._savedAnnotations.getValue(page);
+ }
+
+ // get the page index that the vertical offset passed in is on
+ getPageFromScroll = (vOffset: number) => {
+ let index = 0;
+ let currOffset = vOffset;
+ while (index < this._pageSizes.length && currOffset - this._pageSizes[index].height > 0) {
+ currOffset -= this._pageSizes[index++].height;
+ }
+ return index;
+ }
+
+ getScrollFromPage = (index: number): number => {
+ let counter = 0;
+ for (let i = 0; i < Math.min(this.props.pdf.numPages, index); i++) {
+ counter += this._pageSizes[i].height;
+ }
+ return counter;
+ }
+
+ createAnnotation = (div: HTMLDivElement, page: number) => {
+ if (this._annotationLayer.current) {
+ if (div.style.top) {
+ div.style.top = (parseInt(div.style.top) + this.getScrollFromPage(page)).toString();
+ }
+ this._annotationLayer.current.append(div);
+ let savedPage = this._savedAnnotations.getValue(page);
+ if (savedPage) {
+ savedPage.push(div);
+ this._savedAnnotations.setValue(page, savedPage);
+ }
+ else {
+ this._savedAnnotations.setValue(page, [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
+ });
+ this._rendered = true;
+ });
+ container.addEventListener("pagerendered", () => {
+ console.log("rendered");
+ this._pdfFindController.executeCommand('find',
+ {
+ caseSensitive: false,
+ findPrevious: undefined,
+ highlightAll: true,
+ phraseSearch: true,
+ query: searchString
+ });
+ this._rendered = true;
+ });
+ }
+ }
+
+ // 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._rendered = true;
+ });
+ 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;
+ });
+ 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" }} 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.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 >
+ );
+ }
+}
+
+export enum AnnotationTypes {
+ Region
+}
+
+class SimpleLinkService {
+ externalLinkTarget: any = null;
+ externalLinkRel: any = null;
+ pdf: any = null;
+
+ navigateTo(dest: any) { }
+
+ getDestinationHash(dest: any) { return "#"; }
+
+ getAnchorUrl(hash: any) { return "#"; }
+
+ setHash(hash: any) { }
+
+ executeNamedAction(action: any) { }
+
+ cachePageRef(pageNum: any, pageRef: any) { }
+
+ get pagesCount() {
+ return this.pdf ? this.pdf.numPages : 0;
+ }
+
+ get page() {
+ return 0;
+ }
+
+ setPdf(pdf: any) {
+ this.pdf = pdf;
+ }
+
+ 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
new file mode 100644
index 000000000..c9d442fe5
--- /dev/null
+++ b/src/client/views/pdf/Page.tsx
@@ -0,0 +1,436 @@
+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, DocListCastAsync } from "../../../new_fields/Doc";
+import "./PDFViewer.scss";
+import "pdfjs-dist/web/pdf_viewer.css";
+import { PDFBox } from "../nodes/PDFBox";
+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, 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;
+ page: number;
+ pageLoaded: (index: number, page: Pdfjs.PDFPageViewport) => void;
+ parent: PDFBox;
+ renderAnnotations: (annotations: Doc[], removeOld: boolean) => void;
+ makePin: (x: number, y: number, page: number) => void;
+ sendAnnotations: (annotations: HTMLDivElement[], page: number) => void;
+ createAnnotation: (div: HTMLDivElement, page: number) => void;
+ 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 = 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;
+ @observable private _marqueeY: number = 0;
+ @observable private _marqueeWidth: number = 0;
+ @observable private _marqueeHeight: number = 0;
+ @observable private _rotate: string = "";
+
+ private _canvas: React.RefObject<HTMLCanvasElement>;
+ private _textLayer: React.RefObject<HTMLDivElement>;
+ private _annotationLayer: React.RefObject<HTMLDivElement>;
+ private _marquee: React.RefObject<HTMLDivElement>;
+ // 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);
+ this._canvas = React.createRef();
+ this._textLayer = React.createRef();
+ this._annotationLayer = React.createRef();
+ this._marquee = React.createRef();
+ // this._curly = React.createRef();
+ }
+
+ componentDidMount = (): void => {
+ if (this.props.pdf) {
+ this.update(this.props.pdf);
+ }
+ }
+
+ componentWillUnmount = (): void => {
+ if (this._reactionDisposer) {
+ this._reactionDisposer();
+ }
+ }
+
+ componentDidUpdate = (): void => {
+ if (this.props.pdf) {
+ this.update(this.props.pdf);
+ }
+ }
+
+ private update = (pdf: Pdfjs.PDFDocumentProxy): void => {
+ if (pdf) {
+ this.loadPage(pdf);
+ }
+ else {
+ this._state = "loading";
+ }
+ }
+
+ private loadPage = (pdf: Pdfjs.PDFDocumentProxy): void => {
+ if (this._state === "rendering" || this._page) return;
+
+ pdf.getPage(this._currPage).then(
+ (page: Pdfjs.PDFPageProxy): void => {
+ this._state = "rendering";
+ this.renderPage(page);
+ }
+ );
+ }
+
+ @action
+ private renderPage = (page: Pdfjs.PDFPageProxy): void => {
+ // lower scale = easier to read at small sizes, higher scale = easier to read at large sizes
+ let viewport = page.getViewport(scale);
+ let canvas = this._canvas.current;
+ let textLayer = this._textLayer.current;
+ if (canvas && textLayer) {
+ let ctx = canvas.getContext("2d");
+ canvas.width = viewport.width;
+ this._width = viewport.width;
+ canvas.height = viewport.height;
+ this._height = viewport.height;
+ this.props.pageLoaded(this._currPage, viewport);
+ if (ctx) {
+ // renders the page onto the canvas context
+ page.render({ canvasContext: ctx, viewport: viewport });
+ // renders text onto the text container
+ page.getTextContent().then((res: Pdfjs.TextContent): void => {
+ //@ts-ignore
+ Pdfjs.renderTextLayer({
+ textContent: res,
+ container: textLayer,
+ viewport: viewport
+ });
+ });
+
+ this._page = page;
+ }
+ }
+ }
+
+ @action
+ highlight = (targetDoc?: Doc, color: string = "red") => {
+ // creates annotation documents for current highlights
+ let annotationDoc = this.props.makeAnnotationDocuments(targetDoc, scale, color, false);
+ let targetAnnotations = Cast(this.props.parent.fieldExtensionDoc.annotations, listSpec(Doc));
+ if (targetAnnotations === undefined) {
+ Doc.GetProto(this.props.parent.fieldExtensionDoc).annotations = new List([annotationDoc]);
+ } else {
+ targetAnnotations.push(annotationDoc);
+ }
+ return annotationDoc;
+ }
+
+ /**
+ * This is temporary for creating annotations from highlights. It will
+ * start a drag event and create or put the necessary info into the drag event.
+ */
+ @action
+ 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.Create.TextDocument({ width: 200, height: 200, title: "New Annotation" });
+ targetDoc.targetPage = this.props.page;
+ let annotationDoc = this.highlight(undefined, "red");
+ annotationDoc.linkedToDoc = false;
+ // create dragData and star tdrag
+ let dragData = new DragManager.AnnotationDragData(thisDoc, annotationDoc, targetDoc);
+ if (this._textLayer.current) {
+ DragManager.StartAnnotationDrag([ele], dragData, e.pageX, e.pageY, {
+ handlers: {
+ dragComplete: 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
+ });
+ }
+ }
+
+ // cleans up events and boolean
+ endDrag = (e: PointerEvent): void => {
+ // document.removeEventListener("pointermove", this.startDrag);
+ // document.removeEventListener("pointerup", this.endDrag);
+ 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
+ if (e.altKey && e.button === 0) {
+ e.stopPropagation();
+
+ // document.removeEventListener("pointermove", this.startDrag);
+ // document.addEventListener("pointermove", this.startDrag);
+ // document.removeEventListener("pointerup", this.endDrag);
+ // document.addEventListener("pointerup", this.endDrag);
+ }
+ 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;
+ if (target && target.parentElement === this._textLayer.current) {
+ e.stopPropagation();
+ }
+ else {
+ // set marquee x and y positions to the spatially transformed position
+ let current = this._textLayer.current;
+ if (current) {
+ let boundingRect = current.getBoundingClientRect();
+ 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";
+ }
+ document.removeEventListener("pointermove", this.onSelectStart);
+ document.addEventListener("pointermove", this.onSelectStart);
+ document.removeEventListener("pointerup", this.onSelectEnd);
+ document.addEventListener("pointerup", this.onSelectEnd);
+ if (!e.ctrlKey) {
+ this.props.sendAnnotations([], -1);
+ }
+ }
+ }
+
+ @action
+ onSelectStart = (e: PointerEvent): void => {
+ let target: any = e.target;
+ if (this._marqueeing) {
+ let current = this._textLayer.current;
+ 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._startX;
+ this._marqueeHeight = ((e.clientY - boundingRect.top) * (current.offsetHeight / boundingRect.height)) - this._startY;
+ this._marqueeX = Math.min(this._startX, this._startX + this._marqueeWidth);
+ this._marqueeY = Math.min(this._startY, this._startY + this._marqueeHeight);
+ this._marqueeWidth = Math.abs(this._marqueeWidth);
+ this._marqueeHeight = Math.abs(this._marqueeHeight);
+ let { background, opacity, transform: transform } = this.getCurlyTransform();
+ if (this._marquee.current /*&& this._curly.current*/) {
+ this._marquee.current.style.background = background;
+ // this._curly.current.style.opacity = opacity;
+ this._rotate = transform;
+ }
+ }
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ else if (target && target.parentElement === this._textLayer.current) {
+ e.stopPropagation();
+ }
+ }
+
+ 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";
+ // }
+
+ // // 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
+ onSelectEnd = (e: PointerEvent): void => {
+ if (this._marqueeing) {
+ this._marqueeing = false;
+ if (this._marquee.current) {
+ let copy = document.createElement("div");
+ // make a copy of the marquee
+ 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.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);
+ }
+
+ this._marqueeHeight = this._marqueeWidth = 0;
+ }
+ else {
+ let sel = window.getSelection();
+ if (sel && sel.type === "Range") {
+ this.createTextAnnotation(sel);
+ PDFMenu.Instance.jumpTo(e.clientX, e.clientY);
+ }
+ }
+
+
+ if (PDFMenu.Instance.Highlighting) {
+ this.highlight(undefined, "goldenrod");
+ }
+ else {
+ PDFMenu.Instance.StartDrag = this.startDrag;
+ PDFMenu.Instance.Highlight = this.highlight;
+ }
+ document.removeEventListener("pointermove", this.onSelectStart);
+ document.removeEventListener("pointerup", this.onSelectEnd);
+ }
+
+ @action
+ createTextAnnotation = (sel: Selection) => {
+ let clientRects = sel.getRangeAt(0).getClientRects();
+ if (this._textLayer.current) {
+ let boundingRect = this._textLayer.current.getBoundingClientRect();
+ for (let i = 0; i < clientRects.length; i++) {
+ let rect = clientRects.item(i);
+ if (rect && rect.width !== this._textLayer.current.getBoundingClientRect().width && rect.height !== this._textLayer.current.getBoundingClientRect().height) {
+ let annoBox = document.createElement("div");
+ annoBox.className = "pdfViewer-annotationBox";
+ // transforms the positions from screen onto the pdf div
+ annoBox.style.top = ((rect.top - boundingRect.top) * (this._textLayer.current.offsetHeight / boundingRect.height)).toString();
+ annoBox.style.left = ((rect.left - boundingRect.left) * (this._textLayer.current.offsetWidth / boundingRect.width)).toString();
+ annoBox.style.width = (rect.width * this._textLayer.current.offsetWidth / boundingRect.width).toString();
+ annoBox.style.height = (rect.height * this._textLayer.current.offsetHeight / boundingRect.height).toString();
+ this.props.createAnnotation(annoBox, this.props.page);
+ }
+ }
+ }
+ // clear selection
+ if (sel.empty) { // Chrome
+ sel.empty();
+ } else if (sel.removeAllRanges) { // Firefox
+ sel.removeAllRanges();
+ }
+ }
+
+ doubleClick = (e: React.MouseEvent) => {
+ let target: any = e.target;
+ // if double clicking text
+ if (target && target.parentElement === this._textLayer.current) {
+ // do something to select the paragraph ideally
+ }
+
+ let current = this._textLayer.current;
+ if (current) {
+ let boundingRect = current.getBoundingClientRect();
+ let x = (e.clientX - boundingRect.left) * (current.offsetWidth / boundingRect.width);
+ let y = (e.clientY - boundingRect.top) * (current.offsetHeight / boundingRect.height);
+ this.props.makePin(x, y, this.props.page);
+ }
+ }
+
+ render() {
+ return (
+ <div onPointerDown={this.onPointerDown} onDoubleClick={this.doubleClick} className={"page-cont"} style={{ "width": this._width, "height": this._height }}>
+ <div className="canvasContainer">
+ <canvas ref={this._canvas} />
+ </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: "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` }} />
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx
index 5818519de..a16d7bc76 100644
--- a/src/client/views/presentationview/PresentationElement.tsx
+++ b/src/client/views/presentationview/PresentationElement.tsx
@@ -8,15 +8,15 @@ import "./PresentationView.scss";
import { Utils } from "../../../Utils";
import { library } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { faFile as fileSolid, faLocationArrow, faArrowUp, faSearch } from '@fortawesome/free-solid-svg-icons';
+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";
library.add(faArrowUp);
library.add(fileSolid);
-library.add(fileRegular);
library.add(faLocationArrow);
+library.add(fileRegular as any);
library.add(faSearch);
interface PresentationElementProps {
@@ -370,14 +370,14 @@ export default class PresentationElement extends React.Component<PresentationEle
className += " presentationView-selected";
}
let onEnter = (e: React.PointerEvent) => { p.document.libraryBrush = true; };
- let onLeave = (e: React.PointerEvent) => { p.document.libraryBrush = false; };
+ let onLeave = (e: React.PointerEvent) => { p.document.libraryBrush = undefined; };
return (
<div className={className} key={p.document[Id] + p.index}
onPointerEnter={onEnter} onPointerLeave={onLeave}
style={{
outlineColor: "maroon",
outlineStyle: "dashed",
- outlineWidth: BoolCast(p.document.libraryBrush, false) || BoolCast(p.document.protoBrush, false) ? `1px` : "0px",
+ outlineWidth: BoolCast(p.document.libraryBrush) ? `1px` : "0px",
}}
onClick={e => { p.gotoDocument(p.index, NumCast(this.props.mainDocument.selectedDoc)); e.stopPropagation(); }}>
<strong className="presentationView-name">
@@ -388,8 +388,8 @@ export default class PresentationElement extends React.Component<PresentationEle
<button title="Zoom" className={this.selectedButtons[buttonIndex.Show] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onZoomDocumentClick}><FontAwesomeIcon icon={"search"} /></button>
<button title="Navigate" className={this.selectedButtons[buttonIndex.Navigate] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onNavigateDocumentClick}><FontAwesomeIcon icon={"location-arrow"} /></button>
<button title="Hide Document Till Presented" className={this.selectedButtons[buttonIndex.HideTillPressed] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onHideDocumentUntilPressClick}><FontAwesomeIcon icon={fileSolid} /></button>
- <button title="Fade Document After Presented" className={this.selectedButtons[buttonIndex.FadeAfter] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={fileRegular} color={"gray"} /></button>
- <button title="Hide Document After Presented" className={this.selectedButtons[buttonIndex.HideAfter] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={fileRegular} /></button>
+ <button title="Fade Document After Presented" className={this.selectedButtons[buttonIndex.FadeAfter] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} color={"gray"} /></button>
+ <button title="Hide Document After Presented" className={this.selectedButtons[buttonIndex.HideAfter] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
<button title="Group With Up" className={this.selectedButtons[buttonIndex.Group] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={(e) => {
e.stopPropagation();
this.changeGroupStatus();
diff --git a/src/client/views/presentationview/PresentationList.tsx b/src/client/views/presentationview/PresentationList.tsx
index e2ec343d3..7abd3e366 100644
--- a/src/client/views/presentationview/PresentationList.tsx
+++ b/src/client/views/presentationview/PresentationList.tsx
@@ -82,8 +82,23 @@ export default class PresentationViewList extends React.Component<PresListProps>
return (
<div className="presentationView-listCont">
- {children.map((doc: Doc, index: number) => <PresentationElement ref={(e) => { if (e) { this.props.presElementsMappings.set(doc, e); } }} key={doc[Id]} mainDocument={this.props.mainDocument} document={doc} index={index} deleteDocument={this.props.deleteDocument} gotoDocument={this.props.gotoDocument} groupMappings={this.props.groupMappings} allListElements={children} presStatus={this.props.presStatus} presButtonBackUp={this.props.presButtonBackUp} presGroupBackUp={this.props.presGroupBackUp} />)}
+ {children.map((doc: Doc, index: number) =>
+ <PresentationElement
+ ref={(e) => { if (e) { this.props.presElementsMappings.set(doc, e); } }}
+ key={doc[Id]}
+ mainDocument={this.props.mainDocument}
+ document={doc}
+ index={index}
+ deleteDocument={this.props.deleteDocument}
+ gotoDocument={this.props.gotoDocument}
+ groupMappings={this.props.groupMappings}
+ allListElements={children}
+ presStatus={this.props.presStatus}
+ presButtonBackUp={this.props.presButtonBackUp}
+ presGroupBackUp={this.props.presGroupBackUp}
+ />
+ )}
</div>
);
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/presentationview/PresentationView.tsx b/src/client/views/presentationview/PresentationView.tsx
index 50defa197..edbbeb8f9 100644
--- a/src/client/views/presentationview/PresentationView.tsx
+++ b/src/client/views/presentationview/PresentationView.tsx
@@ -494,6 +494,7 @@ export class PresentationView extends React.Component<PresViewProps> {
/**
* Adds a document to the presentation view
**/
+ @undoBatch
@action
public PinDoc(doc: Doc) {
//add this new doc to props.Document
@@ -590,7 +591,7 @@ export class PresentationView extends React.Component<PresViewProps> {
@action
addNewPresentation = (presTitle: string) => {
//creating a new presentation doc
- let newPresentationDoc = Docs.TreeDocument([], { title: presTitle });
+ let newPresentationDoc = Docs.Create.TreeDocument([], { title: presTitle });
this.props.Documents.push(newPresentationDoc);
//setting that new doc as current
@@ -776,8 +777,18 @@ export class PresentationView extends React.Component<PresViewProps> {
{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} />
+ <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}
+ />
</div>
);
}
-} \ No newline at end of file
+}
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..d07df7e58
--- /dev/null
+++ b/src/client/views/search/SearchBox.tsx
@@ -0,0 +1,331 @@
+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 { DocServer } from '../../DocServer';
+import * as rp from 'request-promise';
+import { Id } from '../../../new_fields/FieldSymbols';
+import { SearchUtil } from '../../util/SearchUtil';
+import { RouteStore } from '../../../server/RouteStore';
+import { FilterBox } from './FilterBox';
+
+
+@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[] = [];
+ private _resultsSet = new Set<Doc>();
+ @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 = 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
+ 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, this.filterQuery, true, 0, 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, this.filterQuery, true, this._maxSearchIndex, 10).then(action(async (res: SearchUtil.DocSearchResult) => {
+
+ // happens at the beginning
+ if (res.numFound !== this._numTotalResults && this._numTotalResults === -1) {
+ this._numTotalResults = res.numFound;
+ }
+
+ const docs = await Promise.all(res.docs.map(doc => Cast(doc.extendsDoc, Doc, doc as any)));
+ let filteredDocs = FilterBox.Instance.filterDocsByType(docs);
+ runInAction(() => {
+ // this._results.push(...filteredDocs);
+ filteredDocs.forEach(doc => {
+ if (!this._resultsSet.has(doc)) {
+ this._results.push(doc);
+ this._resultsSet.add(doc);
+ }
+ });
+ });
+
+ 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 | 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} key={result[Id]} />;
+ this._isSearch[i] = "search";
+ }
+ }
+ else {
+ result = this._results[i];
+ if (result) {
+ this._visibleElements[i] = <SearchItem doc={result} key={result[Id]} />;
+ 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..24dd2eaa3
--- /dev/null
+++ b/src/client/views/search/SearchItem.scss
@@ -0,0 +1,211 @@
+@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 {
+ 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..e34d101a8
--- /dev/null
+++ b/src/client/views/search/SearchItem.tsx
@@ -0,0 +1,265 @@
+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;
+}
+
+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("", `data_l:"${this.props.doc[Id]}"`, true);
+ const map: Map<Doc, Doc> = new Map;
+ const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search("", `data_l:"${doc[Id]}"`, true).then(result => result.docs)));
+ 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" id="result" >{StrCast(this.props.doc.title)}</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
diff --git a/src/debug/Repl.tsx b/src/debug/Repl.tsx
index c2db3bdcb..4f4db13d2 100644
--- a/src/debug/Repl.tsx
+++ b/src/debug/Repl.tsx
@@ -4,6 +4,9 @@ import { observer } from 'mobx-react';
import { observable, computed } from 'mobx';
import { CompileScript } from '../client/util/Scripting';
import { makeInterface } from '../new_fields/Schema';
+import { ObjectField } from '../new_fields/ObjectField';
+import { RefField } from '../new_fields/RefField';
+import { DocServer } from '../client/DocServer';
@observer
class Repl extends React.Component {
@@ -42,7 +45,8 @@ class Repl extends React.Component {
return (
<div style={{ marginTop: "5px" }}>
<p>{command.command}</p>
- <pre>{JSON.stringify(command.result, null, 2)}</pre>
+ {/* <pre>{JSON.stringify(command.result, null, 2)}</pre> */}
+ <pre>{command.result instanceof RefField || command.result instanceof ObjectField ? "object" : String(command.result)}</pre>
</div>
);
});
@@ -60,4 +64,7 @@ class Repl extends React.Component {
}
}
-ReactDOM.render(<Repl />, document.getElementById("root")); \ No newline at end of file
+(async function () {
+ DocServer.init(window.location.protocol, window.location.hostname, 4321, "repl");
+ ReactDOM.render(<Repl />, document.getElementById("root"));
+})(); \ No newline at end of file
diff --git a/src/debug/Test.tsx b/src/debug/Test.tsx
index 57221aa39..0dca4b4b1 100644
--- a/src/debug/Test.tsx
+++ b/src/debug/Test.tsx
@@ -6,7 +6,6 @@ import { ImageField } from '../new_fields/URLField';
import { Doc } from '../new_fields/Doc';
import { List } from '../new_fields/List';
-
const schema1 = createSchema({
hello: "number",
test: "string",
diff --git a/src/debug/Viewer.tsx b/src/debug/Viewer.tsx
index b22300d0b..2b3eed154 100644
--- a/src/debug/Viewer.tsx
+++ b/src/debug/Viewer.tsx
@@ -10,6 +10,15 @@ import { List } from '../new_fields/List';
import { URLField } from '../new_fields/URLField';
import { EditableView } from '../client/views/EditableView';
import { CompileScript } from '../client/util/Scripting';
+import { DateField } from '../new_fields/DateField';
+import { ScriptField } from '../new_fields/ScriptField';
+import CursorField from '../new_fields/CursorField';
+
+DateField;
+URLField;
+ScriptField;
+CursorField;
+
function applyToDoc(doc: { [index: string]: FieldResult }, key: string, scriptString: string): boolean;
function applyToDoc(doc: { [index: number]: FieldResult }, key: number, scriptString: string): boolean;
@@ -169,9 +178,12 @@ class Viewer extends React.Component {
}
}
-ReactDOM.render((
- <div style={{ position: "absolute", width: "100%", height: "100%" }}>
- <Viewer />
- </div>),
- document.getElementById('root')
-); \ No newline at end of file
+(async function () {
+ await DocServer.init(window.location.protocol, window.location.hostname, 4321, "viewer");
+ ReactDOM.render((
+ <div style={{ position: "absolute", width: "100%", height: "100%" }}>
+ <Viewer />
+ </div>),
+ document.getElementById('root')
+ );
+})(); \ No newline at end of file
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
deleted file mode 100644
index ae532c9e2..000000000
--- a/src/fields/ScriptField.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-// import { Field, FieldId } from "./Field";
-// import { Types } from "../server/Message";
-// import { CompileScript, ScriptOptions, CompiledScript } from "../client/util/Scripting";
-// import { Server } from "../client/Server";
-// import { Without } from "../Utils";
-
-// export interface SerializableOptions extends Without<ScriptOptions, "capturedVariables"> {
-// capturedIds: { [id: string]: string };
-// }
-
-// export interface ScriptData {
-// script: string;
-// options: SerializableOptions;
-// }
-
-// export class ScriptField extends Field {
-// private _script?: CompiledScript;
-// get script(): CompiledScript {
-// return this._script!;
-// }
-// private options?: ScriptData;
-
-// constructor(script?: CompiledScript, id?: FieldId, save: boolean = true) {
-// super(id);
-
-// this._script = script;
-
-// if (save) {
-// Server.UpdateField(this);
-// }
-// }
-
-// ToScriptString() {
-// return "new ScriptField(...)";
-// }
-
-// GetValue() {
-// return this.script;
-// }
-
-// TrySetValue(): boolean {
-// throw new Error("Script fields currently can't be modified");
-// }
-
-// UpdateFromServer() {
-// throw new Error("Script fields currently can't be updated");
-// }
-
-// static FromJson(id: string, data: ScriptData): ScriptField {
-// let field = new ScriptField(undefined, id, false);
-// field.options = data;
-// return field;
-// }
-
-// init(callback: (res: Field) => any) {
-// const options = this.options!;
-// const keys = Object.keys(options.options.capturedIds);
-// Server.GetFields(keys).then(fields => {
-// let captured: { [name: string]: Field } = {};
-// keys.forEach(key => captured[options.options.capturedIds[key]] = fields[key]);
-// const opts: ScriptOptions = {
-// addReturn: options.options.addReturn,
-// params: options.options.params,
-// requiredType: options.options.requiredType,
-// capturedVariables: captured
-// };
-// const script = CompileScript(options.script, opts);
-// if (!script.compiled) {
-// throw new Error("Can't compile script");
-// }
-// this._script = script;
-// callback(this);
-// });
-// }
-
-// ToJson() {
-// const { options, originalScript } = this.script;
-// let capturedIds: { [id: string]: string } = {};
-// for (const capt in options.capturedVariables) {
-// capturedIds[options.capturedVariables[capt].Id] = capt;
-// }
-// const opts: SerializableOptions = {
-// ...options,
-// capturedIds
-// };
-// delete (opts as any).capturedVariables;
-// return {
-// id: this.Id,
-// type: Types.Script,
-// data: {
-// script: originalScript,
-// options: opts,
-// },
-// };
-// }
-
-// Copy(): Field {
-// //Script fields are currently immutable, so we can fake copy them
-// return this;
-// }
-// } \ No newline at end of file
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index bfc1738fc..a8f94b746 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -33,7 +33,7 @@ class Uploader extends React.Component {
onClick = async () => {
try {
this.status = "initializing protos";
- await Docs.initProtos();
+ await Docs.Prototypes.initialize();
let imgPrev = document.getElementById("img_preview");
if (imgPrev) {
let files: FileList | null = inputRef.current!.files;
@@ -53,7 +53,7 @@ class Uploader extends React.Component {
const json = await res.json();
json.map(async (file: any) => {
let path = window.location.origin + file;
- var doc = Docs.ImageDocument(path, { nativeWidth: 200, width: 200, title: name });
+ var doc = Docs.Create.ImageDocument(path, { nativeWidth: 200, width: 200, title: name });
this.status = "getting user document";
diff --git a/src/new_fields/DateField.ts b/src/new_fields/DateField.ts
index fc8abb9d9..abec91e06 100644
--- a/src/new_fields/DateField.ts
+++ b/src/new_fields/DateField.ts
@@ -2,7 +2,9 @@ import { Deserializable } from "../client/util/SerializationHelper";
import { serializable, date } from "serializr";
import { ObjectField } from "./ObjectField";
import { Copy, ToScriptString } from "./FieldSymbols";
+import { scriptingGlobal, Scripting } from "../client/util/Scripting";
+@scriptingGlobal
@Deserializable("date")
export class DateField extends ObjectField {
@serializable(date())
@@ -21,3 +23,7 @@ export class DateField extends ObjectField {
return `new DateField(new Date(${this.date.toISOString()}))`;
}
}
+
+Scripting.addGlobal(function d(...dateArgs: any[]) {
+ return new DateField(new (Date as any)(...dateArgs));
+});
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 7f7263cf1..0d9fa540f 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -2,14 +2,25 @@ import { observable, action } from "mobx";
import { serializable, primitive, map, alias, list } from "serializr";
import { autoObject, SerializationHelper, Deserializable } from "../client/util/SerializationHelper";
import { DocServer } from "../client/DocServer";
-import { setter, getter, getField, updateFunction, deleteProperty } from "./util";
-import { Cast, ToConstructor, PromiseValue, FieldValue, NumCast } from "./Types";
+import { setter, getter, getField, updateFunction, deleteProperty, makeEditable, makeReadOnly } from "./util";
+import { Cast, ToConstructor, PromiseValue, FieldValue, NumCast, BoolCast, StrCast } from "./Types";
import { listSpec } from "./Schema";
import { ObjectField } from "./ObjectField";
import { RefField, FieldId } from "./RefField";
import { ToScriptString, SelfProxy, Parent, OnUpdate, Self, HandleUpdate, Update, Id } from "./FieldSymbols";
+import { scriptingGlobal } from "../client/util/Scripting";
+import { List } from "./List";
export namespace Field {
+ export function toKeyValueString(doc: Doc, key: string): string {
+ const onDelegate = Object.keys(doc).includes(key);
+
+ let field = FieldValue(doc[key]);
+ if (Field.IsField(field)) {
+ return (onDelegate ? "=" : "") + Field.toScriptString(field);
+ }
+ return "";
+ }
export function toScriptString(field: Field): string {
if (typeof field === "string") {
return `"${field}"`;
@@ -55,6 +66,7 @@ export function DocListCast(field: FieldResult): Doc[] {
export const WidthSym = Symbol("Width");
export const HeightSym = Symbol("Height");
+@scriptingGlobal
@Deserializable("doc").withFields(["id"])
export class Doc extends RefField {
constructor(id?: FieldId, forceSave?: boolean) {
@@ -132,6 +144,16 @@ export class Doc extends RefField {
this[fKey] = value;
}
}
+ const unset = diff.$unset;
+ if (unset) {
+ for (const key in unset) {
+ if (!key.startsWith("fields.")) {
+ continue;
+ }
+ const fKey = key.substring(7);
+ delete this[fKey];
+ }
+ }
}
}
@@ -146,6 +168,15 @@ export namespace Doc {
// return Cast(field, ctor);
// });
// }
+ export function MakeReadOnly(): { end(): void } {
+ makeReadOnly();
+ return {
+ end() {
+ makeEditable();
+ }
+ };
+ }
+
export function Get(doc: Doc, key: string, ignoreProto: boolean = false): FieldResult {
const self = doc[Self];
return getField(self, key, ignoreProto);
@@ -156,6 +187,14 @@ export namespace Doc {
export function IsPrototype(doc: Doc) {
return GetT(doc, "isPrototype", "boolean", true);
}
+ export async function SetInPlace(doc: Doc, key: string, value: Field | undefined, defaultProto: boolean) {
+ let hasProto = doc.proto instanceof Doc;
+ let onDeleg = Object.getOwnPropertyNames(doc).indexOf(key) !== -1;
+ let onProto = hasProto && Object.getOwnPropertyNames(doc.proto).indexOf(key) !== -1;
+ if (onDeleg || !hasProto || (!onProto && !defaultProto)) {
+ doc[key] = value;
+ } else doc.proto![key] = value;
+ }
export async function SetOnPrototype(doc: Doc, key: string, value: Field) {
const proto = Object.getOwnPropertyNames(doc).indexOf("isPrototype") === -1 ? doc.proto : doc;
@@ -172,6 +211,18 @@ export namespace Doc {
}
return protos;
}
+
+ /**
+ * This function is intended to model Object.assign({}, {}) [https://mzl.la/1Mo3l21], which copies
+ * the values of the properties of a source object into the target.
+ *
+ * This is just a specific, Dash-authored version that serves the same role for our
+ * Doc class.
+ *
+ * @param doc the target document into which you'd like to insert the new fields
+ * @param fields the fields to project onto the target. Its type signature defines a mapping from some string key
+ * to a potentially undefined field, where each entry in this mapping is optional.
+ */
export function assign<K extends string>(doc: Doc, fields: Partial<Record<K, Opt<Field>>>) {
for (const key in fields) {
if (fields.hasOwnProperty(key)) {
@@ -186,7 +237,8 @@ export namespace Doc {
}
// compare whether documents or their protos match
- export function AreProtosEqual(doc: Doc, other: Doc) {
+ export function AreProtosEqual(doc?: Doc, other?: Doc) {
+ if (!doc || !other) return false;
let r = (doc === other);
let r2 = (doc.proto === other);
let r3 = (other.proto === doc);
@@ -196,7 +248,7 @@ export namespace Doc {
// gets the document's prototype or returns the document if it is a prototype
export function GetProto(doc: Doc) {
- return Doc.GetT(doc, "isPrototype", "boolean", true) ? doc : doc.proto!;
+ return Doc.GetT(doc, "isPrototype", "boolean", true) ? doc : (doc.proto || doc);
}
export function allKeys(doc: Doc): string[] {
@@ -211,13 +263,99 @@ export namespace Doc {
return Array.from(results);
}
+ export function AddDocToList(target: Doc, key: string, doc: Doc, relativeTo?: Doc, before?: boolean, first?: boolean, allowDuplicates?: boolean) {
+ if (target[key] === undefined) {
+ Doc.GetProto(target)[key] = new List<Doc>();
+ }
+ let list = Cast(target[key], listSpec(Doc));
+ if (list) {
+ if (allowDuplicates !== true) {
+ let pind = list.reduce((l, d, i) => d instanceof Doc && Doc.AreProtosEqual(d, doc) ? i : l, -1);
+ if (pind !== -1) {
+ list.splice(pind, 1);
+ }
+ }
+ if (first) list.splice(0, 0, doc);
+ else {
+ let ind = relativeTo ? list.indexOf(relativeTo) : -1;
+ if (ind === -1) list.push(doc);
+ else list.splice(before ? ind : ind + 1, 0, doc);
+ }
+ }
+ return true;
+ }
+
+ //
+ // Computes the bounds of the contents of a set of documents.
+ //
+ export function ComputeContentBounds(docList: Doc[]) {
+ let bounds = docList.reduce((bounds, doc) => {
+ var [sptX, sptY] = [NumCast(doc.x), NumCast(doc.y)];
+ let [bptX, bptY] = [sptX + doc[WidthSym](), sptY + doc[HeightSym]()];
+ return {
+ x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y),
+ r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b)
+ };
+ }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE });
+ return bounds;
+ }
+ //
+ // Resolves a reference to a field by returning 'doc' if field extension is specified,
+ // otherwise, it returns the extension document stored in doc.<fieldKey>_ext.
+ // This mechanism allows any fields to be extended with an extension document that can
+ // be used to capture field-specific metadata. For example, an image field can be extended
+ // to store annotations, ink, and other data.
+ //
+ export function resolvedFieldDataDoc(doc: Doc, fieldKey: string, fieldExt: string) {
+ return fieldExt && doc[fieldKey + "_ext"] instanceof Doc ? doc[fieldKey + "_ext"] as Doc : doc;
+ }
+
+ export function UpdateDocumentExtensionForField(doc: Doc, fieldKey: string) {
+ let extensionDoc = doc[fieldKey + "_ext"];
+ if (extensionDoc === undefined) {
+ setTimeout(() => {
+ let docExtensionForField = new Doc(doc[Id] + fieldKey, true);
+ docExtensionForField.title = "Extension of " + doc.title + "'s field:" + fieldKey;
+ docExtensionForField.extendsDoc = doc;
+ let proto: Doc | undefined = doc;
+ while (proto && !Doc.IsPrototype(proto)) {
+ proto = proto.proto;
+ }
+ (proto ? proto : doc)[fieldKey + "_ext"] = docExtensionForField;
+ }, 0);
+ } else if (extensionDoc instanceof Doc && extensionDoc.extendsDoc === undefined) {
+ setTimeout(() => (extensionDoc as Doc).extendsDoc = doc, 0);
+ }
+ }
export function MakeAlias(doc: Doc) {
- const alias = new Doc;
if (!GetT(doc, "isPrototype", "boolean", true)) {
- alias.proto = doc.proto;
+ return Doc.MakeCopy(doc);
+ }
+ return Doc.MakeDelegate(doc); // bcz?
+ }
+
+ export function expandTemplateLayout(templateLayoutDoc: Doc, dataDoc?: Doc) {
+ let resolvedDataDoc = (templateLayoutDoc !== dataDoc) ? dataDoc : undefined;
+ if (!dataDoc || !(templateLayoutDoc && !(Cast(templateLayoutDoc.layout, Doc) instanceof Doc) && resolvedDataDoc && resolvedDataDoc !== templateLayoutDoc)) {
+ return templateLayoutDoc;
}
- return alias;
+ // if we have a data doc that doesn't match the layout, then we're rendering a template.
+ // ... which means we change the layout to be an expanded view of the template layout.
+ // This allows the view override the template's properties and be referenceable as its own document.
+
+ let expandedTemplateLayout = templateLayoutDoc["_expanded_" + dataDoc[Id]];
+ if (expandedTemplateLayout instanceof Doc) {
+ return expandedTemplateLayout;
+ }
+ if (expandedTemplateLayout === undefined && BoolCast(templateLayoutDoc.isTemplate)) {
+ setTimeout(() => {
+ templateLayoutDoc["_expanded_" + dataDoc[Id]] = Doc.MakeDelegate(templateLayoutDoc);
+ (templateLayoutDoc["_expanded_" + dataDoc[Id]] as Doc).title = templateLayoutDoc.title + " applied to " + dataDoc.title;
+ (templateLayoutDoc["_expanded_" + dataDoc[Id]] as Doc).isExpandedTemplate = templateLayoutDoc;
+ }, 0);
+ }
+ return templateLayoutDoc;
}
export function MakeCopy(doc: Doc, copyProto: boolean = false): Doc {
@@ -238,6 +376,7 @@ export namespace Doc {
}
}
});
+
return copy;
}
@@ -251,4 +390,32 @@ export namespace Doc {
delegate.proto = doc;
return delegate;
}
+
+ export function MakeTemplate(fieldTemplate: Doc, metaKey: string, proto: Doc) {
+ // move data doc fields to layout doc as needed (nativeWidth/nativeHeight, data, ??)
+ let backgroundLayout = StrCast(fieldTemplate.backgroundLayout);
+ let fieldLayoutDoc = fieldTemplate;
+ if (fieldTemplate.layout instanceof Doc) {
+ fieldLayoutDoc = Doc.MakeDelegate(fieldTemplate.layout);
+ }
+ let layout = StrCast(fieldLayoutDoc.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
+ if (backgroundLayout) {
+ layout = StrCast(fieldLayoutDoc.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"} fieldExt={"annotations"}`);
+ backgroundLayout = backgroundLayout.replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
+ }
+ let nw = Cast(fieldTemplate.nativeWidth, "number");
+ let nh = Cast(fieldTemplate.nativeHeight, "number");
+
+ let layoutDelegate = fieldTemplate.layout instanceof Doc ? fieldLayoutDoc : fieldTemplate;
+ layoutDelegate.layout = layout;
+
+ fieldTemplate.title = metaKey;
+ fieldTemplate.layout = layoutDelegate !== fieldTemplate ? layoutDelegate : layout;
+ fieldTemplate.backgroundLayout = backgroundLayout;
+ fieldTemplate.nativeWidth = nw;
+ fieldTemplate.nativeHeight = nh;
+ fieldTemplate.isTemplate = true;
+ fieldTemplate.showTitle = "title";
+ fieldTemplate.proto = proto;
+ }
} \ No newline at end of file
diff --git a/src/new_fields/List.ts b/src/new_fields/List.ts
index f1e4c4721..a2133a990 100644
--- a/src/new_fields/List.ts
+++ b/src/new_fields/List.ts
@@ -7,6 +7,7 @@ import { ObjectField } from "./ObjectField";
import { RefField } from "./RefField";
import { ProxyField } from "./Proxy";
import { Self, Update, Parent, OnUpdate, SelfProxy, ToScriptString, Copy } from "./FieldSymbols";
+import { Scripting } from "../client/util/Scripting";
const listHandlers: any = {
/// Mutator methods
@@ -294,4 +295,6 @@ class ListImpl<T extends Field> extends ObjectField {
}
}
export type List<T extends Field> = ListImpl<T> & (T | (T extends RefField ? Promise<T> : never))[];
-export const List: { new <T extends Field>(fields?: T[]): List<T> } = ListImpl as any; \ No newline at end of file
+export const List: { new <T extends Field>(fields?: T[]): List<T> } = ListImpl as any;
+
+Scripting.addGlobal("List", List); \ No newline at end of file
diff --git a/src/new_fields/Proxy.ts b/src/new_fields/Proxy.ts
index 130ec066e..38d874a68 100644
--- a/src/new_fields/Proxy.ts
+++ b/src/new_fields/Proxy.ts
@@ -48,9 +48,8 @@ export class ProxyField<T extends RefField> extends ObjectField {
private failed = false;
private promise?: Promise<any>;
- value(callback?: ((field: T | undefined) => void)): T | undefined | FieldWaiting {
+ value(): T | undefined | FieldWaiting {
if (this.cache) {
- callback && callback(this.cache);
return this.cache;
}
if (this.failed) {
@@ -64,7 +63,6 @@ export class ProxyField<T extends RefField> extends ObjectField {
return field;
}));
}
- callback && this.promise.then(callback);
return this.promise;
}
}
diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts
index 89d077a47..78a3a4067 100644
--- a/src/new_fields/RichTextField.ts
+++ b/src/new_fields/RichTextField.ts
@@ -2,7 +2,9 @@ import { ObjectField } from "./ObjectField";
import { serializable } from "serializr";
import { Deserializable } from "../client/util/SerializationHelper";
import { Copy, ToScriptString } from "./FieldSymbols";
+import { scriptingGlobal } from "../client/util/Scripting";
+@scriptingGlobal
@Deserializable("RichTextField")
export class RichTextField extends ObjectField {
@serializable(true)
diff --git a/src/new_fields/Schema.ts b/src/new_fields/Schema.ts
index 250f3c975..2355304d5 100644
--- a/src/new_fields/Schema.ts
+++ b/src/new_fields/Schema.ts
@@ -1,5 +1,8 @@
import { Interface, ToInterface, Cast, ToConstructor, HasTail, Head, Tail, ListSpec, ToType, DefaultFieldConstructor } from "./Types";
import { Doc, Field } from "./Doc";
+import { ObjectField } from "./ObjectField";
+import { RefField } from "./RefField";
+import { SelfProxy } from "./FieldSymbols";
type AllToInterface<T extends Interface[]> = {
1: ToInterface<Head<T>> & AllToInterface<Tail<T>>,
@@ -10,10 +13,16 @@ export const emptySchema = createSchema({});
export const Document = makeInterface(emptySchema);
export type Document = makeInterface<[typeof emptySchema]>;
+export interface InterfaceFunc<T extends Interface[]> {
+ (docs: Doc[]): makeInterface<T>[];
+ (): makeInterface<T>;
+ (doc: Doc): makeInterface<T>;
+}
+
export type makeInterface<T extends Interface[]> = AllToInterface<T> & Doc & { proto: Doc | undefined };
// export function makeInterface<T extends Interface[], U extends Doc>(schemas: T): (doc: U) => All<T, U>;
// export function makeInterface<T extends Interface, U extends Doc>(schema: T): (doc: U) => makeInterface<T, U>;
-export function makeInterface<T extends Interface[]>(...schemas: T): (doc?: Doc) => makeInterface<T> {
+export function makeInterface<T extends Interface[]>(...schemas: T): InterfaceFunc<T> {
let schema: Interface = {};
for (const s of schemas) {
for (const key in s) {
@@ -25,10 +34,19 @@ export function makeInterface<T extends Interface[]>(...schemas: T): (doc?: Doc)
const field = receiver.doc[prop];
if (prop in schema) {
const desc = (schema as any)[prop];
- if (typeof desc === "object" && "defaultVal" in desc && "type" in desc) {
+ if (typeof desc === "object" && "defaultVal" in desc && "type" in desc) {//defaultSpec
return Cast(field, desc.type, desc.defaultVal);
+ } else if (typeof desc === "function" && !ObjectField.isPrototypeOf(desc) && !RefField.isPrototypeOf(desc)) {
+ const doc = Cast(field, Doc);
+ if (doc === undefined) {
+ return undefined;
+ } else if (doc instanceof Doc) {
+ return desc(doc);
+ } else {
+ return doc.then(doc => doc && desc(doc));
+ }
} else {
- return Cast(field, (schema as any)[prop]);
+ return Cast(field, desc);
}
}
return field;
@@ -38,14 +56,22 @@ export function makeInterface<T extends Interface[]>(...schemas: T): (doc?: Doc)
return true;
}
});
- return function (doc?: Doc) {
- doc = doc || new Doc;
- if (!(doc instanceof Doc)) {
- throw new Error("Currently wrapping a schema in another schema isn't supported");
- }
+ const fn = (doc: Doc) => {
+ doc = doc[SelfProxy];
+ // if (!(doc instanceof Doc)) {
+ // throw new Error("Currently wrapping a schema in another schema isn't supported");
+ // }
const obj = Object.create(proto, { doc: { value: doc, writable: false } });
return obj;
};
+ return function (doc?: Doc | Doc[]) {
+ doc = doc || new Doc;
+ if (doc instanceof Doc) {
+ return fn(doc);
+ } else {
+ return doc.map(fn);
+ }
+ };
}
export type makeStrictInterface<T extends Interface> = Partial<ToInterface<T>>;
diff --git a/src/new_fields/ScriptField.ts b/src/new_fields/ScriptField.ts
new file mode 100644
index 000000000..e2994ed70
--- /dev/null
+++ b/src/new_fields/ScriptField.ts
@@ -0,0 +1,96 @@
+import { ObjectField } from "./ObjectField";
+import { CompiledScript, CompileScript, scriptingGlobal } from "../client/util/Scripting";
+import { Copy, ToScriptString, Parent, SelfProxy } from "./FieldSymbols";
+import { serializable, createSimpleSchema, map, primitive, object, deserialize, PropSchema, custom, SKIP } from "serializr";
+import { Deserializable } from "../client/util/SerializationHelper";
+import { Doc } from "../new_fields/Doc";
+
+function optional(propSchema: PropSchema) {
+ return custom(value => {
+ if (value !== undefined) {
+ return propSchema.serializer(value);
+ }
+ return SKIP;
+ }, (jsonValue: any, context: any, oldValue: any, callback: (err: any, result: any) => void) => {
+ if (jsonValue !== undefined) {
+ return propSchema.deserializer(jsonValue, callback, context, oldValue);
+ }
+ return SKIP;
+ });
+}
+
+const optionsSchema = createSimpleSchema({
+ requiredType: true,
+ addReturn: true,
+ typecheck: true,
+ readonly: true,
+ params: optional(map(primitive()))
+});
+
+const scriptSchema = createSimpleSchema({
+ options: object(optionsSchema),
+ originalScript: true
+});
+
+function deserializeScript(script: ScriptField) {
+ const comp = CompileScript(script.script.originalScript, script.script.options);
+ if (!comp.compiled) {
+ throw new Error("Couldn't compile loaded script");
+ }
+ (script as any).script = comp;
+}
+
+@scriptingGlobal
+@Deserializable("script", deserializeScript)
+export class ScriptField extends ObjectField {
+ @serializable(object(scriptSchema))
+ readonly script: CompiledScript;
+
+ constructor(script: CompiledScript) {
+ super();
+
+ this.script = script;
+ }
+
+ // init(callback: (res: Field) => any) {
+ // const options = this.options!;
+ // const keys = Object.keys(options.options.capturedIds);
+ // Server.GetFields(keys).then(fields => {
+ // let captured: { [name: string]: Field } = {};
+ // keys.forEach(key => captured[options.options.capturedIds[key]] = fields[key]);
+ // const opts: ScriptOptions = {
+ // addReturn: options.options.addReturn,
+ // params: options.options.params,
+ // requiredType: options.options.requiredType,
+ // capturedVariables: captured
+ // };
+ // const script = CompileScript(options.script, opts);
+ // if (!script.compiled) {
+ // throw new Error("Can't compile script");
+ // }
+ // this._script = script;
+ // callback(this);
+ // });
+ // }
+
+ [Copy](): ObjectField {
+ return new ScriptField(this.script);
+ }
+
+ [ToScriptString]() {
+ return "script field";
+ }
+}
+
+@scriptingGlobal
+@Deserializable("computed", deserializeScript)
+export class ComputedField extends ScriptField {
+ //TODO maybe add an observable cache based on what is passed in for doc, considering there shouldn't really be that many possible values for doc
+ value(doc: Doc) {
+ const val = this.script.run({ this: doc });
+ if (val.success) {
+ return val.result;
+ }
+ return undefined;
+ }
+} \ No newline at end of file
diff --git a/src/new_fields/Types.ts b/src/new_fields/Types.ts
index c04dd5e6d..f8a4a30b4 100644
--- a/src/new_fields/Types.ts
+++ b/src/new_fields/Types.ts
@@ -1,8 +1,9 @@
import { Field, Opt, FieldResult, Doc } from "./Doc";
import { List } from "./List";
import { RefField } from "./RefField";
+import { DateField } from "./DateField";
-export type ToType<T extends ToConstructor<Field> | ListSpec<Field> | DefaultFieldConstructor<Field>> =
+export type ToType<T extends InterfaceValue> =
T extends "string" ? string :
T extends "number" ? number :
T extends "boolean" ? boolean :
@@ -10,7 +11,8 @@ export type ToType<T extends ToConstructor<Field> | ListSpec<Field> | DefaultFie
// T extends { new(...args: any[]): infer R } ? (R | Promise<R>) : never;
T extends DefaultFieldConstructor<infer _U> ? never :
T extends { new(...args: any[]): List<Field> } ? never :
- T extends { new(...args: any[]): infer R } ? R : never;
+ T extends { new(...args: any[]): infer R } ? R :
+ T extends (doc?: Doc) => infer R ? R : never;
export type ToConstructor<T extends Field> =
T extends string ? "string" :
@@ -38,14 +40,16 @@ export type Tail<T extends any[]> =
((...t: T) => any) extends ((_: any, ...tail: infer TT) => any) ? TT : [];
export type HasTail<T extends any[]> = T extends ([] | [any]) ? false : true;
+export type InterfaceValue = ToConstructor<Field> | ListSpec<Field> | DefaultFieldConstructor<Field> | ((doc?: Doc) => any);
//TODO Allow you to optionally specify default values for schemas, which should then make that field not be partial
export interface Interface {
- [key: string]: ToConstructor<Field> | ListSpec<Field> | DefaultFieldConstructor<Field>;
+ [key: string]: InterfaceValue;
// [key: string]: ToConstructor<Field> | ListSpec<Field[]>;
}
+export type WithoutRefField<T extends Field> = T extends RefField ? never : T;
export function Cast<T extends ToConstructor<Field> | ListSpec<Field>>(field: FieldResult, ctor: T): FieldResult<ToType<T>>;
-export function Cast<T extends ToConstructor<Field> | ListSpec<Field>>(field: FieldResult, ctor: T, defaultVal: WithoutList<ToType<T>> | null): WithoutList<ToType<T>>;
+export function Cast<T extends ToConstructor<Field> | ListSpec<Field>>(field: FieldResult, ctor: T, defaultVal: WithoutList<WithoutRefField<ToType<T>>> | null): WithoutList<ToType<T>>;
export function Cast<T extends ToConstructor<Field> | ListSpec<Field>>(field: FieldResult, ctor: T, defaultVal?: ToType<T> | null): FieldResult<ToType<T>> | undefined {
if (field instanceof Promise) {
return defaultVal === undefined ? field.then(f => Cast(f, ctor) as any) as any : defaultVal === null ? undefined : defaultVal;
@@ -77,6 +81,9 @@ export function StrCast(field: FieldResult, defaultVal: string | null = "") {
export function BoolCast(field: FieldResult, defaultVal: boolean | null = null) {
return Cast(field, "boolean", defaultVal);
}
+export function DateCast(field: FieldResult) {
+ return Cast(field, DateField, null);
+}
type WithoutList<T extends Field> = T extends List<infer R> ? (R extends RefField ? (R | Promise<R>)[] : R[]) : T;
diff --git a/src/new_fields/URLField.ts b/src/new_fields/URLField.ts
index 6e4cfa2ed..b9ad96450 100644
--- a/src/new_fields/URLField.ts
+++ b/src/new_fields/URLField.ts
@@ -2,6 +2,7 @@ import { Deserializable } from "../client/util/SerializationHelper";
import { serializable, custom } from "serializr";
import { ObjectField } from "./ObjectField";
import { ToScriptString, Copy } from "./FieldSymbols";
+import { Scripting, scriptingGlobal } from "../client/util/Scripting";
function url() {
return custom(
@@ -37,9 +38,9 @@ export abstract class URLField extends ObjectField {
}
}
-@Deserializable("audio") export class AudioField extends URLField { }
-@Deserializable("image") export class ImageField extends URLField { }
-@Deserializable("video") export class VideoField extends URLField { }
-@Deserializable("pdf") export class PdfField extends URLField { }
-@Deserializable("web") export class WebField extends URLField { }
-@Deserializable("youtube") export class YoutubeField extends URLField { } \ No newline at end of file
+@scriptingGlobal @Deserializable("audio") export class AudioField extends URLField { }
+@scriptingGlobal @Deserializable("image") export class ImageField extends URLField { }
+@scriptingGlobal @Deserializable("video") export class VideoField extends URLField { }
+@scriptingGlobal @Deserializable("pdf") export class PdfField extends URLField { }
+@scriptingGlobal @Deserializable("web") export class WebField extends URLField { }
+@scriptingGlobal @Deserializable("youtube") export class YoutubeField extends URLField { }
diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts
index 2b304c373..47e467041 100644
--- a/src/new_fields/util.ts
+++ b/src/new_fields/util.ts
@@ -2,13 +2,17 @@ import { UndoManager } from "../client/util/UndoManager";
import { Doc, Field } from "./Doc";
import { SerializationHelper } from "../client/util/SerializationHelper";
import { ProxyField } from "./Proxy";
-import { FieldValue } from "./Types";
import { RefField } from "./RefField";
import { ObjectField } from "./ObjectField";
import { action } from "mobx";
-import { Parent, OnUpdate, Update, Id } from "./FieldSymbols";
+import { Parent, OnUpdate, Update, Id, SelfProxy, Self } from "./FieldSymbols";
+import { ComputedField } from "./ScriptField";
-export const setter = action(function (target: any, prop: string | symbol | number, value: any, receiver: any): boolean {
+function _readOnlySetter(): never {
+ throw new Error("Documents can't be modified in read-only mode");
+}
+const _setterImpl = action(function (target: any, prop: string | symbol | number, value: any, receiver: any): boolean {
+ //console.log("-set " + target[SelfProxy].title + "(" + target[SelfProxy][prop] + ")." + prop.toString() + " = " + value);
if (SerializationHelper.IsSerializing()) {
target[prop] = value;
return true;
@@ -17,6 +21,9 @@ export const setter = action(function (target: any, prop: string | symbol | numb
target[prop] = value;
return true;
}
+ if (value !== undefined) {
+ value = value[SelfProxy] || value;
+ }
const curValue = target.__fields[prop];
if (curValue === value || (curValue instanceof ProxyField && value instanceof RefField && curValue.fieldId === value[Id])) {
// TODO This kind of checks correctly in the case that curValue is a ProxyField and value is a RefField, but technically
@@ -27,11 +34,10 @@ export const setter = action(function (target: any, prop: string | symbol | numb
value = new ProxyField(value);
}
if (value instanceof ObjectField) {
- //TODO Instead of target, maybe use target[Self]
- if (value[Parent] && value[Parent] !== target) {
+ if (value[Parent] && value[Parent] !== receiver) {
throw new Error("Can't put the same object in multiple documents at the same time");
}
- value[Parent] = target;
+ value[Parent] = receiver;
value[OnUpdate] = updateFunction(target, prop, value, receiver);
}
if (curValue instanceof ObjectField) {
@@ -43,7 +49,9 @@ export const setter = action(function (target: any, prop: string | symbol | numb
} else {
target.__fields[prop] = value;
}
- target[Update]({ '$set': { ["fields." + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : (value === undefined ? null : value) } });
+ if (value === undefined) target[Update]({ '$unset': { ["fields." + prop]: "" } });
+ if (typeof value === "object" && !(value instanceof ObjectField)) debugger;
+ else target[Update]({ '$set': { ["fields." + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : (value === undefined ? null : value) } });
UndoManager.AddEvent({
redo: () => receiver[prop] = value,
undo: () => receiver[prop] = curValue
@@ -51,6 +59,20 @@ export const setter = action(function (target: any, prop: string | symbol | numb
return true;
});
+let _setter: (target: any, prop: string | symbol | number, value: any, receiver: any) => boolean = _setterImpl;
+
+export function makeReadOnly() {
+ _setter = _readOnlySetter;
+}
+
+export function makeEditable() {
+ _setter = _setterImpl;
+}
+
+export function setter(target: any, prop: string | symbol | number, value: any, receiver: any): boolean {
+ return _setter(target, prop, value, receiver);
+}
+
export function getter(target: any, prop: string | symbol | number, receiver: any): any {
if (typeof prop === "symbol") {
return target.__fields[prop] || target[prop];
@@ -58,36 +80,30 @@ export function getter(target: any, prop: string | symbol | number, receiver: an
if (SerializationHelper.IsSerializing()) {
return target[prop];
}
- return getField(target, prop);
-}
-function getProtoField(protoField: Doc | undefined, prop: string | number, cb?: (field: Field | undefined) => void) {
- if (!protoField) return undefined;
- let field = protoField[prop];
- if (field instanceof Promise) {
- cb && field.then(cb);
- return field;
- } else {
- cb && cb(field);
- return field;
- }
+ return getFieldImpl(target, prop, receiver);
}
-//TODO The callback parameter is never being passed in currently, so we should be able to get rid of it.
-export function getField(target: any, prop: string | number, ignoreProto: boolean = false, callback?: (field: Field | undefined) => void): any {
+function getFieldImpl(target: any, prop: string | number, receiver: any, ignoreProto: boolean = false): any {
+ receiver = receiver || target[SelfProxy];
const field = target.__fields[prop];
if (field instanceof ProxyField) {
- return field.value(callback);
+ return field.value();
+ }
+ if (field instanceof ComputedField) {
+ return field.value(receiver);
}
if (field === undefined && !ignoreProto && prop !== "proto") {
- const proto = getField(target, "proto", true);
+ const proto = getFieldImpl(target, "proto", receiver, true);//TODO tfs: instead of receiver we could use target[SelfProxy]... I don't which semantics we want or if it really matters
if (proto instanceof Doc) {
- return getProtoField(proto, prop, callback);
- } else if (proto instanceof Promise) {
- return proto.then(async proto => getProtoField(proto, prop, callback));
+ return getFieldImpl(proto[Self], prop, receiver, ignoreProto);
}
+ return undefined;
}
- callback && callback(field);
return field;
+
+}
+export function getField(target: any, prop: string | number, ignoreProto: boolean = false): any {
+ return getFieldImpl(target, prop, undefined, ignoreProto);
}
export function deleteProperty(target: any, prop: string | number | symbol) {
@@ -95,7 +111,8 @@ export function deleteProperty(target: any, prop: string | number | symbol) {
delete target[prop];
return true;
}
- throw new Error("Currently properties can't be deleted from documents, assign to undefined instead");
+ target[SelfProxy][prop] = undefined;
+ return true;
}
export function updateFunction(target: any, prop: any, value: any, receiver: any) {
diff --git a/src/scraping/acm/.gitignore b/src/scraping/acm/.gitignore
new file mode 100644
index 000000000..caca8b99c
--- /dev/null
+++ b/src/scraping/acm/.gitignore
@@ -0,0 +1,2 @@
+./citations.txt
+./results.txt \ No newline at end of file
diff --git a/src/scraping/acm/chromedriver b/src/scraping/acm/chromedriver
new file mode 100755
index 000000000..9e9b16717
--- /dev/null
+++ b/src/scraping/acm/chromedriver
Binary files differ
diff --git a/src/scraping/acm/chromedriver.exe b/src/scraping/acm/chromedriver.exe
new file mode 100644
index 000000000..6a362fd43
--- /dev/null
+++ b/src/scraping/acm/chromedriver.exe
Binary files differ
diff --git a/src/scraping/acm/citations.txt b/src/scraping/acm/citations.txt
new file mode 100644
index 000000000..e5018ddef
--- /dev/null
+++ b/src/scraping/acm/citations.txt
@@ -0,0 +1,2 @@
+321046
+2412979 \ No newline at end of file
diff --git a/src/scraping/acm/debug.log b/src/scraping/acm/debug.log
new file mode 100644
index 000000000..8c0a148f4
--- /dev/null
+++ b/src/scraping/acm/debug.log
@@ -0,0 +1,38 @@
+[0625/170004.768:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/170004.769:ERROR:exception_snapshot_win.cc(98)] thread ID 17604 not found in process
+[0625/171124.644:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/171124.645:ERROR:exception_snapshot_win.cc(98)] thread ID 14348 not found in process
+[0625/171853.989:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/171853.990:ERROR:exception_snapshot_win.cc(98)] thread ID 12080 not found in process
+[0625/171947.744:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/171947.745:ERROR:exception_snapshot_win.cc(98)] thread ID 16160 not found in process
+[0625/172007.424:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/172007.425:ERROR:exception_snapshot_win.cc(98)] thread ID 13472 not found in process
+[0625/172059.353:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/172059.354:ERROR:exception_snapshot_win.cc(98)] thread ID 6396 not found in process
+[0625/172402.795:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/172402.796:ERROR:exception_snapshot_win.cc(98)] thread ID 10720 not found in process
+[0625/172618.850:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/172618.850:ERROR:exception_snapshot_win.cc(98)] thread ID 21136 not found in process
+[0625/172819.875:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/172819.876:ERROR:exception_snapshot_win.cc(98)] thread ID 17624 not found in process
+[0625/172953.674:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/172953.675:ERROR:exception_snapshot_win.cc(98)] thread ID 15180 not found in process
+[0625/173412.182:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/173412.182:ERROR:exception_snapshot_win.cc(98)] thread ID 13952 not found in process
+[0625/173447.806:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/173447.807:ERROR:exception_snapshot_win.cc(98)] thread ID 1572 not found in process
+[0625/173516.188:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/173516.189:ERROR:exception_snapshot_win.cc(98)] thread ID 5472 not found in process
+[0625/173528.446:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/173528.447:ERROR:exception_snapshot_win.cc(98)] thread ID 20420 not found in process
+[0625/173539.436:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/173539.437:ERROR:exception_snapshot_win.cc(98)] thread ID 16192 not found in process
+[0625/173643.139:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/173643.140:ERROR:exception_snapshot_win.cc(98)] thread ID 15716 not found in process
+[0625/173659.376:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/173659.377:ERROR:exception_snapshot_win.cc(98)] thread ID 11828 not found in process
+[0625/201137.209:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/201137.210:ERROR:exception_snapshot_win.cc(98)] thread ID 7688 not found in process
+[0625/210240.476:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022)
+[0625/210240.477:ERROR:exception_snapshot_win.cc(98)] thread ID 20828 not found in process
diff --git a/src/scraping/acm/index.js b/src/scraping/acm/index.js
new file mode 100644
index 000000000..b71d55226
--- /dev/null
+++ b/src/scraping/acm/index.js
@@ -0,0 +1,279 @@
+const {
+ Builder,
+ By
+} = require('selenium-webdriver');
+const {
+ readFile,
+ writeFile
+} = require('fs');
+
+const target_source = './citations.txt';
+const target_browser = 'chrome';
+const target_dist = './results.txt';
+
+const driver_pause = 500; // milliseconds
+const sample_line_char_max = 100; // characters
+
+const tab_map = {
+ abstract: "11",
+ authors: "14",
+ references: "15",
+ cited_by: "16",
+ index_terms: "17",
+ publication: "18",
+ reviews: "19",
+ comments: "20",
+ table_of_contents: "21"
+};
+
+String.prototype.removeAll = function (replacements, trim = true) {
+ let result = this;
+ for (let expression of replacements) {
+ result = result.replace(expression, "");
+ }
+ return trim ? result.trim() : result;
+};
+
+String.prototype.remove = function (replacement, trim = true) {
+ let result = this.replace(replacement, "");
+ return trim ? result.trim() : result;
+};
+
+Object.prototype.first = function () {
+ return this[Object.keys(this)[0]];
+};
+
+// GENERAL UTILITY FUNCTIONS
+
+function log_read(content) {
+ process.stdout.write("reading " + content + "...");
+}
+
+function log_snippet(result, quotes = true) {
+ let snippet = "failed to create snippet";
+ switch (typeof result) {
+ case "string":
+ let ellipse = result.length > sample_line_char_max;
+ let i = sample_line_char_max;
+ if (ellipse) {
+ while (result[i] != " " && i < -1) {
+ i--;
+ }
+ }
+ snippet = `${result.substring(0, i + 1).trim()}${ellipse ? "..." : ""}`;
+ snippet = quotes ? `"${snippet}"` : snippet;
+ break;
+ case "object":
+ if (Array.isArray(result)) {
+ snippet = result.map(res => {
+ switch (typeof res) {
+ case "string":
+ return res.substring(0, sample_line_char_max / result.length);
+ case "object":
+ return res.first();
+ }
+ }).join(', ');
+ } else {
+ snippet = result.first();
+ }
+ }
+ console.log(snippet);
+ return result;
+}
+
+// DRIVER UTILITY FUNCTIONS
+
+async function navigate_to(url) {
+ await driver.get(url);
+ await driver.sleep(driver_pause);
+}
+
+async function click_on(ref) {
+ await (await locate(ref)).click();
+ await driver.sleep(driver_pause);
+}
+
+async function click_on_acm_tab(target) {
+ await click_on(`//*[@id="tab-10${tab_map[target]}-btnInnerEl"]/span`);
+}
+
+async function locate(ref, multiple = false) {
+ let locator = ref.startsWith("//") ? By.xpath(ref) : By.id(ref);
+ return await multiple ? driver.findElements(locator) : driver.findElement(locator);
+}
+
+async function text_of(ref) {
+ let element = await locate(ref);
+ return await element.getText();
+}
+
+async function text_of_all(ref, delimiter = undefined) {
+ let elements = await locate(ref, true);
+ let results = [];
+ for (let element of elements) {
+ results.push(await element.getText());
+ }
+ return delimiter ? results.join(delimiter) : results;
+}
+
+async function logged_assign(key, value) {
+ log_read(key);
+ result[key] = log_snippet(value);
+}
+
+// TEXT SCRAPING
+
+async function read_authors() {
+ let authors = await text_of('//*[@id="tabpanel-1009-body"]');
+ let sanitize = line => line.length > 0 && !(line.startsWith("No contact information") || line.startsWith("View colleagues of") || line.startsWith("Bibliometrics:"));
+ let author_lines = authors.split("\n").map(line => line.trim()).filter(sanitize);
+
+ let all_authors = [];
+ let i = 0;
+ while (i < author_lines.length) {
+ let individual = [];
+ while (!author_lines[i].startsWith("Average citations")) {
+ individual.push(author_lines[i]);
+ i++;
+ }
+ individual.push(author_lines[i]);
+ all_authors.push(individual);
+ i++;
+ }
+
+ return all_authors.map(parse_author);
+}
+
+async function read_publication() {
+ let publciation_elements = (await text_of("source-body")).split("\n");
+ let publication_module = {};
+
+ let extract = (regex, target, index = 1) => regex.exec(target)[index];
+
+ for (let element of publciation_elements) {
+
+ let location = /Volume (\d+) Issue (\d+), ([\w.\d]+)/g;
+ let pages = /(\d+)-(\d+)/g;
+ let publication_date = /(\d{4}-\d{2}-\d{2})/g;
+ let publisher = /Publisher (.*)/g;
+ let issn = /ISSN: (\d{4}-\d{4})/g;
+ let eissn = /EISSN: ([\dA-Z]{4}-[\dA-Z]{4})/g;
+ let doi = /doi>([\.\d\/A-Z]+)/g;
+
+ if (element.startsWith("Title")) {
+ publication_module.name = element.substring(6).removeAll(["table of contents", "archive", /\w+ Homepage/]);
+ } else if (element.startsWith("Volume ")) {
+ let match = location.exec(element);
+ publication_module.volume = parseInt(match[1]);
+ publication_module.issue = parseInt(match[2]);
+ publication_module.month = match[3];
+ } else if (element.startsWith("Pages ")) {
+ let match = pages.exec(element);
+ publication_module.page_start = parseInt(match[1]);
+ publication_module.page_end = parseInt(match[2]);
+ } else if (element.startsWith("Publication Date ")) {
+ publication_module.publication_date = extract(publication_date, element);
+ } else if (element.startsWith("Publisher ")) {
+ publication_module.publisher = extract(publisher, element);
+ } else if (element.startsWith("ISSN: ")) {
+ publication_module.issn = extract(issn, element);
+ if (element.includes("EISSN: ")) {
+ publication_module.eissn = extract(eissn, element);
+ }
+ publication_module.doi = extract(doi, element);
+ }
+ }
+ return publication_module;
+}
+
+// JSON / DASH CONVERSION AND EXPORT
+
+function parse_author(metadata) {
+ let publicationYears = metadata[1].substring(18).split("-");
+ author = {
+ name: metadata[0],
+ publication_start: parseInt(publicationYears[0]),
+ publication_end: parseInt(publicationYears[1])
+ };
+ for (let count = 2; count < metadata.length; count++) {
+ let attr = metadata[count];
+ let char = attr.length - 1;
+ while (attr[char] != " ") {
+ char--;
+ }
+ let key = attr.substring(0, char).toLowerCase().replace(/ /g, "_").remove(/[\(\)]/g);
+ let value = parseFloat(attr.substring(char + 1).remove(/,/g));
+ author[key] = value;
+ }
+ return author;
+}
+
+function write_results() {
+ console.log();
+ let output = "";
+ results.forEach(res => output += (JSON.stringify(res, null, 4) + "\n"));
+
+ writeFile(target_dist, output, function errorHandler(exception) {
+ console.log(exception || "scraped references successfully written as JSON to ./results.txt");
+ });
+}
+
+async function scrape_targets(error, data) {
+ if (error) {
+ console.log("\nUnable to collect target citations from a citations.txt file stored in this directory.\nPlease make sure one is provided.");
+ return;
+ }
+
+ let references = data.split("\n").map(entry => entry.removeAll(["\r"])).filter(line => line.match(/\d+/g));
+ let quota = references.length;
+ log_snippet(`found ${quota} references to scrape`, false);
+
+ driver = await new Builder().forBrowser(target_browser).build();
+
+ for (let i = 0; i < quota; i++) {
+ try {
+ result = {};
+ let target;
+
+ let id = references[i];
+ let url = `https://dl.acm.org/citation.cfm?id=${id}`;
+ console.log(`\nscraping ${i + 1}/${quota} (${id})`);
+
+ await navigate_to(url);
+
+ logged_assign("url", url);
+ logged_assign("title", await text_of('//*[@id="divmain"]/div/h1'));
+
+ target = "abstract";
+ await click_on_acm_tab(target);
+ logged_assign(target, await text_of_all("abstract-body", " "));
+
+ target = "authors";
+ await click_on_acm_tab(target);
+ logged_assign(target, await read_authors());
+
+ target = "publication";
+ await click_on_acm_tab(target);
+ logged_assign(target, await read_publication());
+ } catch (e) {
+ console.log(e);
+ await driver.quit();
+ }
+
+ results.push(result);
+ }
+
+ write_results();
+
+ await driver.quit();
+}
+
+let driver;
+let results = [];
+let result = {};
+
+log_read("target references");
+
+readFile(target_source, {
+ encoding: "utf8"
+}, scrape_targets);
diff --git a/src/scraping/acm/package.json b/src/scraping/acm/package.json
new file mode 100644
index 000000000..10f4d2156
--- /dev/null
+++ b/src/scraping/acm/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "scraper",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "axios": "^0.19.0",
+ "cheerio": "^1.0.0-rc.3",
+ "selenium-webdriver": "^4.0.0-alpha.4"
+ }
+}
diff --git a/src/scraping/acm/results.txt b/src/scraping/acm/results.txt
new file mode 100644
index 000000000..a15da8b10
--- /dev/null
+++ b/src/scraping/acm/results.txt
@@ -0,0 +1,91 @@
+{
+ "url": "https://dl.acm.org/citation.cfm?id=321046",
+ "title": "Integer Programming Formulation of Traveling Salesman Problems",
+ "abstract": "It has been observed by many people that a striking number of quite diverse mathematical problems can be formulated as problems in integer programming, that is, linear programming problems in which some or all of the variables are required to assume integral values. This fact is rendered quite interesting by recent research on such problems, notably by R. E. Gomory [2, 3], which gives promise of yielding efficient computational techniques for their solution. The present paper provides yet another example of the versatility of integer programming as a mathematical modeling device by representing a generalization of the well-known “Travelling Salesman Problem” in integer programming terms. The authors have developed several such models, of which the one presented here is the most efficient in terms of generality, number of variables, and number of constraints. This model is due to the second author [4] and was presented briefly at the Symposium on Combinatorial Problems held at Princeton University, April 1960, sponsored by SIAM and IBM. The problem treated is: (1) A salesman is required to visit each of n cities, indexed by 1, … , n. He leaves from a “base city” indexed by 0, visits each of the n other cities exactly once, and returns to city 0. During his travels he must return to 0 exactly t times, including his final return (here t may be allowed to vary), and he must visit no more than p cities in one tour. (By a tour we mean a succession of visits to cities without stopping at city 0.) It is required to find such an itinerary which minimizes the total distance traveled by the salesman. Note that if t is fixed, then for the problem to have a solution we must have tp ≧ n. For t = 1, p ≧ n, we have the standard traveling salesman problem. Let dij (i ≠ j = 0, 1, … , n) be the distance covered in traveling from city i to city j. The following integer programming problem will be shown to be equivalent to (1): (2) Minimize the linear form ∑0≦i≠j≦n∑ dijxij over the set determined by the relations ∑ni=0i≠j xij = 1 (j = 1, … , n) ∑nj=0j≠i xij = 1 (i = 1, … , n) ui - uj + pxij ≦ p - 1 (1 ≦ i ≠ j ≦ n) where the xij are non-negative integers and the ui (i = 1, …, n) are arbitrary real numbers. (We shall see that it is permissible to restrict the ui to be non-negative integers as well.) If t is fixed it is necessary to add the additional relation: ∑nu=1 xi0 = t Note that the constraints require that xij = 0 or 1, so that a natural correspondence between these two problems exists if the xij are interpreted as follows: The salesman proceeds from city i to city j if and only if xij = 1. Under this correspondence the form to be minimized in (2) is the total distance to be traveled by the salesman in (1), so the burden of proof is to show that the two feasible sets correspond; i.e., a feasible solution to (2) has xij which do define a legitimate itinerary in (1), and, conversely a legitimate itinerary in (1) defines xij, which, together with appropriate ui, satisfy the constraints of (2). Consider a feasible solution to (2). The number of returns to city 0 is given by ∑ni=1 xi0. The constraints of the form ∑ xij = 1, all xij non-negative integers, represent the conditions that each city (other than zero) is visited exactly once. The ui play a role similar to node potentials in a network and the inequalities involving them serve to eliminate tours that do not begin and end at city 0 and tours that visit more than p cities. Consider any xr0r1 = 1 (r1 ≠ 0). There exists a unique r2 such that xr1r2 = 1. Unless r2 = 0, there is a unique r3 with xr2r3 = 1. We proceed in this fashion until some rj = 0. This must happen since the alternative is that at some point we reach an rk = rj, j + 1 < k. Since none of the r's are zero we have uri - uri + 1 + pxriri + 1 ≦ p - 1 or uri - uri + 1 ≦ - 1. Summing from i = j to k - 1, we have urj - urk = 0 ≦ j + 1 - k, which is a contradiction. Thus all tours include city 0. It remains to observe that no tours is of length greater than p. Suppose such a tour exists, x0r1 , xr1r2 , … , xrprp+1 = 1 with all ri ≠ 0. Then, as before, ur1 - urp+1 ≦ - p or urp+1 - ur1 ≧ p. But we have urp+1 - ur1 + pxrp+1r1 ≦ p - 1 or urp+1 - ur1 ≦ p (1 - xrp+1r1) - 1 ≦ p - 1, which is a contradiction. Conversely, if the xij correspond to a legitimate itinerary, it is clear that the ui can be adjusted so that ui = j if city i is the jth city visited in the tour which includes city i, for we then have ui - uj = - 1 if xij = 1, and always ui - uj ≦ p - 1. The above integer program involves n2 + n constraints (if t is not fixed) in n2 + 2n variables. Since the inequality form of constraint is fundamental for integer programming calculations, one may eliminate 2n variables, say the xi0 and x0j, by means of the equation constraints and produce an equivalent problem with n2 + n inequalities and n2 variables. The currently known integer programming procedures are sufficiently regular in their behavior to cast doubt on the heuristic value of machine experiments with our model. However, it seems appropriate to report the results of the five machine experiments we have conducted so far. The solution procedure used was the all-integer algorithm of R. E. Gomory [3] without the ranking procedure he describes. The first three experiments were simple model verification tests on a four-city standard traveling salesman problem with distance matrix [ 20 23 4 30 7 27 25 5 25 3 21 26 ] The first experiment was with a model, now obsolete, using roughly twice as many constraints and variables as the current model (for this problem, 28 constraints in 21 variables). The machine was halted after 4000 pivot steps had failed to produce a solution. The second experiment used the earlier model with the xi0 and x0j eliminated, resulting in a 28-constraint, 15-variable problem. Here the machine produced the optimal solution in 41 pivot steps. The third experiment used the current formulation with the xi0 and x0j eliminated, yielding 13 constraints and 9 variables. The optimal solution was reached in 7 pivot steps. The fourth and fifth experiments were used on a standard ten-city problem, due to Barachet, solved by Dantzig, Johnson and Fulkerson [1]. The current formulation was used, yielding 91 constraints in 81 variables. The fifth problem differed from the fourth only in that the ordering of the rows was altered to attempt to introduce more favorable pivot choices. In each case the machine was stopped after over 250 pivot steps had failed to produce the solution. In each case the last 100 pivot steps had failed to change the value of the objective function. It seems hopeful that more efficient integer programming procedures now under development will yield a satisfactory algorithmic solution to the traveling salesman problem, when applied to this model. In any case, the model serves to illustrate how problems of this sort may be succinctly formulated in integer programming terms.",
+ "authors": [
+ {
+ "name": "C. E. Miller",
+ "publication_start": 1960,
+ "publication_end": 1960,
+ "publication_count": 1,
+ "citation_count": 179,
+ "available_for_download": 1,
+ "downloads_6_weeks": 124,
+ "downloads_12_months": 923,
+ "downloads_cumulative": 9794,
+ "average_downloads_per_article": 9794,
+ "average_citations_per_article": 179
+ },
+ {
+ "name": "A. W. Tucker",
+ "publication_start": 1960,
+ "publication_end": 1993,
+ "publication_count": 5,
+ "citation_count": 196,
+ "available_for_download": 1,
+ "downloads_6_weeks": 124,
+ "downloads_12_months": 923,
+ "downloads_cumulative": 9794,
+ "average_downloads_per_article": 9794,
+ "average_citations_per_article": 39.2
+ },
+ {
+ "name": "R. A. Zemlin",
+ "publication_start": 1960,
+ "publication_end": 1964,
+ "publication_count": 2,
+ "citation_count": 188,
+ "available_for_download": 2,
+ "downloads_6_weeks": 124,
+ "downloads_12_months": 928,
+ "downloads_cumulative": 10025,
+ "average_downloads_per_article": 5012.5,
+ "average_citations_per_article": 94
+ }
+ ],
+ "publication": {
+ "name": "Journal of the ACM (JACM)",
+ "volume": 7,
+ "issue": 4,
+ "month": "Oct.",
+ "page_start": 326,
+ "page_end": 329,
+ "publication_date": "1960-10-01",
+ "publisher": "ACM New York, NY, USA",
+ "issn": "0004-5411",
+ "eissn": "1557-735X",
+ "doi": "10.1145/321043.321046"
+ }
+}
+{
+ "url": "https://dl.acm.org/citation.cfm?id=2412979",
+ "title": "STRUCT programming analysis system",
+ "abstract": "The STRUCT system utilizes the flexibility of a powerful graphics display system to provide a set of tools for program analysis. These tools allow the analysis of the static prograin structure and the dynamic execution behavior. of programs within the entire operating system/user program environment of the Brown University Graphics System (BUGS). Information is collected and presented in a manner which fully exploits two aspects of this environment. First, the operating system has been developed in a well-structured hierarcal manner following principles laid down by other researchers (2), (3). Second the programs under analysis have been written in a structured programming language following coding conventions which make available, at the source code level, valuable program control information. A new set of pictorial constructs is introduced for presenting a. program structure (static or dynamic) for inspection. These constructs combine the best features of an indented structured source code listing and the box odented nature of traditional flow charts. The graphical tools available are USed to provide for swift changes in. the desired level of detail displayed within a program structure, for traveling linearly through a program structure, for traveling through a complex program structure (following subroutine or system calls), for concurrently viewing multiple related program structures, and for presenting dynamic program behavior data using three-dimensional projections, The volume of a three-dimensional box representing a program block is proportional to the block's resource utilization. The scope of this paper is limited to a description of the STRUCT system. This system is currently being used to predict and analyze the performance advantages available through the migration of function (program modules) between levels of software and between software and firmware within BUGS. The results of this research on migration will be included in a doctoral dissertation currently being written.",
+ "authors": [
+ {
+ "name": "Andries Van Dam",
+ "publication_start": 1975,
+ "publication_end": 1975,
+ "publication_count": 1,
+ "citation_count": 0,
+ "available_for_download": 0,
+ "downloads_6_weeks": 8,
+ "downloads_12_months": 97,
+ "downloads_cumulative": 97,
+ "average_downloads_per_article": 0,
+ "average_citations_per_article": 0
+ }
+ ],
+ "publication": {
+ "name": "IEEE Transactions on Software Engineering",
+ "volume": 1,
+ "issue": 1,
+ "month": "March",
+ "page_start": 384,
+ "page_end": 389,
+ "publication_date": "1975-03-01",
+ "publisher": "IEEE Press Piscataway, NJ, USA",
+ "issn": "0098-5589",
+ "doi": "10.1109/TSE.1975.6312869"
+ }
+}
diff --git a/src/scraping/buxton/scraper.py b/src/scraping/buxton/scraper.py
new file mode 100644
index 000000000..700269727
--- /dev/null
+++ b/src/scraping/buxton/scraper.py
@@ -0,0 +1,386 @@
+import os
+import docx2txt
+from docx import Document
+from docx.opc.constants import RELATIONSHIP_TYPE as RT
+import re
+from pymongo import MongoClient
+import shutil
+import uuid
+import datetime
+from PIL import Image
+import math
+import sys
+
+source = "./source"
+dist = "../../server/public/files"
+
+db = MongoClient("localhost", 27017)["Dash"]
+schema_guids = []
+
+
+def extract_links(fileName):
+ links = []
+ doc = Document(fileName)
+ rels = doc.part.rels
+ for rel in rels:
+ item = rels[rel]
+ if item.reltype == RT.HYPERLINK and ".aspx" not in item._target:
+ links.append(item._target)
+ return text_doc_map(links)
+
+
+def extract_value(kv_string):
+ pieces = kv_string.split(":")
+ return (pieces[1] if len(pieces) > 1 else kv_string).strip()
+
+
+def mkdir_if_absent(path):
+ try:
+ if not os.path.exists(path):
+ os.mkdir(path)
+ except OSError:
+ print("failed to create the appropriate directory structures for %s" % file_name)
+
+
+def guid():
+ return str(uuid.uuid4())
+
+
+def listify(list):
+ return {
+ "fields": list,
+ "__type": "list"
+ }
+
+
+def protofy(fieldId):
+ return {
+ "fieldId": fieldId,
+ "__type": "proxy"
+ }
+
+
+def text_doc_map(string_list):
+ def guid_map(caption):
+ return write_text_doc(caption)
+ return listify(proxify_guids(list(map(guid_map, string_list))))
+
+
+def write_schema(parse_results, display_fields, storage_key):
+ view_guids = parse_results["child_guids"]
+
+ data_doc = parse_results["schema"]
+ fields = data_doc["fields"]
+
+ view_doc_guid = guid()
+
+ view_doc = {
+ "_id": view_doc_guid,
+ "fields": {
+ "proto": protofy(data_doc["_id"]),
+ "x": 10,
+ "y": 10,
+ "width": 900,
+ "height": 600,
+ "panX": 0,
+ "panY": 0,
+ "zoomBasis": 0.5,
+ "zIndex": 2,
+ "libraryBrush": False,
+ "viewType": 2
+ },
+ "__type": "Doc"
+ }
+
+ fields["proto"] = protofy("collectionProto")
+ fields[storage_key] = listify(proxify_guids(view_guids))
+ fields["schemaColumns"] = listify(display_fields)
+ fields["backgroundColor"] = "white"
+ fields["scale"] = 0.5
+ fields["viewType"] = 2
+ fields["author"] = "Bill Buxton"
+ fields["creationDate"] = {
+ "date": datetime.datetime.utcnow().microsecond,
+ "__type": "date"
+ }
+ fields["isPrototype"] = True
+ fields["page"] = -1
+
+ db.newDocuments.insert_one(data_doc)
+ db.newDocuments.insert_one(view_doc)
+
+ data_doc_guid = data_doc["_id"]
+ print(f"inserted view document ({view_doc_guid})")
+ print(f"inserted data document ({data_doc_guid})\n")
+
+ return view_doc_guid
+
+
+def write_text_doc(content):
+ data_doc_guid = guid()
+ view_doc_guid = guid()
+
+ view_doc = {
+ "_id": view_doc_guid,
+ "fields": {
+ "proto": protofy(data_doc_guid),
+ "x": 10,
+ "y": 10,
+ "width": 400,
+ "zIndex": 2,
+ "libraryBrush": False
+ },
+ "__type": "Doc"
+ }
+
+ data_doc = {
+ "_id": data_doc_guid,
+ "fields": {
+ "proto": protofy("textProto"),
+ "data": {
+ "Data": '{"doc":{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"' + content + '"}]}]},"selection":{"type":"text","anchor":1,"head":1}' + '}',
+ "__type": "RichTextField"
+ },
+ "title": content,
+ "nativeWidth": 200,
+ "author": "Bill Buxton",
+ "creationDate": {
+ "date": datetime.datetime.utcnow().microsecond,
+ "__type": "date"
+ },
+ "isPrototype": True,
+ "autoHeight": True,
+ "page": -1,
+ "nativeHeight": 200,
+ "height": 200,
+ "data_text": content
+ },
+ "__type": "Doc"
+ }
+
+ db.newDocuments.insert_one(view_doc)
+ db.newDocuments.insert_one(data_doc)
+
+ return view_doc_guid
+
+
+def write_image(folder, name):
+ path = f"http://localhost:1050/files/{folder}/{name}"
+
+ data_doc_guid = guid()
+ view_doc_guid = guid()
+
+ image = Image.open(f"{dist}/{folder}/{name}")
+ native_width, native_height = image.size
+
+ view_doc = {
+ "_id": view_doc_guid,
+ "fields": {
+ "proto": protofy(data_doc_guid),
+ "x": 10,
+ "y": 10,
+ "width": min(800, native_width),
+ "zIndex": 2,
+ "libraryBrush": False
+ },
+ "__type": "Doc"
+ }
+
+ data_doc = {
+ "_id": data_doc_guid,
+ "fields": {
+ "proto": protofy("imageProto"),
+ "data": {
+ "url": path,
+ "__type": "image"
+ },
+ "title": name,
+ "nativeWidth": native_width,
+ "author": "Bill Buxton",
+ "creationDate": {
+ "date": datetime.datetime.utcnow().microsecond,
+ "__type": "date"
+ },
+ "isPrototype": True,
+ "page": -1,
+ "nativeHeight": native_height,
+ "height": native_height
+ },
+ "__type": "Doc"
+ }
+
+ db.newDocuments.insert_one(view_doc)
+ db.newDocuments.insert_one(data_doc)
+
+ return view_doc_guid
+
+
+def parse_document(file_name: str):
+ print(f"parsing {file_name}...")
+ pure_name = file_name.split(".")[0]
+
+ result = {}
+
+ dir_path = dist + "/" + pure_name
+ mkdir_if_absent(dir_path)
+
+ raw = str(docx2txt.process(source + "/" + file_name, dir_path))
+
+ view_guids = []
+ count = 0
+ for image in os.listdir(dir_path):
+ count += 1
+ view_guids.append(write_image(pure_name, image))
+ os.rename(dir_path + "/" + image, dir_path +
+ "/" + image.replace(".", "_m.", 1))
+ print(f"extracted {count} images...")
+
+ def sanitize(line): return re.sub("[\n\t]+", "", line).replace(u"\u00A0", " ").replace(
+ u"\u2013", "-").replace(u"\u201c", '''"''').replace(u"\u201d", '''"''').strip()
+
+ def sanitize_price(raw: str):
+ raw = raw.replace(",", "")
+ start = raw.find("$")
+ if start > -1:
+ i = start + 1
+ while (i < len(raw) and re.match(r"[0-9\.]", raw[i])):
+ i += 1
+ price = raw[start + 1: i + 1]
+ return float(price)
+ elif (raw.lower().find("nfs")):
+ return -1
+ else:
+ return math.nan
+
+ def remove_empty(line): return len(line) > 1
+
+ lines = list(map(sanitize, raw.split("\n")))
+ lines = list(filter(remove_empty, lines))
+
+ result["file_name"] = file_name
+ result["title"] = lines[2].strip()
+ result["short_description"] = lines[3].strip().replace(
+ "Short Description: ", "")
+
+ cur = 5
+ notes = ""
+ while lines[cur] != "Device Details":
+ notes += lines[cur] + " "
+ cur += 1
+ result["buxton_notes"] = notes.strip()
+
+ cur += 1
+ clean = list(
+ map(lambda data: data.strip().split(":"), lines[cur].split("|")))
+ result["company"] = clean[0][len(clean[0]) - 1].strip()
+ result["year"] = clean[1][len(clean[1]) - 1].strip()
+ result["original_price"] = sanitize_price(
+ clean[2][len(clean[2]) - 1].strip())
+
+ cur += 1
+ result["degrees_of_freedom"] = extract_value(
+ lines[cur]).replace("NA", "N/A")
+ cur += 1
+
+ dimensions = lines[cur].lower()
+ if dimensions.startswith("dimensions"):
+ dim_concat = dimensions[11:].strip()
+ cur += 1
+ while lines[cur] != "Key Words":
+ dim_concat += (" " + lines[cur].strip())
+ cur += 1
+ result["dimensions"] = dim_concat
+ else:
+ result["dimensions"] = "N/A"
+
+ cur += 1
+ result["primary_key"] = extract_value(lines[cur])
+ cur += 1
+ result["secondary_key"] = extract_value(lines[cur])
+
+ while lines[cur] != "Links":
+ result["secondary_key"] += (" " + extract_value(lines[cur]).strip())
+ cur += 1
+
+ cur += 1
+ link_descriptions = []
+ while lines[cur] != "Image":
+ link_descriptions.append(lines[cur].strip())
+ cur += 1
+ result["link_descriptions"] = text_doc_map(link_descriptions)
+
+ result["hyperlinks"] = extract_links(source + "/" + file_name)
+
+ images = []
+ captions = []
+ cur += 3
+ while cur + 1 < len(lines) and lines[cur] != "NOTES:":
+ images.append(lines[cur])
+ captions.append(lines[cur + 1])
+ cur += 2
+ result["images"] = listify(images)
+
+ result["captions"] = text_doc_map(captions)
+
+ notes = []
+ if (cur < len(lines) and lines[cur] == "NOTES:"):
+ cur += 1
+ while cur < len(lines):
+ notes.append(lines[cur])
+ cur += 1
+ if len(notes) > 0:
+ result["notes"] = listify(notes)
+
+ print("writing child schema...")
+
+ return {
+ "schema": {
+ "_id": guid(),
+ "fields": result,
+ "__type": "Doc"
+ },
+ "child_guids": view_guids
+ }
+
+
+def proxify_guids(guids):
+ return list(map(lambda guid: {"fieldId": guid, "__type": "proxy"}, guids))
+
+
+if os.path.exists(dist):
+ shutil.rmtree(dist)
+while os.path.exists(dist):
+ pass
+os.mkdir(dist)
+mkdir_if_absent(source)
+
+candidates = 0
+for file_name in os.listdir(source):
+ if file_name.endswith('.docx'):
+ candidates += 1
+ schema_guids.append(write_schema(
+ parse_document(file_name), ["title", "data"], "image_data"))
+
+print("writing parent schema...")
+parent_guid = write_schema({
+ "schema": {
+ "_id": guid(),
+ "fields": {},
+ "__type": "Doc"
+ },
+ "child_guids": schema_guids
+}, ["title", "short_description", "original_price"], "data")
+
+print("appending parent schema to main workspace...\n")
+db.newDocuments.update_one(
+ {"fields.title": "WS collection 1"},
+ {"$push": {"fields.data.fields": {"fieldId": parent_guid, "__type": "proxy"}}}
+)
+
+print("rewriting .gitignore...\n")
+lines = ['*', '!.gitignore']
+with open(dist + "/.gitignore", 'w') as f:
+ f.write('\n'.join(lines))
+
+suffix = "" if candidates == 1 else "s"
+print(f"conversion complete. {candidates} candidate{suffix} processed.")
diff --git a/src/scraping/buxton/source/Bill_Notes_Bill_Notes_CyKey.docx b/src/scraping/buxton/source/Bill_Notes_Bill_Notes_CyKey.docx
new file mode 100644
index 000000000..06094b4d3
--- /dev/null
+++ b/src/scraping/buxton/source/Bill_Notes_Bill_Notes_CyKey.docx
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_Braun_T3.docx b/src/scraping/buxton/source/Bill_Notes_Braun_T3.docx
new file mode 100644
index 000000000..356697092
--- /dev/null
+++ b/src/scraping/buxton/source/Bill_Notes_Braun_T3.docx
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_CasioC801.docx b/src/scraping/buxton/source/Bill_Notes_CasioC801.docx
new file mode 100644
index 000000000..cd89fb97b
--- /dev/null
+++ b/src/scraping/buxton/source/Bill_Notes_CasioC801.docx
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_Casio_Mini.docx b/src/scraping/buxton/source/Bill_Notes_Casio_Mini.docx
new file mode 100644
index 000000000..a503cddfc
--- /dev/null
+++ b/src/scraping/buxton/source/Bill_Notes_Casio_Mini.docx
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_FingerWorks_Prototype.docx b/src/scraping/buxton/source/Bill_Notes_FingerWorks_Prototype.docx
new file mode 100644
index 000000000..4d13a8cf5
--- /dev/null
+++ b/src/scraping/buxton/source/Bill_Notes_FingerWorks_Prototype.docx
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_Fingerworks_TouchStream.docx b/src/scraping/buxton/source/Bill_Notes_Fingerworks_TouchStream.docx
new file mode 100644
index 000000000..578a1be08
--- /dev/null
+++ b/src/scraping/buxton/source/Bill_Notes_Fingerworks_TouchStream.docx
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_FrogPad.docx b/src/scraping/buxton/source/Bill_Notes_FrogPad.docx
new file mode 100644
index 000000000..d01e1bf5c
--- /dev/null
+++ b/src/scraping/buxton/source/Bill_Notes_FrogPad.docx
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_Gavilan_SC.docx b/src/scraping/buxton/source/Bill_Notes_Gavilan_SC.docx
new file mode 100644
index 000000000..7bd28b376
--- /dev/null
+++ b/src/scraping/buxton/source/Bill_Notes_Gavilan_SC.docx
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_Grandjean_Stenotype.docx b/src/scraping/buxton/source/Bill_Notes_Grandjean_Stenotype.docx
new file mode 100644
index 000000000..0615c4953
--- /dev/null
+++ b/src/scraping/buxton/source/Bill_Notes_Grandjean_Stenotype.docx
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_Matias.docx b/src/scraping/buxton/source/Bill_Notes_Matias.docx
new file mode 100644
index 000000000..547603256
--- /dev/null
+++ b/src/scraping/buxton/source/Bill_Notes_Matias.docx
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_MousePen.docx b/src/scraping/buxton/source/Bill_Notes_MousePen.docx
new file mode 100644
index 000000000..4e1056636
--- /dev/null
+++ b/src/scraping/buxton/source/Bill_Notes_MousePen.docx
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_NewO.docx b/src/scraping/buxton/source/Bill_Notes_NewO.docx
new file mode 100644
index 000000000..a514926d2
--- /dev/null
+++ b/src/scraping/buxton/source/Bill_Notes_NewO.docx
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_OLPC.docx b/src/scraping/buxton/source/Bill_Notes_OLPC.docx
new file mode 100644
index 000000000..bfca0a9bb
--- /dev/null
+++ b/src/scraping/buxton/source/Bill_Notes_OLPC.docx
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_PARCkbd.docx b/src/scraping/buxton/source/Bill_Notes_PARCkbd.docx
new file mode 100644
index 000000000..c0cf6ba9a
--- /dev/null
+++ b/src/scraping/buxton/source/Bill_Notes_PARCkbd.docx
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_Philco_Mystery_Control.docx b/src/scraping/buxton/source/Bill_Notes_Philco_Mystery_Control.docx
new file mode 100644
index 000000000..ad06903f3
--- /dev/null
+++ b/src/scraping/buxton/source/Bill_Notes_Philco_Mystery_Control.docx
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_TASA_Kbd.docx b/src/scraping/buxton/source/Bill_Notes_TASA_Kbd.docx
new file mode 100644
index 000000000..e4c659de9
--- /dev/null
+++ b/src/scraping/buxton/source/Bill_Notes_TASA_Kbd.docx
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_The_Tap.docx b/src/scraping/buxton/source/Bill_Notes_The_Tap.docx
new file mode 100644
index 000000000..8ceebc71e
--- /dev/null
+++ b/src/scraping/buxton/source/Bill_Notes_The_Tap.docx
Binary files differ
diff --git a/src/server/GarbageCollector.ts b/src/server/GarbageCollector.ts
new file mode 100644
index 000000000..ea5388004
--- /dev/null
+++ b/src/server/GarbageCollector.ts
@@ -0,0 +1,150 @@
+import { Database } from './database';
+
+import * as path from 'path';
+import * as fs from 'fs';
+import { Search } from './Search';
+
+function addDoc(doc: any, ids: string[], files: { [name: string]: string[] }) {
+ for (const key in doc) {
+ if (!doc.hasOwnProperty(key)) {
+ continue;
+ }
+ const field = doc[key];
+ if (field === undefined || field === null) {
+ continue;
+ }
+ if (field.__type === "proxy") {
+ ids.push(field.fieldId);
+ } else if (field.__type === "list") {
+ addDoc(field.fields, ids, files);
+ } else if (typeof field === "string") {
+ const re = /"(?:dataD|d)ocumentId"\s*:\s*"([\w\-]*)"/g;
+ let match: string[] | null;
+ while ((match = re.exec(field)) !== null) {
+ ids.push(match[1]);
+ }
+ } else if (field.__type === "RichTextField") {
+ const re = /"href"\s*:\s*"(.*?)"/g;
+ let match: string[] | null;
+ while ((match = re.exec(field.Data)) !== null) {
+ const urlString = match[1];
+ const split = new URL(urlString).pathname.split("doc/");
+ if (split.length > 1) {
+ ids.push(split[split.length - 1]);
+ }
+ }
+ const re2 = /"src"\s*:\s*"(.*?)"/g;
+ while ((match = re2.exec(field.Data)) !== null) {
+ const urlString = match[1];
+ const pathname = new URL(urlString).pathname;
+ const ext = path.extname(pathname);
+ const fileName = path.basename(pathname, ext);
+ let exts = files[fileName];
+ if (!exts) {
+ files[fileName] = exts = [];
+ }
+ exts.push(ext);
+ }
+ } else if (["audio", "image", "video", "pdf", "web"].includes(field.__type)) {
+ const url = new URL(field.url);
+ const pathname = url.pathname;
+ const ext = path.extname(pathname);
+ const fileName = path.basename(pathname, ext);
+ let exts = files[fileName];
+ if (!exts) {
+ files[fileName] = exts = [];
+ }
+ exts.push(ext);
+ }
+ }
+}
+
+async function GarbageCollect(full: boolean = true) {
+ console.log("start GC");
+ const start = Date.now();
+ // await new Promise(res => setTimeout(res, 3000));
+ const cursor = await Database.Instance.query({}, { userDocumentId: 1 }, 'users');
+ const users = await cursor.toArray();
+ const ids: string[] = users.map(user => user.userDocumentId);
+ const visited = new Set<string>();
+ const files: { [name: string]: string[] } = {};
+
+ while (ids.length) {
+ const count = Math.min(ids.length, 1000);
+ const index = ids.length - count;
+ const fetchIds = ids.splice(index, count).filter(id => !visited.has(id));
+ if (!fetchIds.length) {
+ continue;
+ }
+ const docs = await new Promise<{ [key: string]: any }[]>(res => Database.Instance.getDocuments(fetchIds, res, "newDocuments"));
+ for (const doc of docs) {
+ const id = doc.id;
+ if (doc === undefined) {
+ console.log(`Couldn't find field with Id ${id}`);
+ continue;
+ }
+ visited.add(id);
+ addDoc(doc.fields, ids, files);
+ }
+ console.log(`To Go: ${ids.length}, visited: ${visited.size}`);
+ }
+
+ console.log(`Done: ${visited.size}`);
+
+ cursor.close();
+
+ const notToDelete = Array.from(visited);
+ const toDeleteCursor = await Database.Instance.query({ _id: { $nin: notToDelete } }, { _id: 1 });
+ const toDelete: string[] = (await toDeleteCursor.toArray()).map(doc => doc._id);
+ toDeleteCursor.close();
+ if (!full) {
+ await Database.Instance.updateMany({ _id: { $nin: notToDelete } }, { $set: { "deleted": true } });
+ await Database.Instance.updateMany({ _id: { $in: notToDelete } }, { $unset: { "deleted": true } });
+ console.log(await Search.Instance.updateDocuments(
+ notToDelete.map<any>(id => ({
+ id, deleted: { set: null }
+ }))
+ .concat(toDelete.map(id => ({
+ id, deleted: { set: true }
+ })))));
+ console.log("Done with partial GC");
+ console.log(`Took ${(Date.now() - start) / 1000} seconds`);
+ } else {
+ let i = 0;
+ let deleted = 0;
+ while (i < toDelete.length) {
+ const count = Math.min(toDelete.length, 5000);
+ const toDeleteDocs = toDelete.slice(i, i + count);
+ i += count;
+ const result = await Database.Instance.delete({ _id: { $in: toDeleteDocs } }, "newDocuments");
+ deleted += result.deletedCount || 0;
+ }
+ // const result = await Database.Instance.delete({ _id: { $in: toDelete } }, "newDocuments");
+ console.log(`${deleted} documents deleted`);
+
+ await Search.Instance.deleteDocuments(toDelete);
+ console.log("Cleared search documents");
+
+ const folder = "./src/server/public/files/";
+ fs.readdir(folder, (_, fileList) => {
+ const filesToDelete = fileList.filter(file => {
+ const ext = path.extname(file);
+ let base = path.basename(file, ext);
+ const existsInDb = (base in files || (base = base.substring(0, base.length - 2)) in files) && files[base].includes(ext);
+ return file !== ".gitignore" && !existsInDb;
+ });
+ console.log(`Deleting ${filesToDelete.length} files`);
+ filesToDelete.forEach(file => {
+ console.log(`Deleting file ${file}`);
+ try {
+ fs.unlinkSync(folder + file);
+ } catch {
+ console.warn(`Couldn't delete file ${file}`);
+ }
+ });
+ console.log(`Deleted ${filesToDelete.length} files`);
+ });
+ }
+}
+
+GarbageCollect(false);
diff --git a/src/server/Message.ts b/src/server/Message.ts
index 7b208dee9..1e29aef0b 100644
--- a/src/server/Message.ts
+++ b/src/server/Message.ts
@@ -55,4 +55,6 @@ export namespace MessageStore {
export const UpdateField = new Message<Diff>("Update Ref Field");
export const CreateField = new Message<Reference>("Create Ref Field");
export const YoutubeApiQuery = new Message<YoutubeQueryInput>("Youtube Api Query");
+ export const DeleteField = new Message<string>("Delete field");
+ export const DeleteFields = new Message<string[]>("Delete fields");
}
diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts
index c4af5cdaa..5c13495ff 100644
--- a/src/server/RouteStore.ts
+++ b/src/server/RouteStore.ts
@@ -16,6 +16,7 @@ export enum RouteStore {
// USER AND WORKSPACES
getCurrUser = "/getCurrentUser",
+ getUsers = "/getUsers",
getUserDocumentId = "/getUserDocumentId",
updateCursor = "/updateCursor",
diff --git a/src/server/Search.ts b/src/server/Search.ts
index d776480c6..69e327d2d 100644
--- a/src/server/Search.ts
+++ b/src/server/Search.ts
@@ -18,19 +18,34 @@ export class Search {
}
}
- public async search(query: string) {
+ public async updateDocuments(documents: any[]) {
+ try {
+ const res = await rp.post(this.url + "dash/update", {
+ headers: { 'content-type': 'application/json' },
+ body: JSON.stringify(documents)
+ });
+ return res;
+ } catch (e) {
+ // console.warn("Search error: ", e, documents);
+ }
+ }
+
+ public async search(query: string, filterQuery: string = "", start: number = 0, rows: number = 10) {
try {
const searchResults = JSON.parse(await rp.get(this.url + "dash/select", {
qs: {
q: query,
- fl: "id"
+ fq: filterQuery,
+ fl: "id",
+ start,
+ rows,
}
}));
- const fields = searchResults.response.docs;
- const ids = fields.map((field: any) => field.id);
- return ids;
+ const { docs, numFound } = searchResults.response;
+ const ids = docs.map((field: any) => field.id);
+ return { ids, numFound };
} catch {
- return [];
+ return { ids: [], numFound: -1 };
}
}
@@ -46,4 +61,25 @@ export class Search {
});
} catch { }
}
+
+ public deleteDocuments(docs: string[]) {
+ const promises: rp.RequestPromise[] = [];
+ const nToDelete = 1000;
+ let index = 0;
+ while (index < docs.length) {
+ const count = Math.min(docs.length - index, nToDelete);
+ const deleteIds = docs.slice(index, index + count);
+ index += count;
+ promises.push(rp.post(this.url + "dash/update", {
+ body: {
+ delete: {
+ query: deleteIds.map(id => `id:"${id}"`).join(" ")
+ }
+ },
+ json: true
+ }));
+ }
+
+ return Promise.all(promises);
+ }
} \ No newline at end of file
diff --git a/src/server/authentication/controllers/user_controller.ts b/src/server/authentication/controllers/user_controller.ts
index 1dacdf3fa..0e431f1e6 100644
--- a/src/server/authentication/controllers/user_controller.ts
+++ b/src/server/authentication/controllers/user_controller.ts
@@ -12,6 +12,9 @@ import * as nodemailer from 'nodemailer';
import c = require("crypto");
import { RouteStore } from "../../RouteStore";
import { Utils } from "../../../Utils";
+import { Schema } from "mongoose";
+import { Opt } from "../../../new_fields/Doc";
+import { MailOptions } from "nodemailer/lib/stream-transport";
/**
* GET /signup
@@ -42,40 +45,45 @@ export let postSignup = (req: Request, res: Response, next: NextFunction) => {
const errors = req.validationErrors();
if (errors) {
- res.render("signup.pug", {
- title: "Sign Up",
- user: req.user,
- });
return res.redirect(RouteStore.signup);
}
- const email = req.body.email;
+ const email = req.body.email as String;
const password = req.body.password;
- const user = new User({
+ const model = {
email,
password,
userDocumentId: Utils.GenerateGuid()
- });
+ } as Partial<DashUserModel>;
+
+ const user = new User(model);
User.findOne({ email }, (err, existingUser) => {
if (err) { return next(err); }
if (existingUser) {
return res.redirect(RouteStore.login);
}
- user.save((err) => {
+ user.save((err: any) => {
if (err) { return next(err); }
req.logIn(user, (err) => {
- if (err) {
- return next(err);
- }
- res.redirect(RouteStore.home);
+ if (err) { return next(err); }
+ tryRedirectToTarget(req, res);
});
});
});
};
+let tryRedirectToTarget = (req: Request, res: Response) => {
+ if (req.session && req.session.target) {
+ res.redirect(req.session.target);
+ req.session.target = undefined;
+ } else {
+ res.redirect(RouteStore.home);
+ }
+};
+
/**
* GET /login
@@ -83,6 +91,7 @@ export let postSignup = (req: Request, res: Response, next: NextFunction) => {
*/
export let getLogin = (req: Request, res: Response) => {
if (req.user) {
+ req.session!.target = undefined;
return res.redirect(RouteStore.home);
}
res.render("login.pug", {
@@ -115,7 +124,7 @@ export let postLogin = (req: Request, res: Response, next: NextFunction) => {
}
req.logIn(user, (err) => {
if (err) { next(err); return; }
- res.redirect(RouteStore.home);
+ tryRedirectToTarget(req, res);
});
})(req, res, next);
};
@@ -184,8 +193,8 @@ export let postForgot = function (req: Request, res: Response, next: NextFunctio
'Please click on the following link, or paste this into your browser to complete the process:\n\n' +
'http://' + req.headers.host + '/reset/' + token + '\n\n' +
'If you did not request this, please ignore this email and your password will remain unchanged.\n'
- };
- smtpTransport.sendMail(mailOptions, function (err) {
+ } as MailOptions;
+ smtpTransport.sendMail(mailOptions, function (err: Error | null) {
// req.flash('info', 'An e-mail has been sent to ' + user.email + ' with further instructions.');
done(null, err, 'done');
});
@@ -255,7 +264,7 @@ export let postReset = function (req: Request, res: Response) {
subject: 'Your password has been changed',
text: 'Hello,\n\n' +
'This is a confirmation that the password for your account ' + user.email + ' has just been changed.\n'
- };
+ } as MailOptions;
smtpTransport.sendMail(mailOptions, function (err) {
done(null, err);
});
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index e5b7a025b..e796ccb43 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -10,7 +10,7 @@ import { CollectionView } from "../../../client/views/collections/CollectionView
import { Doc } from "../../../new_fields/Doc";
import { List } from "../../../new_fields/List";
import { listSpec } from "../../../new_fields/Schema";
-import { Cast } from "../../../new_fields/Types";
+import { Cast, FieldValue, StrCast } from "../../../new_fields/Types";
import { RouteStore } from "../../RouteStore";
export class CurrentUserUtils {
@@ -29,30 +29,74 @@ export class CurrentUserUtils {
private static createUserDocument(id: string): Doc {
let doc = new Doc(id, true);
doc.viewType = CollectionViewType.Tree;
+ doc.dropAction = "alias";
doc.layout = CollectionView.LayoutString();
doc.title = this.email;
+ this.updateUserDocument(doc);
doc.data = new List<Doc>();
+ doc.gridGap = 5;
+ doc.xMargin = 5;
+ doc.yMargin = 5;
+ doc.boxShadow = "0 0";
doc.excludeFromLibrary = true;
- doc.optionalRightCollection = Docs.StackingDocument([], { title: "New mobile uploads" });
- // doc.library = Docs.TreeDocument([doc], { title: `Library: ${CurrentUserUtils.email}` });
+ doc.optionalRightCollection = Docs.Create.StackingDocument([], { title: "New mobile uploads" });
+ // doc.library = Docs.Create.TreeDocument([doc], { title: `Library: ${CurrentUserUtils.email}` });
// (doc.library as Doc).excludeFromLibrary = true;
return doc;
}
- public static async loadCurrentUser(): Promise<any> {
- let userPromise = rp.get(DocServer.prepend(RouteStore.getCurrUser)).then(response => {
+ static updateUserDocument(doc: Doc) {
+ if (doc.workspaces === undefined) {
+ const workspaces = Docs.Create.TreeDocument([], { title: "Workspaces", height: 100 });
+ workspaces.excludeFromLibrary = true;
+ workspaces.workspaceLibrary = true;
+ workspaces.boxShadow = "0 0";
+ doc.workspaces = workspaces;
+ }
+ if (doc.recentlyClosed === undefined) {
+ const recentlyClosed = Docs.Create.TreeDocument([], { title: "Recently Closed", height: 75 });
+ recentlyClosed.excludeFromLibrary = true;
+ recentlyClosed.boxShadow = "0 0";
+ doc.recentlyClosed = recentlyClosed;
+ }
+ if (doc.sidebar === undefined) {
+ const sidebar = Docs.Create.StackingDocument([doc.workspaces as Doc, doc, doc.recentlyClosed as Doc], { title: "Sidebar" });
+ sidebar.excludeFromLibrary = true;
+ sidebar.gridGap = 5;
+ sidebar.xMargin = 5;
+ sidebar.yMargin = 5;
+ Doc.GetProto(sidebar).backgroundColor = "#aca3a6";
+ sidebar.boxShadow = "1 1 3";
+ doc.sidebar = sidebar;
+ }
+ StrCast(doc.title).indexOf("@") !== -1 && (doc.title = StrCast(doc.title).split("@")[0] + "'s Library");
+
+ }
+
+ public static loadCurrentUser() {
+ return rp.get(DocServer.prepend(RouteStore.getCurrUser)).then(response => {
if (response) {
- let obj = JSON.parse(response);
- CurrentUserUtils.curr_id = obj.id as string;
- CurrentUserUtils.curr_email = obj.email as string;
+ const result: { id: string, email: string } = JSON.parse(response);
+ return result;
} else {
throw new Error("There should be a user! Why does Dash think there isn't one?");
}
});
- let userDocPromise = await rp.get(DocServer.prepend(RouteStore.getUserDocumentId)).then(id => {
+ }
+
+ public static async loadUserDocument({ id, email }: { id: string, email: string }) {
+ this.curr_id = id;
+ this.curr_email = email;
+ await rp.get(DocServer.prepend(RouteStore.getUserDocumentId)).then(id => {
if (id) {
- return DocServer.GetRefField(id).then(field =>
- runInAction(() => this.user_document = field instanceof Doc ? field : this.createUserDocument(id)));
+ return DocServer.GetRefField(id).then(async field => {
+ if (field instanceof Doc) {
+ await this.updateUserDocument(field);
+ runInAction(() => this.user_document = field);
+ } else {
+ runInAction(() => this.user_document = this.createUserDocument(id));
+ }
+ });
} else {
throw new Error("There should be a user id! Why does Dash think there isn't one?");
}
@@ -68,7 +112,6 @@ export class CurrentUserUtils {
} catch (e) {
}
- return Promise.all([userPromise, userDocPromise]);
}
/* Northstar catalog ... really just for testing so this should eventually go away */
@@ -94,12 +137,12 @@ export class CurrentUserUtils {
// new AttributeTransformationModel(atmod, AggregateFunction.None),
// new AttributeTransformationModel(atmod, AggregateFunction.Count),
// new AttributeTransformationModel(atmod, AggregateFunction.Count));
- // schemaDocuments.push(Docs.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! }));
+ // schemaDocuments.push(Docs.Create.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! }));
// }
// })));
// return promises;
// }, [] as Promise<void>[]));
- // return CurrentUserUtils._northstarSchemas.push(Docs.TreeDocument(schemaDocuments, { width: 50, height: 100, title: schema.displayName! }));
+ // return CurrentUserUtils._northstarSchemas.push(Docs.Create.TreeDocument(schemaDocuments, { width: 50, height: 100, title: schema.displayName! }));
// });
// }
}
diff --git a/src/server/authentication/models/user_model.ts b/src/server/authentication/models/user_model.ts
index ee85e1c05..45fbf23b1 100644
--- a/src/server/authentication/models/user_model.ts
+++ b/src/server/authentication/models/user_model.ts
@@ -16,7 +16,7 @@ mongoose.connection.on('disconnected', function () {
console.log('connection closed');
});
export type DashUserModel = mongoose.Document & {
- email: string,
+ email: String,
password: string,
passwordResetToken?: string,
passwordResetExpires?: Date,
@@ -42,7 +42,7 @@ export type AuthToken = {
};
const userSchema = new mongoose.Schema({
- email: { type: String, unique: true },
+ email: String,
password: String,
passwordResetToken: String,
passwordResetExpires: Date,
diff --git a/src/server/database.ts b/src/server/database.ts
index 70b3efced..29a8ffafa 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -41,11 +41,17 @@ export class Database {
}
}
- public delete(id: string, collectionName = Database.DocumentsCollection) {
+ public delete(query: any, collectionName?: string): Promise<mongodb.DeleteWriteOpResultObject>;
+ public delete(id: string, collectionName?: string): Promise<mongodb.DeleteWriteOpResultObject>;
+ public delete(id: any, collectionName = Database.DocumentsCollection) {
+ if (typeof id === "string") {
+ id = { _id: id };
+ }
if (this.db) {
- this.db.collection(collectionName).remove({ id: id });
+ const db = this.db;
+ return new Promise(res => db.collection(collectionName).deleteMany(id, (err, result) => res(result)));
} else {
- this.onConnect.push(() => this.delete(id, collectionName));
+ return new Promise(res => this.onConnect.push(() => res(this.delete(id, collectionName))));
}
}
@@ -120,12 +126,16 @@ export class Database {
}
}
- public query(query: any): Promise<mongodb.Cursor> {
+ public query(query: { [key: string]: any }, projection?: { [key: string]: 0 | 1 }, collectionName = "newDocuments"): Promise<mongodb.Cursor> {
if (this.db) {
- return Promise.resolve<mongodb.Cursor>(this.db.collection('newDocuments').find(query));
+ let cursor = this.db.collection(collectionName).find(query);
+ if (projection) {
+ cursor = cursor.project(projection);
+ }
+ return Promise.resolve<mongodb.Cursor>(cursor);
} else {
return new Promise<mongodb.Cursor>(res => {
- this.onConnect.push(() => res(this.query(query)));
+ this.onConnect.push(() => res(this.query(query, projection, collectionName)));
});
}
}
diff --git a/src/server/index.ts b/src/server/index.ts
index 9e4ad9d86..026950812 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -1,3 +1,4 @@
+require('dotenv').config();
import * as bodyParser from 'body-parser';
import { exec } from 'child_process';
import * as cookieParser from 'cookie-parser';
@@ -7,9 +8,9 @@ import * as expressValidator from 'express-validator';
import * as formidable from 'formidable';
import * as fs from 'fs';
import * as sharp from 'sharp';
+import * as Pdfjs from 'pdfjs-dist';
const imageDataUri = require('image-data-uri');
import * as mobileDetect from 'mobile-detect';
-import { ObservableMap } from 'mobx';
import * as passport from 'passport';
import * as path from 'path';
import * as request from 'request';
@@ -28,6 +29,7 @@ import { MessageStore, Transferable, Types, Diff, YoutubeQueryTypes as YoutubeQu
import { RouteStore } from './RouteStore';
const app = express();
const config = require('../../webpack.config');
+import { createCanvas, loadImage, Canvas } from "canvas";
const compiler = webpack(config);
const port = 1050; // default port to listen
const serverPort = 4321;
@@ -38,18 +40,28 @@ import { Search } from './Search';
import { debug } from 'util';
import _ = require('lodash');
import * as YoutubeApi from './youtubeApi/youtubeApiSample.js';
+import { Response } from 'express-serve-static-core';
const MongoStore = require('connect-mongo')(session);
const mongoose = require('mongoose');
-// let { google } = require('googleapis');
-// let OAuth2 = google.auth.OAuth2;
-
+const probe = require("probe-image-size");
const download = (url: string, dest: fs.PathLike) => request.get(url).pipe(fs.createWriteStream(dest));
let youtubeApiKey: string;
YoutubeApi.readApiKey((apiKey: string) => youtubeApiKey = apiKey);
+const release = process.env.RELEASE === "true";
+if (process.env.RELEASE === "true") {
+ console.log("Running server in release mode");
+} else {
+ console.log("Running server in debug mode");
+}
+console.log(process.env.PWD);
+let clientUtils = fs.readFileSync("./src/client/util/ClientUtils.ts.temp", "utf8");
+clientUtils = `//AUTO-GENERATED FILE: DO NOT EDIT\n${clientUtils.replace('"mode"', String(release))}`;
+fs.writeFileSync("./src/client/util/ClientUtils.ts", clientUtils, "utf8");
+
const mongoUrl = 'mongodb://localhost:27017/Dash';
-mongoose.connect(mongoUrl);
+mongoose.connection.readyState === 0 && mongoose.connect(mongoUrl);
mongoose.connection.on('connected', () => console.log("connected"));
// SESSION MANAGEMENT AND AUTHENTICATION MIDDLEWARE
@@ -94,14 +106,15 @@ enum Method {
*/
function addSecureRoute(method: Method,
handler: (user: DashUserModel, res: express.Response, req: express.Request) => void,
- onRejection: (res: express.Response) => any = (res) => res.redirect(RouteStore.logout),
+ onRejection: (res: express.Response, req: express.Request) => any = res => res.redirect(RouteStore.login),
...subscribers: string[]
) {
let abstracted = (req: express.Request, res: express.Response) => {
if (req.user) {
handler(req.user, res, req);
} else {
- onRejection(res);
+ req.session!.target = `${req.headers.host}${req.originalUrl}`;
+ onRejection(res, req);
}
};
subscribers.forEach(route => {
@@ -134,11 +147,100 @@ app.get("/pull", (req, res) =>
// GETTERS
app.get("/search", async (req, res) => {
- let query = req.query.query || "hello";
- let results = await Search.Instance.search(query);
+ const { query, filterQuery, start, rows } = req.query;
+ if (query === undefined) {
+ res.send([]);
+ return;
+ }
+ let results = await Search.Instance.search(query, filterQuery, start, rows);
res.send(results);
});
+function msToTime(duration: number) {
+ let milliseconds = Math.floor((duration % 1000) / 100),
+ seconds = Math.floor((duration / 1000) % 60),
+ minutes = Math.floor((duration / (1000 * 60)) % 60),
+ hours = Math.floor((duration / (1000 * 60 * 60)) % 24);
+
+ let hoursS = (hours < 10) ? "0" + hours : hours;
+ let minutesS = (minutes < 10) ? "0" + minutes : minutes;
+ let secondsS = (seconds < 10) ? "0" + seconds : seconds;
+
+ return hoursS + ":" + minutesS + ":" + secondsS + "." + milliseconds;
+}
+
+app.get("/whosOnline", (req, res) => {
+ let users: any = { active: {}, inactive: {} };
+ const now = Date.now();
+
+ for (const user in timeMap) {
+ const time = timeMap[user];
+ const key = ((now - time) / 1000) < (60 * 5) ? "active" : "inactive";
+ users[key][user] = `Last active ${msToTime(now - time)} ago`;
+ }
+
+ res.send(users);
+});
+
+app.get("/thumbnail/:filename", (req, res) => {
+ let filename = req.params.filename;
+ let noExt = filename.substring(0, filename.length - ".png".length);
+ let pagenumber = parseInt(noExt[noExt.length - 1]);
+ fs.exists(uploadDir + filename, (exists: boolean) => {
+ console.log(`${uploadDir + filename} ${exists ? "exists" : "does not exist"}`);
+ if (exists) {
+ let input = fs.createReadStream(uploadDir + filename);
+ probe(input, (err: any, result: any) => {
+ if (err) {
+ console.log(err);
+ console.log(filename);
+ return;
+ }
+ res.send({ path: "/files/" + filename, width: result.width, height: result.height });
+ });
+ }
+ else {
+ LoadPage(uploadDir + filename.substring(0, filename.length - "-n.png".length) + ".pdf", pagenumber, res);
+ }
+ });
+});
+
+function LoadPage(file: string, pageNumber: number, res: Response) {
+ console.log(file);
+ Pdfjs.getDocument(file).promise
+ .then((pdf: Pdfjs.PDFDocumentProxy) => {
+ let factory = new NodeCanvasFactory();
+ console.log(pageNumber);
+ pdf.getPage(pageNumber).then((page: Pdfjs.PDFPageProxy) => {
+ console.log("reading " + page);
+ let viewport = page.getViewport(1);
+ let canvasAndContext = factory.create(viewport.width, viewport.height);
+ let renderContext = {
+ canvasContext: canvasAndContext.context,
+ viewport: viewport,
+ canvasFactory: factory
+ };
+ console.log("read " + pageNumber);
+
+ page.render(renderContext).promise
+ .then(() => {
+ console.log("saving " + pageNumber);
+ let stream = canvasAndContext.canvas.createPNGStream();
+ let pngFile = `${file.substring(0, file.length - ".pdf".length)}-${pageNumber}.PNG`;
+ let out = fs.createWriteStream(pngFile);
+ stream.pipe(out);
+ out.on("finish", () => {
+ console.log(`Success! Saved to ${pngFile}`);
+ let name = path.basename(pngFile);
+ res.send({ path: "/files/" + name, width: viewport.width, height: viewport.height });
+ });
+ }, (reason: string) => {
+ console.error(reason + ` ${pageNumber}`);
+ });
+ });
+ });
+}
+
// anyone attempting to navigate to localhost at this port will
// first have to login
addSecureRoute(
@@ -150,6 +252,17 @@ addSecureRoute(
addSecureRoute(
Method.GET,
+ async (_, res) => {
+ const cursor = await Database.Instance.query({}, { email: 1, userDocumentId: 1 }, "users");
+ const results = await cursor.toArray();
+ res.send(results.map(user => ({ email: user.email, userDocumentId: user.userDocumentId })));
+ },
+ undefined,
+ RouteStore.getUsers
+);
+
+addSecureRoute(
+ Method.GET,
(user, res, req) => {
let detector = new mobileDetect(req.headers['user-agent'] || "");
let filename = detector.mobile() !== null ? 'mobile/image.html' : 'index.html';
@@ -174,7 +287,31 @@ addSecureRoute(
RouteStore.getCurrUser
);
+class NodeCanvasFactory {
+ create = (width: number, height: number) => {
+ var canvas = createCanvas(width, height);
+ var context = canvas.getContext('2d');
+ return {
+ canvas: canvas,
+ context: context,
+ };
+ }
+
+ reset = (canvasAndContext: any, width: number, height: number) => {
+ canvasAndContext.canvas.width = width;
+ canvasAndContext.canvas.height = height;
+ }
+
+ destroy = (canvasAndContext: any) => {
+ canvasAndContext.canvas.width = 0;
+ canvasAndContext.canvas.height = 0;
+ canvasAndContext.canvas = null;
+ canvasAndContext.context = null;
+ }
+}
+
const pngTypes = [".png", ".PNG"];
+const pdfTypes = [".pdf", ".PDF"];
const jpgTypes = [".jpg", ".JPG", ".jpeg", ".JPEG"];
const uploadDir = __dirname + "/public/files/";
// SETTERS
@@ -193,9 +330,10 @@ app.post(
const file = path.basename(files[name].path);
const ext = path.extname(file);
let resizers = [
- { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: "_s" },
- { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" },
- { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }), suffix: "_l" },
+ { resizer: sharp().rotate(), suffix: "_o" },
+ { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }).rotate(), suffix: "_s" },
+ { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }).rotate(), suffix: "_m" },
+ { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }).rotate(), suffix: "_l" },
];
let isImage = false;
if (pngTypes.includes(ext)) {
@@ -209,6 +347,38 @@ app.post(
});
isImage = true;
}
+ else if (pdfTypes.includes(ext)) {
+ // Pdfjs.getDocument(uploadDir + file).promise
+ // .then((pdf: Pdfjs.PDFDocumentProxy) => {
+ // let numPages = pdf.numPages;
+ // let factory = new NodeCanvasFactory();
+ // for (let pageNum = 0; pageNum < numPages; pageNum++) {
+ // console.log(pageNum);
+ // pdf.getPage(pageNum + 1).then((page: Pdfjs.PDFPageProxy) => {
+ // console.log("reading " + pageNum);
+ // let viewport = page.getViewport(1);
+ // let canvasAndContext = factory.create(viewport.width, viewport.height);
+ // let renderContext = {
+ // canvasContext: canvasAndContext.context,
+ // viewport: viewport,
+ // canvasFactory: factory
+ // }
+ // console.log("read " + pageNum);
+
+ // page.render(renderContext).promise
+ // .then(() => {
+ // console.log("saving " + pageNum);
+ // let stream = canvasAndContext.canvas.createPNGStream();
+ // let out = fs.createWriteStream(uploadDir + file.substring(0, file.length - ext.length) + `-${pageNum + 1}.PNG`);
+ // stream.pipe(out);
+ // out.on("finish", () => console.log(`Success! Saved to ${uploadDir + file.substring(0, file.length - ext.length) + `-${pageNum + 1}.PNG`}`));
+ // }, (reason: string) => {
+ // console.error(reason + ` ${pageNum}`);
+ // });
+ // });
+ // }
+ // });
+ }
if (isImage) {
resizers.forEach(resizer => {
fs.createReadStream(uploadDir + file).pipe(resizer.resizer).pipe(fs.createWriteStream(uploadDir + file.substring(0, file.length - ext.length) + resizer.suffix + ext));
@@ -284,11 +454,21 @@ app.post(RouteStore.reset, postReset);
app.use(RouteStore.corsProxy, (req, res) =>
req.pipe(request(req.url.substring(1))).pipe(res));
-app.get(RouteStore.delete, (req, res) =>
- deleteFields().then(() => res.redirect(RouteStore.home)));
+app.get(RouteStore.delete, (req, res) => {
+ if (release) {
+ res.send("no");
+ return;
+ }
+ deleteFields().then(() => res.redirect(RouteStore.home));
+});
-app.get(RouteStore.deleteAll, (req, res) =>
- deleteAll().then(() => res.redirect(RouteStore.home)));
+app.get(RouteStore.deleteAll, (req, res) => {
+ if (release) {
+ res.send("no");
+ return;
+ }
+ deleteAll().then(() => res.redirect(RouteStore.home));
+});
app.use(wdm(compiler, { publicPath: config.output.publicPath }));
@@ -304,20 +484,33 @@ interface Map {
}
let clients: Map = {};
+let socketMap = new Map<SocketIO.Socket, string>();
+let timeMap: { [id: string]: number } = {};
+
server.on("connection", function (socket: Socket) {
- console.log("a user has connected");
+ socket.use((packet, next) => {
+ let id = socketMap.get(socket);
+ if (id) {
+ timeMap[id] = Date.now();
+ }
+ next();
+ });
Utils.Emit(socket, MessageStore.Foo, "handshooken");
- Utils.AddServerHandler(socket, MessageStore.Bar, barReceived);
+ Utils.AddServerHandler(socket, MessageStore.Bar, guid => barReceived(socket, guid));
Utils.AddServerHandler(socket, MessageStore.SetField, (args) => setField(socket, args));
Utils.AddServerHandlerCallback(socket, MessageStore.GetField, getField);
Utils.AddServerHandlerCallback(socket, MessageStore.GetFields, getFields);
- Utils.AddServerHandler(socket, MessageStore.DeleteAll, deleteFields);
+ if (!release) {
+ Utils.AddServerHandler(socket, MessageStore.DeleteAll, deleteFields);
+ }
Utils.AddServerHandler(socket, MessageStore.CreateField, CreateField);
Utils.AddServerHandlerCallback(socket, MessageStore.YoutubeApiQuery, HandleYoutubeQuery);
Utils.AddServerHandler(socket, MessageStore.UpdateField, diff => UpdateField(socket, diff));
+ Utils.AddServerHandler(socket, MessageStore.DeleteField, id => DeleteField(socket, id));
+ Utils.AddServerHandler(socket, MessageStore.DeleteFields, ids => DeleteFields(socket, ids));
Utils.AddServerHandlerCallback(socket, MessageStore.GetRefField, GetRefField);
Utils.AddServerHandlerCallback(socket, MessageStore.GetRefFields, GetRefFields);
});
@@ -336,8 +529,10 @@ async function deleteAll() {
await Search.Instance.clear();
}
-function barReceived(guid: String) {
- clients[guid.toString()] = new Client(guid.toString());
+function barReceived(socket: SocketIO.Socket, guid: string) {
+ clients[guid] = new Client(guid.toString());
+ console.log(`User ${guid} has connected`);
+ socketMap.set(socket, guid);
}
function getField([id, callback]: [string, (result?: Transferable) => void]) {
@@ -380,8 +575,8 @@ function HandleYoutubeQuery([query, callback]: [YoutubeQueryInput, (result?: any
const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = {
"number": "_n",
"string": "_t",
- // "boolean": "_b",
- // "image": ["_t", "url"],
+ "boolean": "_b",
+ "image": ["_t", "url"],
"video": ["_t", "url"],
"pdf": ["_t", "url"],
"audio": ["_t", "url"],
@@ -453,6 +648,23 @@ function UpdateField(socket: Socket, diff: Diff) {
}
}
+function DeleteField(socket: Socket, id: string) {
+ Database.Instance.delete({ _id: id }, "newDocuments").then(() => {
+ socket.broadcast.emit(MessageStore.DeleteField.Message, id);
+ });
+
+ Search.Instance.deleteDocuments([id]);
+}
+
+function DeleteFields(socket: Socket, ids: string[]) {
+ Database.Instance.delete({ _id: { $in: ids } }, "newDocuments").then(() => {
+ socket.broadcast.emit(MessageStore.DeleteFields.Message, ids);
+ });
+
+ Search.Instance.deleteDocuments(ids);
+
+}
+
function CreateField(newValue: any) {
Database.Instance.insert(newValue, "newDocuments");
}
diff --git a/src/server/updateSearch.ts b/src/server/updateSearch.ts
index de1fd25e1..906b795f1 100644
--- a/src/server/updateSearch.ts
+++ b/src/server/updateSearch.ts
@@ -6,7 +6,7 @@ import pLimit from 'p-limit';
const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = {
"number": "_n",
"string": "_t",
- // "boolean": "_b",
+ "boolean": "_b",
// "image": ["_t", "url"],
"video": ["_t", "url"],
"pdf": ["_t", "url"],
@@ -67,7 +67,7 @@ async function update() {
if ((numDocs % 50) === 0) {
console.log("updateDoc " + numDocs);
}
- console.log("doc " + numDocs);
+ // console.log("doc " + numDocs);
if (doc.__type !== "Doc") {
return;
}
@@ -88,13 +88,35 @@ async function update() {
}
if (dynfield) {
updates.push(update);
- console.log(updates.length);
+ // console.log(updates.length);
}
}
await cursor.forEach(updateDoc);
- await Promise.all(updates.map(update => {
- return limit(() => Search.Instance.updateDocument(update));
- }));
+ console.log(`Updating ${updates.length} documents`);
+ const result = await Search.Instance.updateDocuments(updates);
+ try {
+ console.log(JSON.parse(result).responseHeader.status);
+ } catch {
+ console.log("Error:");
+ // console.log(updates[i]);
+ console.log(result);
+ console.log("\n");
+ }
+ // for (let i = 0; i < updates.length; i++) {
+ // console.log(i);
+ // const result = await Search.Instance.updateDocument(updates[i]);
+ // try {
+ // console.log(JSON.parse(result).responseHeader.status);
+ // } catch {
+ // console.log("Error:");
+ // console.log(updates[i]);
+ // console.log(result);
+ // console.log("\n");
+ // }
+ // }
+ // await Promise.all(updates.map(update => {
+ // return limit(() => Search.Instance.updateDocument(update));
+ // }));
cursor.close();
}