aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/.DS_Storebin6148 -> 6148 bytes
-rw-r--r--src/Utils.ts31
-rw-r--r--src/client/DocServer.ts85
-rw-r--r--src/client/apis/youtube/YoutubeBox.scss126
-rw-r--r--src/client/apis/youtube/YoutubeBox.tsx362
-rw-r--r--src/client/cognitive_services/CognitiveServices.ts214
-rw-r--r--src/client/documents/Documents.ts163
-rw-r--r--src/client/goldenLayout.js8
-rw-r--r--src/client/util/DictationManager.ts39
-rw-r--r--src/client/util/DocumentManager.ts16
-rw-r--r--src/client/util/DragManager.ts36
-rw-r--r--src/client/util/History.ts2
-rw-r--r--src/client/util/Import & Export/DirectoryImportBox.tsx4
-rw-r--r--src/client/util/LinkManager.ts13
-rw-r--r--src/client/util/ProsemirrorExampleTransfer.ts4
-rw-r--r--src/client/util/RichTextSchema.tsx11
-rw-r--r--src/client/util/Scripting.ts85
-rw-r--r--src/client/util/SearchUtil.ts3
-rw-r--r--src/client/util/SelectionManager.ts1
-rw-r--r--src/client/util/SerializationHelper.ts34
-rw-r--r--src/client/util/TooltipTextMenu.scss95
-rw-r--r--src/client/util/TooltipTextMenu.tsx319
-rw-r--r--src/client/util/type_decls.d2
-rw-r--r--src/client/views/ContextMenu.scss57
-rw-r--r--src/client/views/ContextMenu.tsx12
-rw-r--r--src/client/views/ContextMenuItem.tsx17
-rw-r--r--src/client/views/DocumentDecorations.tsx39
-rw-r--r--src/client/views/EditableView.scss11
-rw-r--r--src/client/views/EditableView.tsx53
-rw-r--r--src/client/views/GlobalKeyHandler.ts50
-rw-r--r--src/client/views/InkingCanvas.tsx6
-rw-r--r--src/client/views/InkingControl.tsx4
-rw-r--r--src/client/views/Main.tsx5
-rw-r--r--src/client/views/MainOverlayTextBox.tsx7
-rw-r--r--src/client/views/MainView.tsx53
-rw-r--r--src/client/views/MetadataEntryMenu.tsx4
-rw-r--r--src/client/views/OverlayView.scss42
-rw-r--r--src/client/views/OverlayView.tsx111
-rw-r--r--src/client/views/ScriptBox.tsx23
-rw-r--r--src/client/views/ScriptingRepl.scss50
-rw-r--r--src/client/views/ScriptingRepl.tsx247
-rw-r--r--src/client/views/SearchBox.tsx183
-rw-r--r--src/client/views/SearchItem.tsx69
-rw-r--r--src/client/views/TemplateMenu.tsx41
-rw-r--r--src/client/views/collections/CollectionBaseView.scss2
-rw-r--r--src/client/views/collections/CollectionBaseView.tsx14
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx62
-rw-r--r--src/client/views/collections/CollectionPDFView.tsx12
-rw-r--r--src/client/views/collections/CollectionSchemaCells.tsx312
-rw-r--r--src/client/views/collections/CollectionSchemaHeaders.tsx366
-rw-r--r--src/client/views/collections/CollectionSchemaMovableTableHOC.tsx245
-rw-r--r--src/client/views/collections/CollectionSchemaView.scss630
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx1032
-rw-r--r--src/client/views/collections/CollectionStackingView.scss235
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx260
-rw-r--r--src/client/views/collections/CollectionStackingViewFieldColumn.tsx357
-rw-r--r--src/client/views/collections/CollectionSubView.tsx41
-rw-r--r--src/client/views/collections/CollectionTreeView.scss30
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx432
-rw-r--r--src/client/views/collections/CollectionVideoView.scss9
-rw-r--r--src/client/views/collections/CollectionVideoView.tsx62
-rw-r--r--src/client/views/collections/CollectionView.tsx97
-rw-r--r--src/client/views/collections/CollectionViewChromes.scss223
-rw-r--r--src/client/views/collections/CollectionViewChromes.tsx440
-rw-r--r--src/client/views/collections/KeyRestrictionRow.tsx55
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx8
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss6
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx423
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx48
-rw-r--r--src/client/views/nodes/ButtonBox.scss12
-rw-r--r--src/client/views/nodes/ButtonBox.tsx77
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx30
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx6
-rw-r--r--src/client/views/nodes/DocumentIcon.tsx65
-rw-r--r--src/client/views/nodes/DocumentView.scss1
-rw-r--r--src/client/views/nodes/DocumentView.tsx242
-rw-r--r--src/client/views/nodes/FaceRectangle.tsx29
-rw-r--r--src/client/views/nodes/FaceRectangles.tsx46
-rw-r--r--src/client/views/nodes/FieldView.tsx5
-rw-r--r--src/client/views/nodes/FormattedTextBox.scss70
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx257
-rw-r--r--src/client/views/nodes/IconBox.tsx12
-rw-r--r--src/client/views/nodes/ImageBox.scss18
-rw-r--r--src/client/views/nodes/ImageBox.tsx202
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx24
-rw-r--r--src/client/views/nodes/KeyValuePair.tsx44
-rw-r--r--src/client/views/nodes/LinkEditor.tsx3
-rw-r--r--src/client/views/nodes/LinkMenu.tsx9
-rw-r--r--src/client/views/nodes/LinkMenuGroup.tsx5
-rw-r--r--src/client/views/nodes/LinkMenuItem.tsx30
-rw-r--r--src/client/views/nodes/PDFBox.tsx6
-rw-r--r--src/client/views/nodes/VideoBox.tsx111
-rw-r--r--src/client/views/nodes/WebBox.tsx16
-rw-r--r--src/client/views/pdf/Annotation.scss4
-rw-r--r--src/client/views/pdf/Annotation.tsx23
-rw-r--r--src/client/views/pdf/PDFAnnotationLayer.scss6
-rw-r--r--src/client/views/pdf/PDFAnnotationLayer.tsx7
-rw-r--r--src/client/views/pdf/PDFViewer.scss9
-rw-r--r--src/client/views/pdf/PDFViewer.tsx160
-rw-r--r--src/client/views/pdf/Page.tsx45
-rw-r--r--src/client/views/presentationview/PresentationElement.tsx589
-rw-r--r--src/client/views/presentationview/PresentationList.tsx48
-rw-r--r--src/client/views/presentationview/PresentationModeMenu.scss30
-rw-r--r--src/client/views/presentationview/PresentationModeMenu.tsx100
-rw-r--r--src/client/views/presentationview/PresentationView.scss28
-rw-r--r--src/client/views/presentationview/PresentationView.tsx286
-rw-r--r--src/client/views/search/CheckBox.scss10
-rw-r--r--src/client/views/search/FieldFilters.scss9
-rw-r--r--src/client/views/search/FilterBox.scss89
-rw-r--r--src/client/views/search/FilterBox.tsx117
-rw-r--r--src/client/views/search/IconBar.scss5
-rw-r--r--src/client/views/search/IconButton.scss14
-rw-r--r--src/client/views/search/IconButton.tsx5
-rw-r--r--src/client/views/search/SearchBox.scss5
-rw-r--r--src/client/views/search/SearchBox.tsx8
-rw-r--r--src/client/views/search/SearchItem.tsx142
-rw-r--r--src/client/views/search/SelectorContextMenu.scss1
-rw-r--r--src/client/views/search/ToggleBar.scss9
-rw-r--r--src/client/views/search/ToggleBar.tsx1
-rw-r--r--src/debug/Test.tsx88
-rw-r--r--src/debug/Viewer.tsx3
-rw-r--r--src/mobile/ImageUpload.tsx5
-rw-r--r--src/new_fields/Doc.ts234
-rw-r--r--src/new_fields/FieldSymbols.ts2
-rw-r--r--src/new_fields/InkField.ts10
-rw-r--r--src/new_fields/List.ts4
-rw-r--r--src/new_fields/ObjectField.ts3
-rw-r--r--src/new_fields/Proxy.ts43
-rw-r--r--src/new_fields/RefField.ts5
-rw-r--r--src/new_fields/RichTextField.ts2
-rw-r--r--src/new_fields/SchemaHeaderField.ts101
-rw-r--r--src/new_fields/ScriptField.ts57
-rw-r--r--src/new_fields/Types.ts2
-rw-r--r--src/new_fields/URLField.ts3
-rw-r--r--src/new_fields/util.ts34
-rw-r--r--[-rwxr-xr-x]src/scraping/acm/chromedriverbin10256192 -> 10256192 bytes
-rw-r--r--src/scraping/buxton/scraper.py43
-rw-r--r--src/scraping/buxton/source/Bill_Notes_3_button_optical_mouse.docxbin0 -> 412208 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Amazon_Kindle_Keyboard.docxbin0 -> 474022 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Apple_Adj_Keyboard.docxbin0 -> 1758498 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_BAT.docxbin0 -> 1349620 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Bill_Notes_CyKey.docxbin1675500 -> 1561425 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Braun_T3.docxbin1671968 -> 1510917 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_CasioC801.docxbin574664 -> 413861 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Casio_CZ-101.docxbin0 -> 523939 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Casio_Mini.docxbin581069 -> 467304 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_FingerWorks_Prototype.docxbin585090 -> 423384 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Fingerworks_TouchStream.docxbin1722555 -> 1558473 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_FrogPad.docxbin840173 -> 679241 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Gavilan_SC.docxbin1695290 -> 1531689 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Grandjean_Stenotype.docxbin2094142 -> 1933004 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Kindle_3G_lighted_cover.docxbin0 -> 919789 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Matias.docxbin590407 -> 476141 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Microwriter.docxbin0 -> 1042556 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_MousePen.docxbin505322 -> 344083 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_NewO.docxbin2264571 -> 2150143 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_OLPC.docxbin6883659 -> 6721592 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_PARCkbd.docxbin631959 -> 517484 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Philco_Mystery_Control.docxbin1994439 -> 1880816 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_TASA_Kbd.docxbin461199 -> 347612 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_The_Tap.docxbin711321 -> 597382 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Twiddler.docxbin0 -> 526307 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_orbiTouch.docbin0 -> 3945306 bytes
-rw-r--r--src/server/Message.ts11
-rw-r--r--src/server/RouteStore.ts3
-rw-r--r--src/server/authentication/controllers/user_controller.ts3
-rw-r--r--src/server/authentication/models/current_user_utils.ts7
-rw-r--r--src/server/database.ts56
-rw-r--r--src/server/index.ts281
-rw-r--r--src/server/remapUrl.ts7
-rw-r--r--src/server/youtubeApi/youtubeApiSample.d.ts2
-rw-r--r--src/server/youtubeApi/youtubeApiSample.js179
172 files changed, 10271 insertions, 2125 deletions
diff --git a/src/.DS_Store b/src/.DS_Store
index 071dafa1e..5b35884bd 100644
--- a/src/.DS_Store
+++ b/src/.DS_Store
Binary files differ
diff --git a/src/Utils.ts b/src/Utils.ts
index e8a80bdc3..502540eb0 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -2,6 +2,7 @@ import v4 = require('uuid/v4');
import v5 = require("uuid/v5");
import { Socket } from 'socket.io';
import { Message } from './server/Message';
+import { RouteStore } from './server/RouteStore';
export class Utils {
@@ -27,6 +28,18 @@ export class Utils {
return { scale, translateX, translateY };
}
+ /**
+ * A convenience method. Prepends the full path (i.e. http://localhost:1050) to the
+ * requested extension
+ * @param extension the specified sub-path to append to the window origin
+ */
+ public static prepend(extension: string): string {
+ return window.location.origin + extension;
+ }
+ public static CorsProxy(url: string): string {
+ return this.prepend(RouteStore.corsProxy + "/") + encodeURIComponent(url);
+ }
+
public static CopyText(text: string) {
var textArea = document.createElement("textarea");
textArea.value = text;
@@ -127,13 +140,15 @@ export function returnOne() { return 1; }
export function returnZero() { return 0; }
+export function returnEmptyString() { return ""; }
+
export function emptyFunction() { }
export type Without<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
export type Predicate<K, V> = (entry: [K, V]) => boolean;
-export function deepCopy<K, V>(source: Map<K, V>, predicate?: Predicate<K, V>) {
+export function DeepCopy<K, V>(source: Map<K, V>, predicate?: Predicate<K, V>) {
let deepCopy = new Map<K, V>();
let entries = source.entries(), next = entries.next();
while (!next.done) {
@@ -144,4 +159,18 @@ export function deepCopy<K, V>(source: Map<K, V>, predicate?: Predicate<K, V>) {
next = entries.next();
}
return deepCopy;
+}
+
+export namespace JSONUtils {
+
+ export function tryParse(source: string) {
+ let results: any;
+ try {
+ results = JSON.parse(source);
+ } catch (e) {
+ results = source;
+ }
+ return results;
+ }
+
} \ No newline at end of file
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index 6737657c8..87a87be92 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -1,11 +1,10 @@
import * as OpenSocket from 'socket.io-client';
-import { MessageStore, Diff } 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
@@ -26,7 +25,6 @@ export namespace DocServer {
// this client's distinct GUID created at initialization
let GUID: string;
// indicates whether or not a document is currently being udpated, and, if so, its id
- let updatingId: string | undefined;
export function init(protocol: string, hostname: string, port: number, identifier: string) {
_cache = {};
@@ -47,14 +45,6 @@ export namespace DocServer {
Utils.AddServerHandler(_socket, MessageStore.DeleteField, respondToDelete);
Utils.AddServerHandler(_socket, MessageStore.DeleteFields, respondToDelete);
}
- /**
- * A convenience method. Prepends the full path (i.e. http://localhost:1050) to the
- * requested extension
- * @param extension the specified sub-path to append to the window origin
- */
- export function prepend(extension: string): string {
- return window.location.origin + extension;
- }
function errorFunc(): never {
throw new Error("Can't use DocServer without calling init first");
@@ -134,17 +124,16 @@ export namespace DocServer {
// 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.
+ const field = await SerializationHelper.Deserialize(fieldJson);
if (field !== undefined) {
- await field.proto;
_cache[id] = field;
} else {
delete _cache[id];
}
return field;
+ // 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.
});
// here, indicate that the document associated with this id is currently
// being retrieved and cached
@@ -167,6 +156,20 @@ export namespace DocServer {
return _GetRefField(id);
}
+ export async function getYoutubeChannels() {
+ let apiKey = await Utils.EmitCallback(_socket, MessageStore.YoutubeApiQuery, { type: YoutubeQueryTypes.Channels });
+ return apiKey;
+ }
+
+ export function getYoutubeVideos(videoTitle: string, callBack: (videos: any[]) => void) {
+ Utils.EmitCallback(_socket, MessageStore.YoutubeApiQuery, { type: YoutubeQueryTypes.SearchVideo, userInput: videoTitle }, callBack);
+ }
+
+ export function getYoutubeVideoDetails(videoIds: string, callBack: (videoDetails: any[]) => void) {
+ Utils.EmitCallback(_socket, MessageStore.YoutubeApiQuery, { type: YoutubeQueryTypes.VideoDetails, videoIds: videoIds }, callBack);
+ }
+
+
/**
* 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
@@ -210,28 +213,37 @@ export namespace DocServer {
// 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 = [];
+ const proms: Promise<void>[] = [];
for (const field of fields) {
if (field !== undefined) {
// deserialize
- let deserialized: any = SerializationHelper.Deserialize(field);
- fieldMap[field.id] = deserialized;
+ let prom = SerializationHelper.Deserialize(field).then(deserialized => {
+ fieldMap[field.id] = deserialized;
+
+ //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 (deserialized !== undefined) {
+ _cache[field.id] = deserialized;
+ } else {
+ delete _cache[field.id];
+ }
+ return deserialized;
+ });
+ // 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
+ _cache[field.id] = prom;
// adds to a list of promises that will be awaited asynchronously
- protosToLoad.push(deserialized.proto);
+ proms.push(prom);
}
}
- // this actually handles the loading of prototypes
- await Promise.all(protosToLoad);
+ await Promise.all(proms);
return fieldMap;
});
- // 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;
@@ -241,14 +253,6 @@ export namespace DocServer {
// 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 {
- delete _cache[id];
- }
map[id] = field;
});
@@ -312,9 +316,6 @@ export namespace DocServer {
}
function _UpdateFieldImpl(id: string, diff: any) {
- if (id === updatingId) {
- return;
- }
Utils.Emit(_socket, MessageStore.UpdateField, { id, diff });
}
@@ -337,11 +338,7 @@ export namespace DocServer {
// 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
diff --git a/src/client/apis/youtube/YoutubeBox.scss b/src/client/apis/youtube/YoutubeBox.scss
new file mode 100644
index 000000000..eabdbb1ac
--- /dev/null
+++ b/src/client/apis/youtube/YoutubeBox.scss
@@ -0,0 +1,126 @@
+.youtubeBox-cont {
+ ul {
+ list-style-type: none;
+ padding-inline-start: 10px;
+ }
+
+
+ li {
+ margin: 4px;
+ display: inline-flex;
+ }
+
+ li:hover {
+ cursor: pointer;
+ opacity: 0.8;
+ }
+
+ .search_wrapper {
+ width: 100%;
+ display: inline-flex;
+ height: 175px;
+
+ .video_duration {
+ // margin: 0;
+ // padding: 0;
+ border: 0;
+ background: transparent;
+ display: inline-block;
+ position: relative;
+ bottom: 25px;
+ left: 85%;
+ margin: 4px;
+ color: #FFFFFF;
+ background-color: rgba(0, 0, 0, 0.80);
+ padding: 2px 4px;
+ border-radius: 2px;
+ letter-spacing: .5px;
+ font-size: 1.2rem;
+ font-weight: 500;
+ line-height: 1.2rem;
+
+ }
+
+ .textual_info {
+ font-family: Arial, Helvetica, sans-serif;
+
+ .videoTitle {
+ margin-left: 4px;
+ // display: inline-block;
+ color: #0D0D0D;
+ -webkit-line-clamp: 2;
+ display: block;
+ max-height: 4.8rem;
+ overflow: hidden;
+ font-size: 1.8rem;
+ font-weight: 400;
+ line-height: 2.4rem;
+ -webkit-box-orient: vertical;
+ text-overflow: ellipsis;
+ white-space: normal;
+ display: -webkit-box;
+ }
+
+ .channelName {
+ color: #606060;
+ margin-left: 4px;
+ font-size: 1.3rem;
+ font-weight: 400;
+ line-height: 1.8rem;
+ text-transform: none;
+ margin-top: 0px;
+ display: inline-block;
+ }
+
+ .video_description {
+ margin-left: 4px;
+ // font-size: 12px;
+ color: #606060;
+ padding-top: 8px;
+ margin-bottom: 8px;
+ display: block;
+ line-height: 1.8rem;
+ max-height: 4.2rem;
+ overflow: hidden;
+ font-size: 1.3rem;
+ font-weight: 400;
+ text-transform: none;
+ }
+
+ .publish_time {
+ //display: inline-block;
+ margin-left: 8px;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ color: #606060;
+ max-width: 100%;
+ line-height: 1.8rem;
+ max-height: 3.6rem;
+ overflow: hidden;
+ font-size: 1.3rem;
+ font-weight: 400;
+ text-transform: none;
+ }
+
+ .viewCount {
+
+ margin-left: 8px;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ color: #606060;
+ max-width: 100%;
+ line-height: 1.8rem;
+ max-height: 3.6rem;
+ overflow: hidden;
+ font-size: 1.3rem;
+ font-weight: 400;
+ text-transform: none;
+ }
+
+
+
+ }
+ }
+} \ No newline at end of file
diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx
new file mode 100644
index 000000000..d73988bb8
--- /dev/null
+++ b/src/client/apis/youtube/YoutubeBox.tsx
@@ -0,0 +1,362 @@
+import { action, observable, runInAction } from 'mobx';
+import { observer } from "mobx-react";
+import { Doc, DocListCastAsync } from "../../../new_fields/Doc";
+import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { Utils } from "../../../Utils";
+import { DocServer } from "../../DocServer";
+import { Docs } from "../../documents/Documents";
+import { DocumentDecorations } from "../../views/DocumentDecorations";
+import { InkingControl } from "../../views/InkingControl";
+import { FieldView, FieldViewProps } from "../../views/nodes/FieldView";
+import "../../views/nodes/WebBox.scss";
+import "./YoutubeBox.scss";
+import React = require("react");
+
+interface VideoTemplate {
+ thumbnailUrl: string;
+ videoTitle: string;
+ videoId: string;
+ duration: string;
+ channelTitle: string;
+ viewCount: string;
+ publishDate: string;
+ videoDescription: string;
+}
+
+/**
+ * This class models the youtube search document that can be dropped on to canvas.
+ */
+@observer
+export class YoutubeBox extends React.Component<FieldViewProps> {
+
+ @observable YoutubeSearchElement: HTMLInputElement | undefined;
+ @observable searchResultsFound: boolean = false;
+ @observable searchResults: any[] = [];
+ @observable videoClicked: boolean = false;
+ @observable selectedVideoUrl: string = "";
+ @observable lisOfBackUp: JSX.Element[] = [];
+ @observable videoIds: string | undefined;
+ @observable videoDetails: any[] = [];
+ @observable curVideoTemplates: VideoTemplate[] = [];
+
+
+ public static LayoutString() { return FieldView.LayoutString(YoutubeBox); }
+
+ /**
+ * When component mounts, last search's results are laoded in based on the back up stored
+ * in the document of the props.
+ */
+ async componentWillMount() {
+ //DocServer.getYoutubeChannels();
+ let castedSearchBackUp = Cast(this.props.Document.cachedSearchResults, Doc);
+ let awaitedBackUp = await castedSearchBackUp;
+ let castedDetailBackUp = Cast(this.props.Document.cachedDetails, Doc);
+ let awaitedDetails = await castedDetailBackUp;
+
+
+ if (awaitedBackUp) {
+
+
+ let jsonList = await DocListCastAsync(awaitedBackUp.json);
+ let jsonDetailList = await DocListCastAsync(awaitedDetails!.json);
+
+ if (jsonList!.length !== 0) {
+ runInAction(() => this.searchResultsFound = true);
+ let index = 0;
+ //getting the necessary information from backUps and building templates that will be used to map in render
+ for (let video of jsonList!) {
+
+ let videoId = await Cast(video.id, Doc);
+ let id = StrCast(videoId!.videoId);
+ let snippet = await Cast(video.snippet, Doc);
+ let videoTitle = this.filterYoutubeTitleResult(StrCast(snippet!.title));
+ let thumbnail = await Cast(snippet!.thumbnails, Doc);
+ let thumbnailMedium = await Cast(thumbnail!.medium, Doc);
+ let thumbnailUrl = StrCast(thumbnailMedium!.url);
+ let videoDescription = StrCast(snippet!.description);
+ let pusblishDate = (this.roundPublishTime(StrCast(snippet!.publishedAt)))!;
+ let channelTitle = StrCast(snippet!.channelTitle);
+ let duration: string = "";
+ let viewCount: string = "";
+ if (jsonDetailList!.length !== 0) {
+ let contentDetails = await Cast(jsonDetailList![index].contentDetails, Doc);
+ let statistics = await Cast(jsonDetailList![index].statistics, Doc);
+ duration = this.convertIsoTimeToDuration(StrCast(contentDetails!.duration));
+ viewCount = this.abbreviateViewCount(parseInt(StrCast(statistics!.viewCount)))!;
+ }
+ index = index + 1;
+ let newTemplate: VideoTemplate = { videoId: id, videoTitle: videoTitle, thumbnailUrl: thumbnailUrl, publishDate: pusblishDate, channelTitle: channelTitle, videoDescription: videoDescription, duration: duration, viewCount: viewCount };
+ runInAction(() => this.curVideoTemplates.push(newTemplate));
+ }
+ }
+ }
+ }
+
+ _ignore = 0;
+ onPreWheel = (e: React.WheelEvent) => {
+ this._ignore = e.timeStamp;
+ }
+ onPrePointer = (e: React.PointerEvent) => {
+ this._ignore = e.timeStamp;
+ }
+ onPostPointer = (e: React.PointerEvent) => {
+ if (this._ignore !== e.timeStamp) {
+ e.stopPropagation();
+ }
+ }
+ onPostWheel = (e: React.WheelEvent) => {
+ if (this._ignore !== e.timeStamp) {
+ e.stopPropagation();
+ }
+ }
+
+ /**
+ * Function that submits the title entered by user on enter press.
+ */
+ onEnterKeyDown = (e: React.KeyboardEvent) => {
+ if (e.keyCode === 13) {
+ let submittedTitle = this.YoutubeSearchElement!.value;
+ this.YoutubeSearchElement!.value = "";
+ this.YoutubeSearchElement!.blur();
+ DocServer.getYoutubeVideos(submittedTitle, this.processesVideoResults);
+
+ }
+ }
+
+ /**
+ * The callback that is passed in to server, which functions as a way to
+ * get videos that is returned by search. It also makes a call to server
+ * to get details for the videos found.
+ */
+ @action
+ processesVideoResults = (videos: any[]) => {
+ this.searchResults = videos;
+ if (this.searchResults.length > 0) {
+ this.searchResultsFound = true;
+ this.videoIds = "";
+ videos.forEach((video) => {
+ if (this.videoIds === "") {
+ this.videoIds = video.id.videoId;
+ } else {
+ this.videoIds = this.videoIds! + ", " + video.id.videoId;
+ }
+ });
+ //Asking for details that include duration and viewCount from server for videoIds
+ DocServer.getYoutubeVideoDetails(this.videoIds, this.processVideoDetails);
+ this.backUpSearchResults(videos);
+ if (this.videoClicked) {
+ this.videoClicked = false;
+ }
+ }
+ }
+
+ /**
+ * The callback that is given to server to process and receive returned details about the videos.
+ */
+ @action
+ processVideoDetails = (videoDetails: any[]) => {
+ this.videoDetails = videoDetails;
+ this.props.Document.cachedDetails = Docs.Get.DocumentHierarchyFromJson(videoDetails, "detailBackUp");
+ }
+
+ /**
+ * The function that stores the search results in the props document.
+ */
+ backUpSearchResults = (videos: any[]) => {
+ this.props.Document.cachedSearchResults = Docs.Get.DocumentHierarchyFromJson(videos, "videosBackUp");
+ }
+
+ /**
+ * The function that filters out escaped characters returned by the api
+ * in the title of the videos.
+ */
+ filterYoutubeTitleResult = (resultTitle: string) => {
+ let processedTitle: string = resultTitle.ReplaceAll("&amp;", "&");
+ processedTitle = processedTitle.ReplaceAll("&#39;", "'");
+ processedTitle = processedTitle.ReplaceAll("&quot;", "\"");
+ return processedTitle;
+ }
+
+
+
+ /**
+ * The function that converts ISO date, which is passed in, to normal date and finds the
+ * difference between today's date and that date, in terms of "ago" to imitate youtube.
+ */
+ roundPublishTime = (publishTime: string) => {
+ let date = new Date(publishTime).getTime();
+ let curDate = new Date().getTime();
+ let timeDif = curDate - date;
+ let totalSeconds = timeDif / 1000;
+ let totalMin = totalSeconds / 60;
+ let totalHours = totalMin / 60;
+ let totalDays = totalHours / 24;
+ let totalMonths = totalDays / 30.417;
+ let totalYears = totalMonths / 12;
+
+
+ let truncYears = Math.trunc(totalYears);
+ let truncMonths = Math.trunc(totalMonths);
+ let truncDays = Math.trunc(totalDays);
+ let truncHours = Math.trunc(totalHours);
+ let truncMin = Math.trunc(totalMin);
+ let truncSec = Math.trunc(totalSeconds);
+
+ let pluralCase = "";
+
+ if (truncYears !== 0) {
+ truncYears > 1 ? pluralCase = "s" : pluralCase = "";
+ return truncYears + " year" + pluralCase + " ago";
+ } else if (truncMonths !== 0) {
+ truncMonths > 1 ? pluralCase = "s" : pluralCase = "";
+ return truncMonths + " month" + pluralCase + " ago";
+ } else if (truncDays !== 0) {
+ truncDays > 1 ? pluralCase = "s" : pluralCase = "";
+ return truncDays + " day" + pluralCase + " ago";
+ } else if (truncHours !== 0) {
+ truncHours > 1 ? pluralCase = "s" : pluralCase = "";
+ return truncHours + " hour" + pluralCase + " ago";
+ } else if (truncMin !== 0) {
+ truncMin > 1 ? pluralCase = "s" : pluralCase = "";
+ return truncMin + " minute" + pluralCase + " ago";
+ } else if (truncSec !== 0) {
+ truncSec > 1 ? pluralCase = "s" : pluralCase = "";
+ return truncSec + " second" + pluralCase + " ago";
+ }
+ }
+
+ /**
+ * The function that converts the passed in ISO time to normal duration time.
+ */
+ convertIsoTimeToDuration = (isoDur: string) => {
+
+ let convertedTime = isoDur.replace(/D|H|M/g, ":").replace(/P|T|S/g, "").split(":");
+
+ if (1 === convertedTime.length) {
+ 2 !== convertedTime[0].length && (convertedTime[0] = "0" + convertedTime[0]), convertedTime[0] = "0:" + convertedTime[0];
+ } else {
+ for (var r = 1, l = convertedTime.length - 1; l >= r; r++) {
+ 2 !== convertedTime[r].length && (convertedTime[r] = "0" + convertedTime[r]);
+ }
+ }
+
+ return convertedTime.join(":");
+ }
+
+ /**
+ * The function that rounds the viewCount to the nearest
+ * thousand, million or billion, given a viewCount number.
+ */
+ abbreviateViewCount = (viewCount: number) => {
+ if (viewCount < 1000) {
+ return viewCount.toString();
+ } else if (viewCount >= 1000 && viewCount < 1000000) {
+ return (Math.trunc(viewCount / 1000)) + "K";
+ } else if (viewCount >= 1000000 && viewCount < 1000000000) {
+ return (Math.trunc(viewCount / 1000000)) + "M";
+ } else if (viewCount >= 1000000000) {
+ return (Math.trunc(viewCount / 1000000000)) + "B";
+ }
+ }
+
+ /**
+ * The function that is called to decide on what'll be rendered by the component.
+ * It renders search Results if found. If user didn't do a new search, it renders from the videoTemplates
+ * generated by the backUps. If none present, renders nothing.
+ */
+ renderSearchResultsOrVideo = () => {
+ if (this.searchResultsFound) {
+ if (this.searchResults.length !== 0) {
+ return <ul>
+ {this.searchResults.map((video, index) => {
+ let filteredTitle = this.filterYoutubeTitleResult(video.snippet.title);
+ let channelTitle = video.snippet.channelTitle;
+ let videoDescription = video.snippet.description;
+ let pusblishDate = this.roundPublishTime(video.snippet.publishedAt);
+ let duration;
+ let viewCount;
+ if (this.videoDetails.length !== 0) {
+ duration = this.convertIsoTimeToDuration(this.videoDetails[index].contentDetails.duration);
+ viewCount = this.abbreviateViewCount(this.videoDetails[index].statistics.viewCount);
+ }
+
+
+ return <li onClick={() => this.embedVideoOnClick(video.id.videoId, filteredTitle)} key={Utils.GenerateGuid()}>
+ <div className="search_wrapper">
+ <div style={{ backgroundColor: "yellow" }}>
+ <img src={video.snippet.thumbnails.medium.url} />
+ <span className="video_duration">{duration}</span>
+ </div>
+ <div className="textual_info">
+ <span className="videoTitle">{filteredTitle}</span>
+ <span className="channelName">{channelTitle}</span>
+ <span className="viewCount">{viewCount}</span>
+ <span className="publish_time">{pusblishDate}</span>
+ <p className="video_description">{videoDescription}</p>
+
+ </div>
+ </div>
+ </li>;
+ })}
+ </ul>;
+ } else if (this.curVideoTemplates.length !== 0) {
+ return <ul>
+ {this.curVideoTemplates.map((video: VideoTemplate) => {
+ return <li onClick={() => this.embedVideoOnClick(video.videoId, video.videoTitle)} key={Utils.GenerateGuid()}>
+ <div className="search_wrapper">
+ <div style={{ backgroundColor: "yellow" }}>
+ <img src={video.thumbnailUrl} />
+ <span className="video_duration">{video.duration}</span>
+ </div>
+ <div className="textual_info">
+ <span className="videoTitle">{video.videoTitle}</span>
+ <span className="channelName">{video.channelTitle}</span>
+ <span className="viewCount">{video.viewCount}</span>
+ <span className="publish_time">{video.publishDate}</span>
+ <p className="video_description">{video.videoDescription}</p>
+ </div>
+ </div>
+ </li>;
+ })}
+ </ul>;
+ }
+ } else {
+ return (null);
+ }
+ }
+
+ /**
+ * Given a videoId and title, creates a new youtube embedded url, and uses that
+ * to create a new video document.
+ */
+ @action
+ embedVideoOnClick = (videoId: string, filteredTitle: string) => {
+ let embeddedUrl = "https://www.youtube.com/embed/" + videoId;
+ this.selectedVideoUrl = embeddedUrl;
+ let addFunction = this.props.addDocument!;
+ let newVideoX = NumCast(this.props.Document.x);
+ let newVideoY = NumCast(this.props.Document.y) + NumCast(this.props.Document.height);
+
+ addFunction(Docs.Create.VideoDocument(embeddedUrl, { title: filteredTitle, width: 400, height: 315, x: newVideoX, y: newVideoY }));
+ this.videoClicked = true;
+ }
+
+ render() {
+ let content =
+ <div className="youtubeBox-cont" style={{ width: "100%", height: "100%", position: "absolute" }} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}>
+ <input type="text" placeholder="Search for a video" onKeyDown={this.onEnterKeyDown} style={{ height: 40, width: "100%", border: "1px solid black", padding: 5, textAlign: "center" }} ref={(e) => this.YoutubeSearchElement = e!} />
+ {this.renderSearchResultsOrVideo()}
+ </div>;
+
+ let frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting;
+
+ let classname = "webBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
+ return (
+ <>
+ <div className={classname} >
+ {content}
+ </div>
+ {!frozen ? (null) : <div className="webBox-overlay" onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer} />}
+ </>);
+ }
+} \ No newline at end of file
diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts
new file mode 100644
index 000000000..c118d91d3
--- /dev/null
+++ b/src/client/cognitive_services/CognitiveServices.ts
@@ -0,0 +1,214 @@
+import * as request from "request-promise";
+import { Doc, Field, Opt } from "../../new_fields/Doc";
+import { Cast } from "../../new_fields/Types";
+import { Docs } from "../documents/Documents";
+import { RouteStore } from "../../server/RouteStore";
+import { Utils } from "../../Utils";
+import { InkData } from "../../new_fields/InkField";
+import { UndoManager } from "../util/UndoManager";
+
+type APIManager<D> = { converter: BodyConverter<D>, requester: RequestExecutor, analyzer: AnalysisApplier };
+type RequestExecutor = (apiKey: string, body: string, service: Service) => Promise<string>;
+type AnalysisApplier = (target: Doc, relevantKeys: string[], ...args: any) => any;
+type BodyConverter<D> = (data: D) => string;
+type Converter = (results: any) => Field;
+
+export type Tag = { name: string, confidence: number };
+export type Rectangle = { top: number, left: number, width: number, height: number };
+
+export enum Service {
+ ComputerVision = "vision",
+ Face = "face",
+ Handwriting = "handwriting"
+}
+
+export enum Confidence {
+ Yikes = 0.0,
+ Unlikely = 0.2,
+ Poor = 0.4,
+ Fair = 0.6,
+ Good = 0.8,
+ Excellent = 0.95
+}
+
+/**
+ * A file that handles all interactions with Microsoft Azure's Cognitive
+ * Services APIs. These machine learning endpoints allow basic data analytics for
+ * various media types.
+ */
+export namespace CognitiveServices {
+
+ const ExecuteQuery = async <D, R>(service: Service, manager: APIManager<D>, data: D): Promise<Opt<R>> => {
+ return fetch(Utils.prepend(`${RouteStore.cognitiveServices}/${service}`)).then(async response => {
+ let apiKey = await response.text();
+ if (!apiKey) {
+ console.log(`No API key found for ${service}: ensure index.ts has access to a .env file in your root directory`);
+ return undefined;
+ }
+
+ let results: Opt<R>;
+ try {
+ results = await manager.requester(apiKey, manager.converter(data), service).then(json => JSON.parse(json));
+ } catch {
+ results = undefined;
+ }
+ return results;
+ });
+ };
+
+ export namespace Image {
+
+ export const Manager: APIManager<string> = {
+
+ converter: (imageUrl: string) => JSON.stringify({ url: imageUrl }),
+
+ requester: async (apiKey: string, body: string, service: Service) => {
+ let uriBase;
+ let parameters;
+
+ switch (service) {
+ case Service.Face:
+ uriBase = 'face/v1.0/detect';
+ parameters = {
+ 'returnFaceId': 'true',
+ 'returnFaceLandmarks': 'false',
+ 'returnFaceAttributes': 'age,gender,headPose,smile,facialHair,glasses,' +
+ 'emotion,hair,makeup,occlusion,accessories,blur,exposure,noise'
+ };
+ break;
+ case Service.ComputerVision:
+ uriBase = 'vision/v2.0/analyze';
+ parameters = {
+ 'visualFeatures': 'Categories,Description,Color,Objects,Tags,Adult',
+ 'details': 'Celebrities,Landmarks',
+ 'language': 'en',
+ };
+ break;
+ }
+
+ const options = {
+ uri: 'https://eastus.api.cognitive.microsoft.com/' + uriBase,
+ qs: parameters,
+ body: body,
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Ocp-Apim-Subscription-Key': apiKey
+ }
+ };
+
+ return request.post(options);
+ },
+
+ analyzer: async (target: Doc, keys: string[], url: string, service: Service, converter: Converter) => {
+ let batch = UndoManager.StartBatch("Image Analysis");
+
+ let storageKey = keys[0];
+ if (!url || await Cast(target[storageKey], Doc)) {
+ return;
+ }
+ let toStore: any;
+ let results = await ExecuteQuery<string, any>(service, Manager, url);
+ if (!results) {
+ toStore = "Cognitive Services could not process the given image URL.";
+ } else {
+ if (!results.length) {
+ toStore = converter(results);
+ } else {
+ toStore = results.length > 0 ? converter(results) : "Empty list returned.";
+ }
+ }
+ target[storageKey] = toStore;
+
+ batch.end();
+ }
+
+ };
+
+ export type Face = { faceAttributes: any, faceId: string, faceRectangle: Rectangle };
+
+ }
+
+ export namespace Inking {
+
+ export const Manager: APIManager<InkData> = {
+
+ converter: (inkData: InkData): string => {
+ let entries = inkData.entries(), next = entries.next();
+ let strokes: AzureStrokeData[] = [], id = 0;
+ while (!next.done) {
+ strokes.push({
+ id: id++,
+ points: next.value[1].pathData.map(point => `${point.x},${point.y}`).join(","),
+ language: "en-US"
+ });
+ next = entries.next();
+ }
+ return JSON.stringify({
+ version: 1,
+ language: "en-US",
+ unit: "mm",
+ strokes: strokes
+ });
+ },
+
+ requester: async (apiKey: string, body: string) => {
+ let xhttp = new XMLHttpRequest();
+ let serverAddress = "https://api.cognitive.microsoft.com";
+ let endpoint = serverAddress + "/inkrecognizer/v1.0-preview/recognize";
+
+ let promisified = (resolve: any, reject: any) => {
+ xhttp.onreadystatechange = function () {
+ if (this.readyState === 4) {
+ let result = xhttp.responseText;
+ switch (this.status) {
+ case 200:
+ return resolve(result);
+ case 400:
+ default:
+ return reject(result);
+ }
+ }
+ };
+
+ xhttp.open("PUT", endpoint, true);
+ xhttp.setRequestHeader('Ocp-Apim-Subscription-Key', apiKey);
+ xhttp.setRequestHeader('Content-Type', 'application/json');
+ xhttp.send(body);
+ };
+
+ return new Promise<any>(promisified);
+ },
+
+ analyzer: async (target: Doc, keys: string[], inkData: InkData) => {
+ let batch = UndoManager.StartBatch("Ink Analysis");
+
+ let results = await ExecuteQuery<InkData, any>(Service.Handwriting, Manager, inkData);
+ if (results) {
+ results.recognitionUnits && (results = results.recognitionUnits);
+ target[keys[0]] = Docs.Get.DocumentHierarchyFromJson(results, "Ink Analysis");
+ let recognizedText = results.map((item: any) => item.recognizedText);
+ let individualWords = recognizedText.filter((text: string) => text && text.split(" ").length === 1);
+ target[keys[1]] = individualWords.join(" ");
+ }
+
+ batch.end();
+ }
+
+ };
+
+ export interface AzureStrokeData {
+ id: number;
+ points: string;
+ language?: string;
+ }
+
+ export interface HandwritingUnit {
+ version: number;
+ language: string;
+ unit: string;
+ strokes: AzureStrokeData[];
+ }
+
+ }
+
+} \ No newline at end of file
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index af2b95659..e804d5440 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -21,8 +21,8 @@ import { AggregateFunction } from "../northstar/model/idea/idea";
import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss";
import { IconBox } from "../views/nodes/IconBox";
import { Field, Doc, Opt } from "../../new_fields/Doc";
-import { OmitKeys } from "../../Utils";
-import { ImageField, VideoField, AudioField, PdfField, WebField } from "../../new_fields/URLField";
+import { OmitKeys, JSONUtils } 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, StrCast, ToConstructor, InterfaceValue, FieldValue } from "../../new_fields/Types";
@@ -33,11 +33,16 @@ import { dropActionType } from "../util/DragManager";
import { DateField } from "../../new_fields/DateField";
import { UndoManager } from "../util/UndoManager";
import { RouteStore } from "../../server/RouteStore";
+import { YoutubeBox } from "../apis/youtube/YoutubeBox";
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";
+import { Scripting, CompileScript } from "../util/Scripting";
+import { ButtonBox } from "../views/nodes/ButtonBox";
+import { SchemaHeaderField, RandomPastel } from "../../new_fields/SchemaHeaderField";
+import { ComputedField } from "../../new_fields/ScriptField";
+import { ProxyField } from "../../new_fields/Proxy";
var requestImageSize = require('../util/request-image-size');
var path = require('path');
@@ -55,7 +60,11 @@ export enum DocumentType {
ICON = "icon",
IMPORT = "import",
LINK = "link",
- LINKDOC = "linkdoc"
+ LINKDOC = "linkdoc",
+ BUTTON = "button",
+ TEMPLATE = "template",
+ EXTENSION = "extension",
+ YOUTUBE = "youtube",
}
export interface DocumentOptions {
@@ -75,12 +84,14 @@ export interface DocumentOptions {
templates?: List<string>;
viewType?: number;
backgroundColor?: string;
+ defaultBackgroundColor?: string;
dropAction?: dropActionType;
backgroundLayout?: string;
+ chromeStatus?: string;
curPage?: number;
documentText?: string;
borderRounding?: string;
- schemaColumns?: List<string>;
+ schemaColumns?: List<SchemaHeaderField>;
dockingConfig?: string;
dbDoc?: Doc;
// [key: string]: Opt<Field>;
@@ -114,7 +125,7 @@ export namespace Docs {
const TemplateMap: TemplateMap = new Map([
[DocumentType.TEXT, {
layout: { view: FormattedTextBox },
- options: { height: 150, backgroundColor: "#f1efeb" }
+ options: { height: 150, backgroundColor: "#f1efeb", defaultBackgroundColor: "#f1efeb" }
}],
[DocumentType.HIST, {
layout: { view: HistogramBox, collectionView: [CollectionView, data] as CollectionViewType },
@@ -160,6 +171,12 @@ export namespace Docs {
data: new List<Doc>(),
layout: { view: EmptyBox },
options: {}
+ }],
+ [DocumentType.YOUTUBE, {
+ layout: { view: YoutubeBox }
+ }],
+ [DocumentType.BUTTON, {
+ layout: { view: ButtonBox },
}]
]);
@@ -179,6 +196,8 @@ export namespace Docs {
* haven't been initialized, the newly initialized prototype document.
*/
export async function initialize(): Promise<void> {
+ ProxyField.initPlugin();
+ ComputedField.initPlugin();
// 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
@@ -275,7 +294,7 @@ export namespace Docs {
* 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) {
+ export function InstanceFromProto(proto: Doc, data: Field | undefined, options: DocumentOptions, delegId?: string) {
const { omit: protoProps, extract: delegateProps } = OmitKeys(options, delegateKeys);
if (!("author" in protoProps)) {
@@ -304,15 +323,18 @@ export namespace Docs {
* @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) {
+ function MakeDataDelegate<D extends Field>(proto: Doc, options: DocumentOptions, value?: D) {
const deleg = Doc.MakeDelegate(proto);
- deleg.data = value;
+ if (value !== undefined) {
+ deleg.data = value;
+ }
return Doc.assign(deleg, options);
}
export function ImageDocument(url: string, options: DocumentOptions = {}) {
- let inst = InstanceFromProto(Prototypes.get(DocumentType.IMG), new ImageField(new URL(url)), { title: path.basename(url), ...options });
- requestImageSize(window.origin + RouteStore.corsProxy + "/" + url)
+ let imgField = new ImageField(new URL(url));
+ let inst = InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: path.basename(url), ...options });
+ requestImageSize(imgField.url.href)
.then((size: any) => {
let aspect = size.height / size.width;
if (!inst.proto!.nativeWidth) {
@@ -329,6 +351,10 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.VID), new VideoField(new URL(url)), options);
}
+ export function YoutubeDocument(url: string, options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.YOUTUBE), 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);
}
@@ -393,19 +419,27 @@ export namespace Docs {
}
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 });
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...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 SchemaDocument(schemaColumns: SchemaHeaderField[], documents: Array<Doc>, options: DocumentOptions) {
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", 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 });
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...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 });
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Stacking });
+ }
+
+ export function MasonryDocument(documents: Array<Doc>, options: DocumentOptions) {
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Masonry });
+ }
+
+ export function ButtonDocument(options?: DocumentOptions) {
+ return InstanceFromProto(Prototypes.get(DocumentType.BUTTON), undefined, { ...(options || {}) });
}
export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) {
@@ -438,6 +472,85 @@ export namespace Docs {
export namespace Get {
+ const primitives = ["string", "number", "boolean"];
+
+ /**
+ * This function takes any valid JSON(-like) data, i.e. parsed or unparsed, and at arbitrarily
+ * deep levels of nesting, converts the data and structure into nested documents with the appropriate fields.
+ *
+ * After building a hierarchy within / below a top-level document, it then returns that top-level parent.
+ *
+ * If we've received a string, treat it like valid JSON and try to parse it into an object. If this fails, the
+ * string is invalid JSON, so we should assume that the input is the result of a JSON.parse()
+ * call that returned a regular string value to be stored as a Field.
+ *
+ * If we've received something other than a string, since the caller might also pass in the results of a
+ * JSON.parse() call, valid input might be an object, an array (still typeof object), a boolean or a number.
+ * Anything else (like a function, etc. passed in naively as any) is meaningless for this operation.
+ *
+ * All TS/JS objects get converted directly to documents, directly preserving the key value structure. Everything else,
+ * lacking the key value structure, gets stored as a field in a wrapper document.
+ *
+ * @param input for convenience and flexibility, either a valid JSON string to be parsed,
+ * or the result of any JSON.parse() call.
+ * @param title an optional title to give to the highest parent document in the hierarchy
+ */
+ export function DocumentHierarchyFromJson(input: any, title?: string): Opt<Doc> {
+ if (input === null || ![...primitives, "object"].includes(typeof input)) {
+ return undefined;
+ }
+ let parsed: any = typeof input === "string" ? JSONUtils.tryParse(input) : input;
+ let converted: Doc;
+ if (typeof parsed === "object" && !(parsed instanceof Array)) {
+ converted = convertObject(parsed, title);
+ } else {
+ (converted = new Doc).json = toField(parsed);
+ }
+ title && (converted.title = title);
+ return converted;
+ }
+
+ /**
+ * For each value of the object, recursively convert it to its appropriate field value
+ * and store the field at the appropriate key in the document if it is not undefined
+ * @param object the object to convert
+ * @returns the object mapped from JSON to field values, where each mapping
+ * might involve arbitrary recursion (since toField might itself call convertObject)
+ */
+ const convertObject = (object: any, title?: string): Doc => {
+ let target = new Doc(), result: Opt<Field>;
+ Object.keys(object).map(key => (result = toField(object[key], key)) && (target[key] = result));
+ title && !target.title && (target.title = title);
+ return target;
+ };
+
+ /**
+ * For each element in the list, recursively convert it to a document or other field
+ * and push the field to the list if it is not undefined
+ * @param list the list to convert
+ * @returns the list mapped from JSON to field values, where each mapping
+ * might involve arbitrary recursion (since toField might itself call convertList)
+ */
+ const convertList = (list: Array<any>): List<Field> => {
+ let target = new List(), result: Opt<Field>;
+ list.map(item => (result = toField(item)) && target.push(result));
+ return target;
+ };
+
+
+ const toField = (data: any, title?: string): Opt<Field> => {
+ if (data === null || data === undefined) {
+ return undefined;
+ }
+ if (primitives.includes(typeof data)) {
+ return data;
+ }
+ if (typeof data === "object") {
+ return data instanceof Array ? convertList(data) : convertObject(data, title);
+ }
+ throw new Error(`How did ${data} of type ${typeof data} end up in JSON?`);
+ };
+
export async function DocumentFromType(type: string, path: string, options: DocumentOptions): Promise<Opt<Doc>> {
let ctor: ((path: string, options: DocumentOptions) => (Doc | Promise<Doc | undefined>)) | undefined = undefined;
if (type.indexOf("image") !== -1) {
@@ -484,16 +597,14 @@ export namespace Docs {
export namespace DocUtils {
export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", tags: string = "Default", sourceContext?: Doc) {
- if (LinkManager.Instance.doesLinkExist(source, target)) return;
+ if (LinkManager.Instance.doesLinkExist(source, target)) return undefined;
let sv = DocumentManager.Instance.getDocumentView(source);
if (sv && sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document === target) return;
- if (target === CurrentUserUtils.UserDocument) return;
+ if (target === CurrentUserUtils.UserDocument) return undefined;
- let linkDoc;
+ let linkDocProto = new Doc();
UndoManager.RunInBatch(() => {
- linkDoc = Docs.Create.TextDocument({ width: 100, height: 30, borderRounding: "100%" });
- linkDoc.type = DocumentType.LINK;
- let linkDocProto = Doc.GetProto(linkDoc);
+ linkDocProto.type = DocumentType.LINK;
linkDocProto.targetContext = targetContext;
linkDocProto.sourceContext = sourceContext;
@@ -509,10 +620,14 @@ export namespace DocUtils {
linkDocProto.anchor2Page = target.curPage;
linkDocProto.anchor2Groups = new List<Doc>([]);
- LinkManager.Instance.addLink(linkDoc);
+ LinkManager.Instance.addLink(linkDocProto);
+ let script = `return links(this)};`;
+ let computed = CompileScript(script, { params: { this: "Doc" }, typecheck: false });
+ computed.compiled && (Doc.GetProto(source).links = new ComputedField(computed));
+ computed.compiled && (Doc.GetProto(target).links = new ComputedField(computed));
}, "make link");
- return linkDoc;
+ return linkDocProto;
}
}
diff --git a/src/client/goldenLayout.js b/src/client/goldenLayout.js
index 28c009645..ad78139c1 100644
--- a/src/client/goldenLayout.js
+++ b/src/client/goldenLayout.js
@@ -3995,9 +3995,11 @@
lm.items.AbstractContentItem.prototype.removeChild.call(this, contentItem, keepChild);
if (this.contentItems.length === 1 && this.config.isClosable === true) {
- childItem = this.contentItems[0];
- this.contentItems = [];
- this.parent.replaceChild(this, childItem, true);
+ // bcz: this has the effect of removing children from the DOM and then re-adding them above where they were before.
+ // in the case of things like an iFrame with a YouTube video, the video will reload for now reason. So let's try leaving these "empty" rows alone.
+ // childItem = this.contentItems[0];
+ // this.contentItems = [];
+ // this.parent.replaceChild(this, childItem, true);
} else {
this.callDownwards('setSize');
this.emitBubblingEvent('stateChanged');
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
new file mode 100644
index 000000000..b58bdb6c7
--- /dev/null
+++ b/src/client/util/DictationManager.ts
@@ -0,0 +1,39 @@
+namespace CORE {
+ export interface IWindow extends Window {
+ webkitSpeechRecognition: any;
+ }
+}
+
+const { webkitSpeechRecognition }: CORE.IWindow = window as CORE.IWindow;
+
+export default class DictationManager {
+ public static Instance = new DictationManager();
+ private isListening = false;
+ private recognizer: any;
+
+ constructor() {
+ this.recognizer = new webkitSpeechRecognition();
+ this.recognizer.interimResults = false;
+ this.recognizer.continuous = true;
+ }
+
+ finish = (handler: any, data: any) => {
+ handler(data);
+ this.isListening = false;
+ this.recognizer.stop();
+ }
+
+ listen = () => {
+ if (this.isListening) {
+ return undefined;
+ }
+ this.isListening = true;
+ this.recognizer.start();
+ return new Promise<string>((resolve, reject) => {
+ this.recognizer.onresult = (e: any) => this.finish(resolve, e.results[0][0].transcript);
+ this.recognizer.onerror = (e: any) => this.finish(reject, e);
+ });
+
+ }
+
+} \ No newline at end of file
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 262194a40..32f728c71 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,16 +1,14 @@
-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, StrCast } from '../../new_fields/Types';
-import { listSpec } from '../../new_fields/Schema';
-import { undoBatch, UndoManager } from './UndoManager';
+import { action, computed, observable } from 'mobx';
+import { Doc } from '../../new_fields/Doc';
+import { Id } from '../../new_fields/FieldSymbols';
+import { BoolCast, Cast, NumCast } from '../../new_fields/Types';
import { CollectionDockingView } from '../views/collections/CollectionDockingView';
-import { CollectionView } from '../views/collections/CollectionView';
import { CollectionPDFView } from '../views/collections/CollectionPDFView';
import { CollectionVideoView } from '../views/collections/CollectionVideoView';
-import { Id } from '../../new_fields/FieldSymbols';
+import { CollectionView } from '../views/collections/CollectionView';
+import { DocumentView } from '../views/nodes/DocumentView';
import { LinkManager } from './LinkManager';
-import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
+import { undoBatch, UndoManager } from './UndoManager';
export class DocumentManager {
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 323908302..a7aaaed7c 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -1,6 +1,6 @@
import { action, runInAction } from "mobx";
import { Doc } from "../../new_fields/Doc";
-import { Cast } from "../../new_fields/Types";
+import { Cast, StrCast } from "../../new_fields/Types";
import { URLField } from "../../new_fields/URLField";
import { emptyFunction } from "../../Utils";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
@@ -8,6 +8,9 @@ import * as globalCssVariables from "../views/globalCssVariables.scss";
import { DocumentManager } from "./DocumentManager";
import { LinkManager } from "./LinkManager";
import { SelectionManager } from "./SelectionManager";
+import { SchemaHeaderField } from "../../new_fields/SchemaHeaderField";
+import { DocumentDecorations } from "../views/DocumentDecorations";
+import { NumberLiteralType } from "typescript";
export type dropActionType = "alias" | "copy" | undefined;
export function SetupDrag(
@@ -138,6 +141,10 @@ export namespace DragManager {
dragHasStarted?: () => void;
withoutShiftDrag?: boolean;
+
+ offsetX?: number;
+
+ offsetY?: number;
}
export interface DragDropDisposer {
@@ -214,6 +221,7 @@ export namespace DragManager {
this.annotationDocument = annotationDoc;
this.xOffset = this.yOffset = 0;
}
+ targetContext: Doc | undefined;
dragDocument: Doc;
annotationDocument: Doc;
dropDocument: Doc;
@@ -288,6 +296,15 @@ export namespace DragManager {
[id: string]: any;
}
+ // for column dragging in schema view
+ export class ColumnDragData {
+ constructor(colKey: SchemaHeaderField) {
+ this.colKey = colKey;
+ }
+ colKey: SchemaHeaderField;
+ [id: string]: any;
+ }
+
export function StartLinkDrag(ele: HTMLElement, dragData: LinkDragData, downX: number, downY: number, options?: DragOptions) {
StartDrag([ele], dragData, downX, downY, options);
}
@@ -296,6 +313,10 @@ export namespace DragManager {
StartDrag([ele], dragData, downX, downY, options);
}
+ export function StartColumnDrag(ele: HTMLElement, dragData: ColumnDragData, 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) {
@@ -383,7 +404,8 @@ export namespace DragManager {
hideSource = options.hideSource();
}
}
- eles.map(ele => (ele.hidden = hideSource));
+ eles.map(ele => (ele.hidden = hideSource) &&
+ (ele.parentElement && ele.parentElement.className.indexOf("collectionFreeFormDocumentView") !== -1 && (ele.parentElement.hidden = hideSource)));
let lastX = downX;
let lastY = downY;
@@ -407,14 +429,16 @@ export namespace DragManager {
lastX = e.pageX;
lastY = e.pageY;
dragElements.map((dragElement, i) => (dragElement.style.transform =
- `translate(${(xs[i] += moveX)}px, ${(ys[i] += moveY)}px) scale(${scaleXs[i]}, ${scaleYs[i]})`)
+ `translate(${(xs[i] += moveX) + (options ? (options.offsetX || 0) : 0)}px, ${(ys[i] += moveY) + (options ? (options.offsetY || 0) : 0)}px) scale(${scaleXs[i]}, ${scaleYs[i]})`)
);
};
let hideDragElements = () => {
- SelectionManager.SetIsDragging(false);
dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement));
- eles.map(ele => (ele.hidden = false));
+ eles.map(ele => {
+ ele.hidden = false;
+ (ele.parentElement && ele.parentElement.className.indexOf("collectionFreeFormDocumentView") !== -1 && (ele.parentElement.hidden = false));
+ });
};
let endDrag = () => {
document.removeEventListener("pointermove", moveHandler, true);
@@ -426,11 +450,13 @@ export namespace DragManager {
AbortDrag = () => {
hideDragElements();
+ SelectionManager.SetIsDragging(false);
endDrag();
};
const upHandler = (e: PointerEvent) => {
hideDragElements();
dispatchDrag(eles, e, dragData, options, finishDrag);
+ SelectionManager.SetIsDragging(false);
endDrag();
};
document.addEventListener("pointermove", moveHandler, true);
diff --git a/src/client/util/History.ts b/src/client/util/History.ts
index cbf5b3fc8..e9ff21b22 100644
--- a/src/client/util/History.ts
+++ b/src/client/util/History.ts
@@ -129,7 +129,7 @@ export namespace HistoryUtil {
function addStringifier(type: string, keys: string[], customStringifier?: (state: ParsedUrl, current: string) => string) {
stringifiers[type] = state => {
- let path = DocServer.prepend(`/${type}`);
+ let path = Utils.prepend(`/${type}`);
if (customStringifier) {
path = customStringifier(state, path);
}
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index c096e9ceb..75b0b52a7 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx
@@ -98,12 +98,12 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
runInAction(() => this.remaining++);
- let prom = fetch(DocServer.prepend(RouteStore.upload), {
+ let prom = fetch(Utils.prepend(RouteStore.upload), {
method: 'POST',
body: formData
}).then(async (res: Response) => {
(await res.json()).map(action((file: any) => {
- let docPromise = Docs.Get.DocumentFromType(type, DocServer.prepend(file), { nativeWidth: 300, width: 300, title: dropFileName });
+ let docPromise = Docs.Get.DocumentFromType(type, Utils.prepend(file), { nativeWidth: 300, width: 300, title: dropFileName });
docPromise.then(doc => {
doc && docs.push(doc) && runInAction(() => this.remaining--);
});
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index a647f22c1..448a8e9cf 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -6,6 +6,7 @@ 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";
+import { Scripting } from "./Scripting";
/*
@@ -42,7 +43,12 @@ export class LinkManager {
}
public getAllLinks(): Doc[] {
- return LinkManager.Instance.LinkManagerDoc ? LinkManager.Instance.LinkManagerDoc.allLinks ? DocListCast(LinkManager.Instance.LinkManagerDoc.allLinks) : [] : [];
+ let ldoc = LinkManager.Instance.LinkManagerDoc;
+ if (ldoc) {
+ let docs = DocListCast(ldoc.allLinks);
+ return docs;
+ }
+ return [];
}
public addLink(linkDoc: Doc): boolean {
@@ -242,4 +248,7 @@ export class LinkManager {
return Cast(linkDoc.anchor1, Doc, null);
}
}
-} \ No newline at end of file
+}
+Scripting.addGlobal(function links(doc: any) {
+ return new List(LinkManager.Instance.getAllRelatedLinks(doc));
+});
diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index fa9e2e5af..c38f84551 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -47,6 +47,10 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
bind("Mod-i", toggleMark(type));
bind("Mod-I", toggleMark(type));
}
+ if (type = schema.marks.underline) {
+ bind("Mod-u", toggleMark(type));
+ bind("Mod-U", toggleMark(type));
+ }
if (type = schema.marks.code) {
bind("Mod-`", toggleMark(type));
}
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 269de0f42..ce9e29b26 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -283,7 +283,7 @@ export const marks: { [index: string]: MarkSpec } = {
},
highlight: {
- parseDOM: [{ style: 'background: #d9dbdd' }],
+ parseDOM: [{ style: 'color: blue' }],
toDOM() {
return ['span', {
style: 'color: blue'
@@ -291,6 +291,15 @@ export const marks: { [index: string]: MarkSpec } = {
}
},
+ search_highlight: {
+ parseDOM: [{ style: 'background: yellow' }],
+ toDOM() {
+ return ['span', {
+ style: 'background: yellow'
+ }];
+ }
+ },
+
// :: MarkSpec Code font mark. Represented as a `<code>` element.
code: {
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index 62c2cfe85..1d0916ac0 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -1,5 +1,7 @@
-// import * as ts from "typescript"
-let ts = (window as any).ts;
+import * as ts from "typescript";
+export { ts };
+// export const ts = (window as any).ts;
+
// // @ts-ignore
// import * as typescriptlib from '!!raw-loader!../../../node_modules/typescript/lib/lib.d.ts'
// // @ts-ignore
@@ -50,10 +52,31 @@ export namespace Scripting {
} else {
throw new Error("Must either register an object with a name, or give a name and an object");
}
- if (scriptingGlobals.hasOwnProperty(n)) {
+ if (_scriptingGlobals.hasOwnProperty(n)) {
throw new Error(`Global with name ${n} is already registered, choose another name`);
}
- scriptingGlobals[n] = obj;
+ _scriptingGlobals[n] = obj;
+ }
+
+ export function makeMutableGlobalsCopy(globals?: { [name: string]: any }) {
+ return { ..._scriptingGlobals, ...(globals || {}) };
+ }
+
+ export function setScriptingGlobals(globals: { [key: string]: any }) {
+ scriptingGlobals = globals;
+ }
+
+ export function resetScriptingGlobals() {
+ scriptingGlobals = _scriptingGlobals;
+ }
+
+ // const types = Object.keys(ts.SyntaxKind).map(kind => ts.SyntaxKind[kind]);
+ export function printNodeType(node: any, indentation = "") {
+ console.log(indentation + ts.SyntaxKind[node.kind]);
+ }
+
+ export function getGlobals() {
+ return Object.keys(scriptingGlobals);
}
}
@@ -61,7 +84,8 @@ export function scriptingGlobal(constructor: { new(...args: any[]): any }) {
Scripting.addGlobal(constructor);
}
-const scriptingGlobals: { [name: string]: any } = {};
+const _scriptingGlobals: { [name: string]: any } = {};
+let scriptingGlobals: { [name: string]: any } = _scriptingGlobals;
function Run(script: string | undefined, customParams: string[], diagnostics: any[], originalScript: string, options: ScriptOptions): CompileResult {
const errors = diagnostics.some(diag => diag.category === ts.DiagnosticCategory.Error);
@@ -162,6 +186,12 @@ class ScriptingCompilerHost {
}
}
+export type Traverser = (node: ts.Node, indentation: string) => boolean | void;
+export type TraverserParam = Traverser | { onEnter: Traverser, onLeave: Traverser };
+export type Transformer = {
+ transformer: ts.TransformerFactory<ts.SourceFile>,
+ getVars?: () => { capturedVariables: { [name: string]: Field } }
+};
export interface ScriptOptions {
requiredType?: string;
addReturn?: boolean;
@@ -169,11 +199,45 @@ export interface ScriptOptions {
capturedVariables?: { [name: string]: Field };
typecheck?: boolean;
editable?: boolean;
+ traverser?: TraverserParam;
+ transformer?: Transformer;
+ globals?: { [name: string]: any };
+}
+
+// function forEachNode(node:ts.Node, fn:(node:any) => void);
+function forEachNode(node: ts.Node, onEnter: Traverser, onExit?: Traverser, indentation = "") {
+ return onEnter(node, indentation) || ts.forEachChild(node, (n: any) => {
+ forEachNode(n, onEnter, onExit, indentation + " ");
+ }) || (onExit && onExit(node, indentation));
}
export function CompileScript(script: string, options: ScriptOptions = {}): CompileResult {
const { requiredType = "", addReturn = false, params = {}, capturedVariables = {}, typecheck = true } = options;
+ if (options.globals) {
+ Scripting.setScriptingGlobals(options.globals);
+ }
let host = new ScriptingCompilerHost;
+ if (options.traverser) {
+ const sourceFile = ts.createSourceFile('script.ts', script, ts.ScriptTarget.ES2015, true);
+ const onEnter = typeof options.traverser === "object" ? options.traverser.onEnter : options.traverser;
+ const onLeave = typeof options.traverser === "object" ? options.traverser.onLeave : undefined;
+ forEachNode(sourceFile, onEnter, onLeave);
+ }
+ if (options.transformer) {
+ const sourceFile = ts.createSourceFile('script.ts', script, ts.ScriptTarget.ES2015, true);
+ const result = ts.transform(sourceFile, [options.transformer.transformer]);
+ if (options.transformer.getVars) {
+ const newCaptures = options.transformer.getVars();
+ // tslint:disable-next-line: prefer-object-spread
+ options.capturedVariables = Object.assign(capturedVariables, newCaptures.capturedVariables) as any;
+ }
+ const transformed = result.transformed;
+ const printer = ts.createPrinter({
+ newLine: ts.NewLineKind.LineFeed
+ });
+ script = printer.printFile(transformed[0]);
+ result.dispose();
+ }
let paramNames: string[] = [];
if ("this" in params || "this" in capturedVariables) {
paramNames.push("this");
@@ -188,14 +252,16 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
});
for (const key in capturedVariables) {
if (key === "this") continue;
+ const val = capturedVariables[key];
paramNames.push(key);
- paramList.push(`${key}: ${capturedVariables[key].constructor.name}`);
+ paramList.push(`${key}: ${typeof val === "object" ? Object.getPrototypeOf(val).constructor.name : typeof val}`);
}
let paramString = paramList.join(", ");
let funcScript = `(function(${paramString})${requiredType ? `: ${requiredType}` : ''} {
${addReturn ? `return ${script};` : script}
})`;
host.writeFile("file.ts", funcScript);
+
if (typecheck) host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib);
let program = ts.createProgram(["file.ts"], {}, host);
let testResult = program.emit();
@@ -203,7 +269,12 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
let diagnostics = ts.getPreEmitDiagnostics(program).concat(testResult.diagnostics);
- return Run(outputText, paramNames, diagnostics, script, options);
+ const result = Run(outputText, paramNames, diagnostics, script, options);
+
+ if (options.globals) {
+ Scripting.resetScriptingGlobals();
+ }
+ return result;
}
Scripting.addGlobal(CompileScript); \ No newline at end of file
diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts
index abf1a7c32..ee5a83710 100644
--- a/src/client/util/SearchUtil.ts
+++ b/src/client/util/SearchUtil.ts
@@ -2,6 +2,7 @@ import * as rp from 'request-promise';
import { DocServer } from '../DocServer';
import { Doc } from '../../new_fields/Doc';
import { Id } from '../../new_fields/FieldSymbols';
+import { Utils } from '../../Utils';
export namespace SearchUtil {
export type HighlightingResult = { [id: string]: { [key: string]: string[] } };
@@ -29,7 +30,7 @@ export namespace SearchUtil {
export function Search(query: string, returnDocs: false, options?: SearchParams): Promise<IdSearchResult>;
export async function Search(query: string, returnDocs: boolean, options: SearchParams = {}) {
query = query || "*"; //If we just have a filter query, search for * as the query
- const result: IdSearchResult = JSON.parse(await rp.get(DocServer.prepend("/search"), {
+ const result: IdSearchResult = JSON.parse(await rp.get(Utils.prepend("/search"), {
qs: { ...options, q: query },
}));
if (!returnDocs) {
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 9efef888d..ee623d082 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -4,6 +4,7 @@ import { DocumentView } from "../views/nodes/DocumentView";
import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
import { NumCast, StrCast } from "../../new_fields/Types";
import { InkingControl } from "../views/InkingControl";
+import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
export namespace SelectionManager {
diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts
index dca539f3b..ff048f647 100644
--- a/src/client/util/SerializationHelper.ts
+++ b/src/client/util/SerializationHelper.ts
@@ -1,9 +1,15 @@
import { PropSchema, serialize, deserialize, custom, setDefaultModelSchema, getDefaultModelSchema, primitive, SKIP } from "serializr";
-import { Field } from "../../new_fields/Doc";
+import { Field, Doc } from "../../new_fields/Doc";
import { ClientUtils } from "./ClientUtils";
+import { emptyFunction } from "../../Utils";
+let serializing = 0;
+export function afterDocDeserialize(cb: (err: any, val: any) => void, err: any, newValue: any) {
+ serializing++;
+ cb(err, newValue);
+ serializing--;
+}
export namespace SerializationHelper {
- let serializing: number = 0;
export function IsSerializing() {
return serializing > 0;
}
@@ -17,18 +23,18 @@ export namespace SerializationHelper {
return obj;
}
- serializing += 1;
+ serializing++;
if (!(obj.constructor.name in reverseMap)) {
throw Error(`type '${obj.constructor.name}' not registered. Make sure you register it using a @Deserializable decorator`);
}
const json = serialize(obj);
json.__type = reverseMap[obj.constructor.name];
- serializing -= 1;
+ serializing--;
return json;
}
- export function Deserialize(obj: any): any {
+ export async function Deserialize(obj: any): Promise<any> {
if (obj === undefined || obj === null) {
return undefined;
}
@@ -37,7 +43,6 @@ export namespace SerializationHelper {
return obj;
}
- serializing += 1;
if (!obj.__type) {
if (ClientUtils.RELEASE) {
console.warn("No property 'type' found in JSON.");
@@ -52,16 +57,15 @@ export namespace SerializationHelper {
}
const type = serializationTypes[obj.__type];
- const value = deserialize(type.ctor, obj);
+ const value = await new Promise(res => deserialize(type.ctor, obj, (err, result) => res(result)));
if (type.afterDeserialize) {
- type.afterDeserialize(value);
+ await type.afterDeserialize(value);
}
- serializing -= 1;
return value;
}
}
-let serializationTypes: { [name: string]: { ctor: { new(): any }, afterDeserialize?: (obj: any) => void } } = {};
+let serializationTypes: { [name: string]: { ctor: { new(): any }, afterDeserialize?: (obj: any) => void | Promise<any> } } = {};
let reverseMap: { [ctor: string]: string } = {};
export interface DeserializableOpts {
@@ -69,7 +73,7 @@ export interface DeserializableOpts {
withFields(fields: string[]): Function;
}
-export function Deserializable(name: string, afterDeserialize?: (obj: any) => void): DeserializableOpts;
+export function Deserializable(name: string, afterDeserialize?: (obj: any) => void | Promise<any>): DeserializableOpts;
export function Deserializable(constructor: { new(...args: any[]): any }): 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 }) {
@@ -88,15 +92,15 @@ export function Deserializable(constructor: { new(...args: any[]): any } | strin
if (typeof constructor === "string") {
return Object.assign((ctor: { new(...args: any[]): any }) => {
addToMap(constructor, ctor);
- }, { withFields: Deserializable.withFields });
+ }, { withFields: (fields: string[]) => Deserializable.withFields(fields, name, afterDeserialize) });
}
addToMap(constructor.name, constructor);
}
export namespace Deserializable {
- export function withFields(fields: string[]) {
+ export function withFields(fields: string[], name?: string, afterDeserialize?: (obj: any) => void | Promise<any>) {
return function (constructor: { new(...fields: any[]): any }) {
- Deserializable(constructor);
+ Deserializable(name || constructor.name, afterDeserialize)(constructor);
let schema = getDefaultModelSchema(constructor);
if (schema) {
schema.factory = context => {
@@ -135,6 +139,6 @@ export namespace Deserializable {
export function autoObject(): PropSchema {
return custom(
(s) => SerializationHelper.Serialize(s),
- (s) => SerializationHelper.Deserialize(s)
+ (json: any, context: any, oldValue: any, cb: (err: any, result: any) => void) => SerializationHelper.Deserialize(json).then(res => cb(null, res))
);
} \ No newline at end of file
diff --git a/src/client/util/TooltipTextMenu.scss b/src/client/util/TooltipTextMenu.scss
index 40ac3abb9..ebf833dbe 100644
--- a/src/client/util/TooltipTextMenu.scss
+++ b/src/client/util/TooltipTextMenu.scss
@@ -18,7 +18,8 @@
.ProseMirror-menuitem {
margin-right: 3px;
display: inline-block;
- z-index: 100000;
+ z-index: 50000;
+ position: relative;
}
.ProseMirror-menuseparator {
@@ -67,7 +68,7 @@
}
.ProseMirror-menu-dropdown-menu {
- z-index: 100000;
+ z-index: 50000;
min-width: 6em;
background: white;
position: absolute;
@@ -235,8 +236,8 @@
}
.tooltipMenu {
- position: relative;
- z-index: 2000;
+ position: absolute;
+ z-index: 20000;
background: #121721;
border: 1px solid silver;
border-radius: 15px;
@@ -247,7 +248,7 @@
//transform: translateX(-50%);
transform: translateY(-85px);
pointer-events: all;
- height: 30px;
+ height: fit-content;
width:550px;
.ProseMirror-example-setup-style hr {
padding: 2px 10px;
@@ -264,28 +265,40 @@
}
}
-// .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;
-// }
+.tooltipExtras {
+ position: absolute;
+ z-index: 20000;
+ background: #121721;
+ border: 1px solid silver;
+ border-radius: 15px;
+ //height: 60px;
+ //padding: 2px 10px;
+ //margin-top: 100px;
+ //-webkit-transform: translateX(-50%);
+ //transform: translateX(-50%);
+ transform: translateY(-115px);
+ pointer-events: all;
+ height: 25px;
+ width:fit-content;
+ .ProseMirror-example-setup-style hr {
+ padding: 2px 10px;
+ border: none;
+ margin: 1em 0;
+ }
+
+ .ProseMirror-example-setup-style hr:after {
+ content: "";
+ display: block;
+ height: 1px;
+ background-color: silver;
+ line-height: 2px;
+ }
+}
+
+.wrapper {
+ position: absolute;
+ pointer-events: all;
+}
.menuicon {
display: inline-block;
@@ -298,6 +311,7 @@
cursor: pointer;
text-align: center;
min-width: 10px;
+
}
.strong, .heading { font-weight: bold; }
.em { font-style: italic; }
@@ -310,9 +324,32 @@
padding-right: 0px;
}
.summarize{
- //margin-left: 15px;
color: white;
height: 20px;
- // background-color: white;
text-align: center;
+ }
+
+ .brush{
+ display: inline-block;
+ width: 1em;
+ height: 1em;
+ stroke-width: 0;
+ stroke: currentColor;
+ fill: currentColor;
+ margin-right: 15px;
+ }
+
+ .brush-active{
+ display: inline-block;
+ width: 1em;
+ height: 1em;
+ stroke-width: 3;
+ stroke: greenyellow;
+ fill: greenyellow;
+ margin-right: 15px;
+ }
+
+ .dragger{
+ color: #eee;
+ margin-left: 5px;
} \ No newline at end of file
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 309d19016..d33a52d7f 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -1,6 +1,8 @@
-import { action } from "mobx";
+import { action, observable, observe } from "mobx";
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faTag, faPlus, faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons';
import { Dropdown, MenuItem, icons, } from "prosemirror-menu"; //no import css
-import { EditorState, NodeSelection, TextSelection } from "prosemirror-state";
+import { EditorState, NodeSelection, TextSelection, Transaction } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { schema } from "./RichTextSchema";
import { Schema, NodeType, MarkType, Mark, ResolvedPos } from "prosemirror-model";
@@ -18,8 +20,13 @@ import { DocServer } from "../DocServer";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
import { DocumentManager } from "./DocumentManager";
import { Id } from "../../new_fields/FieldSymbols";
-import { FormattedTextBoxProps } from "../views/nodes/FormattedTextBox";
-import { SelectionManager } from "./SelectionManager";
+import { FormattedTextBoxProps, FormattedTextBox } from "../views/nodes/FormattedTextBox";
+import { typeAlias } from "babel-types";
+import React, { Children } from "react";
+import ReactDOM from "react-dom";
+import { Utils } from "../../Utils";
+import { LinkManager } from "./LinkManager";
+import { bool } from "prop-types";
//appears above a selection of text in a RichTextBox to give user options such as Bold, Italics, etc.
export class TooltipTextMenu {
@@ -33,6 +40,9 @@ export class TooltipTextMenu {
private fontSizeToNum: Map<MarkType, number>;
private fontStylesToName: Map<MarkType, string>;
private listTypeToIcon: Map<NodeType, string>;
+ //private link: HTMLAnchorElement;
+ private wrapper: HTMLDivElement;
+ private extras: HTMLDivElement;
private linkEditor?: HTMLDivElement;
private linkText?: HTMLDivElement;
@@ -46,13 +56,42 @@ export class TooltipTextMenu {
private _collapseBtn?: MenuItem;
+ private _brushMarks?: Set<Mark>;
+ private _brushIsEmpty: boolean = true;
+ private _brushdom?: Node;
+
+ private _marksToDoms: Map<Mark, HTMLSpanElement> = new Map();
+
+ private _collapsed: boolean = false;
+
+ @observable
+ private _storedMarks: Mark<any>[] | null | undefined;
+
+
constructor(view: EditorView, editorProps: FieldViewProps & FormattedTextBoxProps) {
this.view = view;
this.editorProps = editorProps;
+
+ this.wrapper = document.createElement("div");
this.tooltip = document.createElement("div");
+ this.extras = document.createElement("div");
+
+ this.wrapper.appendChild(this.extras);
+ this.wrapper.appendChild(this.tooltip);
+
this.tooltip.className = "tooltipMenu";
+ this.extras.className = "tooltipExtras";
+ this.wrapper.className = "wrapper";
+
+ const dragger = document.createElement("span");
+ dragger.className = "dragger";
+ dragger.textContent = ">>>";
+ this.extras.appendChild(dragger);
+
+ this.dragElement(dragger);
+
+ this._storedMarks = this.view.state.storedMarks;
- this.dragElement(this.tooltip);
// this.createCollapse();
// if (this._collapseBtn) {
// this.tooltip.appendChild(this._collapseBtn.render(this.view).dom);
@@ -71,13 +110,23 @@ export class TooltipTextMenu {
{ 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") },
];
+
+ this._marksToDoms = new Map();
//add menu items
items.forEach(({ dom, command }) => {
this.tooltip.appendChild(dom);
+ switch (dom.title) {
+ case "Bold":
+ this._marksToDoms.set(schema.mark(schema.marks.strong), dom);
+ break;
+ case "Italic":
+ this._marksToDoms.set(schema.mark(schema.marks.em), dom);
+ break;
+ case "Underline":
+ this._marksToDoms.set(schema.mark(schema.marks.underline), dom);
+ break;
+ }
//pointer down handler to activate button effects
dom.addEventListener("pointerdown", e => {
@@ -86,12 +135,17 @@ export class TooltipTextMenu {
if (dom.contains(e.target as Node)) {
e.stopPropagation();
command(view.state, view.dispatch, view);
+ // if (this.view.state.selection.empty) {
+ // if (dom.style.color === "white") { dom.style.color = "greenyellow"; }
+ // else { dom.style.color = "white"; }
+ // }
}
});
});
this.updateLinkMenu();
+
//list of font styles
this.fontStylesToName = new Map();
this.fontStylesToName.set(schema.marks.timesNewRoman, "Times New Roman");
@@ -125,21 +179,23 @@ export class TooltipTextMenu {
this.listTypeToIcon.set(schema.nodes.ordered_list, "1)");
this.listTypes = Array.from(this.listTypeToIcon.keys());
+ //custom tools
// this.tooltip.appendChild(this.createLink().render(this.view).dom);
+ this._brushdom = this.createBrush().render(this.view).dom;
+ this.tooltip.appendChild(this._brushdom);
+ 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
+ // add tooltip to outerdiv to circumvent scaling problem
const outer_div = this.editorProps.outer_div;
- outer_div && outer_div(this.tooltip);
+ outer_div && outer_div(this.wrapper);
}
//label of dropdown will change to given label
@@ -164,6 +220,8 @@ export class TooltipTextMenu {
this.fontSizeDom = newfontSizeDom;
}
+ // Make the DIV element draggable
+
//label of dropdown will change to given label
updateFontStyleDropdown(label: string) {
//filtering function - might be unecessary
@@ -213,8 +271,8 @@ export class TooltipTextMenu {
let link = node && node.marks.find(m => m.type.name === "link");
if (link) {
let href: string = link.attrs.href;
- if (href.indexOf(DocServer.prepend("/doc/")) === 0) {
- let docid = href.replace(DocServer.prepend("/doc/"), "");
+ if (href.indexOf(Utils.prepend("/doc/")) === 0) {
+ let docid = href.replace(Utils.prepend("/doc/"), "");
DocServer.GetRefField(docid).then(action((f: Opt<Field>) => {
if (f instanceof Doc) {
if (DocumentManager.Instance.getDocumentView(f)) {
@@ -254,11 +312,13 @@ export class TooltipTextMenu {
if (docView && docView.props.ContainingCollectionView) {
proto.sourceContext = docView.props.ContainingCollectionView.props.Document;
}
- linkDoc instanceof Doc && this.makeLink(DocServer.prepend("/doc/" + linkDoc[Id]), ctrlKey ? "onRight" : "inTab");
+ linkDoc instanceof Doc && this.makeLink(Utils.prepend("/doc/" + linkDoc[Id]), ctrlKey ? "onRight" : "inTab");
}),
},
hideSource: false
});
+ e.stopPropagation();
+ e.preventDefault();
};
this.linkEditor.appendChild(this.linkDrag);
// this.linkEditor.appendChild(this.linkText);
@@ -285,6 +345,7 @@ export class TooltipTextMenu {
if (elmnt) {
// if present, the header is where you move the DIV from:
elmnt.onpointerdown = dragMouseDown;
+ elmnt.ondblclick = onClick;
}
const self = this;
@@ -299,6 +360,17 @@ export class TooltipTextMenu {
document.onpointermove = elementDrag;
}
+ function onClick(e: MouseEvent) {
+ self._collapsed = !self._collapsed;
+ const children = self.wrapper.childNodes;
+ if (self._collapsed && children.length > 1) {
+ self.wrapper.removeChild(self.tooltip);
+ }
+ else {
+ self.wrapper.appendChild(self.tooltip);
+ }
+ }
+
function elementDrag(e: PointerEvent) {
e = e || window.event;
//e.preventDefault();
@@ -308,8 +380,11 @@ export class TooltipTextMenu {
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";
+ // elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
+ // elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
+
+ self.wrapper.style.top = (self.wrapper.offsetTop - pos2) + "px";
+ self.wrapper.style.left = (self.wrapper.offsetLeft - pos1) + "px";
}
function closeDragElement() {
@@ -321,6 +396,10 @@ export class TooltipTextMenu {
}
}
+ makeLinkWithState = (state: EditorState, target: string, location: string) => {
+ let link = state.schema.mark(state.schema.marks.link, { href: target, location: location });
+ }
+
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, location: location });
@@ -330,6 +409,27 @@ export class TooltipTextMenu {
link = node && node.marks.find(m => m.type.name === "link");
}
+ deleteLink = () => {
+ let node = this.view.state.selection.$from.nodeAfter;
+ let link = node && node.marks.find(m => m.type.name === "link");
+ let href = link!.attrs.href;
+ if (href) {
+ if (href.indexOf(Utils.prepend("/doc/")) === 0) {
+ const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+ if (linkclicked) {
+ DocServer.GetRefField(linkclicked).then(async linkDoc => {
+ if (linkDoc instanceof Doc) {
+ LinkManager.Instance.deleteLink(linkDoc);
+ this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.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) {
@@ -363,7 +463,7 @@ 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[]) => {
+ changeToMarkInGroup = (markType: MarkType | undefined, view: EditorView, fontMarks: MarkType[]) => {
let { $cursor, ranges } = view.state.selection as TextSelection;
let state = view.state;
let dispatch = view.dispatch;
@@ -389,17 +489,23 @@ export class TooltipTextMenu {
}
}
});
- // fontsize
- if (markType.name[0] === 'p') {
- let size = this.fontSizeToNum.get(markType);
- if (size) { this.updateFontSizeDropdown(String(size) + " pt"); }
+
+ if (markType) {
+ // 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);
}
else {
- let fontName = this.fontStylesToName.get(markType);
- if (fontName) { this.updateFontStyleDropdown(fontName); }
+ return;
}
- //actually apply font
- return toggleMark(markType)(view.state, view.dispatch, view);
}
//remove all node typeand apply the passed-in one to the selected text
@@ -442,6 +548,85 @@ export class TooltipTextMenu {
});
}
+ deleteLinkItem() {
+ const icon = {
+ height: 16, width: 16,
+ path: "M15.898,4.045c-0.271-0.272-0.713-0.272-0.986,0l-4.71,4.711L5.493,4.045c-0.272-0.272-0.714-0.272-0.986,0s-0.272,0.714,0,0.986l4.709,4.711l-4.71,4.711c-0.272,0.271-0.272,0.713,0,0.986c0.136,0.136,0.314,0.203,0.492,0.203c0.179,0,0.357-0.067,0.493-0.203l4.711-4.711l4.71,4.711c0.137,0.136,0.314,0.203,0.494,0.203c0.178,0,0.355-0.067,0.492-0.203c0.273-0.273,0.273-0.715,0-0.986l-4.711-4.711l4.711-4.711C16.172,4.759,16.172,4.317,15.898,4.045z"
+ };
+ return new MenuItem({
+ title: "Delete Link",
+ label: "X",
+ icon: icon,
+ css: "color: red",
+ class: "summarize",
+ execEvent: "",
+ run: (state, dispatch) => {
+ this.deleteLink();
+ }
+ });
+ }
+
+ createBrush(active: boolean = false) {
+ const icon = {
+ height: 32, width: 32,
+ path: "M30.828 1.172c-1.562-1.562-4.095-1.562-5.657 0l-5.379 5.379-3.793-3.793-4.243 4.243 3.326 3.326-14.754 14.754c-0.252 0.252-0.358 0.592-0.322 0.921h-0.008v5c0 0.552 0.448 1 1 1h5c0 0 0.083 0 0.125 0 0.288 0 0.576-0.11 0.795-0.329l14.754-14.754 3.326 3.326 4.243-4.243-3.793-3.793 5.379-5.379c1.562-1.562 1.562-4.095 0-5.657zM5.409 30h-3.409v-3.409l14.674-14.674 3.409 3.409-14.674 14.674z"
+ };
+ return new MenuItem({
+ title: "Brush tool",
+ label: "Brush tool",
+ icon: icon,
+ css: "color:white;",
+ class: active ? "brush-active" : "brush",
+ execEvent: "",
+ run: (state, dispatch) => {
+ this.brush_function(state, dispatch);
+ },
+ active: (state) => {
+ return true;
+ }
+ });
+ }
+
+ // selectionchanged event handler
+
+ brush_function(state: EditorState<any>, dispatch: any) {
+ if (this._brushIsEmpty) {
+ const selected_marks = this.getMarksInSelection(this.view.state);
+ if (this._brushdom) {
+ if (selected_marks.size >= 0) {
+ this._brushMarks = selected_marks;
+ const newbrush = this.createBrush(true).render(this.view).dom;
+ this.tooltip.replaceChild(newbrush, this._brushdom);
+ this._brushdom = newbrush;
+ this._brushIsEmpty = !this._brushIsEmpty;
+ }
+ }
+ }
+ else {
+ let { from, to, $from } = this.view.state.selection;
+ if (this._brushdom) {
+ if (!this.view.state.selection.empty && $from && $from.nodeAfter) {
+ if (this._brushMarks && to - from > 0) {
+ this.view.dispatch(this.view.state.tr.removeMark(from, to));
+ this._brushMarks.forEach((mark: Mark) => {
+ const markType = mark.type;
+ this.changeToMarkInGroup(markType, this.view, []);
+
+ });
+ }
+ }
+ else {
+ const newbrush = this.createBrush(false).render(this.view).dom;
+ this.tooltip.replaceChild(newbrush, this._brushdom);
+ this._brushdom = newbrush;
+ this._brushIsEmpty = !this._brushIsEmpty;
+ }
+ }
+ }
+
+
+ }
+
createCollapse() {
this._collapseBtn = new MenuItem({
title: "Collapse",
@@ -597,20 +782,29 @@ export class TooltipTextMenu {
};
}
- getMarksInSelection(state: EditorState<any>, targets: MarkType<any>[]) {
- let found: Mark<any>[] = [];
+ getMarksInSelection(state: EditorState<any>) {
+ let found = new Set<Mark>();
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);
+ found.add(m);
});
}
});
return found;
}
+ reset_mark_doms() {
+ let iterator = this._marksToDoms.values();
+ let next = iterator.next();
+ while (!next.done) {
+ next.value.style.color = "white";
+ next = iterator.next();
+ }
+ }
+
//updates the tooltip menu when the selection changes
update(view: EditorView, lastState: EditorState | undefined) {
let state = view.state;
@@ -618,13 +812,13 @@ export class TooltipTextMenu {
if (lastState && lastState.doc.eq(state.doc) &&
lastState.selection.eq(state.selection)) return;
+ this.reset_mark_doms();
+
// Hide the tooltip if the selection is empty
if (state.selection.empty) {
//this.tooltip.style.display = "none";
//return;
}
-
-
//UPDATE LIST ITEM DROPDOWN
//UPDATE FONT STYLE DROPDOWN
@@ -662,12 +856,55 @@ export class TooltipTextMenu {
}
}
this.view.dispatch(this.view.state.tr.setStoredMarks(this._activeMarks));
+
+ this.update_mark_doms();
+ }
+
+ public mark_key_pressed(marks: Mark<any>[]) {
+ if (this.view.state.selection.empty) {
+ if (marks) this._activeMarks = marks;
+ this.update_mark_doms();
+ }
+ }
+
+ update_mark_doms() {
+ this.reset_mark_doms();
+ let foundlink = false;
+ let children = this.extras.childNodes;
+ this._activeMarks.forEach((mark) => {
+ if (this._marksToDoms.has(mark)) {
+ let dom = this._marksToDoms.get(mark);
+ if (dom) dom.style.color = "greenyellow";
+ }
+ if (children.length > 1) {
+ foundlink = true;
+ }
+ if (mark.type.name === "link" && children.length === 1) {
+ // let del = document.createElement("button");
+ // del.textContent = "X";
+ // del.style.color = "red";
+ // del.style.height = "10px";
+ // del.style.width = "10px";
+ // del.style.marginLeft = "5px";
+ // del.onclick = this.deleteLink;
+ // this.extras.appendChild(del);
+ let del = this.deleteLinkItem().render(this.view).dom;
+ this.extras.appendChild(del);
+ foundlink = true;
+ }
+ });
+ if (!foundlink) {
+ if (children.length > 1) {
+ this.extras.removeChild(children[1]);
+ }
+ }
+
}
//finds all active marks on selection in given group
activeMarksOnSelection(markGroup: MarkType[]) {
//current selection
- let { empty, ranges } = this.view.state.selection as TextSelection;
+ let { empty, ranges, $to } = this.view.state.selection as TextSelection;
let state = this.view.state;
let dispatch = this.view.dispatch;
let activeMarks: MarkType[];
@@ -682,6 +919,9 @@ export class TooltipTextMenu {
}
return false;
});
+
+ const refnode = this.reference_node($to);
+ this._activeMarks = refnode.marks;
}
else {
const pos = this.view.state.selection.$from;
@@ -692,9 +932,7 @@ export class TooltipTextMenu {
else {
return [];
}
-
this._activeMarks = ref_node.marks;
-
activeMarks = markGroup.filter(mark_type => {
if (dispatch) {
let mark = state.schema.mark(mark_type);
@@ -713,12 +951,12 @@ export class TooltipTextMenu {
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) {
+ if (pos.nodeBefore !== null && pos.nodeBefore !== undefined) {
ref_node = pos.nodeBefore;
}
+ else if (pos.nodeAfter !== null && pos.nodeAfter !== undefined) {
+ ref_node = pos.nodeAfter;
+ }
else if (pos.pos > 0) {
let skip = false;
for (let i: number = pos.pos - 1; i > 0; i--) {
@@ -731,10 +969,13 @@ export class TooltipTextMenu {
});
}
}
+ if (!ref_node.isLeaf) {
+ ref_node = ref_node.child(0);
+ }
return ref_node;
}
destroy() {
- this.tooltip.remove();
+ this.wrapper.remove();
}
}
diff --git a/src/client/util/type_decls.d b/src/client/util/type_decls.d
index 1f95af00c..79a4e50d5 100644
--- a/src/client/util/type_decls.d
+++ b/src/client/util/type_decls.d
@@ -179,7 +179,7 @@ declare class Doc extends RefField {
// [ToScriptString](): string;
}
-declare class ListImpl<T extends Field> extends ObjectField {
+declare class List<T extends Field> extends ObjectField {
constructor(fields?: T[]);
[index: number]: T | (T extends RefField ? Promise<T> : never);
[Copy](): ObjectField;
diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss
index 254163b53..e2c0de8af 100644
--- a/src/client/views/ContextMenu.scss
+++ b/src/client/views/ContextMenu.scss
@@ -6,6 +6,10 @@
z-index: $contextMenu-zindex;
box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw;
flex-direction: column;
+ background: whitesmoke;
+ padding-bottom: 10px;
+ border-radius: 15px;
+ border: solid #BBBBBBBB 1px;
}
// .contextMenu-item:first-child {
@@ -28,12 +32,17 @@
z-index: 1000;
box-shadow: #AAAAAA .2vw .2vw .4vw;
flex-direction: column;
+ border: 1px solid #BBBBBBBB;
+ border-radius: 15px;
+ padding-top: 10px;
+ padding-bottom: 10px;
+ background: whitesmoke;
}
.contextMenu-item {
// width: 11vw; //10vw
height: 30px; //2vh
- background: #DDDDDD;
+ background: whitesmoke;
display: flex; //comment out to allow search icon to be inline with search text
justify-content: left;
align-items: center;
@@ -44,23 +53,33 @@
-ms-user-select: none;
user-select: none;
transition: all .1s;
+ border-style: none;
+ // padding: 10px 0px 10px 0px;
+ white-space: nowrap;
+ font-size: 13px;
+ color: grey;
+ letter-spacing: 2px;
+ text-transform: uppercase;
+ padding-right: 30px;
+}
+
+.contextMenu-item:hover {
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;
+ border-top-style: solid;
}
.contextMenu-itemSelected {
- background: rgb(136, 136, 136)
+ background: lightgoldenrodyellow;
+ border-style: none;
}
.contextMenu-group {
// width: 11vw; //10vw
height: 30px; //2vh
- background: rgb(200, 200, 200);
+ background: lightgray;
display: flex; //comment out to allow search icon to be inline with search text
justify-content: left;
align-items: center;
@@ -74,27 +93,41 @@
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;
+ font-size: 13px;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ padding-left: 5px;
}
.contextMenu-item:hover {
- transition: all 0.1s;
+ transition: all 0.1s ease;
background: $lighter-alt-accent;
}
.contextMenu-description {
- font-size: 20px;
+ margin-left: 5px;
text-align: left;
display: inline; //need this?
}
+.search-icon {
+ margin: 10px;
+}
+
+.search {
+ margin-left: 10px;
+ padding-left: 10px;
+ border: solid black 1px;
+ border-radius: 5px;
+}
+
.icon-background {
pointer-events: none;
- background-color: #DDDDDD;
+ background-color: transparent;
width: 35px;
text-align: center;
- font-size: 22px;
+ font-size: 20px;
+ margin-left: 5px;
} \ No newline at end of file
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 60658b7f0..39192c382 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -85,6 +85,14 @@ export class ContextMenu extends React.Component {
this._items = [];
}
+ findByDescription = (target: string, toLowerCase = false) => {
+ return this._items.find(menuItem => {
+ let reference = menuItem.description;
+ toLowerCase && (reference = reference.toLowerCase());
+ return reference === target;
+ });
+ }
+
@action
addItem(item: ContextMenuProps) {
if (this._items.indexOf(item) === -1) {
@@ -211,11 +219,11 @@ export class ContextMenu extends React.Component {
const contents = (
<>
- <span>
+ <span className={"search-icon"}>
<span className="icon-background">
<FontAwesomeIcon icon="search" size="lg" />
</span>
- <input className="contextMenu-item contextMenu-description" type="text" placeholder="Search . . ." value={this._searchString} onKeyDown={this.onKeyDown} onChange={this.onChange} autoFocus />
+ <input className="contextMenu-item contextMenu-description search" type="text" placeholder="Search . . ." value={this._searchString} onKeyDown={this.onKeyDown} onChange={this.onChange} autoFocus />
</span>
{this.menuItems}
</>
diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index 9bbb97d7e..a1787e78f 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -4,20 +4,22 @@ import { observer } from "mobx-react";
import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
import { faAngleRight } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { UndoManager } from "../util/UndoManager";
library.add(faAngleRight);
export interface OriginalMenuProps {
description: string;
event: () => void;
- icon?: IconProp; //maybe should be optional (icon?)
+ undoable?: boolean;
+ icon: IconProp; //maybe should be optional (icon?)
closeMenu?: () => void;
}
export interface SubmenuProps {
description: string;
subitems: ContextMenuProps[];
- icon?: IconProp; //maybe should be optional (icon?)
+ icon: IconProp; //maybe should be optional (icon?)
closeMenu?: () => void;
}
@@ -35,9 +37,14 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select
}
}
- handleEvent = (e: React.MouseEvent<HTMLDivElement>) => {
+ handleEvent = async (e: React.MouseEvent<HTMLDivElement>) => {
if ("event" in this.props) {
- this.props.event();
+ let batch: UndoManager.Batch | undefined;
+ if (this.props.undoable !== false) {
+ batch = UndoManager.StartBatch(`Context menu event: ${this.props.description}`);
+ }
+ await this.props.event();
+ batch && batch.end();
this.props.closeMenu && this.props.closeMenu();
}
}
@@ -94,7 +101,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select
) : null}
<div className="contextMenu-description">
{this.props.description}
- <FontAwesomeIcon icon={faAngleRight} size="lg" style={{ position: "absolute", right: "5px" }} />
+ <FontAwesomeIcon icon={faAngleRight} size="lg" style={{ position: "absolute", right: "10px" }} />
</div>
{submenu}
</div>
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 8e63ea4c3..209704ad5 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -28,6 +28,8 @@ import { RichTextField } from '../../new_fields/RichTextField';
import { LinkManager } from '../util/LinkManager';
import { ObjectField } from '../../new_fields/ObjectField';
import { MetadataEntryMenu } from './MetadataEntryMenu';
+import { ImageBox } from './nodes/ImageBox';
+import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -54,6 +56,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
private _downY = 0;
private _iconDoc?: Doc = undefined;
private _resizeUndo?: UndoManager.Batch;
+ private _linkDrag?: UndoManager.Batch;
@observable private _minimizedX = 0;
@observable private _minimizedY = 0;
@observable private _title: string = "";
@@ -87,11 +90,17 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
SelectionManager.DeselectAll();
let fieldTemplate = fieldTemplateView.props.Document;
let docTemplate = fieldTemplateView.props.ContainingCollectionView!.props.Document;
- let metaKey = text.slice(1, text.length);
- Doc.MakeTemplate(fieldTemplate, metaKey, Doc.GetProto(docTemplate));
+ let metaKey = text.startsWith(">>") ? text.slice(2, text.length) : text.slice(1, text.length);
+ let proto = Doc.GetProto(docTemplate);
+ Doc.MakeTemplate(fieldTemplate, metaKey, proto);
+ if (text.startsWith(">>")) {
+ proto.detailedLayout = proto.layout;
+ proto.miniLayout = ImageBox.LayoutString(metaKey);
+ }
}
else {
if (SelectionManager.SelectedDocuments().length > 0) {
+ SelectionManager.SelectedDocuments()[0].props.Document.customTitle = true;
let field = SelectionManager.SelectedDocuments()[0].props.Document[this._fieldKey];
if (typeof field === "number") {
SelectionManager.SelectedDocuments().forEach(d => {
@@ -138,7 +147,8 @@ 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.renderDepth === 0) {
+ if (documentView.props.renderDepth === 0 ||
+ Doc.AreProtosEqual(documentView.props.Document, CurrentUserUtils.UserDocument)) {
return bounds;
}
let transform = (documentView.props.ScreenToLocalTransform().scale(documentView.props.ContentScaling())).inverse();
@@ -300,7 +310,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
iconDoc.height = Number(MINIMIZED_ICON_SIZE);
iconDoc.x = NumCast(doc.x);
iconDoc.y = NumCast(doc.y) - 24;
- iconDoc.maximizedDocs = new List<Doc>(selected.map(s => s.props.Document.proto!));
+ iconDoc.maximizedDocs = new List<Doc>(selected.map(s => s.props.Document));
selected.length === 1 && (doc.minimizedDoc = iconDoc);
selected[0].props.addDocument && selected[0].props.addDocument(iconDoc, false);
return iconDoc;
@@ -346,7 +356,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
onRadiusMove = (e: PointerEvent): void => {
this._isMoving = true;
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)}%`);
+ SelectionManager.SelectedDocuments().map(dv => dv.props.Document.layout instanceof Doc ? dv.props.Document.layout : dv.props.Document.isTemplate ? dv.props.Document : Doc.GetProto(dv.props.Document)).
+ map(d => d.borderRounding = `${Math.min(100, dist)}%`);
e.stopPropagation();
e.preventDefault();
}
@@ -413,9 +424,15 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
let container = selDoc.props.ContainingCollectionView ? selDoc.props.ContainingCollectionView.props.Document.proto : undefined;
let dragData = new DragManager.LinkDragData(selDoc.props.Document, container ? [container] : []);
FormattedTextBox.InputBoxOverlay = undefined;
+ this._linkDrag = UndoManager.StartBatch("Drag Link");
DragManager.StartLinkDrag(this._linkerButton.current, dragData, e.pageX, e.pageY, {
handlers: {
- dragComplete: action(emptyFunction),
+ dragComplete: () => {
+ if (this._linkDrag) {
+ this._linkDrag.end();
+ this._linkDrag = undefined;
+ }
+ },
},
hideSource: false
});
@@ -526,14 +543,13 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
let actualdH = Math.max(height + (dH * scale), 20);
doc.x = (doc.x || 0) + dX * (actualdW - width);
doc.y = (doc.y || 0) + dY * (actualdH - height);
- let proto = Doc.GetProto(element.props.Document);
- let fixedAspect = e.ctrlKey || (!BoolCast(proto.ignoreAspect, false) && nwidth && nheight);
+ let proto = doc.isTemplate ? doc : Doc.GetProto(element.props.Document); // bcz: 'doc' didn't work here...
+ let fixedAspect = e.ctrlKey || (!BoolCast(proto.ignoreAspect) && nwidth && nheight);
if (fixedAspect && (!nwidth || !nheight)) {
proto.nativeWidth = nwidth = doc.width || 0;
proto.nativeHeight = nheight = doc.height || 0;
- proto.ignoreAspect = true;
}
- if (nwidth > 0 && nheight > 0) {
+ if (nwidth > 0 && nheight > 0 && !BoolCast(proto.ignoreAspect)) {
if (Math.abs(dW) > Math.abs(dH)) {
if (!fixedAspect) {
Doc.SetInPlace(element.props.Document, "nativeWidth", actualdW / (doc.width || 1) * (doc.nativeWidth || 0), true);
@@ -553,7 +569,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
} else {
dW && (doc.width = actualdW);
dH && (doc.height = actualdH);
- Doc.SetInPlace(element.props.Document, "autoHeight", undefined, true);
+ dH && Doc.SetInPlace(element.props.Document, "autoHeight", undefined, true);
}
}
});
@@ -668,6 +684,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
linkButton = (<Flyout
anchorPoint={anchorPoints.RIGHT_TOP}
content={<LinkMenu docView={selFirst}
+ addDocTab={selFirst.props.addDocTab}
changeFlyout={this.changeFlyoutContent} />}>
<div className={"linkButton-" + (linkCount ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} >{linkCount}</div>
</Flyout >);
diff --git a/src/client/views/EditableView.scss b/src/client/views/EditableView.scss
index dfa110f8d..19512362e 100644
--- a/src/client/views/EditableView.scss
+++ b/src/client/views/EditableView.scss
@@ -1,20 +1,25 @@
-.editableView-container-editing, .editableView-container-editing-oneLine {
+.editableView-container-editing,
+.editableView-container-editing-oneLine {
overflow-wrap: break-word;
word-wrap: break-word;
hyphens: auto;
overflow: hidden;
}
+
.editableView-container-editing-oneLine {
span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
- display:block;
+ display: block;
}
+
input {
- display:block;
+ display: block;
}
}
+
.editableView-input {
width: 100%;
+ background: inherit;
} \ No newline at end of file
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index 989fb1be9..c3612fee9 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -2,6 +2,7 @@ import React = require('react');
import { observer } from 'mobx-react';
import { observable, action, trace } from 'mobx';
import "./EditableView.scss";
+import * as Autosuggest from 'react-autosuggest';
export interface EditableProps {
/**
@@ -28,9 +29,17 @@ export interface EditableProps {
fontSize?: number;
height?: number;
display?: string;
+ autosuggestProps?: {
+ resetValue: () => void;
+ value: string,
+ onChange: (e: React.ChangeEvent, { newValue }: { newValue: string }) => void,
+ autosuggestProps: Autosuggest.AutosuggestProps<string>
+
+ };
oneLine?: boolean;
editing?: boolean;
onClick?: (e: React.MouseEvent) => boolean;
+ isEditingCallback?: (isEditing: boolean) => void;
}
/**
@@ -48,27 +57,45 @@ export class EditableView extends React.Component<EditableProps> {
}
@action
+ componentWillReceiveProps(nextProps: EditableProps) {
+ // this is done because when autosuggest is turned on, the suggestions are passed in as a prop,
+ // so when the suggestions are passed in, and no editing prop is passed in, it used to set it
+ // to false. this will no longer do so -syip
+ if (nextProps.editing && nextProps.editing !== this._editing) {
+ this._editing = nextProps.editing;
+ }
+ }
+
+ @action
onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Tab") {
+ e.stopPropagation();
this.props.OnTab && this.props.OnTab();
} else if (e.key === "Enter") {
+ e.stopPropagation();
if (!e.ctrlKey) {
if (this.props.SetValue(e.currentTarget.value, e.shiftKey)) {
this._editing = false;
+ this.props.isEditingCallback && this.props.isEditingCallback(false);
}
} else if (this.props.OnFillDown) {
this.props.OnFillDown(e.currentTarget.value);
this._editing = false;
+ this.props.isEditingCallback && this.props.isEditingCallback(false);
}
} else if (e.key === "Escape") {
+ e.stopPropagation();
this._editing = false;
+ this.props.isEditingCallback && this.props.isEditingCallback(false);
}
}
@action
onClick = (e: React.MouseEvent) => {
+ e.nativeEvent.stopPropagation();
if (!this.props.onClick || !this.props.onClick(e)) {
this._editing = true;
+ this.props.isEditingCallback && this.props.isEditingCallback(true);
}
e.stopPropagation();
}
@@ -84,15 +111,31 @@ export class EditableView extends React.Component<EditableProps> {
render() {
if (this._editing) {
- return <input className="editableView-input" defaultValue={this.props.GetValue()} onKeyDown={this.onKeyDown} autoFocus
- onBlur={action(() => this._editing = false)} onPointerDown={this.stopPropagation} onClick={this.stopPropagation} onPointerUp={this.stopPropagation}
- style={{ display: this.props.display, fontSize: this.props.fontSize }} />;
+ return this.props.autosuggestProps
+ ? <Autosuggest
+ {...this.props.autosuggestProps.autosuggestProps}
+ inputProps={{
+ className: "editableView-input",
+ onKeyDown: this.onKeyDown,
+ autoFocus: true,
+ onBlur: action(() => this._editing = false),
+ onPointerDown: this.stopPropagation,
+ onClick: this.stopPropagation,
+ onPointerUp: this.stopPropagation,
+ value: this.props.autosuggestProps.value,
+ onChange: this.props.autosuggestProps.onChange
+ }}
+ />
+ : <input className="editableView-input" defaultValue={this.props.GetValue()} onKeyDown={this.onKeyDown} autoFocus
+ onBlur={action(() => { this._editing = false; this.props.isEditingCallback && this.props.isEditingCallback(false); })} onPointerDown={this.stopPropagation} onClick={this.stopPropagation} onPointerUp={this.stopPropagation}
+ style={{ display: this.props.display, fontSize: this.props.fontSize }} />;
} else {
+ if (this.props.autosuggestProps) this.props.autosuggestProps.resetValue();
return (
<div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`}
style={{ display: this.props.display, height: "auto", maxHeight: `${this.props.height}` }}
- onClick={this.onClick} >
- <span style={{ fontStyle: this.props.fontStyle }}>{this.props.contents}</span>
+ onClick={this.onClick}>
+ <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize }}>{this.props.contents}</span>
</div>
);
}
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index f378b6c0c..ea2e3e196 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -1,12 +1,17 @@
-import { UndoManager, undoBatch } from "../util/UndoManager";
+import { UndoManager } 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";
+import { Doc } from "../../new_fields/Doc";
+import { CognitiveServices } from "../cognitive_services/CognitiveServices";
+import DictationManager from "../util/DictationManager";
+import { ContextMenu } from "./ContextMenu";
+import { ContextMenuProps } from "./ContextMenuItem";
const modifiers = ["control", "meta", "shift", "alt"];
-type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo;
+type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise<KeyControlInfo>;
type KeyControlInfo = {
preventDefault: boolean,
stopPropagation: boolean
@@ -24,9 +29,10 @@ export default class KeyManager {
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);
+ this.router.set("1000", this.shift);
}
- public handle = (e: KeyboardEvent) => {
+ public handle = async (e: KeyboardEvent) => {
let keyname = e.key.toLowerCase();
this.handleGreedy(keyname);
@@ -42,7 +48,7 @@ export default class KeyManager {
return;
}
- let control = handleConstrained(keyname, e);
+ let control = await handleConstrained(keyname, e);
control.stopPropagation && e.stopPropagation();
control.preventDefault && e.preventDefault();
@@ -66,6 +72,7 @@ export default class KeyManager {
}
}
MainView.Instance.toggleColorPicker(true);
+ SelectionManager.DeselectAll();
break;
case "delete":
case "backspace":
@@ -82,6 +89,9 @@ export default class KeyManager {
});
}, "delete");
break;
+ case "enter":
+ SelectionManager.SelectedDocuments().map(selected => Doc.ToggleDetailLayout(selected.props.Document));
+ break;
}
return {
@@ -90,6 +100,27 @@ export default class KeyManager {
};
});
+ private shift = async (keyname: string) => {
+ let stopPropagation = false;
+ let preventDefault = false;
+
+ switch (keyname) {
+ case " ":
+ let transcript = await DictationManager.Instance.listen();
+ console.log(`I heard${transcript ? `: ${transcript.toLowerCase()}` : " nothing: I thought I was still listening from an earlier session."}`);
+ let command: ContextMenuProps | undefined;
+ transcript && (command = ContextMenu.Instance.findByDescription(transcript, true)) && "event" in command && command.event();
+ stopPropagation = true;
+ preventDefault = true;
+ break;
+ }
+
+ return {
+ stopPropagation: stopPropagation,
+ preventDefault: preventDefault
+ };
+ }
+
private alt = action((keyname: string) => {
let stopPropagation = true;
let preventDefault = true;
@@ -128,6 +159,13 @@ export default class KeyManager {
}
MainView.Instance.mainFreeform && CollectionDockingView.Instance.CloseRightSplit(MainView.Instance.mainFreeform);
break;
+ case "backspace":
+ if (document.activeElement) {
+ if (document.activeElement.tagName === "INPUT" || document.activeElement.tagName === "TEXTAREA") {
+ return { stopPropagation: false, preventDefault: false };
+ }
+ }
+ break;
case "f":
MainView.Instance.isSearchVisible = !MainView.Instance.isSearchVisible;
break;
@@ -140,14 +178,16 @@ export default class KeyManager {
break;
case "y":
UndoManager.Redo();
+ stopPropagation = false;
break;
case "z":
UndoManager.Undo();
+ stopPropagation = false;
break;
case "a":
- case "c":
case "v":
case "x":
+ case "c":
stopPropagation = false;
preventDefault = false;
break;
diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx
index 3e0d7b476..1c221e3df 100644
--- a/src/client/views/InkingCanvas.tsx
+++ b/src/client/views/InkingCanvas.tsx
@@ -6,7 +6,7 @@ import "./InkingCanvas.scss";
import { InkingControl } from "./InkingControl";
import { InkingStroke } from "./InkingStroke";
import React = require("react");
-import { undoBatch, UndoManager } from "../util/UndoManager";
+import { UndoManager } from "../util/UndoManager";
import { StrokeData, InkField, InkTool } from "../../new_fields/InkField";
import { Doc } from "../../new_fields/Doc";
import { Cast, PromiseValue, NumCast } from "../../new_fields/Types";
@@ -176,9 +176,9 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
}
render() {
- let svgCanvasStyle = InkingControl.Instance.selectedTool !== InkTool.None ? "canSelect" : "noSelect";
+ let svgCanvasStyle = InkingControl.Instance.selectedTool !== InkTool.None && !this.props.Document.isBackground ? "canSelect" : "noSelect";
return (
- <div className="inkingCanvas" >
+ <div className="inkingCanvas">
<div className={`inkingCanvas-${svgCanvasStyle}`} onPointerDown={this.onPointerDown} />
{this.props.children()}
{this.drawnPaths}
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index c7f7bdb66..3f40642b5 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -20,7 +20,7 @@ export class InkingControl extends React.Component {
static Instance: InkingControl = new InkingControl({});
@observable private _selectedTool: InkTool = InkTool.None;
@observable private _selectedColor: string = "rgb(244, 67, 54)";
- @observable private _selectedWidth: string = "25";
+ @observable private _selectedWidth: string = "5";
@observable public _open: boolean = false;
constructor(props: Readonly<{}>) {
@@ -47,7 +47,7 @@ export class InkingControl extends React.Component {
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 targetDoc = view.props.Document.layout instanceof Doc ? view.props.Document.layout : view.props.Document.isTemplate ? view.props.Document : Doc.GetProto(view.props.Document);
let oldColor = StrCast(targetDoc.backgroundColor);
targetDoc.backgroundColor = this._selectedColor;
return {
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 86578af3e..1cf13aa74 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -33,6 +33,11 @@ let swapDocs = async () => {
DocServer.init(window.location.protocol, window.location.hostname, 4321, info.email);
await Docs.Prototypes.initialize();
await CurrentUserUtils.loadUserDocument(info);
+ // updates old user documents to prevent chrome on tree view.
+ (await Cast(CurrentUserUtils.UserDocument.workspaces, Doc))!.chromeStatus = "disabled";
+ (await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc))!.chromeStatus = "disabled";
+ (await Cast(CurrentUserUtils.UserDocument.sidebar, Doc))!.chromeStatus = "disabled";
+ CurrentUserUtils.UserDocument.chromeStatus = "disabled";
await swapDocs();
ReactDOM.render(<MainView />, document.getElementById('root'));
})(); \ No newline at end of file
diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx
index 126efd11c..72eb956e3 100644
--- a/src/client/views/MainOverlayTextBox.tsx
+++ b/src/client/views/MainOverlayTextBox.tsx
@@ -4,7 +4,7 @@ import "normalize.css";
import * as React from 'react';
import { Doc } from '../../new_fields/Doc';
import { BoolCast } from '../../new_fields/Types';
-import { emptyFunction, returnTrue, returnZero, Utils } from '../../Utils';
+import { emptyFunction, returnTrue, returnZero, Utils, returnOne } from '../../Utils';
import { DragManager } from '../util/DragManager';
import { Transform } from '../util/Transform';
import { CollectionDockingView } from './collections/CollectionDockingView';
@@ -12,6 +12,7 @@ import "./MainOverlayTextBox.scss";
import { FormattedTextBox } from './nodes/FormattedTextBox';
interface MainOverlayTextBoxProps {
+ firstinstance?: boolean;
}
@observer
@@ -29,6 +30,7 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
private _outerdiv: HTMLElement | null = null;
private _textBox: FormattedTextBox | undefined;
private _tooltip?: HTMLElement;
+ ChromeHeight?: () => number;
@observable public TextDoc?: Doc;
@observable public TextDataDoc?: Doc;
@@ -49,6 +51,7 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
(box?: FormattedTextBox) => {
this._textBox = box;
if (box) {
+ this.ChromeHeight = box.props.ChromeHeight;
this.TextDoc = box.props.Document;
this.TextDataDoc = box.props.DataDoc;
let xf = () => {
@@ -140,7 +143,7 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
Document={FormattedTextBox.InputBoxOverlay.props.Document}
DataDoc={FormattedTextBox.InputBoxOverlay.props.DataDoc}
isSelected={returnTrue} select={emptyFunction} renderDepth={0} selectOnLoad={true}
- ContainingCollectionView={undefined} whenActiveChanged={emptyFunction} active={returnTrue}
+ ContainingCollectionView={undefined} whenActiveChanged={emptyFunction} active={returnTrue} ContentScaling={returnOne}
ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyFunction} addDocTab={this.addDocTab} outer_div={(tooltip: HTMLElement) => { this._tooltip = tooltip; this.updateTooltip(); }} />
</div>
</div>
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 5cb1d06cb..6cf3b54f2 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,5 +1,5 @@
import { IconName, library } from '@fortawesome/fontawesome-svg-core';
-import { faArrowDown, 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 { faArrowDown, faCloudUploadAlt, faArrowUp, faClone, faCheck, faPlay, faPause, faCaretUp, faLongArrowAltRight, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faPortrait, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faThumbtack, faTree, faUndoAlt, faCat, faBolt } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, configure, observable, runInAction, reaction, trace } from 'mobx';
import { observer } from 'mobx-react';
@@ -7,7 +7,6 @@ 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';
@@ -16,7 +15,7 @@ import { listSpec } from '../../new_fields/Schema';
import { Cast, FieldValue, NumCast, BoolCast, StrCast } from '../../new_fields/Types';
import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
import { RouteStore } from '../../server/RouteStore';
-import { emptyFunction, returnOne, returnTrue } from '../../Utils';
+import { emptyFunction, returnOne, returnTrue, Utils, returnEmptyString } from '../../Utils';
import { DocServer } from '../DocServer';
import { Docs } from '../documents/Documents';
import { SetupDrag } from '../util/DragManager';
@@ -40,7 +39,7 @@ import { FilterBox } from './search/FilterBox';
import { CollectionTreeView } from './collections/CollectionTreeView';
import { ClientUtils } from '../util/ClientUtils';
import { SearchBox } from './search/SearchBox';
-
+import { SchemaHeaderField, RandomPastel } from '../../new_fields/SchemaHeaderField';
@observer
export class MainView extends React.Component {
@@ -94,6 +93,8 @@ export class MainView extends React.Component {
componentWillUnMount() {
window.removeEventListener("keydown", KeyManager.Instance.handle);
+ window.removeEventListener("pointerdown", this.globalPointerDown);
+ window.removeEventListener("pointerup", this.globalPointerUp);
}
constructor(props: Readonly<{}>) {
@@ -125,30 +126,40 @@ export class MainView extends React.Component {
library.add(faFilm);
library.add(faMusic);
library.add(faTree);
+ library.add(faPlay);
+ library.add(faPause);
library.add(faClone);
library.add(faCut);
library.add(faCommentAlt);
library.add(faThumbtack);
+ library.add(faLongArrowAltRight);
library.add(faCheck);
+ library.add(faCaretUp);
library.add(faArrowDown);
library.add(faArrowUp);
library.add(faCloudUploadAlt);
+ library.add(faBolt);
this.initEventListeners();
this.initAuthenticationRouters();
}
+ globalPointerDown = action((e: PointerEvent) => {
+ this.isPointerDown = true;
+ const targets = document.elementsFromPoint(e.x, e.y);
+ if (targets && targets.length && targets[0].className.toString().indexOf("contextMenu") === -1) {
+ ContextMenu.Instance.closeMenu();
+ }
+ });
+
+ globalPointerUp = () => this.isPointerDown = false;
+
initEventListeners = () => {
// window.addEventListener("pointermove", (e) => this.reportLocation(e))
window.addEventListener("drop", (e) => e.preventDefault(), false); // drop event handler
window.addEventListener("dragover", (e) => e.preventDefault(), false); // drag event handler
// click interactions for the context menu
- document.addEventListener("pointerdown", action((e: PointerEvent) => {
- this.isPointerDown = true;
- const targets = document.elementsFromPoint(e.x, e.y);
- if (targets && targets.length && targets[0].className.toString().indexOf("contextMenu") === -1) {
- ContextMenu.Instance.closeMenu();
- }
- }), true);
+ document.addEventListener("pointerdown", this.globalPointerDown);
+ document.addEventListener("pointerup", this.globalPointerUp);
}
initAuthenticationRouters = async () => {
@@ -260,6 +271,7 @@ export class MainView extends React.Component {
PanelWidth={this.getPWidth}
PanelHeight={this.getPHeight}
renderDepth={0}
+ backgroundColor={returnEmptyString}
selectOnLoad={false}
focus={emptyFunction}
parentActive={returnTrue}
@@ -291,7 +303,6 @@ export class MainView extends React.Component {
}
@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;
@@ -325,6 +336,7 @@ export class MainView extends React.Component {
renderDepth={0}
selectOnLoad={false}
focus={emptyFunction}
+ backgroundColor={returnEmptyString}
parentActive={returnTrue}
whenActiveChanged={emptyFunction}
bringToFront={emptyFunction}
@@ -374,28 +386,34 @@ export class MainView extends React.Component {
let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg";
// 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 addSchemaNode = action(() => Docs.Create.SchemaDocument([new SchemaHeaderField("title", "#f1efeb")], [], { width: 200, height: 200, title: "a schema collection" }));
//let addTreeNode = action(() => Docs.TreeDocument([CurrentUserUtils.UserDocument], { width: 250, height: 400, title: "Library:" + CurrentUserUtils.email, dropAction: "alias" }));
// let addTreeNode = action(() => Docs.TreeDocument(this._northstarSchemas, { width: 250, height: 400, title: "northstar schemas", dropAction: "copy" }));
let addColNode = action(() => Docs.Create.FreeformDocument([], { width: this.pwidth * .7, height: this.pheight, title: "a freeform collection" }));
let addTreeNode = action(() => CurrentUserUtils.UserDocument);
let addImageNode = action(() => Docs.Create.ImageDocument(imgurl, { width: 200, title: "an image of a cat" }));
+ let addButtonDocument = action(() => Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" }));
let addImportCollectionNode = action(() => Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 }));
+ let youtubeurl = "https://www.youtube.com/embed/TqcApsGRzWw";
+ let addYoutubeSearcher = action(() => Docs.Create.YoutubeDocument(youtubeurl, { width: 600, height: 600, title: "youtube search" }));
let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc][] = [
[React.createRef<HTMLDivElement>(), "object-group", "Add Collection", addColNode],
+ [React.createRef<HTMLDivElement>(), "bolt", "Add Button", addButtonDocument],
// [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" style={{ left: this.flyoutWidth + 5 }} >
+ return < div id="add-nodes-menu" style={{ left: this.flyoutWidth + 20, bottom: 20 }} >
<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="presentation"><button className="add-button round-button" title="Open Presentation View" onClick={() => PresentationView.Instance.toggle(undefined)}><FontAwesomeIcon icon="table" 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 =>
@@ -405,7 +423,7 @@ export class MainView extends React.Component {
</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 }} >
+ <li key="color"><button className="add-button round-button" title="Select Color" style={{ zIndex: 1000 }} onClick={() => this.toggleColorPicker()}><div className="toolbar-color-button" style={{ backgroundColor: InkingControl.Instance.selectedColor }} >
<div className="toolbar-color-picker" onClick={this.onColorClick} style={this._colorPickerDisplay ? { color: "black", display: "block" } : { color: "black", display: "none" }}>
<SketchPicker color={InkingControl.Instance.selectedColor} onChange={InkingControl.Instance.switchColor} />
</div>
@@ -435,7 +453,7 @@ export class MainView extends React.Component {
return [
this.isSearchVisible ? <div className="main-searchDiv" key="search" style={{ top: '34px', right: '1px', position: 'absolute' }} > <FilterBox /> </div> : null,
<div className="main-buttonDiv" key="logout" style={{ bottom: '0px', right: '1px', position: 'absolute' }} ref={logoutRef}>
- <button onClick={() => window.location.assign(DocServer.prepend(RouteStore.logout))}>Log Out</button></div>
+ <button onClick={() => window.location.assign(Utils.prepend(RouteStore.logout))}>Log Out</button></div>
];
}
@@ -443,6 +461,7 @@ export class MainView extends React.Component {
@observable isSearchVisible = false;
@action.bound
toggleSearch = () => {
+ // console.log("search toggling")
this.isSearchVisible = !this.isSearchVisible;
}
@@ -456,7 +475,7 @@ export class MainView extends React.Component {
{this.nodesMenu()}
{this.miscButtons}
<PDFMenu />
- <MainOverlayTextBox />
+ <MainOverlayTextBox firstinstance={true} />
<OverlayView />
</div >
);
diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx
index bd5a307b3..36c240dd8 100644
--- a/src/client/views/MetadataEntryMenu.tsx
+++ b/src/client/views/MetadataEntryMenu.tsx
@@ -5,6 +5,7 @@ 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';
+import { undoBatch } from '../util/UndoManager';
export type DocLike = Doc | Doc[] | Promise<Doc> | Promise<Doc[]>;
export interface MetadataEntryProps {
@@ -74,8 +75,11 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
this.userModified = e.target.value.trim() !== "";
}
+ @undoBatch
+ @action
onValueKeyDown = async (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
+ e.stopPropagation();
const script = KeyValueBox.CompileKVPScript(this._currentValue);
if (!script) return;
let doc = this.props.docs;
diff --git a/src/client/views/OverlayView.scss b/src/client/views/OverlayView.scss
new file mode 100644
index 000000000..dc122497f
--- /dev/null
+++ b/src/client/views/OverlayView.scss
@@ -0,0 +1,42 @@
+.overlayWindow-outerDiv {
+ border-radius: 5px;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+}
+
+.overlayWindow-outerDiv,
+.overlayView-wrapperDiv {
+ position: absolute;
+ z-index: 1;
+}
+
+.overlayWindow-titleBar {
+ flex: 0 1 30px;
+ background: darkslategray;
+ color: whitesmoke;
+ text-align: center;
+ cursor: move;
+}
+
+.overlayWindow-content {
+ flex: 1 1 auto;
+ display: flex;
+ flex-direction: column;
+}
+
+.overlayWindow-closeButton {
+ float: right;
+ height: 30px;
+ width: 30px;
+}
+
+.overlayWindow-resizeDragger {
+ background-color: rgb(0, 0, 0);
+ position: absolute;
+ right: 0px;
+ bottom: 0px;
+ width: 10px;
+ height: 10px;
+ cursor: nwse-resize;
+} \ No newline at end of file
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index f8fc94274..2f2579057 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -3,6 +3,8 @@ import { observer } from "mobx-react";
import { observable, action } from "mobx";
import { Utils } from "../../Utils";
+import './OverlayView.scss';
+
export type OverlayDisposer = () => void;
export type OverlayElementOptions = {
@@ -10,13 +12,92 @@ export type OverlayElementOptions = {
y: number;
width?: number;
height?: number;
+ title?: string;
};
+export interface OverlayWindowProps {
+ children: JSX.Element;
+ overlayOptions: OverlayElementOptions;
+ onClick: () => void;
+}
+
+@observer
+export class OverlayWindow extends React.Component<OverlayWindowProps> {
+ @observable x: number;
+ @observable y: number;
+ @observable width: number;
+ @observable height: number;
+ constructor(props: OverlayWindowProps) {
+ super(props);
+
+ const opts = props.overlayOptions;
+ this.x = opts.x;
+ this.y = opts.y;
+ this.width = opts.width || 200;
+ this.height = opts.height || 200;
+ }
+
+ onPointerDown = (_: React.PointerEvent) => {
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ document.addEventListener("pointermove", this.onPointerMove);
+ document.addEventListener("pointerup", this.onPointerUp);
+ }
+
+ onResizerPointerDown = (_: React.PointerEvent) => {
+ document.removeEventListener("pointermove", this.onResizerPointerMove);
+ document.removeEventListener("pointerup", this.onResizerPointerUp);
+ document.addEventListener("pointermove", this.onResizerPointerMove);
+ document.addEventListener("pointerup", this.onResizerPointerUp);
+ }
+
+ @action
+ onPointerMove = (e: PointerEvent) => {
+ this.x += e.movementX;
+ this.x = Math.max(Math.min(this.x, window.innerWidth - this.width), 0);
+ this.y += e.movementY;
+ this.y = Math.max(Math.min(this.y, window.innerHeight - this.height), 0);
+ }
+
+ @action
+ onResizerPointerMove = (e: PointerEvent) => {
+ this.width += e.movementX;
+ this.width = Math.max(this.width, 30);
+ this.height += e.movementY;
+ this.height = Math.max(this.height, 30);
+ }
+
+ onPointerUp = (e: PointerEvent) => {
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ }
+
+ onResizerPointerUp = (e: PointerEvent) => {
+ document.removeEventListener("pointermove", this.onResizerPointerMove);
+ document.removeEventListener("pointerup", this.onResizerPointerUp);
+ }
+
+ render() {
+ return (
+ <div className="overlayWindow-outerDiv" style={{ transform: `translate(${this.x}px, ${this.y}px)`, width: this.width, height: this.height }}>
+ <div className="overlayWindow-titleBar" onPointerDown={this.onPointerDown} >
+ {this.props.overlayOptions.title || "Untitled"}
+ <button onClick={this.props.onClick} className="overlayWindow-closeButton">X</button>
+ </div>
+ <div className="overlayWindow-content">
+ {this.props.children}
+ </div>
+ <div className="overlayWindow-resizeDragger" onPointerDown={this.onResizerPointerDown}></div>
+ </div>
+ );
+ }
+}
+
@observer
export class OverlayView extends React.Component {
public static Instance: OverlayView;
@observable.shallow
- private _elements: { ele: JSX.Element, id: string, options: OverlayElementOptions }[] = [];
+ private _elements: JSX.Element[] = [];
constructor(props: any) {
super(props);
@@ -27,20 +108,34 @@ export class OverlayView extends React.Component {
@action
addElement(ele: JSX.Element, options: OverlayElementOptions): OverlayDisposer {
- const eleWithPosition = { ele, options, id: Utils.GenerateGuid() };
- this._elements.push(eleWithPosition);
- return action(() => {
- const index = this._elements.indexOf(eleWithPosition);
+ const remove = action(() => {
+ const index = this._elements.indexOf(ele);
+ if (index !== -1) this._elements.splice(index, 1);
+ });
+ ele = <div key={Utils.GenerateGuid()} className="overlayView-wrapperDiv" style={{
+ transform: `translate(${options.x}px, ${options.y}px)`,
+ width: options.width,
+ height: options.height
+ }}>{ele}</div>;
+ this._elements.push(ele);
+ return remove;
+ }
+
+ @action
+ addWindow(contents: JSX.Element, options: OverlayElementOptions): OverlayDisposer {
+ const remove = action(() => {
+ const index = this._elements.indexOf(contents);
if (index !== -1) this._elements.splice(index, 1);
});
+ contents = <OverlayWindow onClick={remove} key={Utils.GenerateGuid()} overlayOptions={options}>{contents}</OverlayWindow>;
+ this._elements.push(contents);
+ return remove;
}
render() {
return (
<div>
- {this._elements.map(({ ele, options: { x, y, width, height }, id }) => (
- <div key={id} style={{ position: "absolute", transform: `translate(${x}px, ${y}px)`, width, height }}>{ele}</div>
- ))}
+ {this._elements}
</div>
);
}
diff --git a/src/client/views/ScriptBox.tsx b/src/client/views/ScriptBox.tsx
index fa236c2da..d073945e5 100644
--- a/src/client/views/ScriptBox.tsx
+++ b/src/client/views/ScriptBox.tsx
@@ -3,11 +3,15 @@ import { observer } from "mobx-react";
import { observable, action } from "mobx";
import "./ScriptBox.scss";
+import { OverlayView } from "./OverlayView";
+import { DocumentIconContainer } from "./nodes/DocumentIcon";
+import { Opt } from "../../new_fields/Doc";
export interface ScriptBoxProps {
onSave: (text: string, onError: (error: string) => void) => void;
onCancel?: () => void;
initialText?: string;
+ showDocumentIcons?: boolean;
}
@observer
@@ -30,14 +34,31 @@ export class ScriptBox extends React.Component<ScriptBoxProps> {
console.log(error);
}
+ overlayDisposer?: () => void;
+ onFocus = () => {
+ if (this.overlayDisposer) {
+ this.overlayDisposer();
+ }
+ this.overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 });
+ }
+
+ onBlur = () => {
+ this.overlayDisposer && this.overlayDisposer();
+ }
+
render() {
+ let onFocus: Opt<() => void> = undefined, onBlur: Opt<() => void> = undefined;
+ if (this.props.showDocumentIcons) {
+ onFocus = this.onFocus;
+ onBlur = this.onBlur;
+ }
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>
+ <textarea className="scriptBox-textarea" onChange={this.onChange} value={this._scriptText} onFocus={onFocus} onBlur={onBlur}></textarea>
</div>
);
}
diff --git a/src/client/views/ScriptingRepl.scss b/src/client/views/ScriptingRepl.scss
new file mode 100644
index 000000000..f1ef64193
--- /dev/null
+++ b/src/client/views/ScriptingRepl.scss
@@ -0,0 +1,50 @@
+.scriptingRepl-outerContainer {
+ background-color: whitesmoke;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
+.scriptingRepl-resultContainer {
+ padding-bottom: 5px;
+}
+
+.scriptingRepl-commandInput {
+ width: 100%;
+}
+
+.scriptingRepl-commandResult,
+.scriptingRepl-commandString {
+ overflow-wrap: break-word;
+}
+
+.scriptingRepl-commandsContainer {
+ flex: 1 1 auto;
+ overflow-y: scroll;
+}
+
+.documentIcon-outerDiv {
+ background-color: white;
+ border-width: 1px;
+ border-style: solid;
+ border-radius: 25%;
+ padding: 2px;
+}
+
+.scriptingObject-icon {
+ padding: 3px;
+ cursor: pointer;
+}
+
+.scriptingObject-iconCollapsed {
+ padding-left: 4px;
+ padding-right: 5px;
+}
+
+.scriptingObject-fields {
+ padding-left: 10px;
+}
+
+.scriptingObject-leaf {
+ margin-left: 15px;
+} \ No newline at end of file
diff --git a/src/client/views/ScriptingRepl.tsx b/src/client/views/ScriptingRepl.tsx
new file mode 100644
index 000000000..e05195ca0
--- /dev/null
+++ b/src/client/views/ScriptingRepl.tsx
@@ -0,0 +1,247 @@
+import * as React from 'react';
+import { observer } from 'mobx-react';
+import { observable, action } from 'mobx';
+import './ScriptingRepl.scss';
+import { Scripting, CompileScript, ts, Transformer } from '../util/Scripting';
+import { DocumentManager } from '../util/DocumentManager';
+import { OverlayView } from './OverlayView';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faCaretDown, faCaretRight } from '@fortawesome/free-solid-svg-icons';
+import { DocumentIconContainer } from './nodes/DocumentIcon';
+
+library.add(faCaretDown);
+library.add(faCaretRight);
+
+@observer
+export class ScriptingObjectDisplay extends React.Component<{ scrollToBottom: () => void, value: { [key: string]: any }, name?: string }> {
+ @observable collapsed = true;
+
+ @action
+ toggle = () => {
+ this.collapsed = !this.collapsed;
+ this.props.scrollToBottom();
+ }
+
+ render() {
+ const val = this.props.value;
+ const proto = Object.getPrototypeOf(val);
+ const name = (proto && proto.constructor && proto.constructor.name) || String(val);
+ const title = this.props.name ? <><b>{this.props.name} : </b>{name}</> : name;
+ if (this.collapsed) {
+ return (
+ <div className="scriptingObject-collapsed">
+ <span onClick={this.toggle} className="scriptingObject-icon scriptingObject-iconCollapsed"><FontAwesomeIcon icon="caret-right" size="sm" /></span>{title} (+{Object.keys(val).length})
+ </div>
+ );
+ } else {
+ return (
+ <div className="scriptingObject-open">
+ <div>
+ <span onClick={this.toggle} className="scriptingObject-icon"><FontAwesomeIcon icon="caret-down" size="sm" /></span>{title}
+ </div>
+ <div className="scriptingObject-fields">
+ {Object.keys(val).map(key => <ScriptingValueDisplay {...this.props} name={key} />)}
+ </div>
+ </div>
+ );
+ }
+ }
+}
+
+@observer
+export class ScriptingValueDisplay extends React.Component<{ scrollToBottom: () => void, value: any, name?: string }> {
+ render() {
+ const val = this.props.name ? this.props.value[this.props.name] : this.props.value;
+ if (typeof val === "object") {
+ return <ScriptingObjectDisplay scrollToBottom={this.props.scrollToBottom} value={val} name={this.props.name} />;
+ } else if (typeof val === "function") {
+ const name = "[Function]";
+ const title = this.props.name ? <><b>{this.props.name} : </b>{name}</> : name;
+ return <div className="scriptingObject-leaf">{title}</div>;
+ } else {
+ const name = String(val);
+ const title = this.props.name ? <><b>{this.props.name} : </b>{name}</> : name;
+ return <div className="scriptingObject-leaf">{title}</div>;
+ }
+ }
+}
+
+@observer
+export class ScriptingRepl extends React.Component {
+ @observable private commands: { command: string, result: any }[] = [];
+ private commandsHistory: string[] = [];
+
+ @observable private commandString: string = "";
+ private commandBuffer: string = "";
+
+ @observable private historyIndex: number = -1;
+
+ private commandsRef = React.createRef<HTMLDivElement>();
+
+ private args: any = {};
+
+ getTransformer = (): Transformer => {
+ return {
+ transformer: context => {
+ const knownVars: { [name: string]: number } = {};
+ const usedDocuments: number[] = [];
+ Scripting.getGlobals().forEach(global => knownVars[global] = 1);
+ return root => {
+ function visit(node: ts.Node) {
+ let skip = false;
+ if (ts.isIdentifier(node)) {
+ if (ts.isParameter(node.parent)) {
+ skip = true;
+ knownVars[node.text] = 1;
+ }
+ }
+ node = ts.visitEachChild(node, visit, context);
+
+ if (ts.isIdentifier(node)) {
+ const isntPropAccess = !ts.isPropertyAccessExpression(node.parent) || node.parent.expression === node;
+ const isntPropAssign = !ts.isPropertyAssignment(node.parent) || node.parent.name !== node;
+ if (ts.isParameter(node.parent)) {
+ // delete knownVars[node.text];
+ } else if (isntPropAccess && isntPropAssign && !(node.text in knownVars) && !(node.text in globalThis)) {
+ const match = node.text.match(/\d([0-9]+)/);
+ if (match) {
+ const m = parseInt(match[1]);
+ usedDocuments.push(m);
+ } else {
+ return ts.createPropertyAccess(ts.createIdentifier("args"), node);
+ }
+ }
+ }
+
+ return node;
+ }
+ return ts.visitNode(root, visit);
+ };
+ }
+ };
+ }
+
+ @action
+ onKeyDown = (e: React.KeyboardEvent) => {
+ let stopProp = true;
+ switch (e.key) {
+ case "Enter": {
+ const docGlobals: { [name: string]: any } = {};
+ DocumentManager.Instance.DocumentViews.forEach((dv, i) => docGlobals[`d${i}`] = dv.props.Document);
+ const globals = Scripting.makeMutableGlobalsCopy(docGlobals);
+ const script = CompileScript(this.commandString, { typecheck: false, addReturn: true, editable: true, params: { args: "any" }, transformer: this.getTransformer(), globals });
+ if (!script.compiled) {
+ this.commands.push({ command: this.commandString, result: script.errors });
+ return;
+ }
+ const result = script.run({ args: this.args });
+ if (!result.success) {
+ this.commands.push({ command: this.commandString, result: result.error.toString() });
+ return;
+ }
+ this.commands.push({ command: this.commandString, result: result.result });
+ this.commandsHistory.push(this.commandString);
+
+ this.maybeScrollToBottom();
+
+ this.commandString = "";
+ this.commandBuffer = "";
+ this.historyIndex = -1;
+ break;
+ }
+ case "ArrowUp": {
+ if (this.historyIndex < this.commands.length - 1) {
+ this.historyIndex++;
+ if (this.historyIndex === 0) {
+ this.commandBuffer = this.commandString;
+ }
+ this.commandString = this.commandsHistory[this.commands.length - 1 - this.historyIndex];
+ }
+ break;
+ }
+ case "ArrowDown": {
+ if (this.historyIndex >= 0) {
+ this.historyIndex--;
+ if (this.historyIndex === -1) {
+ this.commandString = this.commandBuffer;
+ this.commandBuffer = "";
+ } else {
+ this.commandString = this.commandsHistory[this.commands.length - 1 - this.historyIndex];
+ }
+ }
+ break;
+ }
+ default:
+ stopProp = false;
+ break;
+ }
+
+ if (stopProp) {
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ }
+
+ @action
+ onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this.commandString = e.target.value;
+ }
+
+ private shouldScroll: boolean = false;
+ private maybeScrollToBottom = () => {
+ const ele = this.commandsRef.current;
+ if (ele && ele.scrollTop === (ele.scrollHeight - ele.offsetHeight)) {
+ this.shouldScroll = true;
+ this.forceUpdate();
+ }
+ }
+
+ private scrollToBottom() {
+ const ele = this.commandsRef.current;
+ ele && ele.scroll({ behavior: "auto", top: ele.scrollHeight });
+ }
+
+ componentDidUpdate() {
+ if (this.shouldScroll) {
+ this.shouldScroll = false;
+ this.scrollToBottom();
+ }
+ }
+
+ overlayDisposer?: () => void;
+ onFocus = () => {
+ if (this.overlayDisposer) {
+ this.overlayDisposer();
+ }
+ this.overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 });
+ }
+
+ onBlur = () => {
+ this.overlayDisposer && this.overlayDisposer();
+ }
+
+ render() {
+ return (
+ <div className="scriptingRepl-outerContainer">
+ <div className="scriptingRepl-commandsContainer" ref={this.commandsRef}>
+ {this.commands.map(({ command, result }, i) => {
+ return (
+ <div className="scriptingRepl-resultContainer" key={i}>
+ <div className="scriptingRepl-commandString">{command || <br />}</div>
+ <div className="scriptingRepl-commandResult">{<ScriptingValueDisplay scrollToBottom={this.maybeScrollToBottom} value={result} />}</div>
+ </div>
+ );
+ })}
+ </div>
+ <input
+ className="scriptingRepl-commandInput"
+ onFocus={this.onFocus}
+ onBlur={this.onBlur}
+ value={this.commandString}
+ onChange={this.onChange}
+ onKeyDown={this.onKeyDown}></input>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/SearchBox.tsx b/src/client/views/SearchBox.tsx
new file mode 100644
index 000000000..33cb63df5
--- /dev/null
+++ b/src/client/views/SearchBox.tsx
@@ -0,0 +1,183 @@
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faObjectGroup, faSearch } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import * as rp from 'request-promise';
+import { Doc } from '../../new_fields/Doc';
+import { Id } from '../../new_fields/FieldSymbols';
+import { NumCast } from '../../new_fields/Types';
+import { DocServer } from '../DocServer';
+import { Docs } from '../documents/Documents';
+import { SetupDrag } from '../util/DragManager';
+import { SearchItem } from './search/SearchItem';
+import "./SearchBox.scss";
+import { Utils } from '../../Utils';
+
+library.add(faSearch);
+library.add(faObjectGroup);
+
+@observer
+export class SearchBox extends React.Component {
+ @observable
+ searchString: string = "";
+
+ @observable private _open: boolean = false;
+ @observable private _resultsOpen: boolean = false;
+
+ @observable
+ private _results: Doc[] = [];
+
+ @action.bound
+ onChange(e: React.ChangeEvent<HTMLInputElement>) {
+ this.searchString = e.target.value;
+ }
+
+ @action
+ submitSearch = async () => {
+ let query = this.searchString;
+ //gets json result into a list of documents that can be used
+ const results = await this.getResults(query);
+
+ runInAction(() => {
+ this._resultsOpen = true;
+ this._results = results;
+ });
+ }
+
+ @action
+ getResults = async (query: string) => {
+ let response = await rp.get(Utils.prepend('/search'), {
+ qs: {
+ query
+ }
+ });
+ let res: string[] = JSON.parse(response);
+ const fields = await DocServer.GetRefFields(res);
+ const docs: Doc[] = [];
+ for (const id of res) {
+ const field = fields[id];
+ if (field instanceof Doc) {
+ docs.push(field);
+ }
+ }
+ return docs;
+ }
+
+ @action
+ handleClickFilter = (e: Event): void => {
+ var className = (e.target as any).className;
+ var id = (e.target as any).id;
+ if (className !== "filter-button" && className !== "filter-form") {
+ this._open = false;
+ }
+
+ }
+
+ @action
+ handleClickResults = (e: Event): void => {
+ var className = (e.target as any).className;
+ var id = (e.target as any).id;
+ if (id !== "result") {
+ this._resultsOpen = false;
+ this._results = [];
+ }
+
+ }
+
+ componentWillMount() {
+ document.addEventListener('mousedown', this.handleClickFilter, false);
+ document.addEventListener('mousedown', this.handleClickResults, false);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('mousedown', this.handleClickFilter, false);
+ document.removeEventListener('mousedown', this.handleClickResults, false);
+ }
+
+ @action
+ toggleFilterDisplay = () => {
+ this._open = !this._open;
+ }
+
+ enter = (e: React.KeyboardEvent<HTMLInputElement>) => {
+ if (e.key === "Enter") {
+ this.submitSearch();
+ }
+ }
+
+ collectionRef = React.createRef<HTMLSpanElement>();
+ startDragCollection = async () => {
+ const results = await this.getResults(this.searchString);
+ const docs = results.map(doc => {
+ const isProto = Doc.GetT(doc, "isPrototype", "boolean", true);
+ if (isProto) {
+ return Doc.MakeDelegate(doc);
+ } else {
+ return Doc.MakeAlias(doc);
+ }
+ });
+ let x = 0;
+ let y = 0;
+ for (const doc of docs) {
+ doc.x = x;
+ doc.y = y;
+ const size = 200;
+ const aspect = NumCast(doc.nativeHeight) / NumCast(doc.nativeWidth, 1);
+ if (aspect > 1) {
+ doc.height = size;
+ doc.width = size / aspect;
+ } else if (aspect > 0) {
+ doc.width = size;
+ doc.height = size * aspect;
+ } else {
+ doc.width = size;
+ doc.height = size;
+ }
+ doc.zoomBasis = 1;
+ x += 250;
+ if (x > 1000) {
+ x = 0;
+ y += 300;
+ }
+ }
+ return Docs.Create.FreeformDocument(docs, { width: 400, height: 400, panX: 175, panY: 175, backgroundColor: "grey", title: `Search Docs: "${this.searchString}"` });
+ }
+
+ // Useful queries:
+ // Delegates of a document: {!join from=id to=proto_i}id:{protoId}
+ // Documents in a collection: {!join from=data_l to=id}id:{collectionProtoId}
+ render() {
+ return (
+ <div>
+ <div className="searchBox-container">
+ <div className="searchBox-bar">
+ <span onPointerDown={SetupDrag(this.collectionRef, this.startDragCollection)} ref={this.collectionRef}>
+ <FontAwesomeIcon icon="object-group" className="searchBox-barChild" size="lg" />
+ </span>
+ <input value={this.searchString} onChange={this.onChange} type="text" placeholder="Search..."
+ className="searchBox-barChild searchBox-input" onKeyPress={this.enter}
+ style={{ width: this._resultsOpen ? "500px" : undefined }} />
+ {/* <button className="searchBox-barChild searchBox-filter" onClick={this.toggleFilterDisplay}>Filter</button> */}
+ {/* <FontAwesomeIcon icon="search" size="lg" className="searchBox-barChild searchBox-submit" /> */}
+ </div>
+ {this._resultsOpen ? (
+ <div className="searchBox-results">
+ {this._results.map(result => <SearchItem doc={result} key={result[Id]} highlighting={[]} />)}
+ </div>
+ ) : null}
+ </div>
+ {this._open ? (
+ <div className="filter-form" id="filter" style={this._open ? { display: "flex" } : { display: "none" }}>
+ <div className="filter-form" id="header">Filter Search Results</div>
+ <div className="filter-form" id="option">
+ filter by collection, key, type of node
+ </div>
+
+ </div>
+ ) : null}
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/SearchItem.tsx b/src/client/views/SearchItem.tsx
new file mode 100644
index 000000000..13e4b88f7
--- /dev/null
+++ b/src/client/views/SearchItem.tsx
@@ -0,0 +1,69 @@
+import React = require("react");
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { Doc } from "../../new_fields/Doc";
+import { DocumentManager } from "../util/DocumentManager";
+import { SetupDrag } from "../util/DragManager";
+
+
+export interface SearchProps {
+ doc: Doc;
+}
+
+library.add(faCaretUp);
+library.add(faObjectGroup);
+library.add(faStickyNote);
+library.add(faFilePdf);
+library.add(faFilm);
+
+export class SearchItem extends React.Component<SearchProps> {
+
+ onClick = () => {
+ DocumentManager.Instance.jumpToDocument(this.props.doc, 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 1b32f0ddd..393e97a7e 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -7,6 +7,10 @@ import { DocumentView } from "./nodes/DocumentView";
import { Template } from "./Templates";
import React = require("react");
import { undoBatch } from "../util/UndoManager";
+import { DocumentManager } from "../util/DocumentManager";
+import { NumCast } from "../../new_fields/Types";
+import { DragManager } from "../util/DragManager";
+import { SelectionManager } from "../util/SelectionManager";
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -35,11 +39,43 @@ export interface TemplateMenuProps {
@observer
export class TemplateMenu extends React.Component<TemplateMenuProps> {
@observable private _hidden: boolean = true;
+ dragRef = React.createRef<HTMLUListElement>();
constructor(props: TemplateMenuProps) {
super(props);
}
+ toggleFloat = (e: React.MouseEvent): void => {
+ SelectionManager.DeselectAll();
+ let topDocView = this.props.docs[0];
+ let topDoc = topDocView.props.Document;
+ let xf = topDocView.props.ScreenToLocalTransform();
+ let ex = e.clientX;
+ let ey = e.clientY;
+ undoBatch(action(() => topDoc.z = topDoc.z ? 0 : 1))();
+ if (!topDoc.z) {
+ setTimeout(() => {
+ let newDocView = DocumentManager.Instance.getDocumentView(topDoc);
+ if (newDocView) {
+ let de = new DragManager.DocumentDragData([topDoc], [undefined]);
+ de.moveDocument = topDocView.props.moveDocument;
+ let xf = newDocView.ContentDiv!.getBoundingClientRect();
+ DragManager.StartDocumentDrag([newDocView.ContentDiv!], de, ex, ey, {
+ offsetX: (ex - xf.left), offsetY: (ey - xf.top),
+ handlers: { dragComplete: () => { }, },
+ hideSource: false
+ });
+ }
+ }, 10);
+ } else if (topDocView.props.ContainingCollectionView) {
+ let collView = topDocView.props.ContainingCollectionView;
+ let [sx, sy] = xf.inverse().transformPoint(0, 0);
+ let [x, y] = collView.props.ScreenToLocalTransform().transformPoint(sx, sy);
+ topDoc.x = x;
+ topDoc.y = y;
+ }
+ }
+
@undoBatch
@action
toggleTemplate = (event: React.ChangeEvent<HTMLInputElement>, template: Template): void => {
@@ -89,9 +125,10 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
return (
<div className="templating-menu" >
<div title="Template Options" className="templating-button" onClick={() => this.toggleTemplateActivity()}>+</div>
- <ul id="template-list" style={{ display: this._hidden ? "none" : "block" }}>
+ <ul id="template-list" ref={this.dragRef} style={{ display: this._hidden ? "none" : "block" }}>
{templateMenu}
- <button style={{ display: this._hidden ? "none" : "block" }} onClick={this.clearTemplates}>Clear</button>
+ <button onClick={this.toggleFloat}>Float</button>
+ <button onClick={this.clearTemplates}>Clear</button>
</ul>
</div>
);
diff --git a/src/client/views/collections/CollectionBaseView.scss b/src/client/views/collections/CollectionBaseView.scss
index 34bcb705e..583e6f6ca 100644
--- a/src/client/views/collections/CollectionBaseView.scss
+++ b/src/client/views/collections/CollectionBaseView.scss
@@ -6,7 +6,7 @@
border-radius: 0 0 $border-radius $border-radius;
box-sizing: border-box;
border-radius: inherit;
- pointer-events: all;
width:100%;
height:100%;
+ overflow: auto;
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
index eba69b448..6801b94fd 100644
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -18,7 +18,8 @@ export enum CollectionViewType {
Schema,
Docking,
Tree,
- Stacking
+ Stacking,
+ Masonry
}
export interface CollectionRenderProps {
@@ -31,7 +32,7 @@ export interface CollectionRenderProps {
export interface CollectionViewProps extends FieldViewProps {
onContextMenu?: (e: React.MouseEvent) => void;
- children: (type: CollectionViewType, props: CollectionRenderProps) => JSX.Element | JSX.Element[] | null;
+ children: (type: CollectionViewType, props: CollectionRenderProps) => JSX.Element | JSX.Element[] | null | (JSX.Element | null)[];
className?: string;
contentRef?: React.Ref<HTMLDivElement>;
}
@@ -78,7 +79,6 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
@action.bound
addDocument(doc: Doc, allowDuplicates: boolean = false): boolean {
- let self = this;
var curPage = NumCast(this.props.Document.curPage, -1);
Doc.GetProto(doc).page = curPage;
if (curPage >= 0) {
@@ -124,9 +124,8 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
@action.bound
moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean {
let self = this;
- let targetDataDoc = this.props.fieldExt || this.props.Document.isTemplate ? this.extensionDoc : this.props.Document;
+ let targetDataDoc = this.props.Document;
if (Doc.AreProtosEqual(targetDataDoc, targetCollection)) {
- //if (Doc.AreProtosEqual(this.extensionDoc, targetCollection)) {
return true;
}
if (this.removeDocument(doc)) {
@@ -146,7 +145,10 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
const viewtype = this.collectionViewType;
return (
<div id="collectionBaseView"
- style={{ boxShadow: `#9c9396 ${StrCast(this.props.Document.boxShadow, "0.2vw 0.2vw 0.8vw")}` }}
+ style={{
+ pointerEvents: this.props.Document.isBackground ? "none" : "all",
+ boxShadow: this.props.Document.isBackground ? undefined : `#9c9396 ${StrCast(this.props.Document.boxShadow, "0.2vw 0.2vw 0.8vw")}`
+ }}
className={this.props.className || "collectionView-cont"}
onContextMenu={this.props.onContextMenu} ref={this.props.contentRef}>
{viewtype !== undefined ? this.props.children(viewtype, props) : (null)}
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index a193ff677..f559480ed 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -10,7 +10,7 @@ import { Id } from '../../../new_fields/FieldSymbols';
import { FieldId } from "../../../new_fields/RefField";
import { listSpec } from "../../../new_fields/Schema";
import { Cast, NumCast, StrCast, BoolCast } from "../../../new_fields/Types";
-import { emptyFunction, returnTrue, Utils, returnOne } from "../../../Utils";
+import { emptyFunction, returnTrue, Utils, returnOne, returnEmptyString } from "../../../Utils";
import { DocServer } from "../../DocServer";
import { DocumentManager } from '../../util/DocumentManager';
import { DragLinksAsDocuments, DragManager } from "../../util/DragManager";
@@ -210,8 +210,23 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
docs.push(document);
}
let docContentConfig = CollectionDockingView.makeDocumentConfig(document, dataDocument);
- var newContentItem = stack.layoutManager.createContentItem(docContentConfig, this._goldenLayout);
- stack.addChild(newContentItem.contentItems[0], undefined);
+ if (stack === undefined) {
+ let stack: any = this._goldenLayout.root;
+ while (!stack.isStack) {
+ if (stack.contentItems.length) {
+ stack = stack.contentItems[0];
+ } else {
+ stack.addChild({ type: 'stack', content: [docContentConfig] });
+ stack = undefined;
+ break;
+ }
+ }
+ if (stack) {
+ stack.addChild(docContentConfig);
+ }
+ } else {
+ stack.addChild(docContentConfig, undefined);
+ }
this.layoutChanged();
}
@@ -465,7 +480,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
.off('click') //unbind the current click handler
.click(action(function () {
stack.config.fixed = !stack.config.fixed;
- // var url = DocServer.prepend("/doc/" + stack.contentItems[0].tab.contentItem.config.props.documentId);
+ // var url = Utils.prepend("/doc/" + stack.contentItems[0].tab.contentItem.config.props.documentId);
// let win = window.open(url, stack.contentItems[0].tab.title, "width=300,height=400");
}));
}
@@ -539,17 +554,17 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
}
}
+ panelWidth = () => Math.min(this._panelWidth, Math.max(NumCast(this._document!.width), this.nativeWidth()));
+ panelHeight = () => Math.min(this._panelHeight, Math.max(NumCast(this._document!.height), NumCast(this._document!.nativeHeight, this._panelHeight)));
+
+ nativeWidth = () => !BoolCast(this._document!.ignoreAspect) ? NumCast(this._document!.nativeWidth, this._panelWidth) : 0;
+ nativeHeight = () => !BoolCast(this._document!.ignoreAspect) ? NumCast(this._document!.nativeHeight, this._panelHeight) : 0;
- nativeWidth = () => NumCast(this._document!.nativeWidth, this._panelWidth);
- 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();
- let wscale = this._panelWidth / nativeW;
+ if (!nativeW || !nativeH) return 1;
+ let wscale = this.panelWidth() / nativeW;
return wscale * nativeH > this._panelHeight ? this._panelHeight / nativeH : wscale;
}
@@ -561,18 +576,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
}
return Transform.Identity();
}
- get scaleToFitMultiplier() {
- let docWidth = NumCast(this._document!.width);
- 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);
- return scaling;
- }
- get previewPanelCenteringOffset() { return (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2; }
+ get previewPanelCenteringOffset() { return this.nativeWidth && !BoolCast(this._document!.ignoreAspect) ? (this._panelWidth - this.nativeWidth()) / 2 : 0; }
addDocTab = (doc: Doc, dataDoc: Doc | undefined, location: string) => {
if (doc.dockingConfig) {
@@ -595,14 +599,15 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
addDocument={undefined}
removeDocument={undefined}
ContentScaling={this.contentScaling}
- PanelWidth={this.nativeWidth}
- PanelHeight={this.nativeHeight}
+ PanelWidth={this.panelWidth}
+ PanelHeight={this.panelHeight}
ScreenToLocalTransform={this.ScreenToLocalTransform}
renderDepth={0}
selectOnLoad={false}
parentActive={returnTrue}
whenActiveChanged={emptyFunction}
focus={emptyFunction}
+ backgroundColor={returnEmptyString}
addDocTab={this.addDocTab}
ContainingCollectionView={undefined}
zoomToScale={emptyFunction}
@@ -610,18 +615,15 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
}
@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})` }}>
+ style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)` }}>
{this.docView}
</div >);
}
render() {
- if (!this._isActive) return null;
+ if (!this._isActive || !this._document) 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; })}>
diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx
index 781692eef..70010819a 100644
--- a/src/client/views/collections/CollectionPDFView.tsx
+++ b/src/client/views/collections/CollectionPDFView.tsx
@@ -29,15 +29,7 @@ export class CollectionPDFView extends React.Component<FieldViewProps> {
this._reactionDisposer = reaction(
() => NumCast(this.props.Document.scrollY),
() => {
- // let transform = this.props.ScreenToLocalTransform();
- // if (this._buttonTray.current) {
- // console.log(this._buttonTray.current.offsetHeight);
- // console.log(NumCast(this.props.Document.scrollY));
- let scale = this.nativeWidth() / this.props.Document[WidthSym]();
- this.props.Document.panY = NumCast(this.props.Document.scrollY) - this.props.Document[HeightSym]();
- // console.log(scale);
- // }
- // console.log(this.props.Document[HeightSym]());
+ this.props.Document.panY = NumCast(this.props.Document.scrollY);
},
{ fireImmediately: true }
);
@@ -86,7 +78,7 @@ export class CollectionPDFView extends React.Component<FieldViewProps> {
let props = { ...this.props, ...renderProps };
return (
<>
- <CollectionFreeFormView {...props} setPdfBox={this.setPdfBox} CollectionView={this} />
+ <CollectionFreeFormView {...props} setPdfBox={this.setPdfBox} CollectionView={this} chromeCollapsed={true} />
{renderProps.active() ? this.uIButtons : (null)}
</>
);
diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx
new file mode 100644
index 000000000..7e3061354
--- /dev/null
+++ b/src/client/views/collections/CollectionSchemaCells.tsx
@@ -0,0 +1,312 @@
+import React = require("react");
+import { action, computed, observable, trace, untracked, toJS } from "mobx";
+import { observer } from "mobx-react";
+import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults, Column } from "react-table";
+import "react-table/react-table.css";
+import { emptyFunction, returnFalse, returnZero, returnOne } from "../../../Utils";
+import { Doc, DocListCast, DocListCastAsync, Field, Opt } from "../../../new_fields/Doc";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { SetupDrag, DragManager } from "../../util/DragManager";
+import { CompileScript } from "../../util/Scripting";
+import { Transform } from "../../util/Transform";
+import { COLLECTION_BORDER_WIDTH, MAX_ROW_HEIGHT } from '../globalCssVariables.scss';
+import '../DocumentDecorations.scss';
+import { EditableView } from "../EditableView";
+import { FieldView, FieldViewProps } from "../nodes/FieldView";
+import { CollectionPDFView } from "./CollectionPDFView";
+import "./CollectionSchemaView.scss";
+import { CollectionVideoView } from "./CollectionVideoView";
+import { CollectionView } from "./CollectionView";
+import { NumCast, StrCast, BoolCast, FieldValue, Cast } from "../../../new_fields/Types";
+import { Docs } from "../../documents/Documents";
+import { DocumentContentsView } from "../nodes/DocumentContentsView";
+import { SelectionManager } from "../../util/SelectionManager";
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faExpand } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
+import { KeyCodes } from "../../northstar/utils/KeyCodes";
+import { undoBatch } from "../../util/UndoManager";
+
+library.add(faExpand);
+
+export interface CellProps {
+ row: number;
+ col: number;
+ rowProps: CellInfo;
+ CollectionView: CollectionView | CollectionPDFView | CollectionVideoView;
+ ContainingCollection: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
+ Document: Doc;
+ fieldKey: string;
+ renderDepth: number;
+ addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+ moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
+ isFocused: boolean;
+ changeFocusedCellByIndex: (row: number, col: number) => void;
+ setIsEditing: (isEditing: boolean) => void;
+ isEditable: boolean;
+ setPreviewDoc: (doc: Doc) => void;
+ setComputed: (script: string, doc: Doc, field: string, row: number, col: number) => boolean;
+ getField: (row: number, col?: number) => void;
+}
+
+@observer
+export class CollectionSchemaCell extends React.Component<CellProps> {
+ @observable protected _isEditing: boolean = false;
+ protected _focusRef = React.createRef<HTMLDivElement>();
+ protected _document = this.props.rowProps.original;
+ private _dropDisposer?: DragManager.DragDropDisposer;
+
+ componentDidMount() {
+ document.addEventListener("keydown", this.onKeyDown);
+
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener("keydown", this.onKeyDown);
+ }
+
+ @action
+ onKeyDown = (e: KeyboardEvent): void => {
+ if (this.props.isFocused && this.props.isEditable && e.keyCode === KeyCodes.ENTER) {
+ document.removeEventListener("keydown", this.onKeyDown);
+ this._isEditing = true;
+ this.props.setIsEditing(true);
+
+ }
+ }
+
+ @action
+ isEditingCallback = (isEditing: boolean): void => {
+ document.addEventListener("keydown", this.onKeyDown);
+ this._isEditing = isEditing;
+ this.props.setIsEditing(isEditing);
+ this.props.changeFocusedCellByIndex(this.props.row, this.props.col);
+ }
+
+ @action
+ onPointerDown = (e: React.PointerEvent): void => {
+ this.props.changeFocusedCellByIndex(this.props.row, this.props.col);
+ this.props.setPreviewDoc(this.props.rowProps.original);
+
+ // this._isEditing = true;
+ // this.props.setIsEditing(true);
+
+ let field = this.props.rowProps.original[this.props.rowProps.column.id!];
+ let doc = FieldValue(Cast(field, Doc));
+ if (typeof field === "object" && doc) this.props.setPreviewDoc(doc);
+ }
+
+ @undoBatch
+ applyToDoc = (doc: Doc, row: number, col: number, run: (args?: { [name: string]: any }) => any) => {
+ const res = run({ this: doc, $r: row, $c: col, $: (r: number = 0, c: number = 0) => this.props.getField(r + row, c + col) });
+ if (!res.success) return false;
+ // doc[this.props.fieldKey] = res.result;
+ // return true;
+ doc[this.props.rowProps.column.id as string] = res.result;
+ return true;
+ }
+
+ private drop = (e: Event, de: DragManager.DropEvent) => {
+ if (de.data instanceof DragManager.DocumentDragData) {
+ let fieldKey = this.props.rowProps.column.id as string;
+ if (de.data.draggedDocuments.length === 1) {
+ this._document[fieldKey] = de.data.draggedDocuments[0];
+ }
+ else {
+ let coll = Docs.Create.SchemaDocument([new SchemaHeaderField("title", "#f1efeb")], de.data.draggedDocuments, {});
+ this._document[fieldKey] = coll;
+ }
+ e.stopPropagation();
+ }
+ }
+
+ private dropRef = (ele: HTMLElement | null) => {
+ this._dropDisposer && this._dropDisposer();
+ if (ele) {
+ this._dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
+ }
+ }
+
+ // expandDoc = (e: React.PointerEvent) => {
+ // let field = this.props.rowProps.original[this.props.rowProps.column.id as string];
+ // let doc = FieldValue(Cast(field, Doc));
+
+ // console.log("Expanding doc", StrCast(doc!.title));
+ // this.props.setPreviewDoc(doc!);
+
+ // // this.props.changeFocusedCellByIndex(this.props.row, this.props.col);
+
+ // e.stopPropagation();
+ // }
+
+ renderCellWithType(type: string | undefined) {
+ let dragRef: React.RefObject<HTMLDivElement> = React.createRef();
+
+ let props: FieldViewProps = {
+ Document: this.props.rowProps.original,
+ DataDoc: this.props.rowProps.original,
+ fieldKey: this.props.rowProps.column.id as string,
+ fieldExt: "",
+ ContainingCollectionView: this.props.CollectionView,
+ isSelected: returnFalse,
+ select: emptyFunction,
+ renderDepth: this.props.renderDepth + 1,
+ selectOnLoad: false,
+ ScreenToLocalTransform: Transform.Identity,
+ focus: emptyFunction,
+ active: returnFalse,
+ whenActiveChanged: emptyFunction,
+ PanelHeight: returnZero,
+ PanelWidth: returnZero,
+ addDocTab: this.props.addDocTab,
+ ContentScaling: returnOne
+ };
+
+ let field = props.Document[props.fieldKey];
+ let doc = FieldValue(Cast(field, Doc));
+ let fieldIsDoc = (type === "document" && typeof field === "object") || (typeof field === "object" && doc);
+
+ let onItemDown = (e: React.PointerEvent) => {
+ if (fieldIsDoc) {
+ SetupDrag(this._focusRef, () => this._document[props.fieldKey] instanceof Doc ? this._document[props.fieldKey] : this._document,
+ this._document[props.fieldKey] instanceof Doc ? (doc: Doc, target: Doc, addDoc: (newDoc: Doc) => any) => addDoc(doc) : this.props.moveDocument, this._document[props.fieldKey] instanceof Doc ? "alias" : this.props.Document.schemaDoc ? "copy" : undefined)(e);
+ }
+ };
+ let onPointerEnter = (e: React.PointerEvent): void => {
+ if (e.buttons === 1 && SelectionManager.GetIsDragging() && (type === "document" || type === undefined)) {
+ dragRef.current!.className = "collectionSchemaView-cellContainer doc-drag-over";
+ }
+ };
+ let onPointerLeave = (e: React.PointerEvent): void => {
+ dragRef.current!.className = "collectionSchemaView-cellContainer";
+ };
+
+ let contents: any = "incorrect type";
+ if (type === undefined) contents = <FieldView {...props} />;
+ if (type === "number") contents = typeof field === "number" ? NumCast(field) : "--" + typeof field + "--";
+ if (type === "string") contents = typeof field === "string" ? (StrCast(field) === "" ? "--" : StrCast(field)) : "--" + typeof field + "--";
+ if (type === "boolean") contents = typeof field === "boolean" ? (BoolCast(field) ? "true" : "false") : "--" + typeof field + "--";
+ if (type === "document") {
+ let doc = FieldValue(Cast(field, Doc));
+ contents = typeof field === "object" ? doc ? StrCast(doc.title) === "" ? "--" : StrCast(doc.title) : `--${typeof field}--` : `--${typeof field}--`;
+ }
+
+ let className = "collectionSchemaView-cellWrapper";
+ if (this._isEditing) className += " editing";
+ if (this.props.isFocused && this.props.isEditable) className += " focused";
+ if (this.props.isFocused && !this.props.isEditable) className += " inactive";
+
+
+ // let docExpander = (
+ // <div className="collectionSchemaView-cellContents-docExpander" onPointerDown={this.expandDoc} >
+ // <FontAwesomeIcon icon="expand" size="sm" />
+ // </div>
+ // );
+
+ return (
+ <div className="collectionSchemaView-cellContainer" style={{ cursor: fieldIsDoc ? "grab" : "auto" }} ref={dragRef} onPointerDown={this.onPointerDown} onPointerEnter={onPointerEnter} onPointerLeave={onPointerLeave}>
+ <div className={className} ref={this._focusRef} onPointerDown={onItemDown} tabIndex={-1}>
+ <div className="collectionSchemaView-cellContents" ref={type === undefined || type === "document" ? this.dropRef : null} key={props.Document[Id]}>
+ <EditableView
+ editing={this._isEditing}
+ isEditingCallback={this.isEditingCallback}
+ display={"inline"}
+ contents={contents}
+ height={Number(MAX_ROW_HEIGHT)}
+ GetValue={() => {
+ let field = props.Document[props.fieldKey];
+ if (Field.IsField(field)) {
+ return Field.toScriptString(field);
+ }
+ return "";
+ }
+ }
+ SetValue={(value: string) => {
+ if (value.startsWith(":=")) {
+ return this.props.setComputed(value.substring(2), props.Document, this.props.rowProps.column.id!, this.props.row, this.props.col);
+ }
+ let script = CompileScript(value, { requiredType: type, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
+ if (!script.compiled) {
+ return false;
+ }
+ return this.applyToDoc(props.Document, this.props.row, this.props.col, script.run);
+ }}
+ OnFillDown={async (value: string) => {
+ let script = CompileScript(value, { requiredType: type, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
+ if (!script.compiled) {
+ return;
+ }
+ const run = script.run;
+ const val = await DocListCastAsync(this.props.Document[this.props.fieldKey]);
+ val && val.forEach((doc, i) => this.applyToDoc(doc, i, this.props.col, run));
+ }}
+ />
+ </div >
+ {/* {fieldIsDoc ? docExpander : null} */}
+ </div>
+ </div>
+ );
+ }
+
+ render() {
+ return this.renderCellWithType(undefined);
+ }
+}
+
+@observer
+export class CollectionSchemaNumberCell extends CollectionSchemaCell {
+ render() {
+ return this.renderCellWithType("number");
+ }
+}
+
+@observer
+export class CollectionSchemaBooleanCell extends CollectionSchemaCell {
+ render() {
+ return this.renderCellWithType("boolean");
+ }
+}
+
+@observer
+export class CollectionSchemaStringCell extends CollectionSchemaCell {
+ render() {
+ return this.renderCellWithType("string");
+ }
+}
+
+@observer
+export class CollectionSchemaDocCell extends CollectionSchemaCell {
+ render() {
+ return this.renderCellWithType("document");
+ }
+}
+
+@observer
+export class CollectionSchemaCheckboxCell extends CollectionSchemaCell {
+ @observable private _isChecked: boolean = typeof this.props.rowProps.original[this.props.rowProps.column.id as string] === "boolean" ? BoolCast(this.props.rowProps.original[this.props.rowProps.column.id as string]) : false;
+
+ @action
+ toggleChecked = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this._isChecked = e.target.checked;
+ let script = CompileScript(e.target.checked.toString(), { requiredType: "boolean", addReturn: true, params: { this: Doc.name } });
+ if (script.compiled) {
+ this.applyToDoc(this._document, this.props.row, this.props.col, script.run);
+ }
+ }
+
+ render() {
+ let reference = React.createRef<HTMLDivElement>();
+ let onItemDown = (e: React.PointerEvent) => {
+ (!this.props.CollectionView.props.isSelected() ? undefined :
+ SetupDrag(reference, () => this._document, this.props.moveDocument, this.props.Document.schemaDoc ? "copy" : undefined)(e));
+ };
+ return (
+ <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}>
+ <div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} key={this._document[Id]} ref={reference}>
+ <input type="checkbox" checked={this._isChecked} onChange={this.toggleChecked} />
+ </div >
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/collections/CollectionSchemaHeaders.tsx b/src/client/views/collections/CollectionSchemaHeaders.tsx
new file mode 100644
index 000000000..d24f63fbb
--- /dev/null
+++ b/src/client/views/collections/CollectionSchemaHeaders.tsx
@@ -0,0 +1,366 @@
+import React = require("react");
+import { action, computed, observable, trace, untracked } from "mobx";
+import { observer } from "mobx-react";
+import "./CollectionSchemaView.scss";
+import { faPlus, faFont, faHashtag, faAlignJustify, faCheckSquare, faToggleOn, faSortAmountDown, faSortAmountUp, faTimes } from '@fortawesome/free-solid-svg-icons';
+import { library, IconProp } from "@fortawesome/fontawesome-svg-core";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { Flyout, anchorPoints } from "../DocumentDecorations";
+import { ColumnType } from "./CollectionSchemaView";
+import { emptyFunction } from "../../../Utils";
+import { contains } from "typescript-collections/dist/lib/arrays";
+import { faFile } from "@fortawesome/free-regular-svg-icons";
+import { SchemaHeaderField, RandomPastel, PastelSchemaPalette } from "../../../new_fields/SchemaHeaderField";
+import { undoBatch } from "../../util/UndoManager";
+
+library.add(faPlus, faFont, faHashtag, faAlignJustify, faCheckSquare, faToggleOn, faFile as any, faSortAmountDown, faSortAmountUp, faTimes);
+
+export interface HeaderProps {
+ keyValue: SchemaHeaderField;
+ possibleKeys: string[];
+ existingKeys: string[];
+ keyType: ColumnType;
+ typeConst: boolean;
+ onSelect: (oldKey: string, newKey: string, addnew: boolean) => void;
+ setIsEditing: (isEditing: boolean) => void;
+ deleteColumn: (column: string) => void;
+ setColumnType: (column: SchemaHeaderField, type: ColumnType) => void;
+ setColumnSort: (column: SchemaHeaderField, desc: boolean | undefined) => void;
+ setColumnColor: (column: SchemaHeaderField, color: string) => void;
+
+}
+
+export class CollectionSchemaHeader extends React.Component<HeaderProps> {
+ render() {
+ let icon: IconProp = this.props.keyType === ColumnType.Number ? "hashtag" : this.props.keyType === ColumnType.String ? "font" :
+ this.props.keyType === ColumnType.Boolean ? "check-square" : this.props.keyType === ColumnType.Doc ? "file" : "align-justify";
+ return (
+ <div className="collectionSchemaView-header" style={{ background: this.props.keyValue.color }}>
+ <CollectionSchemaColumnMenu
+ columnField={this.props.keyValue}
+ // keyValue={this.props.keyValue.heading}
+ possibleKeys={this.props.possibleKeys}
+ existingKeys={this.props.existingKeys}
+ // keyType={this.props.keyType}
+ typeConst={this.props.typeConst}
+ menuButtonContent={<div><FontAwesomeIcon icon={icon} size="sm" />{this.props.keyValue.heading}</div>}
+ addNew={false}
+ onSelect={this.props.onSelect}
+ setIsEditing={this.props.setIsEditing}
+ deleteColumn={this.props.deleteColumn}
+ onlyShowOptions={false}
+ setColumnType={this.props.setColumnType}
+ setColumnSort={this.props.setColumnSort}
+ setColumnColor={this.props.setColumnColor}
+ />
+ </div>
+ );
+ }
+}
+
+
+export interface AddColumnHeaderProps {
+ createColumn: () => void;
+}
+
+@observer
+export class CollectionSchemaAddColumnHeader extends React.Component<AddColumnHeaderProps> {
+ render() {
+ return (
+ <button className="add-column" onClick={() => this.props.createColumn()}><FontAwesomeIcon icon="plus" size="sm" /></button>
+ );
+ }
+}
+
+export interface ColumnMenuProps {
+ columnField: SchemaHeaderField;
+ // keyValue: string;
+ possibleKeys: string[];
+ existingKeys: string[];
+ // keyType: ColumnType;
+ typeConst: boolean;
+ menuButtonContent: JSX.Element;
+ addNew: boolean;
+ onSelect: (oldKey: string, newKey: string, addnew: boolean) => void;
+ setIsEditing: (isEditing: boolean) => void;
+ deleteColumn: (column: string) => void;
+ onlyShowOptions: boolean;
+ setColumnType: (column: SchemaHeaderField, type: ColumnType) => void;
+ setColumnSort: (column: SchemaHeaderField, desc: boolean | undefined) => void;
+ anchorPoint?: any;
+ setColumnColor: (column: SchemaHeaderField, color: string) => void;
+}
+@observer
+export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> {
+ @observable private _isOpen: boolean = false;
+ @observable private _node: HTMLDivElement | null = null;
+
+ componentDidMount() {
+ document.addEventListener("pointerdown", this.detectClick);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener("pointerdown", this.detectClick);
+ }
+
+ detectClick = (e: PointerEvent): void => {
+ if (this._node && this._node.contains(e.target as Node)) {
+ } else {
+ this._isOpen = false;
+ this.props.setIsEditing(false);
+ }
+ }
+
+ @action
+ toggleIsOpen = (): void => {
+ this._isOpen = !this._isOpen;
+ this.props.setIsEditing(this._isOpen);
+ }
+
+ changeColumnType = (type: ColumnType): void => {
+ this.props.setColumnType(this.props.columnField, type);
+ }
+
+ changeColumnSort = (desc: boolean | undefined): void => {
+ this.props.setColumnSort(this.props.columnField, desc);
+ }
+
+ changeColumnColor = (color: string): void => {
+ this.props.setColumnColor(this.props.columnField, color);
+ }
+
+ @action
+ setNode = (node: HTMLDivElement): void => {
+ if (node) {
+ this._node = node;
+ }
+ }
+
+ renderTypes = () => {
+ if (this.props.typeConst) return <></>;
+
+ let type = this.props.columnField.type;
+ return (
+ <div className="collectionSchema-headerMenu-group">
+ <label>Column type:</label>
+ <div className="columnMenu-types">
+ <div className={"columnMenu-option" + (type === ColumnType.Any ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Any)}>
+ <FontAwesomeIcon icon={"align-justify"} size="sm" />
+ Any
+ </div>
+ <div className={"columnMenu-option" + (type === ColumnType.Number ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Number)}>
+ <FontAwesomeIcon icon={"hashtag"} size="sm" />
+ Number
+ </div>
+ <div className={"columnMenu-option" + (type === ColumnType.String ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.String)}>
+ <FontAwesomeIcon icon={"font"} size="sm" />
+ Text
+ </div>
+ <div className={"columnMenu-option" + (type === ColumnType.Boolean ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Boolean)}>
+ <FontAwesomeIcon icon={"check-square"} size="sm" />
+ Checkbox
+ </div>
+ <div className={"columnMenu-option" + (type === ColumnType.Doc ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Doc)}>
+ <FontAwesomeIcon icon={"file"} size="sm" />
+ Document
+ </div>
+ </div>
+ </div >
+ );
+ }
+
+ renderSorting = () => {
+ let sort = this.props.columnField.desc;
+ return (
+ <div className="collectionSchema-headerMenu-group">
+ <label>Sort by:</label>
+ <div className="columnMenu-sort">
+ <div className={"columnMenu-option" + (sort === true ? " active" : "")} onClick={() => this.changeColumnSort(true)}>
+ <FontAwesomeIcon icon="sort-amount-down" size="sm" />
+ Sort descending
+ </div>
+ <div className={"columnMenu-option" + (sort === false ? " active" : "")} onClick={() => this.changeColumnSort(false)}>
+ <FontAwesomeIcon icon="sort-amount-up" size="sm" />
+ Sort ascending
+ </div>
+ <div className="columnMenu-option" onClick={() => this.changeColumnSort(undefined)}>
+ <FontAwesomeIcon icon="times" size="sm" />
+ Clear sorting
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ renderColors = () => {
+ let selected = this.props.columnField.color;
+
+ let pink = PastelSchemaPalette.get("pink2");
+ let purple = PastelSchemaPalette.get("purple2");
+ let blue = PastelSchemaPalette.get("bluegreen1");
+ let yellow = PastelSchemaPalette.get("yellow4");
+ let red = PastelSchemaPalette.get("red2");
+ let gray = "#f1efeb";
+
+ return (
+ <div className="collectionSchema-headerMenu-group">
+ <label>Color:</label>
+ <div className="columnMenu-colors">
+ <div className={"columnMenu-colorPicker" + (selected === pink ? " active" : "")} style={{ backgroundColor: pink }} onClick={() => this.changeColumnColor(pink!)}></div>
+ <div className={"columnMenu-colorPicker" + (selected === purple ? " active" : "")} style={{ backgroundColor: purple }} onClick={() => this.changeColumnColor(purple!)}></div>
+ <div className={"columnMenu-colorPicker" + (selected === blue ? " active" : "")} style={{ backgroundColor: blue }} onClick={() => this.changeColumnColor(blue!)}></div>
+ <div className={"columnMenu-colorPicker" + (selected === yellow ? " active" : "")} style={{ backgroundColor: yellow }} onClick={() => this.changeColumnColor(yellow!)}></div>
+ <div className={"columnMenu-colorPicker" + (selected === red ? " active" : "")} style={{ backgroundColor: red }} onClick={() => this.changeColumnColor(red!)}></div>
+ <div className={"columnMenu-colorPicker" + (selected === gray ? " active" : "")} style={{ backgroundColor: gray }} onClick={() => this.changeColumnColor(gray)}></div>
+ </div>
+ </div>
+ );
+ }
+
+ renderContent = () => {
+ return (
+ <div className="collectionSchema-header-menuOptions">
+ <div className="collectionSchema-headerMenu-group">
+ <label>Key:</label>
+ <KeysDropdown
+ keyValue={this.props.columnField.heading}
+ possibleKeys={this.props.possibleKeys}
+ existingKeys={this.props.existingKeys}
+ canAddNew={true}
+ addNew={this.props.addNew}
+ onSelect={this.props.onSelect}
+ setIsEditing={this.props.setIsEditing}
+ />
+ </div>
+ {this.props.onlyShowOptions ? <></> :
+ <>
+ {this.renderTypes()}
+ {this.renderSorting()}
+ {this.renderColors()}
+ <div className="collectionSchema-headerMenu-group">
+ <button onClick={() => this.props.deleteColumn(this.props.columnField.heading)}>Delete Column</button>
+ </div>
+ </>
+ }
+ </div>
+ );
+ }
+
+ render() {
+ return (
+ <div className="collectionSchema-header-menu" ref={this.setNode}>
+ <Flyout anchorPoint={this.props.anchorPoint ? this.props.anchorPoint : anchorPoints.TOP_CENTER} content={this.renderContent()}>
+ <div className="collectionSchema-header-toggler" onClick={() => this.toggleIsOpen()}>{this.props.menuButtonContent}</div>
+ </ Flyout >
+ </div>
+ );
+ }
+}
+
+
+interface KeysDropdownProps {
+ keyValue: string;
+ possibleKeys: string[];
+ existingKeys: string[];
+ canAddNew: boolean;
+ addNew: boolean;
+ onSelect: (oldKey: string, newKey: string, addnew: boolean) => void;
+ setIsEditing: (isEditing: boolean) => void;
+}
+@observer
+class KeysDropdown extends React.Component<KeysDropdownProps> {
+ @observable private _key: string = this.props.keyValue;
+ @observable private _searchTerm: string = this.props.keyValue;
+ @observable private _isOpen: boolean = false;
+ @observable private _canClose: boolean = true;
+ @observable private _inputRef: React.RefObject<HTMLInputElement> = React.createRef();
+
+ @action setSearchTerm = (value: string): void => { this._searchTerm = value; };
+ @action setKey = (key: string): void => { this._key = key; };
+ @action setIsOpen = (isOpen: boolean): void => { this._isOpen = isOpen; };
+
+ @action
+ onSelect = (key: string): void => {
+ this.props.onSelect(this._key, key, this.props.addNew);
+ this.setKey(key);
+ this._isOpen = false;
+ this.props.setIsEditing(false);
+ }
+
+ @undoBatch
+ @action
+ onKeyDown = (e: React.KeyboardEvent): void => {
+ if (e.key === "Enter") {
+ let keyOptions = this._searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1);
+ let exactFound = keyOptions.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1 ||
+ this.props.existingKeys.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1;
+
+ if (!exactFound && this._searchTerm !== "" && this.props.canAddNew) {
+ this.onSelect(this._searchTerm);
+ } else {
+ this._searchTerm = this._key;
+ }
+ }
+ }
+
+ onChange = (val: string): void => {
+ this.setSearchTerm(val);
+ }
+
+ @action
+ onFocus = (e: React.FocusEvent): void => {
+ this._isOpen = true;
+ this.props.setIsEditing(true);
+ }
+
+ @action
+ onBlur = (e: React.FocusEvent): void => {
+ if (this._canClose) {
+ this._isOpen = false;
+ this.props.setIsEditing(false);
+ }
+ }
+
+ @action
+ onPointerEnter = (e: React.PointerEvent): void => {
+ this._canClose = false;
+ }
+
+ @action
+ onPointerOut = (e: React.PointerEvent): void => {
+ this._canClose = true;
+ }
+
+ renderOptions = (): JSX.Element[] | JSX.Element => {
+ if (!this._isOpen) return <></>;
+
+ let keyOptions = this._searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1);
+ let exactFound = keyOptions.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1 ||
+ this.props.existingKeys.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1;
+
+ let options = keyOptions.map(key => {
+ return <div key={key} className="key-option" onClick={() => { this.onSelect(key); this.setSearchTerm(""); }}>{key}</div>;
+ });
+
+ // if search term does not already exist as a group type, give option to create new group type
+ if (!exactFound && this._searchTerm !== "" && this.props.canAddNew) {
+ options.push(<div key={""} className="key-option"
+ onClick={() => { this.onSelect(this._searchTerm); this.setSearchTerm(""); }}>
+ Create "{this._searchTerm}" key</div>);
+ }
+
+ return options;
+ }
+
+ render() {
+ return (
+ <div className="keys-dropdown">
+ <input className="keys-search" ref={this._inputRef} type="text" value={this._searchTerm} placeholder="Column key" onKeyDown={this.onKeyDown}
+ onChange={e => this.onChange(e.target.value)} onFocus={this.onFocus} onBlur={this.onBlur}></input>
+ <div className="keys-options-wrapper" onPointerEnter={this.onPointerEnter} onPointerOut={this.onPointerOut}>
+ {this.renderOptions()}
+ </div>
+ </div >
+ );
+ }
+}
diff --git a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx
new file mode 100644
index 000000000..ec40043cc
--- /dev/null
+++ b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx
@@ -0,0 +1,245 @@
+import React = require("react");
+import { ReactTableDefaults, TableCellRenderer, ComponentPropsGetterR, ComponentPropsGetter0, RowInfo } from "react-table";
+import "./CollectionSchemaView.scss";
+import { Transform } from "../../util/Transform";
+import { Doc } from "../../../new_fields/Doc";
+import { DragManager, SetupDrag } from "../../util/DragManager";
+import { SelectionManager } from "../../util/SelectionManager";
+import { Cast, FieldValue, StrCast } from "../../../new_fields/Types";
+import { ContextMenu } from "../ContextMenu";
+import { action } from "mobx";
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faGripVertical, faTrash } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { DocumentManager } from "../../util/DocumentManager";
+import { PastelSchemaPalette, SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
+import { undoBatch } from "../../util/UndoManager";
+
+library.add(faGripVertical, faTrash);
+
+export interface MovableColumnProps {
+ columnRenderer: TableCellRenderer;
+ columnValue: SchemaHeaderField;
+ allColumns: SchemaHeaderField[];
+ reorderColumns: (toMove: SchemaHeaderField, relativeTo: SchemaHeaderField, before: boolean, columns: SchemaHeaderField[]) => void;
+ ScreenToLocalTransform: () => Transform;
+}
+export class MovableColumn extends React.Component<MovableColumnProps> {
+ private _header?: React.RefObject<HTMLDivElement> = React.createRef();
+ private _colDropDisposer?: DragManager.DragDropDisposer;
+ private _startDragPosition: { x: number, y: number } = { x: 0, y: 0 };
+ private _sensitivity: number = 16;
+ private _dragRef: React.RefObject<HTMLDivElement> = React.createRef();
+
+ onPointerEnter = (e: React.PointerEvent): void => {
+ if (e.buttons === 1 && SelectionManager.GetIsDragging()) {
+ this._header!.current!.className = "collectionSchema-col-wrapper";
+ document.addEventListener("pointermove", this.onDragMove, true);
+ }
+ }
+ onPointerLeave = (e: React.PointerEvent): void => {
+ this._header!.current!.className = "collectionSchema-col-wrapper";
+ document.removeEventListener("pointermove", this.onDragMove, true);
+ document.removeEventListener("pointermove", this.onPointerMove);
+ }
+ onDragMove = (e: PointerEvent): void => {
+ 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.right - rect.left) / 2), rect.top);
+ let before = x[0] < bounds[0];
+ this._header!.current!.className = "collectionSchema-col-wrapper";
+ if (before) this._header!.current!.className += " col-before";
+ if (!before) this._header!.current!.className += " col-after";
+ e.stopPropagation();
+ }
+
+ createColDropTarget = (ele: HTMLDivElement) => {
+ this._colDropDisposer && this._colDropDisposer();
+ if (ele) {
+ this._colDropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.colDrop.bind(this) } });
+ }
+ }
+
+ colDrop = (e: Event, de: DragManager.DropEvent) => {
+ document.removeEventListener("pointermove", this.onDragMove, true);
+ 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.right - rect.left) / 2), rect.top);
+ let before = x[0] < bounds[0];
+ if (de.data instanceof DragManager.ColumnDragData) {
+ this.props.reorderColumns(de.data.colKey, this.props.columnValue, before, this.props.allColumns);
+ return true;
+ }
+ return false;
+ }
+
+ onPointerMove = (e: PointerEvent) => {
+ let onRowMove = (e: PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+
+ document.removeEventListener("pointermove", onRowMove);
+ document.removeEventListener('pointerup', onRowUp);
+ let dragData = new DragManager.ColumnDragData(this.props.columnValue);
+ DragManager.StartColumnDrag(this._dragRef.current!, dragData, e.x, e.y);
+ };
+ let onRowUp = (): void => {
+ document.removeEventListener("pointermove", onRowMove);
+ document.removeEventListener('pointerup', onRowUp);
+ };
+ if (e.buttons === 1) {
+ let [dx, dy] = this.props.ScreenToLocalTransform().transformDirection(e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y);
+ if (Math.abs(dx) + Math.abs(dy) > this._sensitivity) {
+ document.removeEventListener("pointermove", this.onPointerMove);
+ e.stopPropagation();
+
+ document.addEventListener("pointermove", onRowMove);
+ document.addEventListener("pointerup", onRowUp);
+ }
+ }
+ }
+
+ onPointerUp = (e: React.PointerEvent) => {
+ document.removeEventListener("pointermove", this.onPointerMove);
+ }
+
+ @action
+ onPointerDown = (e: React.PointerEvent, ref: React.RefObject<HTMLDivElement>) => {
+ this._dragRef = ref;
+ let [dx, dy] = this.props.ScreenToLocalTransform().transformDirection(e.clientX, e.clientY);
+ this._startDragPosition = { x: dx, y: dy };
+ document.addEventListener("pointermove", this.onPointerMove);
+ }
+
+
+ render() {
+ let reference = React.createRef<HTMLDivElement>();
+
+ return (
+ <div className="collectionSchema-col" ref={this.createColDropTarget}>
+ <div className="collectionSchema-col-wrapper" ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
+ <div className="col-dragger" ref={reference} onPointerDown={e => this.onPointerDown(e, reference)} onPointerUp={this.onPointerUp}>
+ {this.props.columnRenderer}
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+export interface MovableRowProps {
+ rowInfo: RowInfo;
+ ScreenToLocalTransform: () => Transform;
+ addDoc: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean;
+ removeDoc: (doc: Doc) => boolean;
+ rowFocused: boolean;
+ textWrapRow: (doc: Doc) => void;
+ rowWrapped: boolean;
+}
+
+export class MovableRow extends React.Component<MovableRowProps> {
+ private _header?: React.RefObject<HTMLDivElement> = React.createRef();
+ private _rowDropDisposer?: DragManager.DragDropDisposer;
+
+ onPointerEnter = (e: React.PointerEvent): void => {
+ if (e.buttons === 1 && SelectionManager.GetIsDragging()) {
+ this._header!.current!.className = "collectionSchema-row-wrapper";
+ document.addEventListener("pointermove", this.onDragMove, true);
+ }
+ }
+ onPointerLeave = (e: React.PointerEvent): void => {
+ this._header!.current!.className = "collectionSchema-row-wrapper";
+ document.removeEventListener("pointermove", this.onDragMove, true);
+ }
+ onDragMove = (e: PointerEvent): void => {
+ 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];
+ this._header!.current!.className = "collectionSchema-row-wrapper";
+ if (before) this._header!.current!.className += " row-above";
+ if (!before) this._header!.current!.className += " row-below";
+ e.stopPropagation();
+ }
+
+ createRowDropTarget = (ele: HTMLDivElement) => {
+ this._rowDropDisposer && this._rowDropDisposer();
+ if (ele) {
+ this._rowDropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.rowDrop.bind(this) } });
+ }
+ }
+
+ rowDrop = (e: Event, de: DragManager.DropEvent) => {
+ const rowDoc = FieldValue(Cast(this.props.rowInfo.original, Doc));
+ if (!rowDoc) return false;
+
+ 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];
+
+ if (de.data instanceof DragManager.DocumentDragData) {
+ e.stopPropagation();
+ if (de.data.draggedDocuments[0] === rowDoc) return true;
+ let addDocument = (doc: Doc) => this.props.addDoc(doc, rowDoc, before);
+ let movedDocs = de.data.draggedDocuments;
+ return (de.data.dropAction || de.data.userDropAction) ?
+ de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDoc(d, rowDoc, before) || added, false)
+ : (de.data.moveDocument) ?
+ movedDocs.reduce((added: boolean, d) => de.data.moveDocument(d, rowDoc, addDocument) || added, false)
+ : de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDoc(d, rowDoc, before), false);
+ }
+ return false;
+ }
+
+ onRowContextMenu = (e: React.MouseEvent): void => {
+ let description = this.props.rowWrapped ? "Unwrap text on row" : "Text wrap row";
+ ContextMenu.Instance.addItem({ description: description, event: () => this.props.textWrapRow(this.props.rowInfo.original), icon: "file-pdf" });
+ }
+
+ @undoBatch
+ @action
+ move: DragManager.MoveFunction = (doc: Doc, target: Doc, addDoc) => {
+ let targetView = DocumentManager.Instance.getDocumentView(target);
+ if (targetView) {
+ let targetContainingColl = targetView.props.ContainingCollectionView; //.props.ContainingCollectionView.props.Document;
+ if (targetContainingColl) {
+ let targetContCollDoc = targetContainingColl.props.Document;
+ return doc !== target && doc !== targetContCollDoc && this.props.removeDoc(doc) && addDoc(doc);
+ }
+ }
+ return doc !== target && this.props.removeDoc(doc) && addDoc(doc);
+ }
+
+ render() {
+ const { children = null, rowInfo } = this.props;
+ if (!rowInfo) {
+ return <ReactTableDefaults.TrComponent>{children}</ReactTableDefaults.TrComponent>;
+ }
+
+ const { original } = rowInfo;
+ const doc = FieldValue(Cast(original, Doc));
+ if (!doc) return <></>;
+
+ let reference = React.createRef<HTMLDivElement>();
+ let onItemDown = SetupDrag(reference, () => doc, this.move);
+
+ let className = "collectionSchema-row";
+ if (this.props.rowFocused) className += " row-focused";
+ if (this.props.rowWrapped) className += " row-wrapped";
+
+ return (
+ <div className={className} ref={this.createRowDropTarget} onContextMenu={this.onRowContextMenu}>
+ <div className="collectionSchema-row-wrapper" ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
+ <ReactTableDefaults.TrComponent>
+ <div className="row-dragger">
+ <div className="row-option" onClick={() => this.props.removeDoc(this.props.rowInfo.original)}><FontAwesomeIcon icon="trash" size="sm" /></div>
+ <div className="row-option" style={{ cursor: "grab" }} ref={reference} onPointerDown={onItemDown}><FontAwesomeIcon icon="grip-vertical" size="sm" /></div>
+ </div>
+ {children}
+ </ReactTableDefaults.TrComponent>
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss
index 186e006f3..01744fb34 100644
--- a/src/client/views/collections/CollectionSchemaView.scss
+++ b/src/client/views/collections/CollectionSchemaView.scss
@@ -1,7 +1,5 @@
@import "../globalCssVariables";
-
-
.collectionSchemaView-container {
border-width: $COLLECTION_BORDER_WIDTH;
border-color: $intermediate-color;
@@ -9,23 +7,24 @@
border-radius: $border-radius;
box-sizing: border-box;
position: absolute;
+ top: 0;
width: 100%;
height: 100%;
- overflow: hidden;
+ transition: top 0.5s;
+ display: flex;
+ justify-content: space-between;
+ flex-wrap: nowrap;
- .collectionSchemaView-cellContents {
- height: $MAX_ROW_HEIGHT;
- img {
- width: auto;
- max-height: $MAX_ROW_HEIGHT;
- }
+ .collectionSchemaView-tableContainer {
+ width: 100%;
+ height: 100%;
+ overflow: scroll;
}
.collectionSchemaView-previewRegion {
position: relative;
background: $light-color;
- float: left;
height: 100%;
.collectionSchemaView-previewDoc {
@@ -47,329 +46,470 @@
}
}
- .collectionSchemaView-previewHandle {
- position: absolute;
- height: 15px;
- width: 15px;
- z-index: 20;
- right: 0;
- top: 20px;
- background: Black;
- }
-
.collectionSchemaView-dividerDragger {
position: relative;
- background: black;
- float: left;
- height: 37px;
+ height: 100%;
width: 20px;
z-index: 20;
right: 0;
top: 0;
- background: $main-accent;
- }
-
- .collectionSchemaView-columnsHandle {
- position: absolute;
- height: 37px;
- width: 20px;
- z-index: 20;
- left: 0;
- bottom: 0;
- background: $main-accent;
+ background: gray;
+ cursor: col-resize;
}
- .collectionSchemaView-colDividerDragger {
- position: relative;
- box-sizing: border-box;
- border-top: 1px solid $intermediate-color;
- border-bottom: 1px solid $intermediate-color;
- float: top;
- width: 100%;
+ .documentView-node:first-child {
+ background: $light-color;
}
+}
- .collectionSchemaView-dividerDragger {
- position: relative;
- box-sizing: border-box;
- border-left: 1px solid $intermediate-color;
- border-right: 1px solid $intermediate-color;
- float: left;
- height: 100%;
- }
+.ReactTable {
+ width: 100%;
+ background: white;
+ box-sizing: border-box;
+ border: none !important;
+ float: none !important;
- .collectionSchemaView-tableContainer {
- position: relative;
- float: left;
+ .rt-table {
height: 100%;
+ display: -webkit-inline-box;
+ direction: ltr;
+ overflow: visible;
}
- .ReactTable {
- // position: absolute; // display: inline-block;
- // overflow: auto;
- width: 100%;
- height: 100%;
- background: $light-color;
- box-sizing: border-box;
- border: none !important;
+ .rt-thead {
+ width: calc(100% - 52px);
+ margin-left: 50px;
- .rt-table {
- overflow-y: auto;
- overflow-x: auto;
- height: 100%;
- display: -webkit-inline-box;
- direction: ltr; // direction:rtl;
- // display:block;
+ &.-header {
+ font-size: 12px;
+ height: 30px;
+ box-shadow: none;
}
- .rt-tbody {
- //direction: ltr;
- direction: rtl;
+ .rt-resizable-header-content {
+ height: 100%;
+ overflow: visible;
}
- .rt-tr-group {
- direction: ltr;
- max-height: $MAX_ROW_HEIGHT;
+ .rt-th {
+ padding: 0;
+ border: solid lightgray;
+ border-width: 0 1px;
+ border-bottom: 2px solid lightgray;
}
+ }
- .rt-td {
- border-width: 1px;
- border-right-color: $intermediate-color;
+ .rt-th {
+ font-size: 13px;
+ text-align: center;
- .imageBox-cont {
- position: relative;
- max-height: 100%;
- }
+ &:last-child {
+ overflow: visible;
+ }
+ }
- .imageBox-cont img {
- object-fit: contain;
- max-width: 100%;
- height: 100%;
- }
+ .rt-tbody {
+ width: calc(100% - 2px);
+ direction: rtl;
+ overflow: visible;
+ }
- .videoBox-cont {
- object-fit: contain;
- width: auto;
- height: 100%;
- }
- }
+ .rt-tr-group {
+ direction: ltr;
+ flex: 0 1 auto;
+ min-height: 30px;
+ border: 0 !important;
}
- .ReactTable .rt-thead.-header {
- background: $intermediate-color;
- color: $light-color;
- // text-transform: uppercase;
- letter-spacing: 2px;
- font-size: 12px;
- height: 30px;
- padding-top: 4px;
+ .rt-tr {
+ width: 100%;
+ min-height: 30px;
}
- .ReactTable .rt-th,
- .ReactTable .rt-td {
- max-height: $MAX_ROW_HEIGHT;
- padding: 3px 7px;
+ .rt-td {
+ padding: 0;
font-size: 13px;
text-align: center;
+ white-space: nowrap;
+
+ .imageBox-cont {
+ position: relative;
+ max-height: 100%;
+ }
+
+ .imageBox-cont img {
+ object-fit: contain;
+ max-width: 100%;
+ height: 100%;
+ }
+
+ .videoBox-cont {
+ object-fit: contain;
+ width: auto;
+ height: 100%;
+ }
}
- .ReactTable .rt-tbody .rt-tr-group:last-child {
- border-bottom: $intermediate-color;
- border-bottom-style: solid;
- border-bottom-width: 1;
+ .rt-resizer {
+ width: 8px;
+ right: -4px;
}
- .documentView-node-topmost {
- text-align: left;
- transform-origin: center top;
- display: inline-block;
+ .rt-resizable-header {
+ padding: 0;
+ height: 30px;
}
- .documentView-node:first-child {
- background: $light-color;
+ .rt-resizable-header:last-child {
+ overflow: visible;
+
+ .rt-resizer {
+ width: 5px !important;
+ }
}
}
-//options menu styling
-#schemaOptionsMenuBtn {
- position: absolute;
- height: 20px;
- width: 20px;
- border-radius: 50%;
- z-index: 21;
- right: 4px;
- top: 4px;
- pointer-events: auto;
- background-color: black;
+.documentView-node-topmost {
+ text-align: left;
+ transform-origin: center top;
display: inline-block;
- padding: 0px;
- font-size: 100%;
}
-ul {
- list-style-type: disc;
-}
+.collectionSchema-col {
+ height: 100%;
-#schema-options-header {
- text-align: center;
- padding: 0px;
- margin: 0px;
-}
+ .collectionSchema-col-wrapper {
+ &.col-before {
+ border-left: 2px solid red;
+ }
-.schema-options-subHeader {
- color: $intermediate-color;
- margin-bottom: 5px;
+ &.col-after {
+ border-right: 2px solid red;
+ }
+ }
}
-#schemaOptionsMenuBtn:hover {
- transform: scale(1.15);
-}
-#preview-schema-checkbox-div {
- margin-left: 20px;
- font-size: 12px;
+.collectionSchemaView-header {
+ height: 100%;
+ color: gray;
+
+ .collectionSchema-header-menu {
+ height: 100%;
+
+ .collectionSchema-header-toggler {
+ width: 100%;
+ height: 100%;
+ padding: 4px;
+ letter-spacing: 2px;
+ text-transform: uppercase;
+
+ svg {
+ margin-right: 4px;
+ }
+ }
+ }
}
-#options-flyout-div {
- text-align: left;
- padding: 0px;
- z-index: 100;
- font-family: $sans-serif;
- padding-left: 5px;
+button.add-column {
+ width: 28px;
}
-#schema-col-checklist {
- overflow: scroll;
+.collectionSchema-header-menuOptions {
+ color: black;
+ width: 200px;
text-align: left;
- //background-color: $light-color-secondary;
- line-height: 25px;
- max-height: 175px;
- font-family: $sans-serif;
- font-size: 12px;
-}
+ .collectionSchema-headerMenu-group {
+ padding: 7px 0;
+ border-bottom: 1px solid lightgray;
-.Resizer {
- box-sizing: border-box;
- background: #000;
- opacity: 0.5;
- z-index: 1;
- background-clip: padding-box;
-
- &.horizontal {
- height: 11px;
- margin: -5px 0;
- border-top: 5px solid rgba(255, 255, 255, 0);
- border-bottom: 5px solid rgba(255, 255, 255, 0);
- cursor: row-resize;
- width: 100%;
+ &:first-child {
+ padding-top : 0;
+ }
- &:hover {
- border-top: 5px solid rgba(0, 0, 0, 0.5);
- border-bottom: 5px solid rgba(0, 0, 0, 0.5);
+ &:last-child {
+ border: none;
+ text-align: center;
+ padding: 12px 0 0 0;
}
}
- &.vertical {
- width: 11px;
- margin: 0 -5px;
- border-left: 5px solid rgba(255, 255, 255, 0);
- border-right: 5px solid rgba(255, 255, 255, 0);
- cursor: col-resize;
+ label {
+ color: $main-accent;
+ font-weight: normal;
+ letter-spacing: 2px;
+ text-transform: uppercase;
+ }
+
+ input {
+ color: black;
+ width: 100%;
+ }
+
+ .columnMenu-option {
+ cursor: pointer;
+ padding: 3px;
+ background-color: white;
+ transition: background-color 0.2s;
&:hover {
- border-left: 5px solid rgba(0, 0, 0, 0.5);
- border-right: 5px solid rgba(0, 0, 0, 0.5);
+ background-color: $light-color-secondary;
+ }
+
+ &.active {
+ font-weight: bold;
+ border: 2px solid $light-color-secondary;
+ }
+
+ svg {
+ color: gray;
+ margin-right: 5px;
+ width: 10px;
}
}
- &:hover {
- -webkit-transition: all 2s ease;
- transition: all 2s ease;
+ .keys-dropdown {
+ position: relative;
+ width: 100%;
+
+ input {
+ border: 2px solid $light-color-secondary;
+ padding: 3px;
+ height: 28px;
+ font-weight: bold;
+
+ &:focus {
+ font-weight: normal;
+ }
+ }
+
+ .keys-options-wrapper {
+ width: 100%;
+ max-height: 150px;
+ overflow-y: scroll;
+ position: absolute;
+ top: 28px;
+ box-shadow: 0 10px 16px rgba(0,0,0,0.1);
+
+ .key-option {
+ background-color: $light-color;
+ border: 1px solid lightgray;
+ padding: 2px 3px;
+
+ &:not(:first-child) {
+ border-top: 0;
+ }
+
+ &:hover {
+ background-color: $light-color-secondary;
+ }
+ }
+ }
}
-}
-.vertical {
- section {
- width: 100vh;
- height: 100vh;
- display: -webkit-box;
- display: -webkit-flex;
- display: -ms-flexbox;
+ .columnMenu-colors {
display: flex;
- -webkit-box-orient: vertical;
- -webkit-box-direction: normal;
- -webkit-flex-direction: column;
- -ms-flex-direction: column;
- flex-direction: column;
+ justify-content: space-between;
+ flex-wrap: wrap;
+
+ .columnMenu-colorPicker {
+ cursor: pointer;
+ width: 20px;
+ height: 20px;
+ border-radius: 10px;
+
+ &.active {
+ border: 2px solid white;
+ box-shadow: 0 0 0 2px lightgray;
+ }
+ }
}
+}
- header {
- padding: 1rem;
- background: #eee;
+.collectionSchema-row {
+ height: 100%;
+ background-color: white;
+
+ &.row-focused .rt-td {
+ background-color: rgb(255, 246, 246); //$light-color-secondary;
}
- footer {
- padding: 1rem;
- background: #eee;
+ &.row-wrapped {
+ .rt-td {
+ white-space: normal;
+ }
}
-}
-.horizontal {
- section {
- width: 100vh;
- height: 100vh;
+ .row-dragger {
display: flex;
- flex-direction: column;
+ justify-content: space-around;
+ flex: 50 0 auto;
+ width: 50px;
+ max-width: 50px;
+ height: 100%;
+ min-height: 30px;
+ color: lightgray;
+ background-color: white;
+ transition: color 0.1s ease;
+
+ .row-option {
+ // padding: 5px;
+ cursor: pointer;
+ transition: color 0.1s ease;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+
+ &:hover {
+ color: gray;
+ }
+ }
}
- header {
- padding: 1rem;
- background: #eee;
- }
+ .collectionSchema-row-wrapper {
+
+ &.row-above {
+ border-top: 1px solid red;
+ }
- footer {
- padding: 1rem;
- background: #eee;
+ &.row-below {
+ border-bottom: 1px solid red;
+ }
+
+ &.row-inside {
+ border: 1px solid red;
+ }
+
+ .row-dragging {
+ background-color: blue;
+ }
}
}
-.parent {
+.collectionSchemaView-cellContainer {
width: 100%;
height: 100%;
- -webkit-box-flex: 1;
- -webkit-flex: 1;
- -ms-flex: 1;
- flex: 1;
- display: -webkit-box;
- display: -webkit-flex;
- display: -ms-flexbox;
+}
+
+.collectionSchemaView-cellWrapper {
+ height: 100%;
+ padding: 4px;
+ position: relative;
+
+ &:focus {
+ outline: none;
+ }
+
+ &.editing {
+ padding: 0;
+ input {
+ outline: 0;
+ border: none;
+ background-color: rgb(255, 217, 217);
+ width: 100%;
+ height: 100%;
+ padding: 2px 3px;
+ min-height: 26px;
+ }
+ }
+
+ &.focused {
+
+ &.inactive {
+ border: none;
+ }
+ }
+
+ p {
+ width: 100%;
+ height: 100%;
+ }
+
+ &:hover .collectionSchemaView-cellContents-docExpander {
+ display: block;
+ }
+}
+
+.collectionSchemaView-cellContents-docExpander {
+ height: 30px;
+ width: 30px;
+ display: none;
+ position: absolute;
+ top: 0;
+ right: 0;
+ background-color: lightgray;
+}
+
+.doc-drag-over {
+ background-color: red;
+}
+
+.collectionSchemaView-toolbar {
+ height: 30px;
display: flex;
- -webkit-box-orient: vertical;
- -webkit-box-direction: normal;
- -webkit-flex-direction: column;
- -ms-flex-direction: column;
- flex-direction: column;
+ justify-content: flex-end;
+ padding: 0 10px;
+ border-bottom: 2px solid gray;
+
+ .collectionSchemaView-toolbar-item {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ }
}
-.header {
- background: #aaa;
- height: 3rem;
- line-height: 3rem;
+#preview-schema-checkbox-div {
+ margin-left: 20px;
+ font-size: 12px;
+}
+
+.collectionSchemaView-table {
+ width: 100%;
+ height: 100%;
+ overflow: visible;
}
-.wrapper {
- background: #ffa;
- margin: 5rem;
- -webkit-box-flex: 1;
- -webkit-flex: 1;
- -ms-flex: 1;
- flex: 1;
+.sub {
+ padding: 10px 30px;
+ background-color: rgb(252, 252, 252);
+ width: calc(100% - 50px);
+ margin-left: 50px;
+
+ .row-dragger {
+ background-color: rgb(252, 252, 252);
+ }
+
+ .rt-table {
+ background-color: rgb(252, 252, 252);
+ }
+
+ .collectionSchemaView-table {
+ width: 100%;
+ }
}
-.-even {
- background: $light-color !important;
+.collectionSchemaView-expander {
+ height: 100%;
+ min-height: 30px;
+ position: relative;
+ color: gray;
+
+ svg {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ }
}
-.-odd {
- background: $light-color-secondary !important;
+.collectionSchemaView-addRow {
+ color: gray;
+ letter-spacing: 2px;
+ text-transform: uppercase;
+ cursor: pointer;
+ font-size: 10.5px;
+ padding: 10px;
+ margin-left: 50px;
+ margin-top: 10px;
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index f72b1aa07..75787c0a8 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -1,58 +1,57 @@
import React = require("react");
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faCog, faPlus } from '@fortawesome/free-solid-svg-icons';
+import { faCog, faPlus, faTable, faSortUp, faSortDown } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, observable, trace, untracked } from "mobx";
import { observer } from "mobx-react";
-import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table";
+import ReactTable, { CellInfo, ComponentPropsGetterR, Column, RowInfo, ResizedChangeFunction, Resize } from "react-table";
import "react-table/react-table.css";
-import { emptyFunction, returnFalse, returnZero, returnOne } from "../../../Utils";
-import { Doc, DocListCast, DocListCastAsync, Field } from "../../../new_fields/Doc";
+import { emptyFunction, returnOne, returnEmptyString } from "../../../Utils";
+import { Doc, DocListCast, Field, Opt } 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 { Docs, DocumentOptions } from "../../documents/Documents";
+import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
import { Gateway } from "../../northstar/manager/Gateway";
import { SetupDrag, DragManager } from "../../util/DragManager";
-import { CompileScript } from "../../util/Scripting";
+import { CompileScript, ts, Transformer } from "../../util/Scripting";
import { Transform } from "../../util/Transform";
-import { COLLECTION_BORDER_WIDTH, MAX_ROW_HEIGHT } from '../../views/globalCssVariables.scss';
+import { COLLECTION_BORDER_WIDTH } 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 { CollectionVideoView } from "./CollectionVideoView";
import { CollectionView } from "./CollectionView";
import { undoBatch } from "../../util/UndoManager";
-import { timesSeries } from "async";
+import { CollectionSchemaHeader, CollectionSchemaAddColumnHeader } from "./CollectionSchemaHeaders";
+import { CellProps, CollectionSchemaCell, CollectionSchemaNumberCell, CollectionSchemaStringCell, CollectionSchemaBooleanCell, CollectionSchemaCheckboxCell, CollectionSchemaDocCell } from "./CollectionSchemaCells";
+import { MovableColumn, MovableRow } from "./CollectionSchemaMovableTableHOC";
+import { ComputedField } from "../../../new_fields/ScriptField";
+import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
-library.add(faCog);
-library.add(faPlus);
+library.add(faCog, faPlus, faSortUp, faSortDown);
+library.add(faTable);
// bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657
-
-@observer
-class KeyToggle extends React.Component<{ keyName: string, checked: boolean, toggle: (key: string) => void }> {
- constructor(props: any) {
- super(props);
- }
-
- render() {
- return (
- <div key={this.props.keyName}>
- <input type="checkbox" checked={this.props.checked} onChange={() => this.props.toggle(this.props.keyName)} />
- {this.props.keyName}
- </div>
- );
- }
+export enum ColumnType {
+ Any,
+ Number,
+ String,
+ Boolean,
+ Doc,
}
+// this map should be used for keys that should have a const type of value
+const columnTypes: Map<string, ColumnType> = new Map([
+ ["title", ColumnType.String],
+ ["x", ColumnType.Number], ["y", ColumnType.Number], ["width", ColumnType.Number], ["height", ColumnType.Number],
+ ["nativeWidth", ColumnType.Number], ["nativeHeight", ColumnType.Number], ["isPrototype", ColumnType.Boolean],
+ ["page", ColumnType.Number], ["curPage", ColumnType.Number], ["libraryBrush", ColumnType.Boolean], ["zIndex", ColumnType.Number]
+]);
@observer
export class CollectionSchemaView extends CollectionSubView(doc => doc) {
@@ -60,148 +59,34 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
private _startPreviewWidth = 0;
private DIVIDER_WIDTH = 4;
- @observable _columns: Array<string> = ["title", "data", "author"];
- @observable _selectedIndex = 0;
- @observable _columnsPercentage = 0;
- @observable _keys: string[] = [];
- @observable _newKeyName: string = "";
@observable previewScript: string = "";
+ @observable previewDoc: Doc | undefined = undefined;
+ @observable private _node: HTMLDivElement | null = null;
+ @observable private _focusedTable: Doc = this.props.Document;
@computed get previewWidth() { return () => NumCast(this.props.Document.schemaPreviewWidth); }
@computed get previewHeight() { return () => this.props.PanelHeight() - 2 * this.borderWidth; }
@computed get tableWidth() { return this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH - this.previewWidth(); }
- @computed get columns() { return Cast(this.props.Document.schemaColumns, listSpec("string"), []); }
@computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); }
- @computed get tableColumns() {
- return this.columns.map(col => {
- const ref = React.createRef<HTMLParagraphElement>();
- return {
- Header: <p ref={ref} onPointerDown={SetupDrag(ref, () => this.onHeaderDrag(col), undefined, "copy")}>{col}</p>,
- accessor: (doc: Doc) => doc ? doc[col] : 0,
- id: col
- };
- });
- }
- onHeaderDrag = (columnName: string) => {
- let schemaDoc = Cast(this.props.Document.schemaDoc, Doc);
- if (schemaDoc instanceof Doc) {
- let columnDocs = DocListCast(schemaDoc.data);
- if (columnDocs) {
- let ddoc = columnDocs.find(doc => doc.title === columnName);
- if (ddoc) {
- return ddoc;
- }
- }
- }
- return this.props.Document;
- }
-
- 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,
- renderDepth: this.props.renderDepth + 1,
- selectOnLoad: false,
- ScreenToLocalTransform: Transform.Identity,
- focus: emptyFunction,
- active: returnFalse,
- whenActiveChanged: emptyFunction,
- PanelHeight: returnZero,
- PanelWidth: returnZero,
- addDocTab: this.props.addDocTab,
- };
- let fieldContentView = <FieldView {...props} />;
- let reference = React.createRef<HTMLDivElement>();
- 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;
- doc[props.fieldKey] = res.result;
- return true;
- };
- return (
- <div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} key={props.Document[Id]} ref={reference}>
- <EditableView
- display={"inline"}
- contents={fieldContentView}
- height={Number(MAX_ROW_HEIGHT)}
- GetValue={() => {
- let field = props.Document[props.fieldKey];
- if (Field.IsField(field)) {
- return Field.toScriptString(field);
- }
- return "";
- }}
- SetValue={(value: string) => {
- let script = CompileScript(value, { addReturn: true, params: { this: Doc.name } });
- if (!script.compiled) {
- return false;
- }
- return applyToDoc(props.Document, script.run);
- }}
- OnFillDown={async (value: string) => {
- let script = CompileScript(value, { addReturn: true, params: { this: Doc.name } });
- if (!script.compiled) {
- return;
- }
- const run = script.run;
- //TODO This should be able to be refactored to compile the script once
- const val = await DocListCastAsync(this.props.Document[this.props.fieldKey]);
- val && val.forEach(doc => applyToDoc(doc, run));
- }}>
- </EditableView>
- </div >
- );
+ private createTarget = (ele: HTMLDivElement) => {
+ this._mainCont = ele;
+ super.CreateDropTarget(ele);
}
- private getTrProps: ComponentPropsGetterR = (state, rowInfo) => {
- const that = this;
- if (!rowInfo) {
- return {};
- }
- return {
- onClick: action((e: React.MouseEvent, handleOriginal: Function) => {
- that.props.select(e.ctrlKey);
- that._selectedIndex = rowInfo.index;
-
- if (handleOriginal) {
- handleOriginal();
- }
- }),
- style: {
- background: rowInfo.index === this._selectedIndex ? "lightGray" : "white",
- //color: rowInfo.index === this._selectedIndex ? "white" : "black"
- }
- };
+ isFocused = (doc: Doc): boolean => {
+ if (!this.props.isSelected()) return false;
+ return doc === this._focusedTable;
}
- private createTarget = (ele: HTMLDivElement) => {
- this._mainCont = ele;
- super.CreateDropTarget(ele);
+ @action
+ setFocused = (doc: Doc): void => {
+ this._focusedTable = doc;
}
@action
- toggleKey = (key: string) => {
- let list = Cast(this.props.Document.schemaColumns, listSpec("string"));
- if (list === undefined) {
- this.props.Document.schemaColumns = list = new List<string>([key]);
- } else {
- const index = list.indexOf(key);
- if (index === -1) {
- list.push(key);
- } else {
- list.splice(index, 1);
- }
- }
+ setPreviewDoc = (doc: Doc): void => {
+ this.previewDoc = doc;
}
//toggles preview side-panel of schema
@@ -220,8 +105,11 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
@action
onDividerMove = (e: PointerEvent): void => {
let nativeWidth = this._mainCont!.getBoundingClientRect();
- this.props.Document.schemaPreviewWidth = Math.min(nativeWidth.right - nativeWidth.left - 40,
- this.props.ScreenToLocalTransform().transformDirection(nativeWidth.right - e.clientX, 0)[0]);
+ let minWidth = 40;
+ let maxWidth = 1000;
+ let movedWidth = this.props.ScreenToLocalTransform().transformDirection(nativeWidth.right - e.clientX, 0)[0];
+ let width = movedWidth < minWidth ? minWidth : movedWidth > maxWidth ? maxWidth : movedWidth;
+ this.props.Document.schemaPreviewWidth = width;
}
@action
onDividerUp = (e: PointerEvent): void => {
@@ -235,6 +123,9 @@ 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 {
+ this.props.select(false);
+ }
}
}
@@ -244,101 +135,15 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
}
}
- 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: "Make DB", event: this.makeDB });
- }
- }
-
- @action
- makeDB = async () => {
- let csv: string = this.columns.reduce((val, col) => val + col + ",", "");
- csv = csv.substr(0, csv.length - 1) + "\n";
- let self = this;
- DocListCast(this.props.Document.data).map(doc => {
- csv += self.columns.reduce((val, col) => val + (doc[col] ? doc[col]!.toString() : "0") + ",", "");
- csv = csv.substr(0, csv.length - 1) + "\n";
- });
- csv.substring(0, csv.length - 1);
- 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.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;
- }
- }
- }
-
- @action
- addColumn = () => {
- this.columns.push(this._newKeyName);
- this._newKeyName = "";
- }
-
- @action
- newKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- this._newKeyName = e.currentTarget.value;
- }
-
@computed
get previewDocument(): Doc | undefined {
- const selected = this.childDocs.length > this._selectedIndex ? this.childDocs[this._selectedIndex] : undefined;
+ let selected = this.previewDoc;
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)
-
-
- get documentKeysCheckList() {
- const docs = DocListCast(this.props.Document[this.props.fieldKey]);
- let keys: { [key: string]: boolean } = {};
- // bcz: ugh. this is untracked since otherwise a large collection of documents will blast the server for all their fields.
- // then as each document's fields come back, we update the documents _proxies. Each time we do this, the whole schema will be
- // invalidated and re-rendered. This workaround will inquire all of the document fields before the options button is clicked.
- // then by the time the options button is clicked, all of the fields should be in place. If a new field is added while this menu
- // is displayed (unlikely) it won't show up until something else changes.
- //TODO Types
- untracked(() => docs.map(doc => Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => keys[key] = false))));
-
- this.columns.forEach(key => keys[key] = true);
- return Array.from(Object.keys(keys)).map(item =>
- (<KeyToggle checked={keys[item]} key={item} keyName={item} toggle={this.toggleKey} />));
- }
-
- get tableOptionsPanel() {
- return !this.props.active() ? (null) :
- (<Flyout
- anchorPoint={anchorPoints.RIGHT_TOP}
- content={<div>
- <div id="schema-options-header"><h5><b>Options</b></h5></div>
- <div id="options-flyout-div">
- <h6 className="schema-options-subHeader">Preview Window</h6>
- <div id="preview-schema-checkbox-div"><input type="checkbox" key={"Show Preview"} checked={this.previewWidth() !== 0} onChange={this.toggleExpander} /> Show Preview </div>
- <h6 className="schema-options-subHeader" >Displayed Columns</h6>
- <ul id="schema-col-checklist" >
- {this.documentKeysCheckList}
- </ul>
- <input value={this._newKeyName} onChange={this.newKeyChange} />
- <button onClick={this.addColumn}><FontAwesomeIcon style={{ color: "white" }} icon="plus" size="lg" /></button>
- </div>
- </div>
- }>
- <button id="schemaOptionsMenuBtn" ><FontAwesomeIcon style={{ color: "white" }} icon="cog" size="sm" /></button>
- </Flyout>);
- }
-
- @computed
- get reactTable() {
- 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}
- />;
+ getPreviewTransform = (): Transform => {
+ return this.props.ScreenToLocalTransform().translate(- this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth, - this.borderWidth);
}
@computed
@@ -347,17 +152,8 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
<div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} />;
}
-
@computed
get previewPanel() {
- // 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
@@ -380,23 +176,719 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
/>
</div>;
}
+
+ @undoBatch
@action
setPreviewScript = (script: string) => {
this.previewScript = script;
}
+ @computed
+ get schemaTable() {
+ return (
+ <SchemaTable
+ Document={this.props.Document}
+ PanelHeight={this.props.PanelHeight}
+ PanelWidth={this.props.PanelWidth}
+ childDocs={this.childDocs}
+ CollectionView={this.props.CollectionView}
+ ContainingCollectionView={this.props.ContainingCollectionView}
+ fieldKey={this.props.fieldKey}
+ renderDepth={this.props.renderDepth}
+ moveDocument={this.props.moveDocument}
+ ScreenToLocalTransform={this.props.ScreenToLocalTransform}
+ active={this.props.active}
+ onDrop={this.onDrop}
+ addDocTab={this.props.addDocTab}
+ isSelected={this.props.isSelected}
+ isFocused={this.isFocused}
+ setFocused={this.setFocused}
+ setPreviewDoc={this.setPreviewDoc}
+ deleteDocument={this.props.removeDocument}
+ dataDoc={this.props.DataDoc}
+ />
+ );
+ }
+
+ @computed
+ public get schemaToolbar() {
+ return (
+ <div className="collectionSchemaView-toolbar">
+ <div className="collectionSchemaView-toolbar-item">
+ <div id="preview-schema-checkbox-div"><input type="checkbox" key={"Show Preview"} checked={this.previewWidth() !== 0} onChange={this.toggleExpander} />Show Preview</div>
+ </div>
+ </div>
+ );
+ }
+
render() {
+ Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey);
return (
- <div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} onWheel={this.onWheel}
- onDrop={(e: React.DragEvent) => this.onDrop(e, {})} onContextMenu={this.onContextMenu} ref={this.createTarget}>
- {this.reactTable}
+ <div className="collectionSchemaView-container" style={{ height: "100%", marginTop: "0", }}>
+ <div className="collectionSchemaView-tableContainer" onPointerDown={this.onPointerDown} onWheel={this.onWheel} onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createTarget}>
+ {this.schemaTable}
+ </div>
{this.dividerDragger}
{!this.previewWidth() ? (null) : this.previewPanel}
- {this.tableOptionsPanel}
</div>
);
}
}
+
+export interface SchemaTableProps {
+ Document: Doc; // child doc
+ dataDoc?: Doc;
+ PanelHeight: () => number;
+ PanelWidth: () => number;
+ childDocs?: Doc[];
+ CollectionView: CollectionView | CollectionPDFView | CollectionVideoView;
+ ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
+ fieldKey: string;
+ renderDepth: number;
+ deleteDocument: (document: Doc) => boolean;
+ moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
+ ScreenToLocalTransform: () => Transform;
+ active: () => boolean;
+ onDrop: (e: React.DragEvent<Element>, options: DocumentOptions, completed?: (() => void) | undefined) => void;
+ addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+ isSelected: () => boolean;
+ isFocused: (document: Doc) => boolean;
+ setFocused: (document: Doc) => void;
+ setPreviewDoc: (document: Doc) => void;
+}
+
+@observer
+export class SchemaTable extends React.Component<SchemaTableProps> {
+ private DIVIDER_WIDTH = 4;
+
+ @observable _headerIsEditing: boolean = false;
+ @observable _cellIsEditing: boolean = false;
+ @observable _focusedCell: { row: number, col: number } = { row: 0, col: 0 };
+ @observable _openCollections: Array<string> = [];
+
+ @computed get previewWidth() { return () => NumCast(this.props.Document.schemaPreviewWidth); }
+ @computed get previewHeight() { return () => this.props.PanelHeight() - 2 * this.borderWidth; }
+ @computed get tableWidth() { return this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH - this.previewWidth(); }
+
+ @computed get columns() {
+ return Cast(this.props.Document.schemaColumns, listSpec(SchemaHeaderField), []);
+ }
+ set columns(columns: SchemaHeaderField[]) {
+ this.props.Document.schemaColumns = new List<SchemaHeaderField>(columns);
+ }
+
+ @computed get childDocs() {
+ if (this.props.childDocs) return this.props.childDocs;
+
+ let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document;
+ return DocListCast(doc[this.props.fieldKey]);
+ }
+ set childDocs(docs: Doc[]) {
+ let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document;
+ doc[this.props.fieldKey] = new List<Doc>(docs);
+ }
+
+ @computed get textWrappedRows() {
+ return Cast(this.props.Document.textwrappedSchemaRows, listSpec("string"), []);
+ }
+ set textWrappedRows(textWrappedRows: string[]) {
+ this.props.Document.textwrappedSchemaRows = new List<string>(textWrappedRows);
+ }
+
+ @computed get resized(): { "id": string, "value": number }[] {
+ return this.columns.reduce((resized, shf) => {
+ if (shf.width > -1) {
+ resized.push({ "id": shf.heading, "value": shf.width });
+ }
+ return resized;
+ }, [] as { "id": string, "value": number }[]);
+ }
+ @computed get sorted(): { "id": string, "desc"?: true }[] {
+ return this.columns.reduce((sorted, shf) => {
+ if (shf.desc) {
+ sorted.push({ "id": shf.heading, "desc": shf.desc });
+ }
+ return sorted;
+ }, [] as { "id": string, "desc"?: true }[]);
+ }
+
+ @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); }
+ @computed get tableColumns(): Column<Doc>[] {
+ let possibleKeys = this.documentKeys.filter(key => this.columns.findIndex(existingKey => existingKey.heading.toUpperCase() === key.toUpperCase()) === -1);
+ let columns: Column<Doc>[] = [];
+ let tableIsFocused = this.props.isFocused(this.props.Document);
+ let focusedRow = this._focusedCell.row;
+ let focusedCol = this._focusedCell.col;
+ let isEditable = !this._headerIsEditing;// && this.props.isSelected();
+
+ let children = this.childDocs;
+
+ if (children.reduce((found, doc) => found || doc.type === "collection", false)) {
+ columns.push(
+ {
+ expander: true,
+ Header: "",
+ width: 30,
+ Expander: (rowInfo) => {
+ if (rowInfo.original.type === "collection") {
+ if (rowInfo.isExpanded) return <div className="collectionSchemaView-expander" onClick={() => this.onCloseCollection(rowInfo.original)}><FontAwesomeIcon icon={"sort-up"} size="sm" /></div>;
+ if (!rowInfo.isExpanded) return <div className="collectionSchemaView-expander" onClick={() => this.onExpandCollection(rowInfo.original)}><FontAwesomeIcon icon={"sort-down"} size="sm" /></div>;
+ } else {
+ return null;
+ }
+ }
+ }
+ );
+ }
+
+ let cols = this.columns.map(col => {
+ let header = <CollectionSchemaHeader
+ keyValue={col}
+ possibleKeys={possibleKeys}
+ existingKeys={this.columns.map(c => c.heading)}
+ keyType={this.getColumnType(col)}
+ typeConst={columnTypes.get(col.heading) !== undefined}
+ onSelect={this.changeColumns}
+ setIsEditing={this.setHeaderIsEditing}
+ deleteColumn={this.deleteColumn}
+ setColumnType={this.setColumnType}
+ setColumnSort={this.setColumnSort}
+ setColumnColor={this.setColumnColor}
+ />;
+
+ return {
+ Header: <MovableColumn columnRenderer={header} columnValue={col} allColumns={this.columns} reorderColumns={this.reorderColumns} ScreenToLocalTransform={this.props.ScreenToLocalTransform} />,
+ accessor: (doc: Doc) => doc ? doc[col.heading] : 0,
+ id: col.heading,
+ Cell: (rowProps: CellInfo) => {
+ let rowIndex = rowProps.index;
+ let columnIndex = this.columns.map(c => c.heading).indexOf(rowProps.column.id!);
+ let isFocused = focusedRow === rowIndex && focusedCol === columnIndex && tableIsFocused;
+
+ let props: CellProps = {
+ row: rowIndex,
+ col: columnIndex,
+ rowProps: rowProps,
+ isFocused: isFocused,
+ changeFocusedCellByIndex: this.changeFocusedCellByIndex,
+ CollectionView: this.props.CollectionView,
+ ContainingCollection: this.props.ContainingCollectionView,
+ Document: this.props.Document,
+ fieldKey: this.props.fieldKey,
+ renderDepth: this.props.renderDepth,
+ addDocTab: this.props.addDocTab,
+ moveDocument: this.props.moveDocument,
+ setIsEditing: this.setCellIsEditing,
+ isEditable: isEditable,
+ setPreviewDoc: this.props.setPreviewDoc,
+ setComputed: this.setComputed,
+ getField: this.getField,
+ };
+
+ let colType = this.getColumnType(col);
+ if (colType === ColumnType.Number) return <CollectionSchemaNumberCell {...props} />;
+ if (colType === ColumnType.String) return <CollectionSchemaStringCell {...props} />;
+ if (colType === ColumnType.Boolean) return <CollectionSchemaCheckboxCell {...props} />;
+ if (colType === ColumnType.Doc) return <CollectionSchemaDocCell {...props} />;
+ return <CollectionSchemaCell {...props} />;
+ },
+ minWidth: 200,
+ };
+ });
+ columns.push(...cols);
+
+ columns.push({
+ Header: <CollectionSchemaAddColumnHeader createColumn={this.createColumn} />,
+ accessor: (doc: Doc) => 0,
+ id: "add",
+ Cell: (rowProps: CellInfo) => <></>,
+ width: 28,
+ resizable: false
+ });
+ return columns;
+ }
+
+ constructor(props: SchemaTableProps) {
+ super(props);
+ // convert old schema columns (list of strings) into new schema columns (list of schema header fields)
+ let oldSchemaColumns = Cast(this.props.Document.schemaColumns, listSpec("string"), []);
+ if (oldSchemaColumns && oldSchemaColumns.length && typeof oldSchemaColumns[0] !== "object") {
+ let newSchemaColumns = oldSchemaColumns.map(i => typeof i === "string" ? new SchemaHeaderField(i, "#f1efeb") : i);
+ this.props.Document.schemaColumns = new List<SchemaHeaderField>(newSchemaColumns);
+ }
+ }
+
+ componentDidMount() {
+ document.addEventListener("keydown", this.onKeyDown);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener("keydown", this.onKeyDown);
+ }
+
+ tableAddDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => {
+ return Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before);
+ }
+
+ tableRemoveDoc = (document: Doc): boolean => {
+
+ let children = this.childDocs;
+ if (children.indexOf(document) !== -1) {
+ children.splice(children.indexOf(document), 1);
+ this.childDocs = children;
+ return true;
+ }
+ return false;
+ }
+
+ private getTrProps: ComponentPropsGetterR = (state, rowInfo) => {
+ const that = this;
+ if (!rowInfo) {
+ return {};
+ }
+ return {
+ ScreenToLocalTransform: this.props.ScreenToLocalTransform,
+ addDoc: this.tableAddDoc,
+ removeDoc: this.tableRemoveDoc,
+ rowInfo,
+ rowFocused: !this._headerIsEditing && rowInfo.index === this._focusedCell.row && this.props.isFocused(this.props.Document),
+ textWrapRow: this.toggleTextWrapRow,
+ rowWrapped: this.textWrappedRows.findIndex(id => rowInfo.original[Id] === id) > -1
+ };
+ }
+
+ private getTdProps: ComponentPropsGetterR = (state, rowInfo, column, instance) => {
+ if (!rowInfo) return {};
+ if (!column) return {};
+
+ let row = rowInfo.index;
+ //@ts-ignore
+ let col = this.columns.map(c => c.heading).indexOf(column!.id);
+ let isFocused = this._focusedCell.row === row && this._focusedCell.col === col && this.props.isFocused(this.props.Document);
+ let isEditing = this.props.isFocused(this.props.Document) && this._cellIsEditing;
+ // TODO: editing border doesn't work :(
+ return {
+ style: {
+ border: !this._headerIsEditing && isFocused ? "2px solid rgb(255, 160, 160)" : "1px solid #f1efeb"
+ }
+ };
+ }
+
+ @action
+ onExpandCollection = (collection: Doc): void => {
+ this._openCollections.push(collection[Id]);
+ }
+
+ @action
+ onCloseCollection = (collection: Doc): void => {
+ let index = this._openCollections.findIndex(col => col === collection[Id]);
+ if (index > -1) this._openCollections.splice(index, 1);
+ }
+
+ @action
+ setCellIsEditing = (isEditing: boolean): void => {
+ this._cellIsEditing = isEditing;
+ }
+
+ @action
+ setHeaderIsEditing = (isEditing: boolean): void => {
+ this._headerIsEditing = isEditing;
+ }
+
+ onPointerDown = (e: React.PointerEvent): void => {
+ this.props.setFocused(this.props.Document);
+ if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) {
+ if (this.props.isSelected()) e.stopPropagation();
+ }
+ }
+
+ onWheel = (e: React.WheelEvent): void => {
+ if (this.props.active()) {
+ e.stopPropagation();
+ }
+ }
+
+ onKeyDown = (e: KeyboardEvent): void => {
+ if (!this._cellIsEditing && !this._headerIsEditing && this.props.isFocused(this.props.Document)) {// && this.props.isSelected()) {
+ let direction = e.key === "Tab" ? "tab" : e.which === 39 ? "right" : e.which === 37 ? "left" : e.which === 38 ? "up" : e.which === 40 ? "down" : "";
+ this.changeFocusedCellByDirection(direction);
+
+ let children = this.childDocs;
+ const pdoc = FieldValue(children[this._focusedCell.row]);
+ pdoc && this.props.setPreviewDoc(pdoc);
+ }
+ }
+
+ @action
+ changeFocusedCellByDirection = (direction: string): void => {
+ let children = this.childDocs;
+ switch (direction) {
+ case "tab":
+ if (this._focusedCell.col + 1 === this.columns.length && this._focusedCell.row + 1 === children.length) {
+ this._focusedCell = { row: 0, col: 0 };
+ } else if (this._focusedCell.col + 1 === this.columns.length) {
+ this._focusedCell = { row: this._focusedCell.row + 1, col: 0 };
+ } else {
+ this._focusedCell = { row: this._focusedCell.row, col: this._focusedCell.col + 1 };
+ }
+ break;
+ case "right":
+ this._focusedCell = { row: this._focusedCell.row, col: this._focusedCell.col + 1 === this.columns.length ? this._focusedCell.col : this._focusedCell.col + 1 };
+ break;
+ case "left":
+ this._focusedCell = { row: this._focusedCell.row, col: this._focusedCell.col === 0 ? this._focusedCell.col : this._focusedCell.col - 1 };
+ break;
+ case "up":
+ this._focusedCell = { row: this._focusedCell.row === 0 ? this._focusedCell.row : this._focusedCell.row - 1, col: this._focusedCell.col };
+ break;
+ case "down":
+ this._focusedCell = { row: this._focusedCell.row + 1 === children.length ? this._focusedCell.row : this._focusedCell.row + 1, col: this._focusedCell.col };
+ break;
+ }
+ }
+
+ @action
+ changeFocusedCellByIndex = (row: number, col: number): void => {
+ this._focusedCell = { row: row, col: col };
+ this.props.setFocused(this.props.Document);
+ }
+
+ @undoBatch
+ createRow = () => {
+ let children = this.childDocs;
+
+ let newDoc = Docs.Create.TextDocument({ width: 100, height: 30 });
+ let proto = Doc.GetProto(newDoc);
+ proto.title = "";
+ children.push(newDoc);
+
+ this.childDocs = children;
+ }
+
+ @undoBatch
+ @action
+ createColumn = () => {
+ let index = 0;
+ let columns = this.columns;
+ let found = columns.findIndex(col => col.heading.toUpperCase() === "New field".toUpperCase()) > -1;
+ if (!found) {
+ columns.push(new SchemaHeaderField("New field", "#f1efeb"));
+ this.columns = columns;
+ return;
+ }
+ while (found) {
+ index++;
+ found = columns.findIndex(col => col.heading.toUpperCase() === ("New field (" + index + ")").toUpperCase()) > -1;
+ }
+ columns.push(new SchemaHeaderField("New field (" + index + ")", "#f1efeb"));
+ this.columns = columns;
+ }
+
+ @undoBatch
+ @action
+ deleteColumn = (key: string) => {
+ let columns = this.columns;
+ if (columns === undefined) {
+ this.columns = new List<SchemaHeaderField>([]);
+ } else {
+ const index = columns.map(c => c.heading).indexOf(key);
+ if (index > -1) {
+ columns.splice(index, 1);
+ this.columns = columns;
+ }
+ }
+ }
+
+ @undoBatch
+ @action
+ changeColumns = (oldKey: string, newKey: string, addNew: boolean) => {
+ let columns = this.columns;
+ if (columns === undefined) {
+ this.columns = new List<SchemaHeaderField>([new SchemaHeaderField(newKey, "f1efeb")]);
+ } else {
+ if (addNew) {
+ columns.push(new SchemaHeaderField(newKey, "f1efeb"));
+ this.columns = columns;
+ } else {
+ const index = columns.map(c => c.heading).indexOf(oldKey);
+ if (index > -1) {
+ let column = columns[index];
+ column.setHeading(newKey);
+ columns[index] = column;
+ this.columns = columns;
+ }
+ }
+ }
+ }
+
+ getColumnType = (column: SchemaHeaderField): ColumnType => {
+ // added functionality to convert old column type stuff to new column type stuff -syip
+ if (column.type && column.type !== 0) {
+ return column.type;
+ }
+ if (columnTypes.get(column.heading)) {
+ column.type = columnTypes.get(column.heading)!;
+ return columnTypes.get(column.heading)!;
+ }
+ const typesDoc = FieldValue(Cast(this.props.Document.schemaColumnTypes, Doc));
+ if (!typesDoc) {
+ column.type = ColumnType.Any;
+ return ColumnType.Any;
+ }
+ column.type = NumCast(typesDoc[column.heading]);
+ return NumCast(typesDoc[column.heading]);
+ }
+
+ @undoBatch
+ setColumnType = (columnField: SchemaHeaderField, type: ColumnType): void => {
+ if (columnTypes.get(columnField.heading)) return;
+
+ let columns = this.columns;
+ let index = columns.indexOf(columnField);
+ if (index > -1) {
+ columnField.setType(NumCast(type));
+ columns[index] = columnField;
+ this.columns = columns;
+ }
+
+ // const typesDoc = FieldValue(Cast(this.props.Document.schemaColumnTypes, Doc));
+ // if (!typesDoc) {
+ // let newTypesDoc = new Doc();
+ // newTypesDoc[key] = type;
+ // this.props.Document.schemaColumnTypes = newTypesDoc;
+ // return;
+ // } else {
+ // typesDoc[key] = type;
+ // }
+ }
+
+ @undoBatch
+ setColumnColor = (columnField: SchemaHeaderField, color: string): void => {
+ let columns = this.columns;
+ let index = columns.indexOf(columnField);
+ if (index > -1) {
+ columnField.setColor(color);
+ columns[index] = columnField;
+ this.columns = columns; // need to set the columns to trigger rerender
+ }
+ }
+
+ @action
+ setColumns = (columns: SchemaHeaderField[]) => {
+ this.columns = columns;
+ }
+
+ @undoBatch
+ reorderColumns = (toMove: SchemaHeaderField, relativeTo: SchemaHeaderField, before: boolean, columnsValues: SchemaHeaderField[]) => {
+ let columns = [...columnsValues];
+ let oldIndex = columns.indexOf(toMove);
+ let relIndex = columns.indexOf(relativeTo);
+ let newIndex = (oldIndex > relIndex && !before) ? relIndex + 1 : (oldIndex < relIndex && before) ? relIndex - 1 : relIndex;
+
+ if (oldIndex === newIndex) return;
+
+ columns.splice(newIndex, 0, columns.splice(oldIndex, 1)[0]);
+ this.columns = columns;
+ }
+
+ @undoBatch
+ @action
+ setColumnSort = (columnField: SchemaHeaderField, descending: boolean | undefined) => {
+ let columns = this.columns;
+ let index = columns.findIndex(c => c.heading === columnField.heading);
+ let column = columns[index];
+ column.setDesc(descending);
+ columns[index] = column;
+ this.columns = columns;
+ }
+
+ get documentKeys() {
+ let docs = this.childDocs;
+ let keys: { [key: string]: boolean } = {};
+ // bcz: ugh. this is untracked since otherwise a large collection of documents will blast the server for all their fields.
+ // then as each document's fields come back, we update the documents _proxies. Each time we do this, the whole schema will be
+ // invalidated and re-rendered. This workaround will inquire all of the document fields before the options button is clicked.
+ // then by the time the options button is clicked, all of the fields should be in place. If a new field is added while this menu
+ // is displayed (unlikely) it won't show up until something else changes.
+ //TODO Types
+ untracked(() => docs.map(doc => Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => keys[key] = false))));
+
+ this.columns.forEach(key => keys[key.heading] = true);
+ return Array.from(Object.keys(keys));
+ }
+
+ @action
+ toggleTextWrapRow = (doc: Doc): void => {
+ let textWrapped = this.textWrappedRows;
+ let index = textWrapped.findIndex(id => doc[Id] === id);
+
+ if (index > -1) {
+ textWrapped.splice(index, 1);
+ } else {
+ textWrapped.push(doc[Id]);
+ }
+
+ this.textWrappedRows = textWrapped;
+ }
+
+ @computed
+ get reactTable() {
+ let children = this.childDocs;
+ let hasCollectionChild = children.reduce((found, doc) => found || doc.type === "collection", false);
+ let expandedRowsList = this._openCollections.map(col => children.findIndex(doc => doc[Id] === col).toString());
+ let expanded = {};
+ //@ts-ignore
+ expandedRowsList.forEach(row => expanded[row] = true);
+ console.log("text wrapped rows", ...[...this.textWrappedRows]); // TODO: get component to rerender on text wrap change without needign to console.log :((((
+
+ return <ReactTable
+ style={{ position: "relative" }}
+ data={children}
+ page={0}
+ pageSize={children.length}
+ showPagination={false}
+ columns={this.tableColumns}
+ getTrProps={this.getTrProps}
+ getTdProps={this.getTdProps}
+ sortable={false}
+ TrComponent={MovableRow}
+ sorted={this.sorted}
+ expanded={expanded}
+ resized={this.resized}
+ onResizedChange={this.onResizedChange}
+ SubComponent={hasCollectionChild ?
+ row => {
+ if (row.original.type === "collection") {
+ return <div className="sub"><SchemaTable {...this.props} Document={row.original} childDocs={undefined} /></div>;
+ }
+ }
+ : undefined}
+
+ />;
+ }
+
+ onResizedChange = (newResized: Resize[], event: any) => {
+ let columns = this.columns;
+ newResized.forEach(resized => {
+ let index = columns.findIndex(c => c.heading === resized.id);
+ let column = columns[index];
+ column.setWidth(resized.value);
+ columns[index] = column;
+ });
+ this.columns = columns;
+ }
+
+ 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: "Make DB", event: this.makeDB, icon: "table" });
+ }
+ }
+
+ @action
+ makeDB = async () => {
+ let csv: string = this.columns.reduce((val, col) => val + col + ",", "");
+ csv = csv.substr(0, csv.length - 1) + "\n";
+ let self = this;
+ this.childDocs.map(doc => {
+ csv += self.columns.reduce((val, col) => val + (doc[col.heading] ? doc[col.heading]!.toString() : "0") + ",", "");
+ csv = csv.substr(0, csv.length - 1) + "\n";
+ });
+ csv.substring(0, csv.length - 1);
+ 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.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;
+ }
+ }
+ }
+
+ getField = (row: number, col?: number) => {
+ let docs = this.childDocs;
+
+ row = row % docs.length;
+ while (row < 0) row += docs.length;
+ const columns = this.columns;
+ const doc = docs[row];
+ if (col === undefined) {
+ return doc;
+ }
+ if (col >= 0 && col < columns.length) {
+ const column = this.columns[col].heading;
+ return doc[column];
+ }
+ return undefined;
+ }
+
+ createTransformer = (row: number, col: number): Transformer => {
+ const self = this;
+ const captures: { [name: string]: Field } = {};
+
+ const transformer: ts.TransformerFactory<ts.SourceFile> = context => {
+ return root => {
+ function visit(node: ts.Node) {
+ node = ts.visitEachChild(node, visit, context);
+ if (ts.isIdentifier(node)) {
+ const isntPropAccess = !ts.isPropertyAccessExpression(node.parent) || node.parent.expression === node;
+ const isntPropAssign = !ts.isPropertyAssignment(node.parent) || node.parent.name !== node;
+ if (isntPropAccess && isntPropAssign) {
+ if (node.text === "$r") {
+ return ts.createNumericLiteral(row.toString());
+ } else if (node.text === "$c") {
+ return ts.createNumericLiteral(col.toString());
+ } else if (node.text === "$") {
+ if (ts.isCallExpression(node.parent)) {
+ // captures.doc = self.props.Document;
+ // captures.key = self.props.fieldKey;
+ }
+ }
+ }
+ }
+
+ return node;
+ }
+ return ts.visitNode(root, visit);
+ };
+ };
+
+ // const getVars = () => {
+ // return { capturedVariables: captures };
+ // };
+
+ return { transformer, /*getVars*/ };
+ }
+
+ setComputed = (script: string, doc: Doc, field: string, row: number, col: number): boolean => {
+ script =
+ `const $ = (row:number, col?:number) => {
+ if(col === undefined) {
+ return (doc as any)[key][row + ${row}];
+ }
+ return (doc as any)[key][row + ${row}][(doc as any).schemaColumns[col + ${col}].heading];
+ }
+ return ${script}`;
+ const compiled = CompileScript(script, { params: { this: Doc.name }, capturedVariables: { doc: this.props.Document, key: this.props.fieldKey }, typecheck: true, transformer: this.createTransformer(row, col) });
+ if (compiled.compiled) {
+ doc[field] = new ComputedField(compiled);
+ return true;
+ }
+ return false;
+ }
+
+ render() {
+ return (
+ <div className="collectionSchemaView-table" onPointerDown={this.onPointerDown} onWheel={this.onWheel}
+ onDrop={(e: React.DragEvent) => this.props.onDrop(e, {})} onContextMenu={this.onContextMenu} >
+ {this.reactTable}
+ <div className="collectionSchemaView-addRow" onClick={() => this.createRow()}>+ new</div>
+ </div>
+ );
+ }
+}
+
+
interface CollectionSchemaPreviewProps {
Document?: Doc;
DataDocument?: Doc;
@@ -446,8 +938,12 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre
drop = (e: Event, de: DragManager.DropEvent) => {
if (de.data instanceof DragManager.DocumentDragData) {
let docDrag = de.data;
+ let computed = CompileScript("return this.image_data[0]", { params: { this: "Doc" } });
this.props.childDocs && this.props.childDocs.map(otherdoc => {
- Doc.GetProto(otherdoc).layout = Doc.MakeDelegate(docDrag.draggedDocuments[0]);
+ let doc = docDrag.draggedDocuments[0];
+ let target = Doc.GetProto(otherdoc);
+ target.layout = target.detailedLayout = Doc.MakeDelegate(doc);
+ computed.compiled && (target.miniLayout = new ComputedField(computed));
});
e.stopPropagation();
}
@@ -471,20 +967,25 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre
}
return undefined;
}
+
+
render() {
let input = this.props.previewScript === undefined ? (null) :
<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%" }}>
+ return (<div className="collectionSchemaView-previewRegion"
+ style={{ width: this.props.width(), height: this.props.height() }}>
{!this.props.Document || !this.props.width ? (null) : (
<div className="collectionSchemaView-previewDoc"
style={{
transform: `translate(${this.centeringOffset}px, 0px)`,
borderRadius: this.borderRounding,
- height: "100%"
+ display: "inline",
+ height: this.props.height(),
+ width: this.props.width()
}}>
<DocumentView
- DataDoc={this.props.Document.layout instanceof Doc ? this.props.Document : this.props.DataDocument}
+ DataDoc={this.props.DataDocument}
Document={this.props.Document}
fitToBox={this.props.fitToBox}
renderDepth={this.props.renderDepth + 1}
@@ -499,6 +1000,7 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre
PanelHeight={this.PanelHeight}
ContainingCollectionView={this.props.CollectionView}
focus={emptyFunction}
+ backgroundColor={returnEmptyString}
parentActive={this.props.active}
whenActiveChanged={this.props.whenActiveChanged}
bringToFront={emptyFunction}
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index 7e886304d..271ad2d58 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -1,6 +1,18 @@
@import "../globalCssVariables";
+
.collectionStackingView {
+ height: 100%;
+ width: 100%;
+ position: absolute;
+ display: flex;
+ top: 0;
overflow-y: auto;
+ flex-wrap: wrap;
+ transition: top .5s;
+ .collectionSchemaView-previewDoc {
+ height: 100%;
+ position: absolute;
+ }
.collectionStackingView-docView-container {
width: 45%;
@@ -16,21 +28,23 @@
align-items: center;
}
- .collectionStackingView-masonrySingle, .collectionStackingView-masonryGrid {
- width:100%;
- height:100%;
+ .collectionStackingView-masonrySingle,
+ .collectionStackingView-masonryGrid {
+ width: 100%;
+ height: 100%;
position: absolute;
- display:grid;
+ display: grid;
top: 0;
left: 0;
width: 100%;
position: absolute;
}
+
.collectionStackingView-masonrySingle {
- width:100%;
- height:100%;
+ width: 100%;
+ height: 100%;
position: absolute;
- display:flex;
+ display: flex;
flex-direction: column;
top: 0;
left: 0;
@@ -50,25 +64,214 @@
}
.collectionStackingView-columnDragger {
- width: 15;
- height: 15;
+ width: 15;
+ height: 15;
position: absolute;
margin-left: -5;
}
- .collectionStackingView-columnDoc{
+ .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%;
+ margin: auto;
+ }
+
+ .collectionStackingView-sectionHeader {
+ text-align: center;
+ margin-left: 2px;
+ margin-right: 2px;
+ margin-top: 10px;
+ // overflow: hidden; overflow is visible so the color menu isn't hidden -ftong
+
+ .editableView-input {
+ color: black;
+ }
+
+ .editableView-input:hover,
+ .editableView-container-editing:hover,
+ .editableView-container-editing-oneLine:hover {
+ cursor: text
+ }
+
+ .collectionStackingView-sectionHeader-subCont {
+ outline: none;
+ border: 0px;
+ color: $light-color;
+ letter-spacing: 2px;
+ font-size: 75%;
+ transition: transform 0.2s;
+ position: relative;
+
+ .editableView-container-editing-oneLine,
+ .editableView-container-editing {
+ color: grey;
+ padding: 10px;
+ }
+
+ .editableView-input:hover,
+ .editableView-container-editing:hover,
+ .editableView-container-editing-oneLine:hover {
+ cursor: text
+ }
+
+ .editableView-input {
+ padding: 12px 10px 11px 10px;
+ border: 0px;
+ color: grey;
+ text-align: center;
+ letter-spacing: 2px;
+ outline-color: black;
+ }
+ }
+
+ .collectionStackingView-sectionColor {
+ position: absolute;
+ left: 0;
+ top: 0;
+ height: 100%;
+
+ [class*="css"] {
+ max-width: 102px;
+ }
+
+ .collectionStackingView-sectionColorButton {
+ height: 35px;
+ }
+
+ .collectionStackingView-colorPicker {
+ width: 78px;
+
+ .colorOptions {
+ display: flex;
+ flex-wrap: wrap;
+ }
+
+ .colorPicker {
+ cursor: pointer;
+ width: 20px;
+ height: 20px;
+ border-radius: 10px;
+ margin: 3px;
+
+ &.active {
+ border: 2px solid white;
+ box-shadow: 0 0 0 2px lightgray;
+ }
+ }
+ }
+ }
+
+ .collectionStackingView-sectionDelete {
+ position: absolute;
+ right: 0;
+ top: 0;
+ height: 100%;
+ }
}
+
+ .collectionStackingView-addDocumentButton,
+ .collectionStackingView-addGroupButton {
+ display: flex;
+ overflow: hidden;
+ margin: auto;
+ width: 90%;
+ color: lightgrey;
+ overflow: ellipses;
+
+ .editableView-container-editing-oneLine,
+ .editableView-container-editing {
+ color: grey;
+ padding: 10px;
+ width: 100%;
+ }
+
+ .editableView-input:hover,
+ .editableView-container-editing:hover,
+ .editableView-container-editing-oneLine:hover {
+ cursor: text
+ }
+
+ .editableView-input {
+ outline-color: black;
+ letter-spacing: 2px;
+ color: grey;
+ border: 0px;
+ padding: 12px 10px 11px 10px;
+ }
+ }
+
+ .collectionStackingView-addDocumentButton {
+ font-size: 75%;
+ letter-spacing: 2px;
+
+ .editableView-input {
+ outline-color: black;
+ letter-spacing: 2px;
+ color: grey;
+ border: 0px;
+ padding: 12px 10px 11px 10px;
+ }
+ }
+
+ .collectionStackingView-addGroupButton {
+ background: rgb(238, 238, 238);
+ font-size: 75%;
+ text-align: center;
+ letter-spacing: 2px;
+ height: fit-content;
+ }
+
+ .rc-switch {
+ position: absolute;
+ display: inline-block;
+ bottom: 4px;
+ right: 4px;
+ width: 70px;
+ height: 30px;
+ border-radius: 40px 40px;
+ background-color: lightslategrey;
+ }
+
+ .rc-switch:after {
+ position: absolute;
+ width: 22px;
+ height: 22px;
+ left: 3px;
+ top: 4px;
+ border-radius: 50% 50%;
+ background-color: #fff;
+ content: " ";
+ cursor: pointer;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.26);
+ -webkit-transform: scale(1);
+ transform: scale(1);
+ transition: left 0.3s cubic-bezier(0.35, 0, 0.25, 1);
+ -webkit-animation-timing-function: cubic-bezier(0.35, 0, 0.25, 1);
+ animation-timing-function: cubic-bezier(0.35, 0, 0.25, 1);
+ -webkit-animation-duration: 0.3s;
+ animation-duration: 0.3s;
+ }
+
+ .rc-switch-checked:after {
+ left: 44px;
+ }
+
+ .rc-switch-inner {
+ color: #fff;
+ font-size: 12px;
+ position: absolute;
+ left: 28px;
+ top: 8px;
+ }
+
+ .rc-switch-checked .rc-switch-inner {
+ left: 8px;
+ }
+
+
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index fe01103d6..4a751c84c 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -1,43 +1,102 @@
import React = require("react");
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, IReactionDisposer, reaction, untracked } from "mobx";
+import { CursorProperty } from "csstype";
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
+import Switch from 'rc-switch';
import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
-import { BoolCast, NumCast, Cast } from "../../../new_fields/Types";
-import { emptyFunction, Utils } from "../../../Utils";
-import { ContextMenu } from "../ContextMenu";
+import { List } from "../../../new_fields/List";
+import { listSpec } from "../../../new_fields/Schema";
+import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
+import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { emptyFunction } from "../../../Utils";
+import { DocumentType } from "../../documents/Documents";
+import { DragManager } from "../../util/DragManager";
+import { Transform } from "../../util/Transform";
+import { undoBatch } from "../../util/UndoManager";
+import { EditableView } from "../EditableView";
import { CollectionSchemaPreview } from "./CollectionSchemaView";
import "./CollectionStackingView.scss";
+import { CollectionStackingViewFieldColumn } from "./CollectionStackingViewFieldColumn";
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;
- _gridSize = 1;
+ _sectionFilterDisposer?: IReactionDisposer;
_docXfs: any[] = [];
+ _columnStart: number = 0;
+ @observable private cursor: CursorProperty = "grab";
+ get sectionHeaders() { return Cast(this.props.Document.sectionHeaders, listSpec(SchemaHeaderField)); }
@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); }
+ @computed get sectionFilter() { return this.singleColumn ? StrCast(this.props.Document.sectionFilter) : ""; }
+
+ 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;
+ }
+
+
+ get Sections() {
+ if (!this.sectionFilter) return new Map<SchemaHeaderField, Doc[]>();
+
+ if (this.sectionHeaders === undefined) {
+ this.props.Document.sectionHeaders = new List<SchemaHeaderField>();
+ }
+ const sectionHeaders = this.sectionHeaders!;
+ let fields = new Map<SchemaHeaderField, Doc[]>(sectionHeaders.map(sh => [sh, []] as [SchemaHeaderField, []]));
+ this.filteredChildren.map(d => {
+ let sectionValue = (d[this.sectionFilter] ? d[this.sectionFilter] : `NO ${this.sectionFilter.toUpperCase()} VALUE`) as object;
+ // the next five lines ensures that floating point rounding errors don't create more than one section -syip
+ let parsed = parseInt(sectionValue.toString());
+ let castedSectionValue = !isNaN(parsed) ? parsed : sectionValue;
+
+ // look for if header exists already
+ let existingHeader = sectionHeaders.find(sh => sh.heading === (castedSectionValue ? castedSectionValue.toString() : `NO ${this.sectionFilter.toUpperCase()} VALUE`));
+ if (existingHeader) {
+ fields.get(existingHeader)!.push(d);
+ }
+ else {
+ let newSchemaHeader = new SchemaHeaderField(castedSectionValue ? castedSectionValue.toString() : `NO ${this.sectionFilter.toUpperCase()} VALUE`);
+ fields.set(newSchemaHeader, [d]);
+ sectionHeaders.push(newSchemaHeader);
+ }
+ });
+ return fields;
+ }
componentDidMount() {
- this._heightDisposer = reaction(() => [this.yMargin, this.gridGap, this.columnWidth, this.childDocs.map(d => [d.height, d.width, d.zoomBasis, d.nativeHeight, d.nativeWidth, d.isMinimized])],
- () => this.singleColumn &&
- (this.props.Document.height = this.filteredChildren.reduce((height, d, i) =>
- height + this.getDocHeight(d) + (i === this.filteredChildren.length - 1 ? this.yMargin : this.gridGap), this.yMargin))
- , { fireImmediately: true });
+ // is there any reason this needs to exist? -syip
+ this._heightDisposer = reaction(() => [this.yMargin, this.props.Document[WidthSym](), this.gridGap, this.columnWidth, this.childDocs.map(d => [d.height, d.width, d.zoomBasis, d.nativeHeight, d.nativeWidth, d.isMinimized])],
+ () => {
+ if (this.singleColumn && BoolCast(this.props.Document.autoHeight)) {
+ let hgt = this.Sections.size * 50 + this.filteredChildren.reduce((height, d, i) => {
+ let pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, d);
+ return height + this.getDocHeight(pair.layout) + (i === this.filteredChildren.length - 1 ? this.yMargin : this.gridGap);
+ }, this.yMargin);
+ (this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc)
+ .height = hgt * (this.props as any).ContentScaling();
+ }
+ }, { fireImmediately: true });
+
+ // reset section headers when a new filter is inputted
+ this._sectionFilterDisposer = reaction(
+ () => this.sectionFilter,
+ () => this.props.Document.sectionHeaders = new List()
+ );
}
componentWillUnmount() {
- if (this._heightDisposer) this._heightDisposer();
+ this._heightDisposer && this._heightDisposer();
+ this._sectionFilterDisposer && this._sectionFilterDisposer();
}
@action
@@ -53,16 +112,15 @@ export class CollectionStackingView extends CollectionSubView(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;
+ getDisplayDoc(layoutDoc: Doc, dataDoc: Doc | undefined, dxf: () => Transform, width: () => number) {
let height = () => this.getDocHeight(layoutDoc);
let finalDxf = () => dxf().scale(this.columnWidth / layoutDoc[WidthSym]());
return <CollectionSchemaPreview
Document={layoutDoc}
- DataDocument={resolvedDataDoc}
+ DataDocument={dataDoc}
showOverlays={this.overlays}
renderDepth={this.props.renderDepth}
+ fitToBox={true}
width={width}
height={height}
getTransform={finalDxf}
@@ -77,61 +135,21 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
previewScript={undefined}>
</CollectionSchemaPreview>;
}
- getDocHeight(d: Doc) {
+ getDocHeight(d: Doc, columnScale: number = 1) {
let nw = NumCast(d.nativeWidth);
let nh = NumCast(d.nativeHeight);
- let aspect = nw && nh ? nh / nw : 1;
- let wid = Math.min(d[WidthSym](), this.columnWidth);
- return (nw && nh) ? wid * aspect : d[HeightSym]();
- }
-
-
- offsetTransform(doc: Doc, translateX: number, translateY: number) {
- let outerXf = Utils.GetScreenTransform(this._masonryGridRef!);
- let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY);
- return this.props.ScreenToLocalTransform().translate(offset[0], offset[1]).scale(NumCast(doc.width, 1) / this.columnWidth);
- }
- getDocTransform(doc: Doc, dref: HTMLDivElement) {
- let { scale, translateX, translateY } = Utils.GetScreenTransform(dref);
- return this.offsetTransform(doc, translateX, translateY);
- }
- getSingleDocTransform(doc: Doc, ind: number, width: number) {
- let localY = this.filteredChildren.reduce((height, d, i) =>
- height + (i < ind ? this.getDocHeight(Doc.expandTemplateLayout(d, this.props.DataDoc)) + this.gridGap : 0), this.yMargin);
- let translate = this.props.ScreenToLocalTransform().inverse().transformPoint((this.props.PanelWidth() - width) / 2, localY);
- return this.offsetTransform(doc, translate[0], translate[1]);
- }
-
- @computed
- get children() {
- 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>;
- }
- });
+ if (!BoolCast(d.ignoreAspect) && nw && nh) {
+ let aspect = nw && nh ? nh / nw : 1;
+ let wid = Math.min(d[WidthSym](), this.columnWidth / columnScale);
+ return wid * aspect;
+ }
+ return d[HeightSym]();
}
- _columnStart: number = 0;
columnDividerDown = (e: React.PointerEvent) => {
e.stopPropagation();
e.preventDefault();
+ runInAction(() => this.cursor = "grabbing");
document.addEventListener("pointermove", this.onDividerMove);
document.addEventListener('pointerup', this.onDividerUp);
this._columnStart = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0];
@@ -141,35 +159,27 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
let dragPos = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0];
let delta = dragPos - this._columnStart;
this._columnStart = dragPos;
-
- this.props.Document.columnWidth = this.columnWidth + delta;
+ this.layoutDoc.columnWidth = this.columnWidth + delta;
}
@action
onDividerUp = (e: PointerEvent): void => {
+ runInAction(() => this.cursor = "grab");
document.removeEventListener("pointermove", this.onDividerMove);
document.removeEventListener('pointerup', this.onDividerUp);
}
@computed get columnDragger() {
- return <div className="collectionStackingView-columnDragger" onPointerDown={this.columnDividerDown} ref={this._draggerRef} style={{ left: `${this.columnWidth + this.xMargin}px` }} >
- <FontAwesomeIcon icon={"caret-down"} />
+ return <div className="collectionStackingView-columnDragger" onPointerDown={this.columnDividerDown} ref={this._draggerRef} style={{ cursor: this.cursor, left: `${this.columnWidth + this.xMargin}px` }} >
+ <FontAwesomeIcon icon={"arrows-alt-h"} />
</div>;
}
- onContextMenu = (e: React.MouseEvent): void => {
- if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
- ContextMenu.Instance.addItem({
- description: "Toggle multi-column",
- event: () => this.props.Document.singleColumn = !BoolCast(this.props.Document.singleColumn, true), icon: "file-pdf"
- });
- }
- }
@undoBatch
@action
drop = (e: Event, de: DragManager.DropEvent) => {
- let targInd = -1;
let where = [de.x, de.y];
+ let targInd = -1;
if (de.data instanceof DragManager.DocumentDragData) {
this._docXfs.map((cd, i) => {
let pos = cd.dxf().inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap);
@@ -215,28 +225,78 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
});
}
- render() {
- let cols = this.singleColumn ? 1 : Math.max(1, Math.min(this.filteredChildren.length,
+ headings = () => Array.from(this.Sections.keys());
+ section = (heading: SchemaHeaderField | undefined, docList: Doc[]) => {
+ let key = this.sectionFilter;
+ let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined;
+ let types = docList.length ? docList.map(d => typeof d[key]) : this.childDocs.map(d => typeof d[key]);
+ if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) {
+ type = types[0];
+ }
+ 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 <CollectionStackingViewFieldColumn
+ key={heading ? heading.heading : ""}
+ cols={cols}
+ headings={this.headings}
+ heading={heading ? heading.heading : ""}
+ headingObject={heading}
+ docList={docList}
+ parent={this}
+ type={type}
+ createDropTarget={this.createDropTarget}
+ screenToLocalTransform={this.props.ScreenToLocalTransform}
+ />;
+ }
+
+ @action
+ addGroup = (value: string) => {
+ if (value && this.sectionHeaders) {
+ this.sectionHeaders.push(new SchemaHeaderField(value));
+ return true;
+ }
+ return false;
+ }
+
+ sortFunc = (a: [SchemaHeaderField, Doc[]], b: [SchemaHeaderField, Doc[]]): 1 | -1 => {
+ let descending = BoolCast(this.props.Document.stackingHeadersSortDescending);
+ let firstEntry = descending ? b : a;
+ let secondEntry = descending ? a : b;
+ return firstEntry[0].heading > secondEntry[0].heading ? 1 : -1;
+ }
+
+ onToggle = (checked: Boolean) => {
+ this.props.CollectionView.props.Document.chromeSatus = checked ? "collapsed" : "view-mode";
+ }
+
+ render() {
+ let headings = Array.from(this.Sections.keys());
+ let editableViewProps = {
+ GetValue: () => "",
+ SetValue: this.addGroup,
+ contents: "+ ADD A GROUP"
+ };
+ Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey);
+
+ // let uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
return (
- <div className="collectionStackingView" ref={this.createRef} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu} onWheel={(e: React.WheelEvent) => e.stopPropagation()} >
- <div className={`collectionStackingView-masonry${this.singleColumn ? "Single" : "Grid"}`}
- style={{
- padding: this.singleColumn ? `${this.yMargin}px ${this.xMargin}px ${this.yMargin}px ${this.xMargin}px` : `${this.yMargin}px ${this.xMargin}px`,
- margin: "auto",
- width: this.singleColumn ? undefined : `${cols * (this.columnWidth + this.gridGap) + 2 * this.xMargin - this.gridGap}px`,
- height: "100%",
- position: "relative",
- gridGap: this.gridGap,
- gridTemplateColumns: this.singleColumn ? undefined : templatecols,
- gridAutoRows: this.singleColumn ? undefined : `${this._gridSize}px`
- }}
- >
- {this.children}
- {this.singleColumn ? (null) : this.columnDragger}
- </div>
+ <div className="collectionStackingView"
+ ref={this.createRef} onDrop={this.onDrop.bind(this)} onWheel={(e: React.WheelEvent) => e.stopPropagation()} >
+ {this.sectionFilter ? Array.from(this.Sections.entries()).sort(this.sortFunc).
+ map((section: [SchemaHeaderField, Doc[]]) => this.section(section[0], section[1])) :
+ this.section(undefined, this.filteredChildren)}
+ {(this.sectionFilter && (this.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.CollectionView.props.Document.chromeStatus !== 'disabled')) ?
+ <div key={`${this.props.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton"
+ style={{ width: (this.columnWidth / (headings.length + ((this.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.CollectionView.props.Document.chromeStatus !== 'disabled') ? 1 : 0))) - 10, marginTop: 10 }}>
+ <EditableView {...editableViewProps} />
+ </div> : null}
+ {this.props.CollectionView.props.Document.chromeStatus !== 'disabled' ? <Switch
+ onChange={this.onToggle}
+ onClick={this.onToggle}
+ defaultChecked={this.props.CollectionView.props.Document.chromeStatus !== 'view-mode'}
+ checkedChildren="edit"
+ unCheckedChildren="view"
+ /> : null}
</div>
);
}
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
new file mode 100644
index 000000000..df03da376
--- /dev/null
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -0,0 +1,357 @@
+import React = require("react");
+import { observer } from "mobx-react";
+import { number } from "prop-types";
+import { Doc, WidthSym } from "../../../new_fields/Doc";
+import { CollectionStackingView } from "./CollectionStackingView";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { Utils } from "../../../Utils";
+import { NumCast, StrCast, BoolCast } from "../../../new_fields/Types";
+import { EditableView } from "../EditableView";
+import { action, observable, computed } from "mobx";
+import { undoBatch } from "../../util/UndoManager";
+import { DragManager } from "../../util/DragManager";
+import { DocumentManager } from "../../util/DocumentManager";
+import { SelectionManager } from "../../util/SelectionManager";
+import "./CollectionStackingView.scss";
+import { Docs } from "../../documents/Documents";
+import { SchemaHeaderField, PastelSchemaPalette } from "../../../new_fields/SchemaHeaderField";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { ScriptField } from "../../../new_fields/ScriptField";
+import { CompileScript } from "../../util/Scripting";
+import { RichTextField } from "../../../new_fields/RichTextField";
+import { Transform } from "../../util/Transform";
+import { Flyout, anchorPoints } from "../DocumentDecorations";
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faPalette } from '@fortawesome/free-solid-svg-icons';
+
+library.add(faPalette);
+
+
+interface CSVFieldColumnProps {
+ cols: () => number;
+ headings: () => object[];
+ heading: string;
+ headingObject: SchemaHeaderField | undefined;
+ docList: Doc[];
+ parent: CollectionStackingView;
+ type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined;
+ createDropTarget: (ele: HTMLDivElement) => void;
+ screenToLocalTransform: () => Transform;
+}
+
+@observer
+export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldColumnProps> {
+ @observable private _background = "inherit";
+
+ private _dropRef: HTMLDivElement | null = null;
+ private dropDisposer?: DragManager.DragDropDisposer;
+ private _headerRef: React.RefObject<HTMLDivElement> = React.createRef();
+ private _startDragPosition: { x: number, y: number } = { x: 0, y: 0 };
+ private _sensitivity: number = 16;
+
+ @observable _heading = this.props.headingObject ? this.props.headingObject.heading : this.props.heading;
+ @observable _color = this.props.headingObject ? this.props.headingObject.color : "#f1efeb";
+
+ createColumnDropRef = (ele: HTMLDivElement | null) => {
+ this._dropRef = ele;
+ this.dropDisposer && this.dropDisposer();
+ if (ele) {
+ this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.columnDrop.bind(this) } });
+ }
+ }
+
+ @undoBatch
+ @action
+ columnDrop = (e: Event, de: DragManager.DropEvent) => {
+ if (de.data instanceof DragManager.DocumentDragData) {
+ let key = StrCast(this.props.parent.props.Document.sectionFilter);
+ let castedValue = this.getValue(this._heading);
+ if (castedValue) {
+ de.data.droppedDocuments.forEach(d => d[key] = castedValue);
+ }
+ else {
+ de.data.droppedDocuments.forEach(d => d[key] = undefined);
+ }
+ this.props.parent.drop(e, de);
+ e.stopPropagation();
+ }
+ }
+
+ children(docs: Doc[]) {
+ let parent = this.props.parent;
+ parent._docXfs.length = 0;
+ return docs.map((d, i) => {
+ let headings = this.props.headings();
+ let uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
+ let pair = Doc.GetLayoutDataDocPair(parent.props.Document, parent.props.DataDoc, parent.props.fieldKey, d);
+ let width = () => (d.nativeWidth && !BoolCast(d.ignoreAspect) ? Math.min(pair.layout[WidthSym](), parent.columnWidth / (uniqueHeadings.length + 1)) : parent.columnWidth / (uniqueHeadings.length + 1));/// (uniqueHeadings.length + 1);
+ let height = () => parent.getDocHeight(pair.layout, uniqueHeadings.length + 1);// / (d.nativeWidth && !BoolCast(d.ignoreAspect) ? uniqueHeadings.length + 1 : 1);
+ let dref = React.createRef<HTMLDivElement>();
+ // if (uniqueHeadings.length > 0) {
+ let dxf = () => this.getDocTransform(pair.layout, dref.current!);
+ this.props.parent._docXfs.push({ dxf: dxf, width: width, height: height });
+ // }
+ // else {
+ // //have to add the height of all previous single column sections or the doc decorations will be in the wrong place.
+ // let dxf = () => this.getDocTransform(layoutDoc, i, width());
+ // this.props.parent._docXfs.push({ dxf: dxf, width: width, height: height });
+ // }
+ let rowHgtPcnt = height();
+ let rowSpan = Math.ceil((height() + parent.gridGap) / parent.gridGap);
+ let style = parent.singleColumn ? { width: width(), margin: "auto", marginTop: i === 0 ? 0 : parent.gridGap, height: `${rowHgtPcnt}` } : { gridRowEnd: `span ${rowSpan}` };
+ return <div className={`collectionStackingView-${parent.singleColumn ? "columnDoc" : "masonryDoc"}`} key={d[Id]} ref={dref} style={style} >
+ {this.props.parent.getDisplayDoc(pair.layout, pair.data, dxf, width)}
+ </div>;
+ // } else {
+ // let dref = React.createRef<HTMLDivElement>();
+ // let dxf = () => this.getDocTransform(layoutDoc, dref.current!);
+ // this.props.parent._docXfs.push({ dxf: dxf, width: width, height: height });
+ // let rowHgtPcnt = height();
+ // let rowSpan = Math.ceil((height() + parent.gridGap) / parent.gridGap);
+ // let divStyle = parent.singleColumn ? { width: width(), marginTop: i === 0 ? 0 : parent.gridGap, height: `${rowHgtPcnt}` } : { gridRowEnd: `span ${rowSpan}` };
+ // return <div className="collectionStackingView-masonryDoc" key={d[Id]} ref={dref} style={divStyle} >
+ // {this.props.parent.getDisplayDoc(layoutDoc, d, dxf, width)}
+ // </div>;
+ // }
+ });
+ }
+
+ getDocTransform(doc: Doc, dref: HTMLDivElement) {
+ let { scale, translateX, translateY } = Utils.GetScreenTransform(dref);
+ let outerXf = Utils.GetScreenTransform(this.props.parent._masonryGridRef!);
+ let offset = this.props.parent.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY);
+ return this.props.parent.props.ScreenToLocalTransform().
+ translate(offset[0], offset[1]).
+ scale(NumCast(doc.width, 1) / this.props.parent.columnWidth);
+ }
+
+ getValue = (value: string): any => {
+ let parsed = parseInt(value);
+ if (!isNaN(parsed)) {
+ return parsed;
+ }
+ if (value.toLowerCase().indexOf("true") > -1) {
+ return true;
+ }
+ if (value.toLowerCase().indexOf("false") > -1) {
+ return false;
+ }
+ return value;
+ }
+
+ @action
+ headingChanged = (value: string, shiftDown?: boolean) => {
+ let key = StrCast(this.props.parent.props.Document.sectionFilter);
+ let castedValue = this.getValue(value);
+ if (castedValue) {
+ if (this.props.parent.sectionHeaders) {
+ if (this.props.parent.sectionHeaders.map(i => i.heading).indexOf(castedValue.toString()) > -1) {
+ return false;
+ }
+ }
+ this.props.docList.forEach(d => d[key] = castedValue);
+ if (this.props.headingObject) {
+ this.props.headingObject.setHeading(castedValue.toString());
+ this._heading = this.props.headingObject.heading;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @action
+ changeColumnColor = (color: string) => {
+ if (this.props.headingObject) {
+ this.props.headingObject.setColor(color);
+ this._color = color;
+ }
+ }
+
+ @action
+ pointerEntered = () => {
+ if (SelectionManager.GetIsDragging()) {
+ this._background = "#b4b4b4";
+ }
+ }
+
+ @action
+ pointerLeave = () => {
+ this._background = "inherit";
+ document.removeEventListener("pointermove", this.startDrag);
+ }
+
+ @action
+ addDocument = (value: string, shiftDown?: boolean) => {
+ let key = StrCast(this.props.parent.props.Document.sectionFilter);
+ let newDoc = Docs.Create.TextDocument({ height: 18, width: 200, title: value });
+ newDoc[key] = this.getValue(this.props.heading);
+ return this.props.parent.props.addDocument(newDoc);
+ }
+
+ @action
+ deleteColumn = () => {
+ let key = StrCast(this.props.parent.props.Document.sectionFilter);
+ this.props.docList.forEach(d => d[key] = undefined);
+ if (this.props.parent.sectionHeaders && this.props.headingObject) {
+ let index = this.props.parent.sectionHeaders.indexOf(this.props.headingObject);
+ this.props.parent.sectionHeaders.splice(index, 1);
+ }
+ }
+
+ startDrag = (e: PointerEvent) => {
+ let [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y);
+ if (Math.abs(dx) + Math.abs(dy) > this._sensitivity) {
+ let alias = Doc.MakeAlias(this.props.parent.props.Document);
+ let key = StrCast(this.props.parent.props.Document.sectionFilter);
+ let value = this.getValue(this._heading);
+ value = typeof value === "string" ? `"${value}"` : value;
+ let script = `return doc.${key} === ${value}`;
+ let compiled = CompileScript(script, { params: { doc: Doc.name } });
+ if (compiled.compiled) {
+ let scriptField = new ScriptField(compiled);
+ alias.viewSpecScript = scriptField;
+ let dragData = new DragManager.DocumentDragData([alias], [alias.proto]);
+ DragManager.StartDocumentDrag([this._headerRef.current!], dragData, e.clientX, e.clientY);
+ }
+
+ e.stopPropagation();
+ document.removeEventListener("pointermove", this.startDrag);
+ document.removeEventListener("pointerup", this.pointerUp);
+ }
+ }
+
+ pointerUp = (e: PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+
+ document.removeEventListener("pointermove", this.startDrag);
+ document.removeEventListener("pointerup", this.pointerUp);
+ }
+
+ headerDown = (e: React.PointerEvent<HTMLDivElement>) => {
+ e.stopPropagation();
+ e.preventDefault();
+
+ let [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX, e.clientY);
+ this._startDragPosition = { x: dx, y: dy };
+
+ document.removeEventListener("pointermove", this.startDrag);
+ document.addEventListener("pointermove", this.startDrag);
+ document.removeEventListener("pointerup", this.pointerUp);
+ document.addEventListener("pointerup", this.pointerUp);
+ }
+
+ renderColorPicker = () => {
+ let selected = this.props.headingObject ? this.props.headingObject.color : "#f1efeb";
+
+ let pink = PastelSchemaPalette.get("pink2");
+ let purple = PastelSchemaPalette.get("purple4");
+ let blue = PastelSchemaPalette.get("bluegreen1");
+ let yellow = PastelSchemaPalette.get("yellow4");
+ let red = PastelSchemaPalette.get("red2");
+ let green = PastelSchemaPalette.get("bluegreen7");
+ let cyan = PastelSchemaPalette.get("bluegreen5");
+ let orange = PastelSchemaPalette.get("orange1");
+ let gray = "#f1efeb";
+
+ return (
+ <div className="collectionStackingView-colorPicker">
+ <div className="colorOptions">
+ <div className={"colorPicker" + (selected === pink ? " active" : "")} style={{ backgroundColor: pink }} onClick={() => this.changeColumnColor(pink!)}></div>
+ <div className={"colorPicker" + (selected === purple ? " active" : "")} style={{ backgroundColor: purple }} onClick={() => this.changeColumnColor(purple!)}></div>
+ <div className={"colorPicker" + (selected === blue ? " active" : "")} style={{ backgroundColor: blue }} onClick={() => this.changeColumnColor(blue!)}></div>
+ <div className={"colorPicker" + (selected === yellow ? " active" : "")} style={{ backgroundColor: yellow }} onClick={() => this.changeColumnColor(yellow!)}></div>
+ <div className={"colorPicker" + (selected === red ? " active" : "")} style={{ backgroundColor: red }} onClick={() => this.changeColumnColor(red!)}></div>
+ <div className={"colorPicker" + (selected === gray ? " active" : "")} style={{ backgroundColor: gray }} onClick={() => this.changeColumnColor(gray)}></div>
+ <div className={"colorPicker" + (selected === green ? " active" : "")} style={{ backgroundColor: green }} onClick={() => this.changeColumnColor(green!)}></div>
+ <div className={"colorPicker" + (selected === cyan ? " active" : "")} style={{ backgroundColor: cyan }} onClick={() => this.changeColumnColor(cyan!)}></div>
+ <div className={"colorPicker" + (selected === orange ? " active" : "")} style={{ backgroundColor: orange }} onClick={() => this.changeColumnColor(orange!)}></div>
+ </div>
+ </div>
+ );
+ }
+
+ render() {
+ let cols = this.props.cols();
+ let key = StrCast(this.props.parent.props.Document.sectionFilter);
+ let templatecols = "";
+ let headings = this.props.headings();
+ let heading = this._heading;
+ let style = this.props.parent;
+ let singleColumn = style.singleColumn;
+ let uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
+ let evContents = heading ? heading : this.props.type && this.props.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`;
+ let headerEditableViewProps = {
+ GetValue: () => evContents,
+ SetValue: this.headingChanged,
+ contents: evContents,
+ oneLine: true
+ };
+ let newEditableViewProps = {
+ GetValue: () => "",
+ SetValue: this.addDocument,
+ contents: "+ NEW"
+ };
+ let headingView = this.props.headingObject ?
+ <div key={heading} className="collectionStackingView-sectionHeader" ref={this._headerRef}
+ style={{
+ width: (style.columnWidth) /
+ ((uniqueHeadings.length +
+ ((this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled') ? 1 : 0)) || 1)
+ }}>
+ {/* the default bucket (no key value) has a tooltip that describes what it is.
+ Further, it does not have a color and cannot be deleted. */}
+ <div className="collectionStackingView-sectionHeader-subCont" onPointerDown={this.headerDown}
+ title={evContents === `NO ${key.toUpperCase()} VALUE` ?
+ `Documents that don't have a ${key} value will go here. This column cannot be removed.` : ""}
+ style={{
+ width: "100%",
+ background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this._color : "lightgrey",
+ color: "grey"
+ }}>
+ <EditableView {...headerEditableViewProps} />
+ {evContents === `NO ${key.toUpperCase()} VALUE` ? (null) :
+ <div className="collectionStackingView-sectionColor">
+ <Flyout anchorPoint={anchorPoints.TOP_CENTER} content={this.renderColorPicker()}>
+ <button className="collectionStackingView-sectionColorButton">
+ <FontAwesomeIcon icon="palette" size="sm" />
+ </button>
+ </ Flyout >
+ </div>
+ }
+ {evContents === `NO ${key.toUpperCase()} VALUE` ?
+ (null) :
+ <button className="collectionStackingView-sectionDelete" onClick={this.deleteColumn}>
+ <FontAwesomeIcon icon="trash" />
+ </button>}
+ </div>
+ </div> : (null);
+ for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth}px `;
+ return (
+ <div key={heading} style={{ width: `${100 / ((uniqueHeadings.length + ((this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled') ? 1 : 0)) || 1)}%`, background: this._background }}
+ ref={this.createColumnDropRef} onPointerEnter={this.pointerEntered} onPointerLeave={this.pointerLeave}>
+ {headingView}
+ <div key={`${heading}-stack`} className={`collectionStackingView-masonry${singleColumn ? "Single" : "Grid"}`}
+ style={{
+ padding: singleColumn ? `${style.yMargin}px ${0}px ${style.yMargin}px ${0}px` : `${style.yMargin}px ${0}px`,
+ margin: "auto",
+ width: "max-content", //singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`,
+ height: 'max-content',
+ position: "relative",
+ gridGap: style.gridGap,
+ gridTemplateColumns: singleColumn ? undefined : templatecols,
+ gridAutoRows: singleColumn ? undefined : "0px"
+ }}
+ >
+ {this.children(this.props.docList)}
+ {singleColumn ? (null) : this.props.parent.columnDragger}
+ </div>
+ {(this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled') ?
+ <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton"
+ style={{ width: style.columnWidth / (uniqueHeadings.length + 1) }}>
+ <EditableView {...newEditableViewProps} />
+ </div> : null}
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 4fd11add4..077f3f941 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -1,15 +1,17 @@
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 { 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 { ScriptField } from "../../../new_fields/ScriptField";
import { BoolCast, Cast } from "../../../new_fields/Types";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
import { RouteStore } from "../../../server/RouteStore";
+import { Utils } from "../../../Utils";
import { DocServer } from "../../DocServer";
-import { Docs, DocumentOptions } from "../../documents/Documents";
+import { Docs, DocumentOptions, DocumentType } from "../../documents/Documents";
import { DragManager } from "../../util/DragManager";
import { undoBatch, UndoManager } from "../../util/UndoManager";
import { DocComponent } from "../DocComponent";
@@ -19,7 +21,6 @@ import { CollectionPDFView } from "./CollectionPDFView";
import { CollectionVideoView } from "./CollectionVideoView";
import { CollectionView } from "./CollectionView";
import React = require("react");
-import { MainView } from "../MainView";
export interface CollectionViewProps extends FieldViewProps {
addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
@@ -27,6 +28,7 @@ export interface CollectionViewProps extends FieldViewProps {
moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
PanelWidth: () => number;
PanelHeight: () => number;
+ chromeCollapsed: boolean;
}
export interface SubCollectionViewProps extends CollectionViewProps {
@@ -53,7 +55,21 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
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]);
+ let docs = DocListCast(this.extensionDoc[this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey]);
+ let viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField);
+ if (viewSpecScript) {
+ let script = viewSpecScript.script;
+ docs = docs.filter(d => {
+ let res = script.run({ doc: d });
+ if (res.success) {
+ return res.result;
+ }
+ else {
+ console.log(res.error);
+ }
+ });
+ }
+ return docs;
}
get childDocList() {
//TODO tfs: This might not be what we want?
@@ -74,7 +90,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
return;
}
// The following conditional detects a recurring bug we've seen on the server
- if (proto[Id] === "collectionProto") {
+ if (proto[Id] === Docs.Prototypes.get(DocumentType.COL)[Id]) {
alert("COLLECTION PROTO CURSOR ISSUE DETECTED! Check console for more info...");
console.log(doc);
console.log(proto);
@@ -97,13 +113,20 @@ 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.mods === "AltKey" && de.data.draggedDocuments.length) {
+ this.childDocs.map(doc =>
+ Doc.ApplyTemplateTo(de.data.draggedDocuments[0], doc, undefined)
+ );
+ e.stopPropagation();
+ return true;
+ }
let added = false;
if (de.data.dropAction || de.data.userDropAction) {
added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false);
} else if (de.data.moveDocument) {
let movedDocs = de.data.options === this.props.Document[Id] ? de.data.draggedDocuments : de.data.droppedDocuments;
added = movedDocs.reduce((added: boolean, d) =>
- de.data.moveDocument(d, /*this.props.DataDoc ? this.props.DataDoc :*/ this.props.Document, this.props.addDocument) || added, false);
+ de.data.moveDocument(d, this.props.Document, this.props.addDocument) || added, false);
} else {
added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false);
}
@@ -164,7 +187,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
} else {
let path = window.location.origin + "/doc/";
if (text.startsWith(path)) {
- let docid = text.replace(DocServer.prepend("/doc/"), "").split("?")[0];
+ let docid = text.replace(Utils.prepend("/doc/"), "").split("?")[0];
DocServer.GetRefField(docid).then(f => {
if (f instanceof Doc) {
if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView
@@ -193,7 +216,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
if (item.kind === "string" && item.type.indexOf("uri") !== -1) {
let str: string;
let prom = new Promise<string>(resolve => e.dataTransfer.items[i].getAsString(resolve))
- .then(action((s: string) => rp.head(DocServer.prepend(RouteStore.corsProxy + "/" + (str = s)))))
+ .then(action((s: string) => rp.head(Utils.CorsProxy(str = s))))
.then(result => {
let type = result["content-type"];
if (type) {
@@ -219,7 +242,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
}).then(async (res: Response) => {
(await res.json()).map(action((file: any) => {
let full = { ...options, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300, width: 300, title: dropFileName };
- let path = DocServer.prepend(file);
+ let path = Utils.prepend(file);
Docs.Get.DocumentFromType(type, path, full).then(doc => doc && this.props.addDocument(doc));
}));
});
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index 5205f4313..990979109 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -7,6 +7,9 @@
border-radius: inherit;
box-sizing: border-box;
height: 100%;
+ width:100%;
+ position: absolute;
+ top:0;
padding-top: 20px;
padding-left: 10px;
padding-right: 0px;
@@ -31,6 +34,7 @@
margin-top: 4px;
transform: scale(1.3, 1.3);
}
+
.editableView-container {
font-weight: bold;
}
@@ -43,18 +47,20 @@
display: inline;
}
- .editableView-input, .editableView-container-editing {
+ .editableView-input,
+ .editableView-container-editing {
display: block;
text-overflow: ellipsis;
font-size: 24px;
white-space: nowrap;
}
}
+
.collectionTreeView-keyHeader {
font-style: italic;
font-size: 8pt;
margin-left: 3px;
- display:none;
+ display: none;
background: lightgray;
}
@@ -72,28 +78,31 @@
// width:100%;//width: max-content;
}
+
.treeViewItem-openRight {
display: none;
}
.treeViewItem-border {
- display:inherit;
+ display: inherit;
border-left: dashed 1px #00000042;
}
.treeViewItem-header:hover {
.collectionTreeView-keyHeader {
- display:inherit;
+ display: inherit;
}
+
.treeViewItem-openRight {
display: inline-block;
- height:13px;
- margin-top:2px;
+ height: 13px;
+ margin-top: 2px;
margin-left: 5px;
+
// display: inline;
svg {
- display:block;
- padding:0px;
+ display: block;
+ padding: 0px;
margin: 0px;
}
}
@@ -101,14 +110,17 @@
.treeViewItem-header {
border: transparent 1px solid;
- display:flex;
+ 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 c212cc97c..02b2583cd 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -1,9 +1,9 @@
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faAngleRight, faCamera, faExpand, faTrash, faBell, faCaretDown, faCaretRight, faCaretSquareDown, faCaretSquareRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
+import { faAngleRight, faCamera, faExpand, faTrash, faBell, faCaretDown, faCaretRight, faArrowsAltH, faCaretSquareDown, faCaretSquareRight, faTrashAlt, faPlus, faMinus } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, observable, trace, untracked } from "mobx";
import { observer } from "mobx-react";
-import { Doc, DocListCast, HeightSym, WidthSym, Opt } from '../../../new_fields/Doc';
+import { Doc, DocListCast, HeightSym, WidthSym, Opt, Field } from '../../../new_fields/Doc';
import { Id } from '../../../new_fields/FieldSymbols';
import { List } from '../../../new_fields/List';
import { Document, listSpec } from '../../../new_fields/Schema';
@@ -25,7 +25,9 @@ import { CollectionSchemaPreview } from './CollectionSchemaView';
import { CollectionSubView } from "./CollectionSubView";
import "./CollectionTreeView.scss";
import React = require("react");
-import { LinkManager } from '../../util/LinkManager';
+import { ComputedField } from '../../../new_fields/ScriptField';
+import { KeyValueBox } from '../nodes/KeyValueBox';
+import { exportNamedDeclaration } from 'babel-types';
export interface TreeViewProps {
@@ -58,53 +60,71 @@ library.add(faCaretDown);
library.add(faCaretRight);
library.add(faCaretSquareDown);
library.add(faCaretSquareRight);
-
+library.add(faArrowsAltH);
+library.add(faPlus, faMinus);
@observer
/**
* Component that takes in a document prop and a boolean whether it's collapsed or not.
*/
class TreeView extends React.Component<TreeViewProps> {
+ static loadId = "";
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;
-
+ @computed get treeViewExpandedView() { return StrCast(this.props.document.treeViewExpandedView, "fields"); }
+ @computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.document.maxEmbedHeight, 300); }
+ @computed get dataDoc() { return this.resolvedDataDoc ? this.resolvedDataDoc : this.props.document; }
@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];
+ let splits = StrCast(this.props.document.layout).split("fieldKey={\"");
+ return splits.length > 1 ? splits[1].split("\"")[0] : "data";
+ }
+ @computed get childDocs() {
+ let layout = this.props.document.layout instanceof Doc ? this.props.document.layout : undefined;
+ return (this.props.dataDoc ? Cast(this.props.dataDoc[this.fieldKey], listSpec(Doc)) : undefined) ||
+ (layout ? Cast(layout[this.fieldKey], listSpec(Doc)) : undefined) ||
+ Cast(this.props.document[this.fieldKey], listSpec(Doc));
+ }
+ @computed get childLinks() {
+ let layout = this.props.document.layout instanceof Doc ? this.props.document.layout : undefined;
+ return (this.props.dataDoc ? Cast(this.props.dataDoc.links, listSpec(Doc)) : undefined) ||
+ (layout instanceof Doc ? Cast(layout.links, listSpec(Doc)) : undefined) ||
+ Cast(this.props.document.links, listSpec(Doc));
+ }
+ @computed get resolvedDataDoc() {
+ if (this.props.dataDoc === undefined && this.props.document.layout instanceof Doc) {
+ // if there is no dataDoc (ie, we're not rendering a template layout), but this document
+ // has a template layout document, then we will render the template layout but use
+ // this document as the data document for the layout.
+ return this.props.document;
}
- return keyList.length ? keyList[0] : "data";
+ return this.props.dataDoc;
+ }
+ @computed get boundsOfCollectionDocument() {
+ return StrCast(this.props.document.type).indexOf(DocumentType.COL) === -1 ? undefined :
+ Doc.ComputeContentBounds(DocListCast(this.props.document.data));
}
- @computed get resolvedDataDoc() { return BoolCast(this.props.document.isTemplate) && this.props.dataDoc ? this.props.dataDoc : this.props.document; }
+ @undoBatch delete = () => this.props.deleteDoc(this.dataDoc);
+ @undoBatch openRight = () => this.props.addDocTab(this.props.document, undefined, "onRight");
+ @undoBatch indent = () => this.props.addDocument(this.props.document) && this.delete();
+ @undoBatch move = (doc: Doc, target: Doc, addDoc: (doc: Doc) => boolean) => {
+ return this.props.document !== target && this.props.deleteDoc(doc) && addDoc(doc);
+ }
+ @undoBatch @action remove = (document: Document, key: string): boolean => {
+ let children = Cast(this.dataDoc[key], listSpec(Doc), []);
+ if (children.indexOf(document) !== -1) {
+ children.splice(children.indexOf(document), 1);
+ return true;
+ }
+ return false;
+ }
protected createTreeDropTarget = (ele: HTMLDivElement) => {
this._treedropDisposer && this._treedropDisposer();
- if (ele) {
- this._treedropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.treeDrop.bind(this) } });
- }
+ ele && (this._treedropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.treeDrop.bind(this) } }));
}
- @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);
@@ -114,12 +134,12 @@ class TreeView extends React.Component<TreeViewProps> {
}
}
onPointerLeave = (e: React.PointerEvent): void => {
- this.props.document.libraryBrush = undefined;
+ this.props.document.libraryBrush = false;
this._header!.current!.className = "treeViewItem-header";
document.removeEventListener("pointermove", this.onDragMove, true);
}
onDragMove = (e: PointerEvent): void => {
- this.props.document.libraryBrush = undefined;
+ this.props.document.libraryBrush = false;
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);
@@ -132,121 +152,38 @@ class TreeView extends React.Component<TreeViewProps> {
e.stopPropagation();
}
- @action
- 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 = (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() {
- 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>;
- }
-
- static loadId = "";
editableView = (key: string, style?: string) => (<EditableView
oneLine={true}
display={"inline"}
- editing={this.resolvedDataDoc[Id] === TreeView.loadId}
+ editing={this.dataDoc[Id] === TreeView.loadId}
contents={StrCast(this.props.document[key])}
height={36}
fontStyle={style}
fontSize={12}
GetValue={() => StrCast(this.props.document[key])}
- SetValue={(value: string) => (Doc.GetProto(this.resolvedDataDoc)[key] = value) ? true : true}
+ SetValue={(value: string) => (Doc.GetProto(this.dataDoc)[key] = value) ? true : true}
OnFillDown={(value: string) => {
- Doc.GetProto(this.resolvedDataDoc)[key] = value;
- let doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
+ Doc.GetProto(this.dataDoc)[key] = value;
+ let doc = this.props.document.detailedLayout instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.document.detailedLayout)) : undefined;
+ if (!doc) doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
TreeView.loadId = doc[Id];
return this.props.addDocument(doc);
}}
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.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" />
- </div>);
- 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 >
- {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: (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, this.resolvedDataDoc, "inTab"), icon: "folder" });
ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, this.resolvedDataDoc, "onRight"), icon: "caret-square-right" });
- if (DocumentManager.Instance.getDocumentViews(this.resolvedDataDoc).length) {
- ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.resolvedDataDoc).map(view => view.props.focus(this.props.document, true)), icon: "camera" });
+ if (DocumentManager.Instance.getDocumentViews(this.dataDoc).length) {
+ ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.dataDoc).map(view => view.props.focus(this.props.document, true)), icon: "camera" });
}
- ContextMenu.Instance.addItem({ description: "Delete Item", event: undoBatch(() => this.props.deleteDoc(this.props.document)), icon: "trash-alt" });
+ ContextMenu.Instance.addItem({ description: "Delete Item", event: () => this.props.deleteDoc(this.props.document), icon: "trash-alt" });
} else {
- ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => MainView.Instance.openWorkspace(this.resolvedDataDoc)), icon: "caret-square-right" });
- ContextMenu.Instance.addItem({ description: "Delete Workspace", event: undoBatch(() => this.props.deleteDoc(this.props.document)), icon: "trash-alt" });
+ ContextMenu.Instance.addItem({ description: "Open as Workspace", event: () => MainView.Instance.openWorkspace(this.dataDoc), icon: "caret-square-right" });
+ ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.props.deleteDoc(this.props.document), icon: "trash-alt" });
}
ContextMenu.Instance.addItem({ description: "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);
@@ -273,7 +210,7 @@ class TreeView extends React.Component<TreeViewProps> {
if (de.data.draggedDocuments[0] === this.props.document) return true;
let addDoc = (doc: Doc) => this.props.addDocument(doc, this.resolvedDataDoc, before);
if (inside) {
- let docList = Cast(this.resolvedDataDoc.data, listSpec(Doc));
+ let docList = Cast(this.dataDoc.data, listSpec(Doc));
if (docList !== undefined) {
addDoc = (doc: Doc) => { docList && docList.push(doc); return true; };
}
@@ -295,39 +232,6 @@ class TreeView extends React.Component<TreeViewProps> {
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 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));
@@ -343,51 +247,136 @@ class TreeView extends React.Component<TreeViewProps> {
})());
}
- 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 >;
+ expandedField = (doc: Doc) => {
+ let ids: { [key: string]: string } = {};
+ doc && Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key));
+
+ let rows: JSX.Element[] = [];
+ for (let key of Object.keys(ids).sort()) {
+ let contents = doc[key];
+ let contentElement: JSX.Element[] | JSX.Element = [];
+
+ if (contents instanceof Doc || Cast(contents, listSpec(Doc))) {
+ let remDoc = (doc: Doc) => this.remove(doc, key);
+ let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before);
+ contentElement = TreeView.GetChildElements(contents instanceof Doc ? [contents] :
+ DocListCast(contents), this.props.treeViewId, doc, undefined, key, 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);
} 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>;
+ contentElement = <EditableView
+ key="editableView"
+ contents={contents !== undefined ? contents.toString() : "null"}
+ height={13}
+ fontSize={12}
+ GetValue={() => Field.toKeyValueString(doc, key)}
+ SetValue={(value: string) => KeyValueBox.SetField(doc, key, value)} />;
}
+ rows.push(<div style={{ display: "flex" }} key={key}>
+ <span style={{ fontWeight: "bold" }}>{key + ":"}</span>
+ &nbsp;
+ {contentElement}
+ </div>);
}
+ return rows;
+ }
+
+ noOverlays = (doc: Doc) => ({ title: "", caption: "" });
+
+ @computed get renderContent() {
+ const expandKey = this.treeViewExpandedView === this.fieldKey ? this.fieldKey : this.treeViewExpandedView === "links" ? "links" : undefined;
+ if (expandKey !== undefined) {
+ let remDoc = (doc: Doc) => this.remove(doc, expandKey);
+ let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, expandKey, doc, addBefore, before);
+ let docs = expandKey === "links" ? this.childLinks : this.childDocs;
+ return <ul key={expandKey + "more"}>
+ {!docs ? (null) :
+ TreeView.GetChildElements(docs as Doc[], this.props.treeViewId, this.props.document.layout as Doc,
+ this.resolvedDataDoc, expandKey, 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 if (this.treeViewExpandedView === "fields") {
+ return <ul><div ref={this._dref} style={{ display: "inline-block" }} key={this.props.document[Id] + this.props.document.title}>
+ {this.dataDoc ? this.expandedField(this.dataDoc) : (null)}
+ </div></ul>;
+ } else {
+ let layoutDoc = this.props.document;
+ return <div ref={this._dref} style={{ display: "inline-block", height: this.docHeight() }} key={this.props.document[Id] + this.props.document.title}>
+ <CollectionSchemaPreview
+ Document={layoutDoc}
+ DataDocument={this.resolvedDataDoc}
+ renderDepth={this.props.renderDepth}
+ showOverlays={this.noOverlays}
+ fitToBox={this.boundsOfCollectionDocument !== undefined}
+ width={this.docWidth}
+ height={this.docHeight}
+ 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>;
+ }
+ }
+
+ @computed
+ get renderBullet() {
+ 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 ? (this.childDocs ? "caret-square-right" : "caret-right") : (this.childDocs ? "caret-square-down" : "caret-down")} />}
+ </div>;
+ }
+ /**
+ * Renders the EditableView title element for placement into the tree.
+ */
+ @computed
+ get renderTitle() {
+ let reference = React.createRef<HTMLDivElement>();
+ let onItemDown = SetupDrag(reference, () => this.dataDoc, this.move, this.props.dropAction, this.props.treeViewId, true);
+
+ let headerElements = (
+ <span className="collectionTreeView-keyHeader" key={this.treeViewExpandedView}
+ onPointerDown={action(() => {
+ this.props.document.treeViewExpandedView = this.treeViewExpandedView === this.fieldKey ? "fields" :
+ this.treeViewExpandedView === "fields" && this.props.document.layout ? "layout" :
+ this.treeViewExpandedView === "layout" && this.props.document.links ? "links" :
+ this.childDocs ? this.fieldKey : "fields";
+ this._collapsed = false;
+ })}>
+ {this.treeViewExpandedView}
+ </span>);
+ let dataDocs = CollectionDockingView.Instance ? Cast(CollectionDockingView.Instance.props.Document[this.fieldKey], listSpec(Doc), []) : [];
+ let openRight = dataDocs && dataDocs.indexOf(this.dataDoc) !== -1 ? (null) : (
+ <div className="treeViewItem-openRight" onPointerDown={this.onPointerDown} onClick={this.openRight}>
+ <FontAwesomeIcon icon="angle-right" size="lg" />
+ </div>);
+ 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 >
+ {headerElements}
+ {openRight}
+ </>;
+ }
+
+ render() {
return <div className="treeViewItem-container" ref={this.createTreeDropTarget} onContextMenu={this.onWorkspaceContextMenu}>
<li className="collection-child">
<div className="treeViewItem-header" ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
- {this.renderBullet()}
- {this.renderTitle()}
+ {this.renderBullet}
+ {this.renderTitle}
</div>
<div className="treeViewItem-border">
- {contentElement}
+ {this._collapsed ? (null) : this.renderContent}
</div>
</li>
</div>;
@@ -412,6 +401,8 @@ class TreeView extends React.Component<TreeViewProps> {
let docList = docs.filter(child => !child.excludeFromLibrary);
let rowWidth = () => panelWidth() - 20;
return docList.map((child, i) => {
+ let pair = Doc.GetLayoutDataDocPair(containingCollection, dataDoc, key, child);
+
let indent = i === 0 ? undefined : () => {
if (StrCast(docList[i - 1].layout).indexOf("CollectionView") !== -1) {
let fieldKeysub = StrCast(docList[i - 1].layout).split("fieldKey")[1];
@@ -428,11 +419,11 @@ class TreeView extends React.Component<TreeViewProps> {
return aspect ? Math.min(child[WidthSym](), rowWidth()) / aspect : child[HeightSym]();
};
return <TreeView
- document={child}
- dataDoc={dataDoc}
+ document={pair.layout}
+ dataDoc={pair.data}
containingCollection={containingCollection}
treeViewId={treeViewId}
- key={child[Id] + "child " + i}
+ key={child[Id]}
indentDocument={indent}
renderDepth={renderDepth}
deleteDoc={remove}
@@ -455,6 +446,10 @@ export class CollectionTreeView extends CollectionSubView(Document) {
private treedropDisposer?: DragManager.DragDropDisposer;
private _mainEle?: HTMLDivElement;
+ @observable static NotifsCol: Opt<Doc>;
+
+ @computed get resolvedDataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; }
+
protected createTreeDropTarget = (ele: HTMLDivElement) => {
this.treedropDisposer && this.treedropDisposer();
if (this._mainEle = ele) {
@@ -478,28 +473,22 @@ export class CollectionTreeView extends CollectionSubView(Document) {
onContextMenu = (e: React.MouseEvent): void => {
// need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout
if (!e.isPropagationStopped() && this.props.Document.workspaceLibrary) { // 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)) });
+ ContextMenu.Instance.addItem({ description: "Create Workspace", event: () => MainView.Instance.createNewWorkspace(), icon: "plus" });
+ ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.remove(this.props.Document), icon: "minus" });
e.stopPropagation();
e.preventDefault();
ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
}
}
-
- @computed get resolvedDataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; }
-
outerXf = () => Utils.GetScreenTransform(this._mainEle!);
onTreeDrop = (e: React.DragEvent) => this.onDrop(e, {});
-
-
- @observable static NotifsCol: Opt<Doc>;
-
openNotifsCol = () => {
if (CollectionTreeView.NotifsCol && CollectionDockingView.Instance) {
CollectionDockingView.Instance.AddRightSplit(CollectionTreeView.NotifsCol, undefined);
}
}
- @computed get notifsButton() {
+
+ @computed get renderNotifsButton() {
const length = CollectionTreeView.NotifsCol ? DocListCast(CollectionTreeView.NotifsCol.data).length : 0;
const notifsRef = React.createRef<HTMLDivElement>();
const dragNotifs = action(() => CollectionTreeView.NotifsCol!);
@@ -515,19 +504,17 @@ export class CollectionTreeView extends CollectionSubView(Document) {
</div>
</div >;
}
- @computed get clearButton() {
+ @computed get renderClearButton() {
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>
+ <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 >;
}
-
render() {
+ Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey);
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);
@@ -546,12 +533,13 @@ export class CollectionTreeView extends CollectionSubView(Document) {
SetValue={(value: string) => (Doc.GetProto(this.resolvedDataDoc).title = value) ? true : true}
OnFillDown={(value: string) => {
Doc.GetProto(this.props.Document).title = value;
- let doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
+ let doc = this.props.Document.detailedLayout instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.Document.detailedLayout)) : undefined;
+ if (!doc) doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
TreeView.loadId = doc[Id];
Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, this.childDocs.length ? this.childDocs[0] : undefined, true);
}} />
- {this.props.Document.workspaceLibrary ? this.notifsButton : (null)}
- {this.props.Document.allowClear ? this.clearButton : (null)}
+ {this.props.Document.workspaceLibrary ? this.renderNotifsButton : (null)}
+ {this.props.Document.allowClear ? this.renderClearButton : (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,
diff --git a/src/client/views/collections/CollectionVideoView.scss b/src/client/views/collections/CollectionVideoView.scss
index 9d2c23d3e..509851ebb 100644
--- a/src/client/views/collections/CollectionVideoView.scss
+++ b/src/client/views/collections/CollectionVideoView.scss
@@ -6,6 +6,7 @@
top: 0;
left:0;
z-index: -1;
+ display:inline-table;
}
.collectionVideoView-time{
color : white;
@@ -15,6 +16,14 @@
background-color: rgba(50, 50, 50, 0.2);
transform-origin: left top;
}
+.collectionVideoView-snapshot{
+ color : white;
+ top :25px;
+ right : 25px;
+ position: absolute;
+ background-color: rgba(50, 50, 50, 0.2);
+ transform-origin: left top;
+}
.collectionVideoView-play {
width: 25px;
height: 20px;
diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx
index d7d5773ba..5185d9d0e 100644
--- a/src/client/views/collections/CollectionVideoView.tsx
+++ b/src/client/views/collections/CollectionVideoView.tsx
@@ -7,6 +7,9 @@ import { CollectionBaseView, CollectionRenderProps, CollectionViewType } from ".
import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";
import "./CollectionVideoView.scss";
import React = require("react");
+import { InkingControl } from "../InkingControl";
+import { InkTool } from "../../../new_fields/InkField";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@observer
@@ -19,18 +22,21 @@ export class CollectionVideoView extends React.Component<FieldViewProps> {
private get uIButtons() {
let scaling = Math.min(1.8, this.props.ScreenToLocalTransform().Scale);
let curTime = NumCast(this.props.Document.curPage);
- return (VideoBox._showControls ? [] : [
- <div className="collectionVideoView-time" key="time" onPointerDown={this.onResetDown} style={{ transform: `scale(${scaling}, ${scaling})` }}>
- <span>{"" + Math.round(curTime)}</span>
- <span style={{ fontSize: 8 }}>{" " + Math.round((curTime - Math.trunc(curTime)) * 100)}</span>
+ return ([<div className="collectionVideoView-time" key="time" onPointerDown={this.onResetDown} style={{ transform: `scale(${scaling})` }}>
+ <span>{"" + Math.round(curTime)}</span>
+ <span style={{ fontSize: 8 }}>{" " + Math.round((curTime - Math.trunc(curTime)) * 100)}</span>
+ </div>,
+ <div className="collectionVideoView-snapshot" key="time" onPointerDown={this.onSnapshot} style={{ transform: `scale(${scaling})` }}>
+ <FontAwesomeIcon icon="camera" size="lg" />
+ </div>,
+ VideoBox._showControls ? (null) : [
+ <div className="collectionVideoView-play" key="play" onPointerDown={this.onPlayDown} style={{ transform: `scale(${scaling})` }}>
+ <FontAwesomeIcon icon={this._videoBox && this._videoBox.Playing ? "pause" : "play"} size="lg" />
</div>,
- <div className="collectionVideoView-play" key="play" onPointerDown={this.onPlayDown} style={{ transform: `scale(${scaling}, ${scaling})` }}>
- {this._videoBox && this._videoBox.Playing ? "\"" : ">"}
- </div>,
- <div className="collectionVideoView-full" key="full" onPointerDown={this.onFullDown} style={{ transform: `scale(${scaling}, ${scaling})` }}>
+ <div className="collectionVideoView-full" key="full" onPointerDown={this.onFullDown} style={{ transform: `scale(${scaling})` }}>
F
- </div>
- ]);
+ </div>
+ ]]);
}
@action
@@ -54,18 +60,48 @@ export class CollectionVideoView extends React.Component<FieldViewProps> {
}
@action
- onResetDown = () => {
+ onSnapshot = (e: React.PointerEvent) => {
+ if (this._videoBox) {
+ this._videoBox.Snapshot();
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ }
+
+ _isclick = 0;
+ @action
+ onResetDown = (e: React.PointerEvent) => {
if (this._videoBox) {
this._videoBox.Pause();
- this.props.Document.curPage = 0;
+ e.stopPropagation();
+ this._isclick = 0;
+ document.addEventListener("pointermove", this.onPointerMove, true);
+ document.addEventListener("pointerup", this.onPointerUp, true);
+ InkingControl.Instance.switchTool(InkTool.Eraser);
+ }
+ }
+
+ @action
+ onPointerMove = (e: PointerEvent) => {
+ this._isclick += Math.abs(e.movementX) + Math.abs(e.movementY);
+ if (this._videoBox) {
+ this._videoBox.Seek(Math.max(0, NumCast(this.props.Document.curPage, 0) + Math.sign(e.movementX) * 0.0333));
}
+ e.stopImmediatePropagation();
+ }
+ @action
+ onPointerUp = (e: PointerEvent) => {
+ document.removeEventListener("pointermove", this.onPointerMove, true);
+ document.removeEventListener("pointerup", this.onPointerUp, true);
+ InkingControl.Instance.switchTool(InkTool.None);
+ this._isclick < 10 && (this.props.Document.curPage = 0);
}
setVideoBox = (videoBox: VideoBox) => { this._videoBox = videoBox; };
private subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => {
let props = { ...this.props, ...renderProps };
return (<>
- <CollectionFreeFormView {...props} setVideoBox={this.setVideoBox} CollectionView={this} />
+ <CollectionFreeFormView {...props} setVideoBox={this.setVideoBox} CollectionView={this} chromeCollapsed={true} />
{this.props.isSelected() ? this.uIButtons : (null)}
</>);
}
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 56750668d..f59fee985 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -1,12 +1,13 @@
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree } from '@fortawesome/free-solid-svg-icons';
+import { faEye } from '@fortawesome/free-regular-svg-icons';
+import { faColumns, faEllipsisV, faFingerprint, faImage, faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree } from '@fortawesome/free-solid-svg-icons';
+import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
import { observer } from "mobx-react";
import * as React from 'react';
import { Doc } from '../../../new_fields/Doc';
import { Id } from '../../../new_fields/FieldSymbols';
+import { StrCast } from '../../../new_fields/Types';
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';
import { FieldView, FieldViewProps } from '../nodes/FieldView';
@@ -16,55 +17,93 @@ import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormV
import { CollectionSchemaView } from "./CollectionSchemaView";
import { CollectionStackingView } from './CollectionStackingView';
import { CollectionTreeView } from "./CollectionTreeView";
+import { CollectionViewBaseChrome } from './CollectionViewChromes';
export const COLLECTION_BORDER_WIDTH = 2;
-library.add(faTh);
-library.add(faTree);
-library.add(faSquare);
-library.add(faProjectDiagram);
-library.add(faSignature);
-library.add(faThList);
+library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faEllipsisV, faImage, faEye as any);
@observer
export class CollectionView extends React.Component<FieldViewProps> {
+ @observable private _collapsed = true;
+
+ private _reactionDisposer: IReactionDisposer | undefined;
+
public static LayoutString(fieldStr: string = "data", fieldExt: string = "") { return FieldView.LayoutString(CollectionView, fieldStr, fieldExt); }
- private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => {
+ componentDidMount = () => {
+ this._reactionDisposer = reaction(() => StrCast(this.props.Document.chromeStatus),
+ () => {
+ // chrome status is one of disabled, collapsed, or visible. this determines initial state from document
+ // chrome status may also be view-mode, in reference to stacking view's toggle mode. it is essentially disabled mode, but prevents the toggle button from showing up on the left sidebar.
+ let chromeStatus = this.props.Document.chromeStatus;
+ if (chromeStatus && (chromeStatus === "disabled" || chromeStatus === "collapsed")) {
+ runInAction(() => this._collapsed = true);
+ }
+ });
+ }
+
+ componentWillUnmount = () => {
+ this._reactionDisposer && this._reactionDisposer();
+ }
+
+ private SubViewHelper = (type: CollectionViewType, renderProps: CollectionRenderProps) => {
let props = { ...this.props, ...renderProps };
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} />);
- case CollectionViewType.Stacking: return (<CollectionStackingView {...props} CollectionView={this} />);
+ case CollectionViewType.Schema: return (<CollectionSchemaView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />);
+ // currently cant think of a reason for collection docking view to have a chrome. mind may change if we ever have nested docking views -syip
+ case CollectionViewType.Docking: return (<CollectionDockingView chromeCollapsed={true} key="collview" {...props} CollectionView={this} />);
+ case CollectionViewType.Tree: return (<CollectionTreeView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />);
+ case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />); }
+ case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />); }
case CollectionViewType.Freeform:
default:
- return (<CollectionFreeFormView {...props} CollectionView={this} />);
+ return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />);
}
return (null);
}
+ @action
+ private collapse = (value: boolean) => {
+ this._collapsed = value;
+ this.props.Document.chromeStatus = value ? "collapsed" : "visible";
+ }
+
+ private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => {
+ // currently cant think of a reason for collection docking view to have a chrome. mind may change if we ever have nested docking views -syip
+ if (this.isAnnotationOverlay || this.props.Document.chromeStatus === "disabled" || type === CollectionViewType.Docking) {
+ return [(null), this.SubViewHelper(type, renderProps)];
+ }
+ else {
+ return [
+ (<CollectionViewBaseChrome CollectionView={this} key="chrome" type={type} collapse={this.collapse} />),
+ this.SubViewHelper(type, renderProps)
+ ];
+ }
+ }
+
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
let subItems: ContextMenuProps[] = [];
- subItems.push({ description: "Freeform", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Freeform), icon: "signature" });
+ subItems.push({ description: "Freeform", event: () => this.props.Document.viewType = CollectionViewType.Freeform, icon: "signature" });
if (CollectionBaseView.InSafeMode()) {
- ContextMenu.Instance.addItem({ description: "Test Freeform", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Invalid), icon: "project-diagram" });
+ ContextMenu.Instance.addItem({ description: "Test Freeform", event: () => this.props.Document.viewType = CollectionViewType.Invalid, icon: "project-diagram" });
+ }
+ subItems.push({ description: "Schema", event: () => this.props.Document.viewType = CollectionViewType.Schema, icon: "th-list" });
+ subItems.push({ description: "Treeview", event: () => this.props.Document.viewType = CollectionViewType.Tree, icon: "tree" });
+ subItems.push({ description: "Stacking", event: () => this.props.Document.viewType = CollectionViewType.Stacking, icon: "ellipsis-v" });
+ subItems.push({ description: "Masonry", event: () => this.props.Document.viewType = CollectionViewType.Masonry, icon: "columns" });
+ switch (this.props.Document.viewType) {
+ case CollectionViewType.Freeform: {
+ subItems.push({ description: "Custom", icon: "fingerprint", event: CollectionFreeFormView.AddCustomLayout(this.props.Document, this.props.fieldKey) });
+ break;
+ }
}
- subItems.push({ description: "Schema", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Schema), icon: "th-list" });
- subItems.push({ description: "Treeview", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Tree), icon: "tree" });
- subItems.push({ description: "Stacking", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Stacking), icon: "th-list" });
- ContextMenu.Instance.addItem({ description: "View Modes...", subitems: subItems });
+ ContextMenu.Instance.addItem({ description: "View Modes...", subitems: subItems, icon: "eye" });
+ ContextMenu.Instance.addItem({ description: "Apply Template", event: () => this.props.addDocTab && this.props.addDocTab(Doc.ApplyTemplate(this.props.Document)!, undefined, "onRight"), icon: "project-diagram" });
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"
+ description: this.props.Document.chromeStatus !== "disabled" ? "Hide Chrome" : "Show Chrome", event: () => this.props.Document.chromeStatus = (this.props.Document.chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram"
});
}
}
diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionViewChromes.scss
new file mode 100644
index 000000000..793cb7a8b
--- /dev/null
+++ b/src/client/views/collections/CollectionViewChromes.scss
@@ -0,0 +1,223 @@
+@import "../globalCssVariables";
+@import '~js-datepicker/dist/datepicker.min.css';
+
+.collectionViewChrome-cont {
+ position: relative;
+ opacity: 0.9;
+ z-index: 9001;
+ transition: top .5s;
+ background: lightgrey;
+ padding: 10px;
+
+ .collectionViewChrome {
+ display: grid;
+ grid-template-columns: 1fr auto;
+ padding-bottom: 10px;
+ border-bottom: .5px solid rgb(180, 180, 180);
+ overflow: hidden;
+
+ .collectionViewBaseChrome {
+ display: flex;
+
+ .collectionViewBaseChrome-viewPicker {
+ font-size: 75%;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ background: rgb(238, 238, 238);
+ color: grey;
+ outline-color: black;
+ border: none;
+ padding: 12px 10px 11px 10px;
+ margin-left: 50px;
+ }
+
+ .collectionViewBaseChrome-viewPicker:active {
+ outline-color: black;
+ }
+
+ .collectionViewBaseChrome-collapse {
+ transition: all .5s, opacity 0.3s;
+ position: absolute;
+ width: 40px;
+ transform-origin: top left;
+ // margin-top: 10px;
+ }
+
+ .collectionViewBaseChrome-viewSpecs {
+ margin-left: 10px;
+ display: grid;
+
+ .collectionViewBaseChrome-viewSpecsInput {
+ padding: 12px 10px 11px 10px;
+ border: 0px;
+ color: grey;
+ text-align: center;
+ letter-spacing: 2px;
+ outline-color: black;
+ font-size: 75%;
+ background: rgb(238, 238, 238);
+ height: 100%;
+ width: 150px;
+ }
+
+ .collectionViewBaseChrome-viewSpecsMenu {
+ overflow: hidden;
+ transition: height .5s, display .5s;
+ position: absolute;
+ top: 60px;
+ z-index: 100;
+ display: flex;
+ flex-direction: column;
+ background: rgb(238, 238, 238);
+ box-shadow: grey 2px 2px 4px;
+
+ .qs-datepicker {
+ left: unset;
+ right: 0;
+ }
+
+ .collectionViewBaseChrome-viewSpecsMenu-row {
+ display: grid;
+ grid-template-columns: 150px 200px 150px;
+ margin-top: 10px;
+ margin-right: 10px;
+
+ .collectionViewBaseChrome-viewSpecsMenu-rowLeft,
+ .collectionViewBaseChrome-viewSpecsMenu-rowMiddle,
+ .collectionViewBaseChrome-viewSpecsMenu-rowRight {
+ font-size: 75%;
+ letter-spacing: 2px;
+ color: grey;
+ margin-left: 10px;
+ padding: 5px;
+ border: none;
+ outline-color: black;
+ }
+ }
+
+ .collectionViewBaseChrome-viewSpecsMenu-lastRow {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ grid-gap: 10px;
+ margin: 10px;
+ }
+ }
+ }
+ }
+
+
+ .collectionStackingViewChrome-cont {
+ display: flex;
+ justify-content: space-between;
+ }
+
+ .collectionStackingViewChrome-sort {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ .collectionStackingViewChrome-sortIcon {
+ transition: transform .5s;
+ margin-left: 10px;
+ }
+ }
+
+ button:hover {
+ transform: scale(1);
+ }
+
+
+ .collectionStackingViewChrome-sectionFilter-cont {
+ justify-self: right;
+ display: flex;
+ font-size: 75%;
+ letter-spacing: 2px;
+
+ .collectionStackingViewChrome-sectionFilter-label {
+ vertical-align: center;
+ padding: 10px;
+ }
+
+ .collectionStackingViewChrome-sectionFilter {
+ color: white;
+ width: 100px;
+ text-align: center;
+ background: rgb(238, 238, 238);
+
+ .editable-view-input,
+ input,
+ .editableView-container-editing-oneLine,
+ .editableView-container-editing {
+ padding: 12px 10px 11px 10px;
+ border: 0px;
+ color: grey;
+ text-align: center;
+ letter-spacing: 2px;
+ outline-color: black;
+ height: 100%;
+ }
+
+ .react-autosuggest__container {
+ margin: 0;
+ color: grey;
+ padding: 0px;
+ }
+ }
+ }
+
+ .collectionStackingViewChrome-sectionFilter:hover {
+ cursor: text;
+ }
+ }
+}
+
+.collectionSchemaViewChrome-cont {
+ display: flex;
+ font-size: 10.5px;
+
+ .collectionSchemaViewChrome-toggle {
+ display: flex;
+ margin-left: 10px;
+ }
+
+ .collectionSchemaViewChrome-label {
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ margin-right: 5px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ }
+
+ .collectionSchemaViewChrome-toggler {
+ width: 100px;
+ height: 41px;
+ background-color: black;
+ position: relative;
+ }
+
+ .collectionSchemaViewChrome-togglerButton {
+ width: 47px;
+ height: 35px;
+ background-color: $light-color-secondary;
+ // position: absolute;
+ transition: all 0.5s ease;
+ // top: 3px;
+ margin-top: 3px;
+ color: gray;
+ letter-spacing: 2px;
+ text-transform: uppercase;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ text-align: center;
+
+ &.on {
+ margin-left: 3px;
+ }
+
+ &.off {
+ margin-left: 50px;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
new file mode 100644
index 000000000..1b2561953
--- /dev/null
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -0,0 +1,440 @@
+import * as React from "react";
+import { CollectionView } from "./CollectionView";
+import "./CollectionViewChromes.scss";
+import { CollectionViewType } from "./CollectionBaseView";
+import { undoBatch } from "../../util/UndoManager";
+import { action, observable, runInAction, computed, IObservable, IObservableValue } from "mobx";
+import { observer } from "mobx-react";
+import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { DocLike } from "../MetadataEntryMenu";
+import * as Autosuggest from 'react-autosuggest';
+import { EditableView } from "../EditableView";
+import { StrCast, NumCast, BoolCast, Cast } from "../../../new_fields/Types";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { Utils } from "../../../Utils";
+import KeyRestrictionRow from "./KeyRestrictionRow";
+import { CompileScript } from "../../util/Scripting";
+import { ScriptField } from "../../../new_fields/ScriptField";
+import { CollectionSchemaView } from "./CollectionSchemaView";
+import { COLLECTION_BORDER_WIDTH } from "../globalCssVariables.scss";
+import { listSpec } from "../../../new_fields/Schema";
+import { List } from "../../../new_fields/List";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { threadId } from "worker_threads";
+const datepicker = require('js-datepicker');
+
+interface CollectionViewChromeProps {
+ CollectionView: CollectionView;
+ type: CollectionViewType;
+ collapse?: (value: boolean) => any;
+}
+
+let stopPropagation = (e: React.SyntheticEvent) => e.stopPropagation();
+
+@observer
+export class CollectionViewBaseChrome extends React.Component<CollectionViewChromeProps> {
+ @observable private _viewSpecsOpen: boolean = false;
+ @observable private _dateWithinValue: string = "";
+ @observable private _dateValue: Date | string = "";
+ @observable private _keyRestrictions: [JSX.Element, string][] = [];
+ @observable private _collapsed: boolean = false;
+ @computed private get filterValue() { return Cast(this.props.CollectionView.props.Document.viewSpecScript, ScriptField); }
+
+ private _picker: any;
+ private _datePickerElGuid = Utils.GenerateGuid();
+
+ componentDidMount = () => {
+ setTimeout(() => this._picker = datepicker("#" + this._datePickerElGuid, {
+ disabler: (date: Date) => date > new Date(),
+ onSelect: (instance: any, date: Date) => runInAction(() => this._dateValue = date),
+ dateSelected: new Date()
+ }), 1000);
+
+ runInAction(() => {
+ this._keyRestrictions.push([<KeyRestrictionRow key={Utils.GenerateGuid()} contains={true} script={(value: string) => runInAction(() => this._keyRestrictions[0][1] = value)} />, ""]);
+ this._keyRestrictions.push([<KeyRestrictionRow key={Utils.GenerateGuid()} contains={false} script={(value: string) => runInAction(() => this._keyRestrictions[1][1] = value)} />, ""]);
+
+ // chrome status is one of disabled, collapsed, or visible. this determines initial state from document
+ let chromeStatus = this.props.CollectionView.props.Document.chromeStatus;
+ if (chromeStatus) {
+ if (chromeStatus === "disabled") {
+ throw new Error("how did you get here, if chrome status is 'disabled' on a collection, a chrome shouldn't even be instantiated!");
+ }
+ else if (chromeStatus === "collapsed") {
+ this._collapsed = true;
+ if (this.props.collapse) {
+ this.props.collapse(true);
+ }
+ }
+ }
+ });
+ }
+
+ @undoBatch
+ viewChanged = (e: React.ChangeEvent) => {
+ //@ts-ignore
+ this.props.CollectionView.props.Document.viewType = parseInt(e.target.selectedOptions[0].value);
+ }
+
+ @action
+ openViewSpecs = (e: React.SyntheticEvent) => {
+ this._viewSpecsOpen = true;
+
+ //@ts-ignore
+ if (!e.target.classList[0].startsWith("qs")) {
+ this.closeDatePicker();
+ }
+
+ e.stopPropagation();
+ document.removeEventListener("pointerdown", this.closeViewSpecs);
+ document.addEventListener("pointerdown", this.closeViewSpecs);
+ }
+
+ @action closeViewSpecs = () => { this._viewSpecsOpen = false; document.removeEventListener("pointerdown", this.closeViewSpecs); };
+
+ @action
+ openDatePicker = (e: React.PointerEvent) => {
+ this.openViewSpecs(e);
+ if (this._picker) {
+ this._picker.alwaysShow = true;
+ this._picker.show();
+ // TODO: calendar is offset when zoomed in/out
+ // this._picker.calendar.style.position = "absolute";
+ // let transform = this.props.CollectionView.props.ScreenToLocalTransform();
+ // let x = parseInt(this._picker.calendar.style.left) / transform.Scale;
+ // let y = parseInt(this._picker.calendar.style.top) / transform.Scale;
+ // this._picker.calendar.style.left = x;
+ // this._picker.calendar.style.top = y;
+
+ e.stopPropagation();
+ }
+ }
+
+ @action
+ addKeyRestriction = (e: React.MouseEvent) => {
+ let index = this._keyRestrictions.length;
+ this._keyRestrictions.push([<KeyRestrictionRow key={Utils.GenerateGuid()} contains={true} script={(value: string) => runInAction(() => this._keyRestrictions[index][1] = value)} />, ""]);
+
+ this.openViewSpecs(e);
+ }
+
+ @action
+ applyFilter = (e: React.MouseEvent) => {
+ this.openViewSpecs(e);
+
+ let keyRestrictionScript = `${this._keyRestrictions.map(i => i[1])
+ .reduce((acc: string, value: string, i: number) => value ? `${acc} && ${value}` : acc)}`;
+ let yearOffset = this._dateWithinValue[1] === 'y' ? 1 : 0;
+ let monthOffset = this._dateWithinValue[1] === 'm' ? parseInt(this._dateWithinValue[0]) : 0;
+ let weekOffset = this._dateWithinValue[1] === 'w' ? parseInt(this._dateWithinValue[0]) : 0;
+ let dayOffset = (this._dateWithinValue[1] === 'd' ? parseInt(this._dateWithinValue[0]) : 0) + weekOffset * 7;
+ let dateRestrictionScript = "";
+ if (this._dateValue instanceof Date) {
+ let lowerBound = new Date(this._dateValue.getFullYear() - yearOffset, this._dateValue.getMonth() - monthOffset, this._dateValue.getDate() - dayOffset);
+ let upperBound = new Date(this._dateValue.getFullYear() + yearOffset, this._dateValue.getMonth() + monthOffset, this._dateValue.getDate() + dayOffset + 1);
+ dateRestrictionScript = `((doc.creationDate as any).date >= ${lowerBound.valueOf()} && (doc.creationDate as any).date <= ${upperBound.valueOf()})`;
+ }
+ else {
+ let createdDate = new Date(this._dateValue);
+ if (!isNaN(createdDate.getTime())) {
+ let lowerBound = new Date(createdDate.getFullYear() - yearOffset, createdDate.getMonth() - monthOffset, createdDate.getDate() - dayOffset);
+ let upperBound = new Date(createdDate.getFullYear() + yearOffset, createdDate.getMonth() + monthOffset, createdDate.getDate() + dayOffset + 1);
+ dateRestrictionScript = `((doc.creationDate as any).date >= ${lowerBound.valueOf()} && (doc.creationDate as any).date <= ${upperBound.valueOf()})`;
+ }
+ }
+ let fullScript = dateRestrictionScript.length || keyRestrictionScript.length ? dateRestrictionScript.length ?
+ `return ${dateRestrictionScript} ${keyRestrictionScript.length ? "&&" : ""} ${keyRestrictionScript}` :
+ `return ${keyRestrictionScript} ${dateRestrictionScript.length ? "&&" : ""} ${dateRestrictionScript}` :
+ "return true";
+ let compiled = CompileScript(fullScript, { params: { doc: Doc.name }, typecheck: false });
+ if (compiled.compiled) {
+ this.props.CollectionView.props.Document.viewSpecScript = new ScriptField(compiled);
+ }
+ }
+
+ @action
+ closeDatePicker = () => {
+ if (this._picker) {
+ this._picker.alwaysShow = false;
+ this._picker.hide();
+ }
+ document.removeEventListener("pointerdown", this.closeDatePicker);
+ }
+
+ @action
+ toggleCollapse = () => {
+ this._collapsed = !this._collapsed;
+ if (this.props.collapse) {
+ this.props.collapse(this._collapsed);
+ }
+ }
+
+ subChrome = () => {
+ switch (this.props.type) {
+ case CollectionViewType.Stacking: return (
+ <CollectionStackingViewChrome
+ key="collchrome"
+ CollectionView={this.props.CollectionView}
+ type={this.props.type} />);
+ case CollectionViewType.Schema: return (
+ <CollectionSchemaViewChrome
+ key="collchrome"
+ CollectionView={this.props.CollectionView}
+ type={this.props.type}
+ />);
+ default:
+ return null;
+ }
+ }
+
+ render() {
+ return (
+ <div className="collectionViewChrome-cont" style={{ top: this._collapsed ? -70 : 0 }}>
+ <div className="collectionViewChrome">
+ <div className="collectionViewBaseChrome">
+ <button className="collectionViewBaseChrome-collapse"
+ style={{
+ top: this._collapsed ? 70 : 10,
+ transform: `rotate(${this._collapsed ? 180 : 0}deg) scale(${this._collapsed ? 0.5 : 1}) translate(${this._collapsed ? "-100%, -100%" : "0, 0"})`,
+ opacity: (this._collapsed && !this.props.CollectionView.props.isSelected()) ? 0 : 0.9,
+ left: (this._collapsed ? 0 : "unset"),
+ }}
+ title="Collapse collection chrome" onClick={this.toggleCollapse}>
+ <FontAwesomeIcon icon="caret-up" size="2x" />
+ </button>
+ <select
+ className="collectionViewBaseChrome-viewPicker"
+ onPointerDown={stopPropagation}
+ onChange={this.viewChanged}
+ value={NumCast(this.props.CollectionView.props.Document.viewType)}>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="1">Freeform View</option>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="2">Schema View</option>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="4">Tree View</option>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="5">Stacking View</option>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="6">Masonry View</option>
+ </select>
+ <div className="collectionViewBaseChrome-viewSpecs" style={{ display: this._collapsed ? "none" : "grid" }}>
+ <input className="collectionViewBaseChrome-viewSpecsInput"
+ placeholder="FILTER DOCUMENTS"
+ value={this.filterValue ? this.filterValue.script.originalScript : ""}
+ onChange={(e) => { }}
+ onPointerDown={this.openViewSpecs} />
+ <div className="collectionViewBaseChrome-viewSpecsMenu"
+ onPointerDown={this.openViewSpecs}
+ style={{
+ height: this._viewSpecsOpen ? "fit-content" : "0px",
+ overflow: this._viewSpecsOpen ? "initial" : "hidden"
+ }}>
+ {this._keyRestrictions.map(i => i[0])}
+ <div className="collectionViewBaseChrome-viewSpecsMenu-row">
+ <div className="collectionViewBaseChrome-viewSpecsMenu-rowLeft">
+ CREATED WITHIN:
+ </div>
+ <select className="collectionViewBaseChrome-viewSpecsMenu-rowMiddle"
+ style={{ textTransform: "uppercase", textAlign: "center" }}
+ value={this._dateWithinValue}
+ onChange={(e) => runInAction(() => this._dateWithinValue = e.target.value)}>
+ <option value="1d">1 day of</option>
+ <option value="3d">3 days of</option>
+ <option value="1w">1 week of</option>
+ <option value="2w">2 weeks of</option>
+ <option value="1m">1 month of</option>
+ <option value="2m">2 months of</option>
+ <option value="6m">6 months of</option>
+ <option value="1y">1 year of</option>
+ </select>
+ <input className="collectionViewBaseChrome-viewSpecsMenu-rowRight"
+ id={this._datePickerElGuid}
+ value={this._dateValue instanceof Date ? this._dateValue.toLocaleDateString() : this._dateValue}
+ onChange={(e) => runInAction(() => this._dateValue = e.target.value)}
+ onPointerDown={this.openDatePicker}
+ placeholder="Value" />
+ </div>
+ <div className="collectionViewBaseChrome-viewSpecsMenu-lastRow">
+ <button className="collectonViewBaseChrome-viewSpecsMenu-lastRowButton" onClick={this.addKeyRestriction}>
+ ADD KEY RESTRICTION
+ </button>
+ <button className="collectonViewBaseChrome-viewSpecsMenu-lastRowButton" onClick={this.applyFilter}>
+ APPLY FILTER
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ {this.subChrome()}
+ </div>
+ </div>
+ );
+ }
+}
+
+@observer
+export class CollectionStackingViewChrome extends React.Component<CollectionViewChromeProps> {
+ @observable private _currentKey: string = "";
+ @observable private suggestions: string[] = [];
+
+ @computed private get descending() { return BoolCast(this.props.CollectionView.props.Document.stackingHeadersSortDescending); }
+ @computed get sectionFilter() { return StrCast(this.props.CollectionView.props.Document.sectionFilter); }
+
+ getKeySuggestions = async (value: string): Promise<string[]> => {
+ value = value.toLowerCase();
+ let docs: Doc | Doc[] | Promise<Doc> | Promise<Doc[]> | (() => DocLike)
+ = () => DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldExt ? this.props.CollectionView.props.fieldExt : this.props.CollectionView.props.fieldKey]);
+ if (typeof docs === "function") {
+ docs = docs();
+ }
+ 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));
+ }
+ }
+
+ @action
+ onKeyChange = (e: React.ChangeEvent, { newValue }: { newValue: string }) => {
+ this._currentKey = newValue;
+ }
+
+ 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 = [];
+ }
+
+ setValue = (value: string) => {
+ this.props.CollectionView.props.Document.sectionFilter = value;
+ return true;
+ }
+
+ @action toggleSort = () => { this.props.CollectionView.props.Document.stackingHeadersSortDescending = !this.props.CollectionView.props.Document.stackingHeadersSortDescending; };
+ @action resetValue = () => { this._currentKey = this.sectionFilter; };
+
+ render() {
+ return (
+ <div className="collectionStackingViewChrome-cont">
+ <button className="collectionStackingViewChrome-sort" onClick={this.toggleSort}>
+ <div className="collectionStackingViewChrome-sortLabel">
+ Sort
+ </div>
+ <div className="collectionStackingViewChrome-sortIcon" style={{ transform: `rotate(${this.descending ? "180" : "0"}deg)` }}>
+ <FontAwesomeIcon icon="caret-up" size="2x" color="white" />
+ </div>
+ </button>
+ <div className="collectionStackingViewChrome-sectionFilter-cont">
+ <div className="collectionStackingViewChrome-sectionFilter-label">
+ GROUP ITEMS BY:
+ </div>
+ <div className="collectionStackingViewChrome-sectionFilter">
+ <EditableView
+ GetValue={() => this.sectionFilter}
+ autosuggestProps={
+ {
+ resetValue: this.resetValue,
+ value: this._currentKey,
+ onChange: this.onKeyChange,
+ autosuggestProps: {
+ inputProps:
+ {
+ value: this._currentKey,
+ onChange: this.onKeyChange
+ },
+ getSuggestionValue: this.getSuggestionValue,
+ suggestions: this.suggestions,
+ alwaysRenderSuggestions: true,
+ renderSuggestion: this.renderSuggestion,
+ onSuggestionsFetchRequested: this.onSuggestionFetch,
+ onSuggestionsClearRequested: this.onSuggestionClear
+ }
+ }}
+ oneLine
+ SetValue={this.setValue}
+ contents={this.sectionFilter ? this.sectionFilter : "N/A"}
+ />
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+
+@observer
+export class CollectionSchemaViewChrome extends React.Component<CollectionViewChromeProps> {
+ // private _textwrapAllRows: boolean = Cast(this.props.CollectionView.props.Document.textwrappedSchemaRows, listSpec("string"), []).length > 0;
+
+ @undoBatch
+ togglePreview = () => {
+ let dividerWidth = 4;
+ let borderWidth = Number(COLLECTION_BORDER_WIDTH);
+ let panelWidth = this.props.CollectionView.props.PanelWidth();
+ let previewWidth = NumCast(this.props.CollectionView.props.Document.schemaPreviewWidth);
+ let tableWidth = panelWidth - 2 * borderWidth - dividerWidth - previewWidth;
+ this.props.CollectionView.props.Document.schemaPreviewWidth = previewWidth === 0 ? Math.min(tableWidth / 3, 200) : 0;
+ }
+
+ @undoBatch
+ @action
+ toggleTextwrap = async () => {
+ let textwrappedRows = Cast(this.props.CollectionView.props.Document.textwrappedSchemaRows, listSpec("string"), []);
+ if (textwrappedRows.length) {
+ this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>([]);
+ } else {
+ let docs: Doc | Doc[] | Promise<Doc> | Promise<Doc[]> | (() => DocLike)
+ = () => DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldExt ? this.props.CollectionView.props.fieldExt : this.props.CollectionView.props.fieldKey]);
+ if (typeof docs === "function") {
+ docs = docs();
+ }
+ docs = await docs;
+ if (docs instanceof Doc) {
+ let allRows = [docs[Id]];
+ this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>(allRows);
+ } else {
+ let allRows = docs.map(doc => doc[Id]);
+ this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>(allRows);
+ }
+ }
+ }
+
+
+ render() {
+ let previewWidth = NumCast(this.props.CollectionView.props.Document.schemaPreviewWidth);
+ let textWrapped = Cast(this.props.CollectionView.props.Document.textwrappedSchemaRows, listSpec("string"), []).length > 0;
+
+ return (
+ <div className="collectionSchemaViewChrome-cont">
+ <div className="collectionSchemaViewChrome-toggle">
+ <div className="collectionSchemaViewChrome-label">Wrap Text: </div>
+ <div className="collectionSchemaViewChrome-toggler" onClick={this.toggleTextwrap}>
+ <div className={"collectionSchemaViewChrome-togglerButton" + (textWrapped ? " on" : " off")}>
+ {textWrapped ? "on" : "off"}
+ </div>
+ </div>
+ </div>
+
+ <div className="collectionSchemaViewChrome-toggle">
+ <div className="collectionSchemaViewChrome-label">Show Preview: </div>
+ <div className="collectionSchemaViewChrome-toggler" onClick={this.togglePreview}>
+ <div className={"collectionSchemaViewChrome-togglerButton" + (previewWidth !== 0 ? " on" : " off")}>
+ {previewWidth !== 0 ? "on" : "off"}
+ </div>
+ </div>
+ </div>
+ </div >
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/KeyRestrictionRow.tsx b/src/client/views/collections/KeyRestrictionRow.tsx
new file mode 100644
index 000000000..1b59547d8
--- /dev/null
+++ b/src/client/views/collections/KeyRestrictionRow.tsx
@@ -0,0 +1,55 @@
+import * as React from "react";
+import { observable, runInAction } from "mobx";
+import { observer } from "mobx-react";
+import { PastelSchemaPalette } from "../../../new_fields/SchemaHeaderField";
+import { Doc } from "../../../new_fields/Doc";
+
+interface IKeyRestrictionProps {
+ contains: boolean;
+ script: (value: string) => void;
+}
+
+@observer
+export default class KeyRestrictionRow extends React.Component<IKeyRestrictionProps> {
+ @observable private _key = "";
+ @observable private _value = "";
+ @observable private _contains = this.props.contains;
+
+ render() {
+ if (this._key && this._value) {
+ let parsedValue: string | number = `"${this._value}"`;
+ let parsed = parseInt(this._value);
+ let type = "string";
+ if (!isNaN(parsed)) {
+ parsedValue = parsed;
+ type = "number";
+ }
+ let scriptText = `${this._contains ? "" : "!"}(((doc.${this._key} && (doc.${this._key} as ${type})${type === "string" ? ".includes" : "<="}(${parsedValue}))) ||
+ ((doc.data_ext && doc.data_ext.${this._key}) && (doc.data_ext.${this._key} as ${type})${type === "string" ? ".includes" : "<="}(${parsedValue}))))`;
+ // let doc = new Doc();
+ // ((doc.data_ext && doc.data_ext!.text) && (doc.data_ext!.text as string).includes("hello"));
+ this.props.script(scriptText);
+ }
+ else {
+ this.props.script("");
+ }
+
+ return (
+ <div className="collectionViewBaseChrome-viewSpecsMenu-row">
+ <input className="collectionViewBaseChrome-viewSpecsMenu-rowLeft"
+ value={this._key}
+ onChange={(e) => runInAction(() => this._key = e.target.value)}
+ placeholder="KEY" />
+ <button className="collectionViewBaseChrome-viewSpecsMenu-rowMiddle"
+ style={{ background: this._contains ? "#77dd77" : "#ff6961" }}
+ onClick={() => runInAction(() => this._contains = !this._contains)}>
+ {this._contains ? "CONTAINS" : "DOES NOT CONTAIN"}
+ </button>
+ <input className="collectionViewBaseChrome-viewSpecsMenu-rowRight"
+ value={this._value}
+ onChange={(e) => runInAction(() => this._value = e.target.value)}
+ placeholder="VALUE" />
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index b546d1b78..6af87b138 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -21,10 +21,10 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
if (e.button === 0 && !InkingControl.Instance.selectedTool) {
let a = this.props.A;
let b = this.props.B;
- let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : a[WidthSym]() / 2);
- 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);
+ let x1 = NumCast(a.x) + (BoolCast(a.isMinimized) ? 5 : a[WidthSym]() / 2);
+ let y1 = NumCast(a.y) + (BoolCast(a.isMinimized) ? 5 : a[HeightSym]() / 2);
+ let x2 = NumCast(b.x) + (BoolCast(b.isMinimized) ? 5 : b[WidthSym]() / 2);
+ let y2 = NumCast(b.y) + (BoolCast(b.isMinimized) ? 5 : b[HeightSym]() / 2);
// this.props.LinkDocs.map(l => {
// let width = l[WidthSym]();
// l.x = (x1 + x2) / 2 - width / 2;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index 00407d39a..c4311fa52 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -19,6 +19,11 @@
transform-origin: left top;
}
+.collectionFreeform-customText {
+ position: absolute;
+ text-align: center;
+}
+
.collectionfreeformview-container {
.collectionfreeformview>.jsx-parser {
position: inherit;
@@ -41,6 +46,7 @@
border-radius: inherit;
box-sizing: border-box;
position: absolute;
+ overflow: hidden;
.marqueeView {
overflow: hidden;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 4532239d6..a08a12426 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,37 +1,44 @@
-import { action, computed } from "mobx";
+import { library } from "@fortawesome/fontawesome-svg-core";
+import { faEye } from "@fortawesome/free-regular-svg-icons";
+import { faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faPaintBrush, faTable, faUpload, faChalkboard, faBraille } from "@fortawesome/free-solid-svg-icons";
+import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
-import { Doc, DocListCastAsync, HeightSym, WidthSym, DocListCast } from "../../../../new_fields/Doc";
+import { Doc, DocListCastAsync, HeightSym, WidthSym } 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 { ScriptField } from "../../../../new_fields/ScriptField";
+import { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../../../new_fields/Types";
+import { emptyFunction, returnOne, Utils, returnFalse, returnEmptyString } from "../../../../Utils";
+import { CognitiveServices } from "../../../cognitive_services/CognitiveServices";
+import { DocServer } from "../../../DocServer";
import { DocumentManager } from "../../../util/DocumentManager";
import { DragManager } from "../../../util/DragManager";
import { HistoryUtil } from "../../../util/History";
+import { CompileScript } from "../../../util/Scripting";
import { SelectionManager } from "../../../util/SelectionManager";
import { Transform } from "../../../util/Transform";
import { undoBatch, UndoManager } from "../../../util/UndoManager";
import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss";
import { ContextMenu } from "../../ContextMenu";
+import { ContextMenuProps } from "../../ContextMenuItem";
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 { OverlayElementOptions, OverlayView } from "../../OverlayView";
import PDFMenu from "../../pdf/PDFMenu";
+import { ScriptBox } from "../../ScriptBox";
import { CollectionSubView } from "../CollectionSubView";
import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView";
import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors";
import "./CollectionFreeFormView.scss";
import { MarqueeView } from "./MarqueeView";
import React = require("react");
-import v5 = require("uuid/v5");
-import { ScriptField } from "../../../../new_fields/ScriptField";
-import { OverlayView, OverlayElementOptions } from "../../OverlayView";
-import { ScriptBox } from "../../ScriptBox";
-import { CompileScript } from "../../../util/Scripting";
+import { DocumentType, Docs } from "../../../documents/Documents";
+library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard);
export const panZoomSchema = createSchema({
panX: "number",
@@ -51,26 +58,47 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
private _lastY: number = 0;
private get _pwidth() { return this.props.PanelWidth(); }
private get _pheight() { return this.props.PanelHeight(); }
+ private inkKey = "ink";
+
+ get parentScaling() {
+ return (this.props as any).ContentScaling && this.fitToBox && !this.isAnnotationOverlay ? (this.props as any).ContentScaling() : 1;
+ }
+
+ ComputeContentBounds(boundsList: { x: number, y: number, width: number, height: number }[]) {
+ let bounds = boundsList.reduce((bounds, b) => {
+ var [sptX, sptY] = [b.x, b.y];
+ let [bptX, bptY] = [sptX + NumCast(b.width, 1), sptY + NumCast(b.height, 1)];
+ 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.MAX_VALUE, b: -Number.MAX_VALUE });
+ return bounds;
+ }
@computed get contentBounds() {
- let bounds = this.props.fitToBox && !NumCast(this.nativeWidth) ? Doc.ComputeContentBounds(DocListCast(this.props.Document.data)) : undefined;
- return {
+ let bounds = this.fitToBox && !this.isAnnotationOverlay ? this.ComputeContentBounds(this.elements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!)) : undefined;
+ let res = {
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
+ scale: (bounds ? Math.min(this.props.PanelHeight() / (bounds.b - bounds.y), this.props.PanelWidth() / (bounds.r - bounds.x)) : this.Document.scale || 1) / this.parentScaling
};
+ if (res.scale === 0) res.scale = 1;
+ return res;
}
- @computed get nativeWidth() { return this.Document.nativeWidth || 0; }
- @computed get nativeHeight() { return this.Document.nativeHeight || 0; }
+ @computed get fitToBox() { return this.props.fitToBox || this.props.Document.fitToBox; }
+ @computed get nativeWidth() { return this.fitToBox ? 0 : this.Document.nativeWidth || 0; }
+ @computed get nativeHeight() { return this.fitToBox ? 0 : this.Document.nativeHeight || 0; }
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.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 centeringShiftX = () => !this.nativeWidth && !this.isAnnotationOverlay ? this._pwidth / 2 / this.parentScaling : 0; // shift so pan position is at center of window for non-overlay collections
+ private centeringShiftY = () => !this.nativeHeight && !this.isAnnotationOverlay ? this._pheight / 2 / this.parentScaling : 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());
+ private getTransformOverlay = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth + 1, -this.borderWidth + 1);
private getContainerTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth, -this.borderWidth);
private getLocalTransform = (): Transform => Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY());
private addLiveTextBox = (newBox: Doc) => {
@@ -80,6 +108,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
private addDocument = (newBox: Doc, allowDuplicates: boolean) => {
this.props.addDocument(newBox, false);
this.bringToFront(newBox);
+ this.updateClusters();
return true;
}
private selectDocuments = (docs: Doc[]) => {
@@ -99,17 +128,38 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, "true");
}
-
+ intersectRect(r1: { left: number, top: number, width: number, height: number },
+ r2: { left: number, top: number, width: number, height: number }) {
+ return !(r2.left > r1.left + r1.width || r2.left + r2.width < r1.left || r2.top > r1.top + r1.height || r2.top + r2.height < r1.top);
+ }
+ _clusterDistance = 75;
+ boundsOverlap(doc: Doc, doc2: Doc) {
+ var x2 = NumCast(doc2.x) - this._clusterDistance;
+ var y2 = NumCast(doc2.y) - this._clusterDistance;
+ var w2 = NumCast(doc2.width) + this._clusterDistance;
+ var h2 = NumCast(doc2.height) + this._clusterDistance;
+ var x = NumCast(doc.x) - this._clusterDistance;
+ var y = NumCast(doc.y) - this._clusterDistance;
+ var w = NumCast(doc.width) + this._clusterDistance;
+ var h = NumCast(doc.height) + this._clusterDistance;
+ if (doc.z === doc2.z && this.intersectRect({ left: x, top: y, width: w, height: h }, { left: x2, top: y2, width: w2, height: h2 })) {
+ return true;
+ }
+ return false;
+ }
@undoBatch
@action
drop = (e: Event, de: DragManager.DropEvent) => {
+ let xf = this.getTransform();
+ let xfo = this.getTransformOverlay();
+ let [xp, yp] = xf.transformPoint(de.x, de.y);
+ let [xpo, ypo] = xfo.transformPoint(de.x, de.y);
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 z = NumCast(de.data.draggedDocuments[0].z);
+ let x = (z ? xpo : xp) - de.data.xOffset;
+ let y = (z ? ypo : 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 => {
@@ -125,18 +175,21 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
this.bringToFront(d);
});
+
+ this.updateClusters();
}
}
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;
+ de.data.targetContext = this.props.Document;
+ dragDoc.targetContext = this.props.Document;
this.bringToFront(dragDoc);
}
}
@@ -144,6 +197,87 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
return false;
}
+ tryDragCluster(e: PointerEvent) {
+ let probe = this.getTransform().transformPoint(e.clientX, e.clientY);
+ let cluster = this.childDocs.reduce((cluster, cd) => {
+ let cx = NumCast(cd.x) - this._clusterDistance;
+ let cy = NumCast(cd.y) - this._clusterDistance;
+ let cw = NumCast(cd.width) + 2 * this._clusterDistance;
+ let ch = NumCast(cd.height) + 2 * this._clusterDistance;
+ if (!cd.z && this.intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 })) {
+ return NumCast(cd.cluster);
+ }
+ return cluster;
+ }, -1);
+ if (cluster !== -1) {
+ let eles = this.childDocs.filter(cd => NumCast(cd.cluster) === cluster);
+ this.selectDocuments(eles);
+ let clusterDocs = SelectionManager.SelectedDocuments();
+ SelectionManager.DeselectAll();
+ let de = new DragManager.DocumentDragData(eles, eles.map(d => undefined));
+ de.moveDocument = this.props.moveDocument;
+ const [left, top] = clusterDocs[0].props.ScreenToLocalTransform().scale(clusterDocs[0].props.ContentScaling()).inverse().transformPoint(0, 0);
+ const [xoff, yoff] = this.getTransform().transformDirection(e.x - left, e.y - top);
+ de.dropAction = e.ctrlKey || e.altKey ? "alias" : undefined;
+ de.xOffset = xoff;
+ de.yOffset = yoff;
+ DragManager.StartDocumentDrag(clusterDocs.map(v => v.ContentDiv!), de, e.clientX, e.clientY, {
+ handlers: { dragComplete: action(emptyFunction) },
+ hideSource: !de.dropAction
+ });
+ return true;
+ }
+
+ return false;
+ }
+ @observable sets: (Doc[])[] = [];
+ @action
+ updateClusters() {
+ this.sets.length = 0;
+ this.childDocs.map(c => {
+ let included = [];
+ for (let i = 0; i < this.sets.length; i++) {
+ for (let member of this.sets[i]) {
+ if (this.boundsOverlap(c, member)) {
+ included.push(i);
+ break;
+ }
+ }
+ }
+ if (included.length === 0) {
+ this.sets.push([c]);
+ } else if (included.length === 1) {
+ this.sets[included[0]].push(c);
+ } else {
+ this.sets[included[0]].push(c);
+ for (let s = 1; s < included.length; s++) {
+ this.sets[included[0]].push(...this.sets[included[s]]);
+ this.sets[included[s]].length = 0;
+ }
+ }
+ });
+ this.sets.map((set, i) => set.map(member => member.cluster = i));
+ }
+
+ getClusterColor = (doc: Doc) => {
+ if (this.props.Document.useClusters) {
+ let cluster = NumCast(doc.cluster);
+ if (this.sets.length <= cluster) {
+ setTimeout(() => this.updateClusters(), 0);
+ return;
+ }
+ let set = this.sets.length > cluster ? this.sets[cluster] : undefined;
+ let colors = ["#da42429e", "#31ea318c", "#8c4000", "#4a7ae2c4", "#d809ff", "#ff7601", "#1dffff", "yellow", "#1b8231f2", "#000000ad"];
+ let clusterColor = colors[cluster % colors.length];
+ set && set.filter(s => !s.isBackground).map(s =>
+ s.backgroundColor && s.backgroundColor !== s.defaultBackgroundColor && (clusterColor = StrCast(s.backgroundColor)));
+ set && set.filter(s => s.isBackground).map(s =>
+ s.backgroundColor && s.backgroundColor !== s.defaultBackgroundColor && (clusterColor = StrCast(s.backgroundColor)));
+ return clusterColor;
+ }
+ return "";
+ }
+
@action
onPointerDown = (e: React.PointerEvent): void => {
if (e.button === 0 && !e.shiftKey && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1) && this.props.active()) {
@@ -164,6 +298,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
onPointerMove = (e: PointerEvent): void => {
if (!e.cancelBubble) {
+ if (this.props.Document.useClusters && this.tryDragCluster(e)) {
+ e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
+ e.preventDefault();
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ return;
+ }
let x = this.Document.panX || 0;
let y = this.Document.panY || 0;
let docs = this.childDocs || [];
@@ -263,11 +404,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX));
const newPanY = Math.min((1 - 1 / scale) * this.nativeHeight, Math.max(0, panY));
this.props.Document.panX = this.isAnnotationOverlay ? newPanX : panX;
- this.props.Document.panY = this.isAnnotationOverlay ? newPanY : panY;
- // this.props.Document.panX = panX;
- // this.props.Document.panY = panY;
+ this.props.Document.panY = this.isAnnotationOverlay && StrCast(this.props.Document.backgroundLayout).indexOf("PDFBox") === -1 ? newPanY : panY;
if (this.props.Document.scrollY) {
- this.props.Document.scrollY = panY - this.props.Document[HeightSym]();
+ this.props.Document.scrollY = panY - scale * this.props.Document[HeightSym]();
}
}
@@ -280,7 +419,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
onDragOver = (): void => {
}
- bringToFront = (doc: Doc) => {
+ bringToFront = (doc: Doc, sendToBack?: boolean) => {
+ if (sendToBack || doc.isBackground) {
+ doc.zIndex = 0;
+ return;
+ }
const docs = this.childDocs;
docs.slice().sort((doc1, doc2) => {
if (doc1 === doc) return 1;
@@ -290,7 +433,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
doc.zIndex = docs.length + 1;
}
- focusDocument = (doc: Doc, willZoom: boolean) => {
+ focusDocument = (doc: Doc, willZoom: boolean, scale?: number) => {
const panX = this.Document.panX;
const panY = this.Document.panY;
const id = this.Document[Id];
@@ -322,20 +465,20 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
this.props.Document.panTransformType = "Ease";
this.props.focus(this.props.Document);
if (willZoom) {
- this.setScaleToZoom(doc);
+ this.setScaleToZoom(doc, scale);
}
}
- setScaleToZoom = (doc: Doc) => {
+ setScaleToZoom = (doc: Doc, scale: number = 0.5) => {
let p = this.props;
let PanelHeight = p.PanelHeight();
let panelWidth = p.PanelWidth();
let docHeight = NumCast(doc.height);
let docWidth = NumCast(doc.width);
- let targetHeight = 0.5 * PanelHeight;
- let targetWidth = 0.5 * panelWidth;
+ let targetHeight = scale * PanelHeight;
+ let targetWidth = scale * panelWidth;
let maxScaleX: number = targetWidth / docWidth;
let maxScaleY: number = targetHeight / docHeight;
@@ -357,22 +500,22 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
getChildDocumentViewProps(childDocLayout: Doc): DocumentViewProps {
let self = this;
- let resolvedDataDoc = !this.props.Document.isTemplate && this.props.DataDoc !== this.props.Document ? this.props.DataDoc : undefined;
- let layoutDoc = Doc.expandTemplateLayout(childDocLayout, resolvedDataDoc);
+ let pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, childDocLayout);
return {
- DataDoc: resolvedDataDoc !== layoutDoc && resolvedDataDoc ? resolvedDataDoc : undefined,
- Document: layoutDoc,
+ DataDoc: pair.data,
+ Document: pair.layout,
addDocument: this.props.addDocument,
removeDocument: this.props.removeDocument,
moveDocument: this.props.moveDocument,
- ScreenToLocalTransform: this.getTransform,
+ ScreenToLocalTransform: pair.layout.z ? this.getTransformOverlay : this.getTransform,
renderDepth: this.props.renderDepth + 1,
- selectOnLoad: layoutDoc[Id] === this._selectOnLoaded,
- PanelWidth: layoutDoc[WidthSym],
- PanelHeight: layoutDoc[HeightSym],
+ selectOnLoad: pair.layout[Id] === this._selectOnLoaded,
+ PanelWidth: pair.layout[WidthSym],
+ PanelHeight: pair.layout[HeightSym],
ContentScaling: returnOne,
ContainingCollectionView: this.props.CollectionView,
focus: this.focusDocument,
+ backgroundColor: this.getClusterColor,
parentActive: this.props.active,
whenActiveChanged: this.props.whenActiveChanged,
bringToFront: this.bringToFront,
@@ -389,13 +532,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
removeDocument: this.props.removeDocument,
moveDocument: this.props.moveDocument,
ScreenToLocalTransform: this.getTransform,
- renderDepth: this.props.renderDepth + 1,
+ renderDepth: this.props.renderDepth,
selectOnLoad: layoutDoc[Id] === this._selectOnLoaded,
PanelWidth: layoutDoc[WidthSym],
PanelHeight: layoutDoc[HeightSym],
ContentScaling: returnOne,
ContainingCollectionView: this.props.CollectionView,
focus: this.focusDocument,
+ backgroundColor: returnEmptyString,
parentActive: this.props.active,
whenActiveChanged: this.props.whenActiveChanged,
bringToFront: this.bringToFront,
@@ -405,54 +549,129 @@ 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 } {
+ getCalculatedPositions(script: ScriptField, params: { doc: Doc, index: number, collection: Doc, docs: Doc[], state: any }): { x?: number, y?: number, z?: number, width?: number, height?: number, state?: any } {
const result = script.script.run(params);
if (!result.success) {
return {};
}
- return result.result === undefined ? {} : result.result;
+ let doc = params.doc;
+ return result.result === undefined ? { x: Cast(doc.x, "number"), y: Cast(doc.y, "number"), z: Cast(doc.z, "number"), width: Cast(doc.width, "number"), height: Cast(doc.height, "number") } : result.result;
+ }
+
+ private viewDefToJSX(viewDef: any): { ele: JSX.Element, bounds?: { x: number, y: number, z?: number, width: number, height: number } } | undefined {
+ if (viewDef.type === "text") {
+ const text = Cast(viewDef.text, "string");
+ const x = Cast(viewDef.x, "number");
+ const y = Cast(viewDef.y, "number");
+ const z = Cast(viewDef.z, "number");
+ const width = Cast(viewDef.width, "number");
+ const height = Cast(viewDef.height, "number");
+ const fontSize = Cast(viewDef.fontSize, "number");
+ if ([text, x, y, width, height].some(val => val === undefined)) {
+ return undefined;
+ }
+
+ return {
+ ele: <div className="collectionFreeform-customText" style={{
+ transform: `translate(${x}px, ${y}px)`,
+ width, height, fontSize
+ }}>{text}</div>, bounds: { x: x!, y: y!, z: z, width: width!, height: height! }
+ };
+ }
}
@computed.struct
- get views() {
+ get elements() {
let curPage = FieldValue(this.Document.curPage, -1);
const initScript = this.Document.arrangeInit;
const script = this.Document.arrangeScript;
let state: any = undefined;
const docs = this.childDocs;
+ let elements: { ele: JSX.Element, bounds?: { x: number, y: number, z?: number, width: number, height: number } }[] = [];
if (initScript) {
const initResult = initScript.script.run({ docs, collection: this.Document });
if (initResult.success) {
- state = initResult.result;
+ const result = initResult.result;
+ const { state: scriptState, views } = result;
+ state = scriptState;
+ if (Array.isArray(views)) {
+ elements = views.reduce<typeof elements>((prev, ele) => {
+ const jsx = this.viewDefToJSX(ele);
+ jsx && prev.push(jsx);
+ return prev;
+ }, elements);
+ }
}
}
- let docviews = docs.reduce((prev, doc) => {
- if (!(doc instanceof Doc)) return prev;
+ let docviews = docs.filter(doc => doc instanceof Doc).reduce((prev, doc) => {
var page = NumCast(doc.page, -1);
if ((Math.abs(Math.round(page) - Math.round(curPage)) < 3) || page === -1) {
- let minim = BoolCast(doc.isMinimized, false);
+ let minim = BoolCast(doc.isMinimized);
if (minim === undefined || !minim) {
- const pos = script ? this.getCalculatedPositions(script, { doc, index: prev.length, collection: this.Document, docs, state }) : {};
+ const pos = script ? this.getCalculatedPositions(script, { doc, index: prev.length, collection: this.Document, docs, state }) :
+ { x: Cast(doc.x, "number"), y: Cast(doc.y, "number"), z: Cast(doc.z, "number"), width: Cast(doc.width, "number"), height: Cast(doc.height, "number") };
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)} />);
+ prev.push({
+ ele: <CollectionFreeFormDocumentView key={doc[Id]}
+ x={script ? pos.x : undefined} y={script ? pos.y : undefined}
+ width={script ? pos.width : undefined} height={script ? pos.height : undefined} {...this.getChildDocumentViewProps(doc)} />,
+ bounds: (pos.x !== undefined && pos.y !== undefined) ? { x: pos.x, y: pos.y, z: pos.z, width: NumCast(pos.width), height: NumCast(pos.height) } : undefined
+ });
}
}
return prev;
- }, [] as JSX.Element[]);
+ }, elements);
setTimeout(() => this._selectOnLoaded = "", 600);// bcz: surely there must be a better way ....
return docviews;
}
+ @computed.struct
+ get views() {
+ return this.elements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele);
+ }
+ @computed.struct
+ get overlayViews() {
+ return this.elements.filter(ele => ele.bounds && ele.bounds.z).map(ele => ele.ele);
+ }
+
+
@action
onCursorMove = (e: React.PointerEvent) => {
super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY));
}
- onContextMenu = () => {
- ContextMenu.Instance.addItem({
+ onContextMenu = (e: React.MouseEvent) => {
+ let layoutItems: ContextMenuProps[] = [];
+ layoutItems.push({
+ description: `${this.fitToBox ? "Unset" : "Set"} Fit To Container`,
+ event: async () => this.props.Document.fitToBox = !this.fitToBox,
+ icon: !this.fitToBox ? "expand-arrows-alt" : "compress-arrows-alt"
+ });
+ layoutItems.push({
+ description: "reset view", event: () => {
+ this.props.Document.panX = this.props.Document.panY = 0;
+ this.props.Document.scale = 1;
+ }, icon: "compress-arrows-alt"
+ });
+ layoutItems.push({
+ description: `${this.props.Document.useClusters ? "Uncluster" : "Use Clusters"}`,
+ event: async () => {
+ Docs.Prototypes.get(DocumentType.TEXT).defaultBackgroundColor = "#f1efeb"; // backward compatibility with databases that didn't have a default background color on prototypes
+ Docs.Prototypes.get(DocumentType.COL).defaultBackgroundColor = "white";
+ this.props.Document.useClusters = !this.props.Document.useClusters;
+ },
+ icon: !this.props.Document.useClusters ? "braille" : "braille"
+ });
+ layoutItems.push({
+ description: `${this.props.Document.clusterOverridesDefaultBackground ? "Use Default Backgrounds" : "Clusters Override Defaults"}`,
+ event: async () => this.props.Document.clusterOverridesDefaultBackground = !this.props.Document.clusterOverridesDefaultBackground,
+ icon: !this.props.Document.useClusters ? "chalkboard" : "chalkboard"
+ });
+ layoutItems.push({
description: "Arrange contents in grid",
+ icon: "table",
event: async () => {
const docs = await DocListCastAsync(this.Document[this.props.fieldKey]);
UndoManager.RunInBatch(() => {
@@ -477,46 +696,87 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}, "arrange contents");
}
});
+ ContextMenu.Instance.addItem({ description: "Layout...", subitems: layoutItems, icon: "compass" });
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);
+ description: "Analyze Strokes", event: async () => {
+ let data = Cast(this.fieldExtensionDoc[this.inkKey], InkField);
+ if (!data) {
+ return;
+ }
+ let relevantKeys = ["inkAnalysis", "handwriting"];
+ CognitiveServices.Inking.Manager.analyzer(this.fieldExtensionDoc, relevantKeys, data.inkData);
+ }, icon: "paint-brush"
+ });
+ ContextMenu.Instance.addItem({
+ description: "Import document", icon: "upload", event: () => {
+ const input = document.createElement("input");
+ input.type = "file";
+ input.accept = ".zip";
+ input.onchange = async _e => {
+ const files = input.files;
+ if (!files) return;
+ const file = files[0];
+ let formData = new FormData();
+ formData.append('file', file);
+ formData.append('remap', "true");
+ const upload = Utils.prepend("/uploadDoc");
+ const response = await fetch(upload, { method: "POST", body: formData });
+ const json = await response.json();
+ if (json === "error") {
+ return;
+ }
+ const doc = await DocServer.GetRefField(json);
+ if (!doc || !(doc instanceof Doc)) {
+ return;
+ }
+ const [x, y] = this.props.ScreenToLocalTransform().transformPoint(e.pageX, e.pageY);
+ doc.x = x, doc.y = y;
+ this.addDocument(doc, false);
};
- 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}");
+ input.click();
}
});
}
+
private childViews = () => [
<CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />,
...this.views
]
+ private overlayChildViews = () => {
+ return [...this.overlayViews];
+ }
+
+ public static AddCustomLayout(doc: Doc, dataKey: string): () => void {
+ return () => {
+ let addOverlay = (key: "arrangeScript" | "arrangeInit", options: OverlayElementOptions, params?: Record<string, string>, requiredType?: string) => {
+ let overlayDisposer: () => void = emptyFunction;
+ const script = Cast(doc[key], ScriptField);
+ 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;
+ }
+ doc[key] = new ScriptField(script);
+ overlayDisposer();
+ }} />;
+ overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, options);
+ };
+ addOverlay("arrangeInit", { x: 400, y: 100, width: 400, height: 300, title: "Layout Initialization" }, { collection: "Doc", docs: "Doc[]" }, undefined);
+ addOverlay("arrangeScript", { x: 400, y: 500, width: 400, height: 300, title: "Layout Script" }, { doc: "Doc", index: "number", collection: "Doc", state: "any", docs: "Doc[]" }, "{x: number, y: number, width?: number, height?: number}");
+ };
+ }
+
render() {
const easing = () => this.props.Document.panTransformType === "Ease";
-
Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey);
return (
<div className={"collectionfreeformview-container"} ref={this.createDropTarget} onWheel={this.onPointerWheel}
@@ -534,6 +794,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
<CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />
</CollectionFreeFormViewPannableContents>
</MarqueeView>
+ {this.overlayChildViews()}
<CollectionFreeFormOverlayView {...this.props} {...this.getDocumentViewProps(this.props.Document)} />
</div>
);
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index b765517a2..aad26efa0 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -19,6 +19,7 @@ import { CollectionViewType } from "../CollectionBaseView";
import { CollectionFreeFormView } from "./CollectionFreeFormView";
import "./MarqueeView.scss";
import React = require("react");
+import { SchemaHeaderField, RandomPastel } from "../../../../new_fields/SchemaHeaderField";
interface MarqueeViewProps {
getContainerTransform: () => Transform;
@@ -134,7 +135,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
doc.width = 200;
docList.push(doc);
}
- let newCol = Docs.Create.SchemaDocument([...(groupAttr ? ["_group"] : []), ...columns.filter(c => c)], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 });
+ let newCol = Docs.Create.SchemaDocument([...(groupAttr ? [new SchemaHeaderField("_group", "#f1efeb")] : []), ...columns.filter(c => c).map(c => new SchemaHeaderField(c, "#f1efeb"))], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 });
this.props.addDocument(newCol, false);
}
@@ -225,15 +226,17 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
}
get ink() {
- let container = this.props.container.Document;
+ let container = this.props.container.props.Document;
let containerKey = this.props.container.props.fieldKey;
- return Cast(container[containerKey + "_ink"], InkField);
+ let extensionDoc = Doc.resolvedFieldDataDoc(container, containerKey, "true");
+ return Cast(extensionDoc.ink, InkField);
}
set ink(value: InkField | undefined) {
- let container = Doc.GetProto(this.props.container.Document);
+ let container = Doc.GetProto(this.props.container.props.Document);
let containerKey = this.props.container.props.fieldKey;
- container[containerKey + "_ink"] = value;
+ let extensionDoc = Doc.resolvedFieldDataDoc(container, containerKey, "true");
+ extensionDoc.ink = value;
}
@undoBatch
@@ -246,7 +249,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
this._commandExecuted = true;
e.stopPropagation();
(e as any).propagationIsStopped = true;
- this.marqueeSelect().map(d => this.props.removeDocument(d));
+ this.marqueeSelect(false).map(d => this.props.removeDocument(d));
if (this.ink) {
this.marqueeInkDelete(this.ink.inkData);
}
@@ -260,7 +263,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
e.preventDefault();
(e as any).propagationIsStopped = true;
let bounds = this.Bounds;
- let selected = this.marqueeSelect();
+ let selected = this.marqueeSelect(false);
if (e.key === "c") {
selected.map(d => {
this.props.removeDocument(d);
@@ -277,11 +280,13 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
panX: 0,
panY: 0,
backgroundColor: this.props.container.isAnnotationOverlay ? undefined : "white",
+ defaultBackgroundColor: this.props.container.isAnnotationOverlay ? undefined : "white",
width: bounds.width,
height: bounds.height,
title: e.key === "s" || e.key === "S" ? "-summary-" : "a nested collection",
});
- newCollection.data_ink = inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined;
+ let dataExtensionField = Doc.CreateDocumentExtensionForField(newCollection, "data");
+ dataExtensionField.ink = inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined;
this.marqueeInkDelete(inkData);
if (e.key === "s") {
@@ -292,15 +297,16 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
d.page = -1;
return d;
});
+ newCollection.chromeStatus = "disabled";
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.Create.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, chromeStatus: "disabled", title: "-summary-" });
container.viewType = CollectionViewType.Stacking;
+ container.autoHeight = true;
this.props.addLiveTextDocument(container);
// });
} else if (e.key === "S") {
@@ -311,6 +317,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
d.page = -1;
return d;
});
+ newCollection.chromeStatus = "disabled";
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];
@@ -318,6 +325,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
//this.props.addDocument(newCollection, false);
summary.proto!.summarizedDocs = new List<Doc>(selected);
summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight"
+ summary.autoHeight = true;
this.props.addLiveTextDocument(summary);
}
@@ -362,19 +370,29 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
}
}
- marqueeSelect() {
+ marqueeSelect(selectBackgrounds: boolean = true) {
let selRect = this.Bounds;
let selection: Doc[] = [];
- this.props.activeDocuments().map(doc => {
- var z = NumCast(doc.zoomBasis, 1);
+ this.props.activeDocuments().filter(doc => !doc.isBackground).map(doc => {
var x = NumCast(doc.x);
var y = NumCast(doc.y);
- var w = NumCast(doc.width) / z;
- var h = NumCast(doc.height) / z;
+ var w = NumCast(doc.width);
+ var h = NumCast(doc.height);
if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) {
selection.push(doc);
}
});
+ if (!selection.length && selectBackgrounds) {
+ this.props.activeDocuments().map(doc => {
+ var x = NumCast(doc.x);
+ var y = NumCast(doc.y);
+ var w = NumCast(doc.width);
+ var h = NumCast(doc.height);
+ if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) {
+ selection.push(doc);
+ }
+ });
+ }
return selection;
}
diff --git a/src/client/views/nodes/ButtonBox.scss b/src/client/views/nodes/ButtonBox.scss
new file mode 100644
index 000000000..92beafa15
--- /dev/null
+++ b/src/client/views/nodes/ButtonBox.scss
@@ -0,0 +1,12 @@
+.buttonBox-outerDiv {
+ width: 100%;
+ height: 100%;
+ pointer-events: all;
+ border-radius: inherit;
+}
+
+.buttonBox-mainButton {
+ width: 100%;
+ height: 100%;
+ border-radius: inherit;
+} \ No newline at end of file
diff --git a/src/client/views/nodes/ButtonBox.tsx b/src/client/views/nodes/ButtonBox.tsx
new file mode 100644
index 000000000..640795789
--- /dev/null
+++ b/src/client/views/nodes/ButtonBox.tsx
@@ -0,0 +1,77 @@
+import * as React from 'react';
+import { FieldViewProps, FieldView } from './FieldView';
+import { createSchema, makeInterface } from '../../../new_fields/Schema';
+import { ScriptField } from '../../../new_fields/ScriptField';
+import { DocComponent } from '../DocComponent';
+import { ContextMenu } from '../ContextMenu';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faEdit } from '@fortawesome/free-regular-svg-icons';
+import { emptyFunction } from '../../../Utils';
+import { ScriptBox } from '../ScriptBox';
+import { CompileScript } from '../../util/Scripting';
+import { OverlayView } from '../OverlayView';
+import { Doc } from '../../../new_fields/Doc';
+
+import './ButtonBox.scss';
+import { observer } from 'mobx-react';
+import { DocumentIconContainer } from './DocumentIcon';
+
+library.add(faEdit as any);
+
+const ButtonSchema = createSchema({
+ onClick: ScriptField,
+ text: "string"
+});
+
+type ButtonDocument = makeInterface<[typeof ButtonSchema]>;
+const ButtonDocument = makeInterface(ButtonSchema);
+
+@observer
+export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(ButtonDocument) {
+ public static LayoutString() { return FieldView.LayoutString(ButtonBox); }
+
+ onClick = (e: React.MouseEvent) => {
+ const onClick = this.Document.onClick;
+ if (!onClick) {
+ return;
+ }
+ e.stopPropagation();
+ e.preventDefault();
+ onClick.script.run({ this: this.props.Document });
+ }
+
+ onContextMenu = () => {
+ ContextMenu.Instance.addItem({
+ description: "Edit OnClick script", icon: "edit", event: () => {
+ let overlayDisposer: () => void = emptyFunction;
+ const script = this.Document.onClick;
+ let originalText: string | undefined = undefined;
+ if (script) originalText = script.script.originalScript;
+ // tslint:disable-next-line: no-unnecessary-callback-wrapper
+ let scriptingBox = <ScriptBox initialText={originalText} onCancel={() => overlayDisposer()} onSave={(text, onError) => {
+ const script = CompileScript(text, {
+ params: { this: Doc.name },
+ typecheck: false,
+ editable: true,
+ transformer: DocumentIconContainer.getTransformer()
+ });
+ if (!script.compiled) {
+ onError(script.errors.map(error => error.messageText).join("\n"));
+ return;
+ }
+ this.Document.onClick = new ScriptField(script);
+ overlayDisposer();
+ }} showDocumentIcons />;
+ overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, { x: 400, y: 200, width: 500, height: 400, title: `${this.Document.title || ""} OnClick` });
+ }
+ });
+ }
+
+ render() {
+ return (
+ <div className="buttonBox-outerDiv" onContextMenu={this.onContextMenu}>
+ <button className="buttonBox-mainButton" onClick={this.onClick}>{this.Document.text || this.Document.title || "Button"}</button>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index b09538d1a..ee596c841 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -8,6 +8,7 @@ import { DocumentView, DocumentViewProps, positionSchema } from "./DocumentView"
import "./DocumentView.scss";
import React = require("react");
import { Doc } from "../../../new_fields/Doc";
+import { returnEmptyString } from "../../../Utils";
export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
x?: number;
@@ -35,21 +36,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
@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); }
-
- set width(w: number) {
- this.Document.width = w;
- if (this.nativeWidth && this.nativeHeight) {
- this.Document.height = this.nativeHeight / this.nativeWidth * w;
- }
- }
- set height(h: number) {
- this.Document.height = h;
- if (this.nativeWidth && this.nativeHeight) {
- 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;
+
+ contentScaling = () => this.nativeWidth > 0 && !BoolCast(this.props.Document.ignoreAspect) ? this.width / this.nativeWidth : 1;
panelWidth = () => this.props.PanelWidth();
panelHeight = () => this.props.PanelHeight();
getTransform = (): Transform => this.props.ScreenToLocalTransform()
@@ -81,16 +70,26 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
return undefined;
}
+ @computed
+ get clusterColor() { return this.props.backgroundColor(this.props.Document); }
+
+ clusterColorFunc = (doc: Doc) => this.clusterColor;
+
render() {
+ const hasPosition = this.props.x !== undefined || this.props.y !== undefined;
return (
<div className="collectionFreeFormDocumentView-container"
style={{
transformOrigin: "left top",
position: "absolute",
backgroundColor: "transparent",
+ boxShadow: this.props.Document.z ? `#9c9396 ${StrCast(this.props.Document.boxShadow, "10px 10px 0.9vw")}` :
+ this.clusterColor ? (
+ this.props.Document.isBackground ? `0px 0px 50px 50px ${this.clusterColor}` :
+ `${this.clusterColor} ${StrCast(this.props.Document.boxShadow, `0vw 0vw ${50 / this.props.ContentScaling()}px`)}`) : undefined,
borderRadius: this.borderRounding(),
transform: this.transform,
- transition: StrCast(this.props.Document.transition),
+ transition: hasPosition ? "transform 1s" : StrCast(this.props.Document.transition),
width: this.width,
height: this.height,
zIndex: this.Document.zIndex || 0,
@@ -98,6 +97,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
<DocumentView {...this.props}
ContentScaling={this.contentScaling}
ScreenToLocalTransform={this.getTransform}
+ backgroundColor={this.clusterColorFunc}
PanelWidth={this.panelWidth}
PanelHeight={this.panelHeight}
animateBetweenIcon={this.animateBetweenIcon}
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index ed6b224a7..396233551 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -11,12 +11,14 @@ import { DocumentViewProps } from "./DocumentView";
import "./DocumentView.scss";
import { FormattedTextBox } from "./FormattedTextBox";
import { ImageBox } from "./ImageBox";
+import { ButtonBox } from "./ButtonBox";
import { IconBox } from "./IconBox";
import { KeyValueBox } from "./KeyValueBox";
import { PDFBox } from "./PDFBox";
import { VideoBox } from "./VideoBox";
import { FieldView } from "./FieldView";
import { WebBox } from "./WebBox";
+import { YoutubeBox } from "./../../apis/youtube/YoutubeBox";
import { HistogramBox } from "../../northstar/dash-nodes/HistogramBox";
import React = require("react");
import { FieldViewProps } from "./FieldView";
@@ -64,7 +66,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
get dataDoc() {
if (this.props.DataDoc === undefined && this.props.Document.layout instanceof Doc) {
- // if there is no dataDoc (ie, we're not rendering a temlplate layout), but this document
+ // if there is no dataDoc (ie, we're not rendering a template layout), but this document
// has a template layout document, then we will render the template layout but use
// this document as the data document for the layout.
return this.props.Document;
@@ -97,7 +99,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
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, DirectoryImportBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }}
+ components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, ButtonBox, 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/DocumentIcon.tsx b/src/client/views/nodes/DocumentIcon.tsx
new file mode 100644
index 000000000..f56f5e829
--- /dev/null
+++ b/src/client/views/nodes/DocumentIcon.tsx
@@ -0,0 +1,65 @@
+import { observer } from "mobx-react";
+import * as React from "react";
+import { DocumentView } from "./DocumentView";
+import { DocumentManager } from "../../util/DocumentManager";
+import { Transformer, Scripting, ts } from "../../util/Scripting";
+import { Field } from "../../../new_fields/Doc";
+
+@observer
+export class DocumentIcon extends React.Component<{ view: DocumentView, index: number }> {
+ render() {
+ const view = this.props.view;
+ const transform = view.props.ScreenToLocalTransform().scale(view.props.ContentScaling()).inverse();
+ const { x, y, width, height } = transform.transformBounds(0, 0, view.props.PanelWidth(), view.props.PanelHeight());
+
+ return (
+ <div className="documentIcon-outerDiv" style={{
+ position: "absolute",
+ transform: `translate(${x + width / 2}px, ${y}px)`,
+ }}>
+ <p>d{this.props.index}</p>
+ </div>
+ );
+ }
+}
+
+@observer
+export class DocumentIconContainer extends React.Component {
+ public static getTransformer(): Transformer {
+ const usedDocuments = new Set<number>();
+ return {
+ transformer: context => {
+ return root => {
+ function visit(node: ts.Node) {
+ node = ts.visitEachChild(node, visit, context);
+
+ if (ts.isIdentifier(node)) {
+ const isntPropAccess = !ts.isPropertyAccessExpression(node.parent) || node.parent.expression === node;
+ const isntPropAssign = !ts.isPropertyAssignment(node.parent) || node.parent.name !== node;
+ const isntParameter = !ts.isParameter(node.parent);
+ if (isntPropAccess && isntPropAssign && isntParameter && !(node.text in globalThis)) {
+ const match = node.text.match(/d([0-9]+)/);
+ if (match) {
+ const m = parseInt(match[1]);
+ usedDocuments.add(m);
+ }
+ }
+ }
+
+ return node;
+ }
+ return ts.visitNode(root, visit);
+ };
+ },
+ getVars() {
+ const docs = DocumentManager.Instance.DocumentViews;
+ const capturedVariables: { [name: string]: Field } = {};
+ usedDocuments.forEach(index => capturedVariables[`d${index}`] = docs[index].props.Document);
+ return { capturedVariables };
+ }
+ };
+ }
+ render() {
+ return DocumentManager.Instance.DocumentViews.map((dv, i) => <DocumentIcon key={i} index={i} view={dv} />);
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 3a4b46b7e..7c72fb6e6 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -4,7 +4,6 @@
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 245dd319d..a7b4f33db 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,19 +1,24 @@
import { library } from '@fortawesome/fontawesome-svg-core';
import * as fa from '@fortawesome/free-solid-svg-icons';
-import { action, computed, IReactionDisposer, reaction, trace, observable, runInAction } from "mobx";
+import { action, computed, IReactionDisposer, reaction, runInAction, trace } from "mobx";
import { observer } from "mobx-react";
-import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocListCastAsync } from "../../../new_fields/Doc";
+import * as rp from "request-promise";
+import { Doc, DocListCast, DocListCastAsync, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc";
+import { Copy, Id } from '../../../new_fields/FieldSymbols';
import { List } from "../../../new_fields/List";
import { ObjectField } from "../../../new_fields/ObjectField";
-import { createSchema, makeInterface, listSpec } from "../../../new_fields/Schema";
-import { BoolCast, Cast, FieldValue, StrCast, NumCast, PromiseValue } from "../../../new_fields/Types";
+import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema";
+import { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
-import { emptyFunction, Utils, returnFalse, returnTrue } from "../../../Utils";
+import { RouteStore } from '../../../server/RouteStore';
+import { emptyFunction, returnTrue, Utils } from "../../../Utils";
import { DocServer } from "../../DocServer";
-import { Docs, DocUtils, DocumentType } from "../../documents/Documents";
+import { Docs, DocUtils } from "../../documents/Documents";
+import { ClientUtils } from '../../util/ClientUtils';
+import DictationManager from '../../util/DictationManager';
import { DocumentManager } from "../../util/DocumentManager";
import { DragManager, dropActionType } from "../../util/DragManager";
-import { SearchUtil } from "../../util/SearchUtil";
+import { LinkManager } from '../../util/LinkManager';
import { SelectionManager } from "../../util/SelectionManager";
import { Transform } from "../../util/Transform";
import { undoBatch, UndoManager } from "../../util/UndoManager";
@@ -22,23 +27,22 @@ import { CollectionPDFView } from "../collections/CollectionPDFView";
import { CollectionVideoView } from "../collections/CollectionVideoView";
import { CollectionView } from "../collections/CollectionView";
import { ContextMenu } from "../ContextMenu";
+import { ContextMenuProps } from '../ContextMenuItem';
import { DocComponent } from "../DocComponent";
+import { EditableView } from '../EditableView';
+import { OverlayView } from '../OverlayView';
import { PresentationView } from "../presentationview/PresentationView";
-import { Template, Templates } from "./../Templates";
+import { ScriptingRepl } from '../ScriptingRepl';
+import { Template } 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';
+import React = require("react");
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
library.add(fa.faTrash);
library.add(fa.faShare);
+library.add(fa.faDownload);
library.add(fa.faExpandArrowsAlt);
library.add(fa.faCompressArrowsAlt);
library.add(fa.faLayerGroup);
@@ -56,7 +60,7 @@ library.add(fa.faCrosshairs);
library.add(fa.faDesktop);
library.add(fa.faUnlock);
library.add(fa.faLock);
-
+library.add(fa.faLaptopCode, fa.faMale, fa.faCopy, fa.faHandPointRight, fa.faCompass, fa.faSnowflake, fa.faMicrophone);
// const linkSchema = createSchema({
// title: "string",
@@ -83,16 +87,18 @@ export interface DocumentViewProps {
ContentScaling: () => number;
PanelWidth: () => number;
PanelHeight: () => number;
- focus: (doc: Doc, willZoom: boolean) => void;
+ focus: (doc: Doc, willZoom: boolean, scale?: number) => void;
selectOnLoad: boolean;
parentActive: () => boolean;
whenActiveChanged: (isActive: boolean) => void;
- bringToFront: (doc: Doc) => void;
+ bringToFront: (doc: Doc, sendToBack?: boolean) => void;
addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void;
collapseToPoint?: (scrpt: number[], expandedDocs: Doc[] | undefined) => void;
zoomToScale: (scale: number) => void;
+ backgroundColor: (doc: Doc) => string | undefined;
getScale: () => number;
animateBetweenIcon?: (iconPos: number[], startTime: number, maximizing: boolean) => void;
+ ChromeHeight?: () => number;
}
const schema = createSchema({
@@ -111,6 +117,7 @@ export const positionSchema = createSchema({
height: "number",
x: "number",
y: "number",
+ z: "number",
});
export type PositionDocument = makeInterface<[typeof positionSchema]>;
@@ -190,10 +197,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
DocumentView.animateBetweenIconFunc(doc, width, height, stime, maximizing, cb);
}
else {
- Doc.GetProto(doc).isMinimized = !maximizing;
- Doc.GetProto(doc).isIconAnimating = undefined;
+ doc.isMinimized = !maximizing;
+ doc.isIconAnimating = undefined;
}
- Doc.GetProto(doc).willMaximize = false;
+ doc.willMaximize = false;
},
2);
}
@@ -270,7 +277,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
let iconAnimating = Cast(maximizedDoc.isIconAnimating, List);
if (!iconAnimating || (Date.now() - iconAnimating[2] > 1000)) {
if (isMinimized === undefined) {
- isMinimized = BoolCast(maximizedDoc.isMinimized, false);
+ isMinimized = BoolCast(maximizedDoc.isMinimized);
}
maximizedDoc.willMaximize = isMinimized;
maximizedDoc.isMinimized = false;
@@ -285,22 +292,25 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
onClick = async (e: React.MouseEvent) => {
+ if (e.nativeEvent.cancelBubble) return; // needed because EditableView may stopPropagation which won't apparently stop this event from firing.
e.stopPropagation();
let altKey = e.altKey;
let ctrlKey = e.ctrlKey;
if (this._doubleTap && this.props.renderDepth) {
let fullScreenAlias = Doc.MakeAlias(this.props.Document);
fullScreenAlias.templates = new List<string>();
+ Doc.UseDetailLayout(fullScreenAlias);
+ fullScreenAlias.showCaption = true;
this.props.addDocTab(fullScreenAlias, this.dataDoc, "inTab");
SelectionManager.DeselectAll();
- this.props.Document.libraryBrush = undefined;
+ this.props.Document.libraryBrush = false;
}
else if (CurrentUserUtils.MainDocId !== this.props.Document[Id] &&
(Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD &&
Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) {
SelectionManager.SelectDoc(this, e.ctrlKey);
let isExpander = (e.target as any).id === "isExpander";
- if (BoolCast(this.props.Document.isButton, false) || isExpander) {
+ if (BoolCast(this.props.Document.isButton) || isExpander) {
SelectionManager.DeselectAll();
let subBulletDocs = await DocListCastAsync(this.props.Document.subBulletDocs);
let maximizedDocs = await DocListCastAsync(this.props.Document.maximizedDocs);
@@ -312,20 +322,19 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
expandedDocs = summarizedDocs ? [...summarizedDocs, ...expandedDocs] : expandedDocs;
// let expandedDocs = [...(subBulletDocs ? subBulletDocs : []), ...(maximizedDocs ? maximizedDocs : []), ...(summarizedDocs ? summarizedDocs : []),];
if (expandedDocs.length) { // bcz: need a better way to associate behaviors with click events on widget-documents
- 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 || 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);
+ let hadView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedDocs[0], this.props.ContainingCollectionView);
+ let wasMinimized = !hadView && expandedDocs.reduce((min, d) => !min && !BoolCast(d.IsMinimized), false);
expandedDocs.forEach(maxDoc => Doc.GetProto(maxDoc).isMinimized = false);
- let hasView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedProtoDocs[0], this.props.ContainingCollectionView);
+ let hasView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedDocs[0], this.props.ContainingCollectionView);
if (!hasView) {
this.props.addDocument && expandedDocs.forEach(async maxDoc => this.props.addDocument!(getDispDoc(maxDoc), false));
}
- expandedProtoDocs.forEach(maxDoc => maxDoc.isMinimized = wasMinimized);
+ expandedDocs.forEach(maxDoc => maxDoc.isMinimized = wasMinimized);
}
}
if (maxLocation && maxLocation !== "inPlace" && CollectionDockingView.Instance) {
@@ -337,7 +346,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
} else {
let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(NumCast(this.Document.width) / 2, NumCast(this.Document.height) / 2);
- this.collapseTargetsToPoint(scrpt, expandedProtoDocs);
+ this.collapseTargetsToPoint(scrpt, expandedDocs);
}
}
else if (linkedDocs.length) {
@@ -347,12 +356,17 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
// @TODO: shouldn't always follow target context
let linkedFwdContextDocs = [first.length ? await (first[0].targetContext) as Doc : undefined, undefined];
- let linkedFwdPage = [first.length ? NumCast(first[0].linkedToPage, undefined) : undefined, undefined];
+ let linkedFwdPage = [first.length ? NumCast(first[0].anchor2Page, undefined) : undefined, undefined];
if (!linkedFwdDocs.some(l => l instanceof Promise)) {
let maxLocation = StrCast(linkedFwdDocs[0].maximizeLocation, "inTab");
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);
+ DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, false, document => {
+ this.props.focus(this.props.Document, true, 1);
+ setTimeout(() =>
+ this.props.addDocTab(document, undefined, maxLocation), 1000);
+ }
+ , linkedFwdPage[altKey ? 1 : 0], targetContext);
}
}
}
@@ -402,7 +416,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@undoBatch
makeBtnClicked = (): void => {
let doc = Doc.GetProto(this.props.Document);
- doc.isButton = !BoolCast(doc.isButton, false);
+ doc.isButton = !BoolCast(doc.isButton);
if (doc.isButton) {
if (!doc.nativeWidth) {
doc.nativeWidth = this.props.Document[WidthSym]();
@@ -426,7 +440,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
e.stopPropagation();
let annotationDoc = de.data.annotationDocument;
annotationDoc.linkedToDoc = true;
+ de.data.targetContext = this.props.ContainingCollectionView!.props.Document;
let targetDoc = this.props.Document;
+ targetDoc.targetContext = de.data.targetContext;
let annotations = await DocListCastAsync(annotationDoc.annotations);
if (annotations) {
annotations.forEach(anno => {
@@ -435,7 +451,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
let pdfDoc = await Cast(annotationDoc.pdfDoc, Doc);
if (pdfDoc) {
- DocUtils.MakeLink(annotationDoc, targetDoc, undefined, `Annotation from ${StrCast(pdfDoc.title)}`, "", StrCast(pdfDoc.title));
+ DocUtils.MakeLink(annotationDoc, targetDoc, this.props.ContainingCollectionView!.props.Document, `Annotation from ${StrCast(pdfDoc.title)}`, "", StrCast(pdfDoc.title));
}
}
if (de.data instanceof DragManager.LinkDragData) {
@@ -499,13 +515,19 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@undoBatch
@action
freezeNativeDimensions = (): void => {
- let proto = Doc.GetProto(this.props.Document);
- if (proto.ignoreAspect === undefined && !proto.nativeWidth) {
+ let proto = this.props.Document.isTemplate ? this.props.Document : Doc.GetProto(this.props.Document);
+ this.props.Document.autoHeight = proto.autoHeight = false;
+ proto.ignoreAspect = !BoolCast(proto.ignoreAspect);
+ if (!BoolCast(proto.ignoreAspect) && !proto.nativeWidth) {
proto.nativeWidth = this.props.PanelWidth();
proto.nativeHeight = this.props.PanelHeight();
- proto.ignoreAspect = true;
}
- proto.ignoreAspect = !BoolCast(proto.ignoreAspect, false);
+ }
+ @undoBatch
+ @action
+ makeBackground = (): void => {
+ this.props.Document.isBackground = !this.props.Document.isBackground;
+ this.props.Document.isBackground && this.props.bringToFront(this.props.Document, true);
}
@undoBatch
@@ -514,6 +536,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
this.props.Document.lockedPosition = BoolCast(this.props.Document.lockedPosition) ? undefined : true;
}
+ listen = async () => {
+ let transcript = await DictationManager.Instance.listen();
+ transcript && (Doc.GetProto(this.props.Document).transcript = transcript);
+ }
+
@action
onContextMenu = async (e: React.MouseEvent): Promise<void> => {
e.persist();
@@ -534,36 +561,57 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
subitems.push({ description: "Open Right Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.dataDoc, "onRight"), icon: "caret-square-right" });
subitems.push({ description: "Open Fields", event: this.fieldsClicked, icon: "layer-group" });
cm.addItem({ description: "Open...", subitems: subitems, icon: "external-link-alt" });
- cm.addItem({ description: BoolCast(this.props.Document.ignoreAspect, false) || !this.props.Document.nativeWidth || !this.props.Document.nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "edit" });
- cm.addItem({ description: "Pin to Pres", event: () => PresentationView.Instance.PinDoc(this.props.Document), icon: "map-pin" });
- cm.addItem({ description: BoolCast(this.props.Document.lockedPosition) ? "Unlock Pos" : "Lock Pos", event: this.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({
+ cm.addItem({ description: BoolCast(this.props.Document.ignoreAspect, false) || !this.props.Document.nativeWidth || !this.props.Document.nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "snowflake" });
+ cm.addItem({ description: "Pin to Presentation", event: () => PresentationView.Instance.PinDoc(this.props.Document), icon: "map-pin" });
+ cm.addItem({ description: BoolCast(this.props.Document.lockedPosition) ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.props.Document.lockedPosition) ? "unlock" : "lock" });
+ cm.addItem({ description: "Transcribe Speech", event: this.listen, icon: "microphone" });
+ let makes: ContextMenuProps[] = [];
+ makes.push({ description: this.props.Document.isBackground ? "Remove Background" : "Make Background", event: this.makeBackground, icon: BoolCast(this.props.Document.lockedPosition) ? "unlock" : "lock" });
+ makes.push({ description: this.props.Document.isButton ? "Remove Button" : "Make Button", event: this.makeBtnClicked, icon: "concierge-bell" });
+ makes.push({
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]);
+ //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);
+ //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);
+ DocUtils.MakeLink(this.props.Document, portal, undefined, this.props.Document.title + ".portal");
+ this.makeBtnClicked();
+
}, icon: "window-restore"
});
+ cm.addItem({ description: "Make...", subitems: makes, icon: "hand-point-right" });
+ if (this.props.Document.detailedLayout && !this.props.Document.isTemplate) {
+ cm.addItem({ description: "Toggle detail", event: () => Doc.ToggleDetailLayout(this.props.Document), icon: "image" });
+ }
+ cm.addItem({ description: "Add Repl", icon: "laptop-code", event: () => OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" }) });
+ let existing = ContextMenu.Instance.findByDescription("Layout...");
+ let layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : [];
+ layoutItems.push({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" });
+ layoutItems.push({ description: "Zoom to Document", event: () => this.props.focus(this.props.Document, true), icon: "search" });
+ !existing && cm.addItem({ description: "Layout...", subitems: layoutItems, icon: "compass" });
+ if (!ClientUtils.RELEASE) {
+ let copies: ContextMenuProps[] = [];
+ copies.push({ description: "Copy URL", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "link" });
+ copies.push({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" });
+ cm.addItem({ description: "Copy...", subitems: copies, icon: "copy" });
+ }
cm.addItem({
- description: "Find aliases", event: async () => {
- const aliases = await SearchUtil.GetAliasesOfDocument(this.props.Document);
- this.props.addDocTab && this.props.addDocTab(Docs.Create.SchemaDocument(["title"], aliases, {}), undefined, "onRight"); // bcz: dataDoc?
- }, icon: "search"
+ description: "Download document", icon: "download", event: () => {
+ const a = document.createElement("a");
+ const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`);
+ a.href = url;
+ a.download = `DocExport-${this.props.Document[Id]}.zip`;
+ a.click();
+ }
});
- cm.addItem({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" });
- cm.addItem({ description: "Zoom to Document", event: () => this.props.focus(this.props.Document, true), icon: "search" });
- cm.addItem({ description: "Copy URL", event: () => Utils.CopyText(DocServer.prepend("/doc/" + this.props.Document[Id])), icon: "link" });
- cm.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" });
cm.addItem({ description: "Delete", event: this.deleteClicked, icon: "trash" });
type User = { email: string, userDocumentId: string };
let usersMenu: ContextMenuProps[] = [];
try {
- let stuff = await rp.get(DocServer.prepend(RouteStore.getUsers));
+ let stuff = await rp.get(Utils.prepend(RouteStore.getUsers));
const users: User[] = JSON.parse(stuff);
usersMenu = users.filter(({ email }) => email !== CurrentUserUtils.email).map(({ email, userDocumentId }) => ({
description: email, event: async () => {
@@ -581,7 +629,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
notifDoc.data = new List([sharedDoc]);
}
}
- }
+ }, icon: "male"
}));
} catch {
@@ -600,7 +648,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
onPointerEnter = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = true; };
- onPointerLeave = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = undefined; };
+ onPointerLeave = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = false; };
isSelected = () => SelectionManager.IsSelected(this);
@action select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); };
@@ -608,39 +656,60 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@computed get nativeWidth() { return this.Document.nativeWidth || 0; }
@computed get nativeHeight() { return this.Document.nativeHeight || 0; }
@computed get contents() {
- return (<DocumentContentsView {...this.props} isSelected={this.isSelected} select={this.select} selectOnLoad={this.props.selectOnLoad} layoutKey={"layout"} DataDoc={this.dataDoc} />);
+ return (<DocumentContentsView {...this.props}
+ ChromeHeight={this.chromeHeight}
+ isSelected={this.isSelected} select={this.select}
+ selectOnLoad={this.props.selectOnLoad}
+ layoutKey={"layout"}
+ fitToBox={BoolCast(this.props.Document.fitToBox) ? true : this.props.fitToBox}
+ DataDoc={this.dataDoc} />);
+ }
+
+ chromeHeight = () => {
+ let showOverlays = this.props.showOverlays ? this.props.showOverlays(this.layoutDoc) : undefined;
+ let showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : StrCast(this.layoutDoc.showTitle);
+ return showTitle ? 25 : 0;
+ }
+
+ 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;
}
render() {
- 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%";
+ let backgroundColor = this.layoutDoc.isBackground || (this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document.clusterOverridesDefaultBackground && this.layoutDoc.backgroundColor === this.layoutDoc.defaultBackgroundColor) ?
+ this.props.backgroundColor(this.layoutDoc) || StrCast(this.layoutDoc.backgroundColor) :
+ StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor(this.layoutDoc);
+ let foregroundColor = StrCast(this.layoutDoc.color);
+ var nativeWidth = this.nativeWidth > 0 && !BoolCast(this.props.Document.ignoreAspect) ? `${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) {
+ let showOverlays = this.props.showOverlays ? this.props.showOverlays(this.layoutDoc) : undefined;
+ let showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : StrCast(this.layoutDoc.showTitle);
+ let showCaption = showOverlays && "caption" in showOverlays ? showOverlays.caption : StrCast(this.layoutDoc.showCaption);
+ let templates = Cast(this.layoutDoc.templates, listSpec("string"));
+ if (!showOverlays && templates instanceof List) {
templates.map(str => {
- if (str.indexOf("{props.Document.title}") !== -1) showTitle = "title";
- if (str.indexOf("fieldKey={\"caption\"}") !== -1) showCaption = "caption";
+ if (!showTitle && str.indexOf("{props.Document.title}") !== -1) showTitle = "title";
+ if (!showCaption && 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;
+ let showTextTitle = showTitle && StrCast(this.layoutDoc.layout).startsWith("<FormattedTextBox") ? showTitle : undefined;
return (
<div className={`documentView-node${this.topMost ? "-topmost" : ""}`}
ref={this._mainCont}
style={{
+ pointerEvents: this.layoutDoc.isBackground && !this.isSelected() ? "none" : "all",
color: foregroundColor,
outlineColor: "maroon",
outlineStyle: "dashed",
- outlineWidth: BoolCast(this.props.Document.libraryBrush) && !StrCast(this.props.Document.borderRounding) ?
+ outlineWidth: BoolCast(this.layoutDoc.libraryBrush) && !StrCast(Doc.GetProto(this.props.Document).borderRounding) ?
`${this.props.ScreenToLocalTransform().Scale}px` : "0px",
- border: BoolCast(this.props.Document.libraryBrush) && StrCast(this.props.Document.borderRounding) ?
+ marginLeft: BoolCast(this.layoutDoc.libraryBrush) && StrCast(Doc.GetProto(this.props.Document).borderRounding) ?
+ `${-1 * this.props.ScreenToLocalTransform().Scale}px` : undefined,
+ marginTop: BoolCast(this.layoutDoc.libraryBrush) && StrCast(Doc.GetProto(this.props.Document).borderRounding) ?
+ `${-1 * this.props.ScreenToLocalTransform().Scale}px` : undefined,
+ border: BoolCast(this.layoutDoc.libraryBrush) && StrCast(Doc.GetProto(this.props.Document).borderRounding) ?
`dashed maroon ${this.props.ScreenToLocalTransform().Scale}px` : undefined,
borderRadius: "inherit",
background: backgroundColor,
@@ -654,13 +723,25 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
>
{!showTitle && !showCaption ? this.contents :
<div style={{ position: "absolute", display: "inline-block", width: "100%", height: "100%", pointerEvents: "none" }}>
+
+ <div style={{ width: "100%", height: showTextTitle ? "calc(100% - 33px)" : "100%", display: "inline-block", position: "absolute", top: showTextTitle ? "29px" : undefined }}>
+ {this.contents}
+ </div>
{!showTitle ? (null) :
<div style={{
- position: showTextTitle ? "relative" : "absolute", top: 0, textAlign: "center", textOverflow: "ellipsis", whiteSpace: "pre",
+ position: showTextTitle ? "relative" : "absolute", top: 0, padding: "4px", textAlign: "center", textOverflow: "ellipsis", whiteSpace: "pre",
+ pointerEvents: SelectionManager.GetIsDragging() ? "none" : "all",
overflow: "hidden", width: `${100 * this.props.ContentScaling()}%`, height: 25, background: "rgba(0, 0, 0, .4)", color: "white",
transformOrigin: "top left", transform: `scale(${1 / this.props.ContentScaling()})`
}}>
- <span>{this.props.Document[showTitle]}</span>
+ <EditableView
+ contents={(this.layoutDoc.isTemplate || !this.dataDoc ? this.layoutDoc : this.dataDoc)[showTitle]}
+ display={"block"}
+ height={72}
+ fontSize={12}
+ GetValue={() => StrCast((this.layoutDoc.isTemplate || !this.dataDoc ? this.layoutDoc : this.dataDoc)[showTitle!])}
+ SetValue={(value: string) => (Doc.GetProto(this.layoutDoc)[showTitle!] = value) ? true : true}
+ />
</div>
}
{!showCaption ? (null) :
@@ -668,9 +749,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
<FormattedTextBox {...this.props} DataDoc={this.dataDoc} active={returnTrue} isSelected={this.isSelected} focus={emptyFunction} select={this.select} selectOnLoad={this.props.selectOnLoad} fieldExt={""} hideOnLeave={true} fieldKey={showCaption} />
</div>
}
- <div style={{ width: "100%", height: showTextTitle ? "calc(100% - 25px)" : "100%", display: "inline-block", position: showTextTitle ? "relative" : "absolute" }}>
- {this.contents}
- </div>
</div>
}
</div>
diff --git a/src/client/views/nodes/FaceRectangle.tsx b/src/client/views/nodes/FaceRectangle.tsx
new file mode 100644
index 000000000..887efc0d5
--- /dev/null
+++ b/src/client/views/nodes/FaceRectangle.tsx
@@ -0,0 +1,29 @@
+import React = require("react");
+import { observer } from "mobx-react";
+import { observable, runInAction } from "mobx";
+import { RectangleTemplate } from "./FaceRectangles";
+
+@observer
+export default class FaceRectangle extends React.Component<{ rectangle: RectangleTemplate }> {
+ @observable private opacity = 0;
+
+ componentDidMount() {
+ setTimeout(() => runInAction(() => this.opacity = 1), 500);
+ }
+
+ render() {
+ let rectangle = this.props.rectangle;
+ return (
+ <div
+ style={{
+ ...rectangle.style,
+ opacity: this.opacity,
+ transition: "1s ease opacity",
+ position: "absolute",
+ borderRadius: 5
+ }}
+ />
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/nodes/FaceRectangles.tsx b/src/client/views/nodes/FaceRectangles.tsx
new file mode 100644
index 000000000..acf1aced3
--- /dev/null
+++ b/src/client/views/nodes/FaceRectangles.tsx
@@ -0,0 +1,46 @@
+import React = require("react");
+import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { Cast, NumCast } from "../../../new_fields/Types";
+import { observer } from "mobx-react";
+import { Id } from "../../../new_fields/FieldSymbols";
+import FaceRectangle from "./FaceRectangle";
+
+interface FaceRectanglesProps {
+ document: Doc;
+ color: string;
+ backgroundColor: string;
+}
+
+export interface RectangleTemplate {
+ id: string;
+ style: Partial<React.CSSProperties>;
+}
+
+@observer
+export default class FaceRectangles extends React.Component<FaceRectanglesProps> {
+
+ render() {
+ let faces = DocListCast(this.props.document.faces);
+ let templates: RectangleTemplate[] = faces.map(faceDoc => {
+ let rectangle = Cast(faceDoc.faceRectangle, Doc) as Doc;
+ let style = {
+ top: NumCast(rectangle.top),
+ left: NumCast(rectangle.left),
+ width: NumCast(rectangle.width),
+ height: NumCast(rectangle.height),
+ backgroundColor: `${this.props.backgroundColor}33`,
+ border: `solid 2px ${this.props.color}`,
+ } as React.CSSProperties;
+ return {
+ id: rectangle[Id],
+ style: style
+ };
+ });
+ return (
+ <div>
+ {templates.map(rectangle => <FaceRectangle key={rectangle.id} rectangle={rectangle} />)}
+ </div>
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index ea6730cd0..da54ecc3a 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -48,6 +48,8 @@ export interface FieldViewProps {
PanelHeight: () => number;
setVideoBox?: (player: VideoBox) => void;
setPdfBox?: (player: PDFBox) => void;
+ ContentScaling: () => number;
+ ChromeHeight?: () => number;
}
@observer
@@ -87,7 +89,8 @@ export class FieldView extends React.Component<FieldViewProps> {
return <p>{field.date.toLocaleString()}</p>;
}
else if (field instanceof Doc) {
- return <p><b>{field.title + " : id= " + field[Id]}</b></p>;
+ return <p><b>{field.title}</b></p>;
+ //return <p><b>{field.title + " : id= " + field[Id]}</b></p>;
// let returnHundred = () => 100;
// return (
// <DocumentContentsView Document={field}
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index d3045ae2f..247f7d1ea 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -1,34 +1,37 @@
@import "../globalCssVariables";
+
.ProseMirror {
- width: 100%;
- height: 100%;
- min-height: 100%;
- font-family: $serif;
+ width: 100%;
+ height: 100%;
+ min-height: 100%;
+ font-family: $serif;
}
.ProseMirror:focus {
- outline: none !important;
+ outline: none !important;
}
-.formattedTextBox-cont-scroll, .formattedTextBox-cont-hidden {
- background: inherit;
- padding: 0;
- border-width: 0px;
- border-radius: inherit;
- border-color: $intermediate-color;
- box-sizing: border-box;
- background-color: inherit;
- border-style: solid;
- overflow-y: auto;
- overflow-x: hidden;
- color: initial;
- height: 100%;
- pointer-events: all;
+.formattedTextBox-cont-scroll,
+.formattedTextBox-cont-hidden {
+ background: inherit;
+ padding: 0;
+ border-width: 0px;
+ border-radius: inherit;
+ border-color: $intermediate-color;
+ box-sizing: border-box;
+ background-color: inherit;
+ border-style: solid;
+ overflow-y: auto;
+ overflow-x: hidden;
+ color: initial;
+ height: 100%;
+ pointer-events: all;
}
.formattedTextBox-cont-hidden {
pointer-events: none;
}
+
.formattedTextBox-inner-rounded {
height: calc(100% - 25px);
width: calc(100% - 40px);
@@ -38,23 +41,28 @@
left: 20;
}
+.formattedTextBox-inner-rounded div,
+.formattedTextBox-inner div {
+ padding: 10px 10px;
+}
+
.menuicon {
- display: inline-block;
- border-right: 1px solid rgba(0, 0, 0, 0.2);
- color: #888;
- line-height: 1;
- padding: 0 7px;
- margin: 1px;
- cursor: pointer;
- text-align: center;
- min-width: 1.4em;
+ display: inline-block;
+ border-right: 1px solid rgba(0, 0, 0, 0.2);
+ color: #888;
+ line-height: 1;
+ padding: 0 7px;
+ margin: 1px;
+ cursor: pointer;
+ text-align: center;
+ min-width: 1.4em;
}
.strong,
.heading {
- font-weight: bold;
+ font-weight: bold;
}
.em {
- font-style: italic;
-}
+ font-style: italic;
+} \ No newline at end of file
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 066cc40e2..9dd0643df 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -1,21 +1,22 @@
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faEdit, faSmile } from '@fortawesome/free-solid-svg-icons';
-import { action, IReactionDisposer, observable, reaction, runInAction, computed, trace } from "mobx";
+import { faEdit, faSmile, faTextHeight } from '@fortawesome/free-solid-svg-icons';
+import { action, IReactionDisposer, observable, reaction, runInAction, computed, Lambda, trace } from "mobx";
import { observer } from "mobx-react";
import { baseKeymap } from "prosemirror-commands";
import { history } from "prosemirror-history";
import { keymap } from "prosemirror-keymap";
-import { NodeType } from 'prosemirror-model';
-import { EditorState, Plugin, Transaction } from "prosemirror-state";
+import { Node as ProsNode } from "prosemirror-model";
+import { EditorState, Plugin, Transaction, Selection } from "prosemirror-state";
+import { NodeType, Slice, Node, Fragment } from 'prosemirror-model';
import { EditorView } from "prosemirror-view";
-import { Doc, Opt } from "../../../new_fields/Doc";
+import { Doc, Opt, DocListCast } 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, listSpec, makeInterface } from "../../../new_fields/Schema";
import { BoolCast, Cast, NumCast, StrCast, DateCast } from "../../../new_fields/Types";
import { DocServer } from "../../DocServer";
-import { Docs } from '../../documents/Documents';
+import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentManager } from '../../util/DocumentManager';
import { DragManager } from "../../util/DragManager";
import buildKeymap from "../../util/ProsemirrorExampleTransfer";
@@ -33,11 +34,13 @@ import { Templates } from '../Templates';
import { FieldView, FieldViewProps } from "./FieldView";
import "./FormattedTextBox.scss";
import React = require("react");
+import { For } from 'babel-types';
import { DateField } from '../../../new_fields/DateField';
-import { thisExpression } from 'babel-types';
+import { Utils } from '../../../Utils';
+import { MainOverlayTextBox } from '../MainOverlayTextBox';
library.add(faEdit);
-library.add(faSmile);
+library.add(faSmile, faTextHeight);
// FormattedTextBox: Displays an editable plain text node that maps to a specified Key of a Document
//
@@ -48,6 +51,7 @@ export interface FormattedTextBoxProps {
height?: string;
color?: string;
outer_div?: (domminus: HTMLElement) => void;
+ firstinstance?: boolean;
}
const richTextSchema = createSchema({
@@ -62,14 +66,16 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
public static LayoutString(fieldStr: string = "data") {
return FieldView.LayoutString(FormattedTextBox, fieldStr);
}
+ public static Instance: FormattedTextBox;
private _ref: React.RefObject<HTMLDivElement>;
private _outerdiv?: (dominus: HTMLElement) => void;
private _proseRef?: HTMLDivElement;
private _editorView: Opt<EditorView>;
- private _toolTipTextMenu: TooltipTextMenu | undefined = undefined;
+ private static _toolTipTextMenu: TooltipTextMenu | undefined = undefined;
private _applyingChange: boolean = false;
private _linkClicked = "";
private _reactionDisposer: Opt<IReactionDisposer>;
+ private _searchReactionDisposer?: Lambda;
private _textReactionDisposer: Opt<IReactionDisposer>;
private _proxyReactionDisposer: Opt<IReactionDisposer>;
private dropDisposer?: DragManager.DragDropDisposer;
@@ -99,6 +105,10 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
return "";
}
+ public static getToolTip() {
+ return this._toolTipTextMenu;
+ }
+
@undoBatch
public setFontColor(color: string) {
let self = this;
@@ -114,6 +124,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
constructor(props: FieldViewProps) {
super(props);
+ //if (this.props.firstinstance) {
+ FormattedTextBox.Instance = this;
+ //}
if (this.props.outer_div) {
this._outerdiv = this.props.outer_div;
}
@@ -122,23 +135,57 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
if (this.props.isOverlay) {
DragManager.StartDragFunctions.push(() => FormattedTextBox.InputBoxOverlay = undefined);
}
+
+ document.addEventListener("paste", this.paste);
}
@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); }
+ @computed get dataDoc() { return this.props.DataDoc && (BoolCast(this.props.Document.isTemplate) || BoolCast(this.props.DataDoc.isTemplate) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
+
+
+ paste = (e: ClipboardEvent) => {
+ if (e.clipboardData && this._editorView) {
+ let pdfPasteText = `${Utils.GenerateDeterministicGuid("pdf paste")}`;
+ for (let i = 0; i < e.clipboardData.items.length; i++) {
+ let item = e.clipboardData.items.item(i);
+ if (item.type === "text/plain") {
+ item.getAsString((text) => {
+ let pdfPasteIndex = text.indexOf(pdfPasteText);
+ if (pdfPasteIndex > -1) {
+ let insertText = text.substr(0, pdfPasteIndex);
+ const tx = this._editorView!.state.tr.insertText(insertText);
+ // tx.setSelection(new Selection(tx.))
+ const state = this._editorView!.state;
+ this._editorView!.dispatch(tx);
+ if (FormattedTextBox._toolTipTextMenu) {
+ // this._toolTipTextMenu.makeLinkWithState(state)
+ }
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ });
+ }
+ }
+ }
+ }
dispatchTransaction = (tx: Transaction) => {
if (this._editorView) {
const state = this._editorView.state.apply(tx);
this._editorView.updateState(state);
+ if (state.selection.empty && FormattedTextBox._toolTipTextMenu) {
+ const marks = tx.storedMarks;
+ if (marks) { FormattedTextBox._toolTipTextMenu.mark_key_pressed(marks); }
+ }
this._applyingChange = true;
+ const fieldkey = "preview";
if (this.extensionDoc) this.extensionDoc.text = state.doc.textBetween(0, state.doc.content.size, "\n\n");
if (this.extensionDoc) this.extensionDoc.lastModified = new DateField(new Date(Date.now()));
this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON()));
this._applyingChange = false;
let title = StrCast(this.dataDoc.title);
- if (title && title.startsWith("-") && this._editorView) {
+ if (title && title.startsWith("-") && this._editorView && !this.Document.customTitle) {
let str = this._editorView.state.doc.textContent;
let titlestr = str.substr(0, Math.min(40, str.length));
this.dataDoc.title = "-" + titlestr + (str.length > 40 ? "..." : "");
@@ -146,6 +193,50 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
+ public highlightSearchTerms = (terms: String[]) => {
+ if (this._editorView && (this._editorView as any).docView) {
+ const fieldkey = "preview";
+ const doc = this._editorView.state.doc;
+ const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
+ doc.nodesBetween(0, doc.content.size, (node: ProsNode, pos: number, parent: ProsNode, index: number) => {
+ if (node.isLeaf && node.isText && node.text) {
+ let nodeText: String = node.text;
+ let tokens = nodeText.split(" ");
+ let start = pos;
+ tokens.forEach((word) => {
+ if (terms.includes(word) && this._editorView) {
+ this._editorView.dispatch(this._editorView.state.tr.addMark(start, start + word.length, mark).removeStoredMark(mark));
+ // else {
+ // this._editorView.state.tr.addMark(start, start + word.length, mark).removeStoredMark(mark);
+ // }
+ }
+ start += word.length + 1;
+ });
+ }
+ });
+ }
+ }
+
+ public unhighlightSearchTerms = () => {
+ if (this._editorView && (this._editorView as any).docView) {
+ const doc = this._editorView.state.doc;
+ const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
+ doc.nodesBetween(0, doc.content.size, (node: ProsNode, pos: number, parent: ProsNode, index: number) => {
+ if (node.isLeaf && node.isText && node.text) {
+ if (node.marks.includes(mark) && this._editorView) {
+ this._editorView.dispatch(this._editorView.state.tr.removeMark(pos, pos + node.nodeSize, mark));
+ }
+ }
+ });
+ // const fieldkey = 'search_string';
+ // if (Object.keys(this.props.Document).indexOf(fieldkey) !== -1) {
+ // this.props.Document[fieldkey] = undefined;
+ // }
+ // else this.props.Document.proto![fieldkey] = undefined;
+ // }
+ }
+ }
+
protected createDropTarget = (ele: HTMLDivElement) => {
this._proseRef = ele;
if (this.dropDisposer) {
@@ -232,10 +323,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
return field ? field.Data : `{"doc":{"type":"doc","content":[]},"selection":{"type":"text","anchor":0,"head":0}}`;
},
field2 => {
- if (StrCast(this.props.Document.layout).indexOf("\"" + this.props.fieldKey + "\"") !== -1) { // bcz: UGH! why is this needed... something is happening out of order. test with making a collection, then adding a text note and converting that to a template field.
- this._editorView && !this._applyingChange &&
- this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field2)));
- }
+ this._editorView && !this._applyingChange &&
+ this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field2)));
}
);
@@ -250,6 +339,108 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}, { fireImmediately: true });
this.setupEditor(config, this.dataDoc, this.props.fieldKey);
+
+ this._searchReactionDisposer = reaction(() => {
+ return StrCast(this.props.Document.search_string);
+ }, searchString => {
+ const fieldkey = 'preview';
+ let preview = false;
+ // if (!this._editorView && Object.keys(this.props.Document).indexOf(fieldkey) !== -1) {
+ // preview = true;
+ // }
+ if (searchString) {
+ this.highlightSearchTerms([searchString]);
+ }
+ else {
+ this.unhighlightSearchTerms();
+ }
+ }, { fireImmediately: true });
+ }
+
+ clipboardTextSerializer = (slice: Slice): string => {
+ let text = "", separated = true;
+ const from = 0, to = slice.content.size;
+ slice.content.nodesBetween(from, to, (node, pos) => {
+ if (node.isText) {
+ text += node.text!.slice(Math.max(from, pos) - pos, to - pos);
+ separated = false;
+ } else if (!separated && node.isBlock) {
+ text += "\n";
+ separated = true;
+ } else if (node.type.name === "hard_break") {
+ text += "\n";
+ }
+ }, 0);
+ return text;
+ }
+
+ sliceSingleNode(slice: Slice) {
+ return slice.openStart === 0 && slice.openEnd === 0 && slice.content.childCount === 1 ? slice.content.firstChild : null;
+ }
+
+ handlePaste = (view: EditorView, event: Event, slice: Slice): boolean => {
+ let cbe = event as ClipboardEvent;
+ let docId: string;
+ let regionId: string;
+ if (!cbe.clipboardData) {
+ return false;
+ }
+ let linkId: string;
+ docId = cbe.clipboardData.getData("dash/pdfOrigin");
+ regionId = cbe.clipboardData.getData("dash/pdfRegion");
+ if (!docId || !regionId) {
+ return false;
+ }
+
+ DocServer.GetRefField(docId).then(doc => {
+ DocServer.GetRefField(regionId).then(region => {
+ if (!(doc instanceof Doc) || !(region instanceof Doc)) {
+ return;
+ }
+
+ let annotations = DocListCast(region.annotations);
+ annotations.forEach(anno => anno.target = this.props.Document);
+ let fieldExtDoc = Doc.resolvedFieldDataDoc(doc, "data", "true");
+ let targetAnnotations = DocListCast(fieldExtDoc.annotations);
+ if (targetAnnotations) {
+ targetAnnotations.push(region);
+ fieldExtDoc.annotations = new List<Doc>(targetAnnotations);
+ }
+
+ let link = DocUtils.MakeLink(this.props.Document, region, doc);
+ if (link) {
+ cbe.clipboardData!.setData("dash/linkDoc", link[Id]);
+ linkId = link[Id];
+ let frag = addMarkToFrag(slice.content);
+ slice = new Slice(frag, slice.openStart, slice.openEnd);
+ var tr = view.state.tr.replaceSelection(slice);
+ view.dispatch(tr.scrollIntoView().setMeta("paste", true).setMeta("uiEvent", "paste"));
+ }
+ });
+ });
+
+ return true;
+
+ function addMarkToFrag(frag: Fragment) {
+ const nodes: Node[] = [];
+ frag.forEach(node => nodes.push(addLinkMark(node)));
+ return Fragment.fromArray(nodes);
+ }
+ function addLinkMark(node: Node) {
+ if (!node.isText) {
+ const content = addMarkToFrag(node.content);
+ return node.copy(content);
+ }
+ const marks = [...node.marks];
+ const linkIndex = marks.findIndex(mark => mark.type.name === "link");
+ const link = view.state.schema.mark(view.state.schema.marks.link, { href: `http://localhost:1050/doc/${linkId}`, location: "onRight" });
+ if (linkIndex !== -1) {
+ marks.splice(linkIndex, 1, link);
+ } else {
+ marks.push(link);
+ }
+ return node.mark(marks);
+ }
}
private setupEditor(config: any, doc: Doc, fieldKey: string) {
@@ -271,7 +462,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
nodeViews: {
image(node, view, getPos) { return new ImageResizeView(node, view, getPos); },
star(node, view, getPos) { return new SummarizedView(node, view, getPos); }
- }
+ },
+ clipboardTextSerializer: this.clipboardTextSerializer,
+ handlePaste: this.handlePaste,
});
if (startup) {
Doc.GetProto(doc).documentText = undefined;
@@ -296,7 +489,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
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) {
+ if (FormattedTextBox._toolTipTextMenu && FormattedTextBox._toolTipTextMenu.tooltip) {
//this._toolTipTextMenu.tooltip.style.opacity = "0";
}
}
@@ -311,8 +504,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
href = parent.childNodes[0].href ? parent.childNodes[0].href : parent.href;
}
if (href) {
- if (href.indexOf(DocServer.prepend("/doc/")) === 0) {
- this._linkClicked = href.replace(DocServer.prepend("/doc/"), "").split("?")[0];
+ if (href.indexOf(Utils.prepend("/doc/")) === 0) {
+ this._linkClicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
if (this._linkClicked) {
DocServer.GetRefField(this._linkClicked).then(async linkDoc => {
if (linkDoc instanceof Doc) {
@@ -328,6 +521,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
if (targetContext) {
DocumentManager.Instance.jumpToDocument(targetContext, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab"));
+ } else if (jumpToDoc) {
+ DocumentManager.Instance.jumpToDocument(jumpToDoc, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab"));
+
}
}
});
@@ -349,7 +545,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
onPointerUp = (e: React.PointerEvent): void => {
- if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip) {
+ if (FormattedTextBox._toolTipTextMenu && FormattedTextBox._toolTipTextMenu.tooltip) {
//this._toolTipTextMenu.tooltip.style.opacity = "1";
}
if (e.buttons === 1 && this.props.isSelected() && !e.altKey) {
@@ -390,7 +586,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
tooltipTextMenuPlugin() {
let myprops = this.props;
- let self = this;
+ let self = FormattedTextBox;
return new Plugin({
view(_editorView) {
return self._toolTipTextMenu = new TooltipTextMenu(_editorView, myprops);
@@ -423,7 +619,7 @@ 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.dataDoc.title).startsWith("-") && this._editorView) {
+ if (StrCast(this.dataDoc.title).startsWith("-") && this._editorView && !this.Document.customTitle) {
let str = this._editorView.state.doc.textContent;
let titlestr = str.substr(0, Math.min(40, str.length));
this.dataDoc.title = "-" + titlestr + (str.length > 40 ? "..." : "");
@@ -443,7 +639,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
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;
+ const ChromeHeight = MainOverlayTextBox.Instance.ChromeHeight;
+ this.props.Document.height = (nh ? dh / nh * sh : sh) + (ChromeHeight ? ChromeHeight() : 0);
this.dataDoc.nativeHeight = nh ? sh : undefined;
}
}
@@ -460,16 +657,17 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
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"
+ description: BoolCast(this.props.Document.autoHeight) ? "Manual Height" : "Auto Height",
+ event: action(() => Doc.GetProto(this.props.Document).autoHeight = !BoolCast(this.props.Document.autoHeight)), icon: "expand-arrows-alt"
});
- ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: subitems });
+ ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: subitems, icon: "text-height" });
}
render() {
let self = this;
let style = this.props.isOverlay ? "scroll" : "hidden";
let rounded = StrCast(this.props.Document.borderRounding) === "100%" ? "-rounded" : "";
- let interactive = InkingControl.Instance.selectedTool ? "" : "interactive";
+ let interactive: "all" | "none" = InkingControl.Instance.selectedTool || this.props.Document.isBackground ||
+ (this.props.Document.isButton && !this.props.isSelected()) ? "none" : "all";
Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
return (
<div className={`formattedTextBox-cont-${style}`} ref={this._ref}
@@ -478,7 +676,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
background: this.props.hideOnLeave ? "rgba(0,0,0,0.4)" : undefined,
opacity: this.props.hideOnLeave ? (this._entered || this.props.isSelected() || this.props.Document.libraryBrush ? 1 : 0.1) : 1,
color: this.props.color ? this.props.color : this.props.hideOnLeave ? "white" : "inherit",
- pointerEvents: interactive ? "all" : "none",
+ pointerEvents: interactive,
fontSize: "13px"
}}
onKeyDown={this.onKeyPress}
@@ -489,12 +687,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
onPointerUp={this.onPointerUp}
onPointerDown={this.onPointerDown}
onMouseDown={this.onMouseDown}
- // tfs: do we need this event handler
onWheel={this.onPointerWheel}
onPointerEnter={this.onPointerEnter}
onPointerLeave={this.onPointerLeave}
>
- <div className={`formattedTextBox-inner${rounded}`} ref={this.createDropTarget} style={{ whiteSpace: "pre-wrap", pointerEvents: this.props.Document.isButton && !this.props.isSelected() ? "none" : "all" }} />
+ <div className={`formattedTextBox-inner${rounded}`} ref={this.createDropTarget} style={{ whiteSpace: "pre-wrap" }} />
</div>
);
}
diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx
index d6ab2a34a..7e78ec684 100644
--- a/src/client/views/nodes/IconBox.tsx
+++ b/src/client/views/nodes/IconBox.tsx
@@ -1,6 +1,6 @@
import React = require("react");
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons';
+import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faTag, faTextHeight } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { computed, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
@@ -18,7 +18,7 @@ library.add(faCaretUp);
library.add(faObjectGroup);
library.add(faStickyNote);
library.add(faFilePdf);
-library.add(faFilm);
+library.add(faFilm, faTag, faTextHeight);
@observer
export class IconBox extends React.Component<FieldViewProps> {
@@ -47,13 +47,15 @@ export class IconBox extends React.Component<FieldViewProps> {
specificContextMenu = (): void => {
ContextMenu.Instance.addItem({
description: BoolCast(this.props.Document.hideLabel) ? "Show label with icon" : "Remove label from icon",
- event: this.setLabelField
+ event: this.setLabelField,
+ icon: "tag"
});
let maxDocs = DocListCast(this.props.Document.maximizedDocs);
if (maxDocs.length === 1 && !BoolCast(this.props.Document.hideLabel)) {
ContextMenu.Instance.addItem({
description: BoolCast(this.props.Document.useOwnTitle) ? "Use target title for label" : "Use own title label",
- event: this.setUseOwnTitleField
+ event: this.setUseOwnTitleField,
+ icon: "text-height"
});
}
}
@@ -64,7 +66,7 @@ export class IconBox extends React.Component<FieldViewProps> {
let hideLabel = BoolCast(this.props.Document.hideLabel);
let maxDocs = DocListCast(this.props.Document.maximizedDocs);
let firstDoc = maxDocs.length ? maxDocs[0] : undefined;
- let label = hideLabel ? "" : (firstDoc && labelField && !BoolCast(this.props.Document.useOwnTitle, false) ? firstDoc[labelField] : this.props.Document.title);
+ let label = hideLabel ? "" : (firstDoc && labelField && !BoolCast(this.props.Document.useOwnTitle) ? firstDoc[labelField] : this.props.Document.title);
return (
<div className="iconBox-container" onContextMenu={this.specificContextMenu}>
{this.minimizedIcon}
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index f1b73a676..697f19f0d 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -38,4 +38,22 @@
border: none;
width: 100%;
height: 100%;
+}
+
+.imageBox-audioBackground {
+ display: inline-block;
+ width: 10%;
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ border-radius: 25px;
+ background: white;
+ opacity: 0.3;
+ svg {
+ width: 90% !important;
+ height: 70%;
+ position: absolute;
+ left: 5%;
+ top: 15%;
+ }
} \ No newline at end of file
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 4c5ad7a7d..0d9c2bb8a 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,33 +1,40 @@
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faImage } from '@fortawesome/free-solid-svg-icons';
-import { action, observable, computed } from 'mobx';
+import { faEye } from '@fortawesome/free-regular-svg-icons';
+import { faAsterisk, faFileAudio, faImage, faPaintBrush } 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 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 { Doc, DocListCast, HeightSym, WidthSym } 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 { ComputedField } from '../../../new_fields/ScriptField';
+import { BoolCast, Cast, FieldValue, NumCast, StrCast } from '../../../new_fields/Types';
+import { AudioField, ImageField } from '../../../new_fields/URLField';
+import { RouteStore } from '../../../server/RouteStore';
import { Utils } from '../../../Utils';
+import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices';
+import { Docs } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
+import { CompileScript } from '../../util/Scripting';
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 FaceRectangles from './FaceRectangles';
import { FieldView, FieldViewProps } from './FieldView';
import "./ImageBox.scss";
import React = require("react");
-import { RouteStore } from '../../../server/RouteStore';
-import { Docs } from '../../documents/Documents';
-import { DocServer } from '../../DocServer';
var requestImageSize = require('../../util/request-image-size');
var path = require('path');
+const { Howl } = require('howler');
-library.add(faImage);
+library.add(faImage, faEye as any, faPaintBrush);
+library.add(faFileAudio, faAsterisk);
export const pageSchema = createSchema({
@@ -58,7 +65,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
private dropDisposer?: DragManager.DragDropDisposer;
- @computed get dataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; }
+ @computed get dataDoc() { return this.props.DataDoc && (BoolCast(this.props.Document.isTemplate) || BoolCast(this.props.DataDoc.isTemplate) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : this.props.Document; }
protected createDropTarget = (ele: HTMLDivElement) => {
@@ -78,10 +85,14 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
@computed get extensionDoc() { return Doc.resolvedFieldDataDoc(this.dataDoc, this.props.fieldKey, "Alternates"); }
@undoBatch
+ @action
drop = (e: Event, de: DragManager.DropEvent) => {
if (de.data instanceof DragManager.DocumentDragData) {
de.data.droppedDocuments.forEach(action((drop: Doc) => {
- if (de.mods === "AltKey" && /*this.dataDoc !== this.props.Document &&*/ drop.data instanceof ImageField) {
+ if (de.mods === "CtrlKey") {
+ Doc.ApplyTemplateTo(drop, this.props.Document, this.props.DataDoc);
+ e.stopPropagation();
+ } else 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") {
@@ -97,8 +108,6 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
e.stopPropagation();
}
}
- } else if (!this.props.isSelected()) {
- e.stopPropagation();
}
}));
// de.data.removeDocument() bcz: need to implement
@@ -161,15 +170,15 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
recorder.ondataavailable = async function (e: any) {
const formData = new FormData();
formData.append("file", e.data);
- const res = await fetch(DocServer.prepend(RouteStore.upload), {
+ const res = await fetch(Utils.prepend(RouteStore.upload), {
method: 'POST',
body: formData
});
const files = await res.json();
- const url = DocServer.prepend(files[0]);
+ const url = Utils.prepend(files[0]);
// upload to server with known URL
let audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", x: NumCast(self.props.Document.x), y: NumCast(self.props.Document.y), width: 200, height: 32 });
- audioDoc.embed = true;
+ audioDoc.treeViewExpandedView = "layout";
let audioAnnos = Cast(self.extensionDoc.audioAnnotations, listSpec(Doc));
if (audioAnnos === undefined) {
self.extensionDoc.audioAnnotations = new List([audioDoc]);
@@ -177,45 +186,90 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
audioAnnos.push(audioDoc);
}
};
+ runInAction(() => self._audioState = 2);
recorder.start();
setTimeout(() => {
recorder.stop();
-
+ runInAction(() => self._audioState = 0);
gumStream.getAudioTracks()[0].stop();
}, 5000);
});
}
+ @undoBatch
+ rotate = action(() => {
+ let proto = Doc.GetProto(this.props.Document);
+ let nw = this.props.Document.nativeWidth;
+ let nh = this.props.Document.nativeHeight;
+ let w = this.props.Document.width;
+ 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;
+ });
+
specificContextMenu = (e: React.MouseEvent): void => {
let field = Cast(this.Document[this.props.fieldKey], ImageField);
if (field) {
let url = field.url.href;
- let subitems: ContextMenuProps[] = [];
- subitems.push({ description: "Copy path", event: () => Utils.CopyText(url), icon: "expand-arrows-alt" });
- subitems.push({ description: "Record 1sec audio", event: this.recordAudioAnnotation, icon: "expand-arrows-alt" });
- subitems.push({
- description: "Rotate", event: action(() => {
- let proto = Doc.GetProto(this.props.Document);
- let nw = this.props.Document.nativeWidth;
- let nh = this.props.Document.nativeHeight;
- let w = this.props.Document.width;
- 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"
- });
- ContextMenu.Instance.addItem({ description: "Image Funcs...", subitems: subitems });
+ let funcs: ContextMenuProps[] = [];
+ funcs.push({ description: "Copy path", event: () => Utils.CopyText(url), icon: "expand-arrows-alt" });
+ funcs.push({ description: "Record 1sec audio", event: this.recordAudioAnnotation, icon: "expand-arrows-alt" });
+ funcs.push({ description: "Rotate", event: this.rotate, icon: "expand-arrows-alt" });
+
+ let modes: ContextMenuProps[] = [];
+ modes.push({ description: "Generate Tags", event: this.generateMetadata, icon: "tag" });
+ modes.push({ description: "Find Faces", event: this.extractFaces, icon: "camera" });
+
+ ContextMenu.Instance.addItem({ description: "Image Funcs...", subitems: funcs, icon: "asterisk" });
+ ContextMenu.Instance.addItem({ description: "Analyze...", subitems: modes, icon: "eye" });
}
}
+ extractFaces = () => {
+ let converter = (results: any) => {
+ let faceDocs = new List<Doc>();
+ results.map((face: CognitiveServices.Image.Face) => faceDocs.push(Docs.Get.DocumentHierarchyFromJson(face, `Face: ${face.faceId}`)!));
+ return faceDocs;
+ };
+ CognitiveServices.Image.Manager.analyzer(this.extensionDoc, ["faces"], this.url, Service.Face, converter);
+ }
+
+ generateMetadata = (threshold: Confidence = Confidence.Excellent) => {
+ let converter = (results: any) => {
+ let tagDoc = new Doc;
+ let tagsList = new List();
+ results.tags.map((tag: Tag) => {
+ tagsList.push(tag.name);
+ let sanitized = tag.name.replace(" ", "_");
+ let script = `return (${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`;
+ let computed = CompileScript(script, { params: { this: "Doc" } });
+ computed.compiled && (tagDoc[sanitized] = new ComputedField(computed));
+ });
+ this.extensionDoc.generatedTags = tagsList;
+ tagDoc.title = "Generated Tags Doc";
+ tagDoc.confidence = threshold;
+ return tagDoc;
+ };
+ CognitiveServices.Image.Manager.analyzer(this.extensionDoc, ["generatedTagsDoc"], this.url, Service.ComputerVision, converter);
+ }
+
@action
onDotDown(index: number) {
this.Document.curPage = index;
}
+ @computed get fieldExtensionDoc() {
+ return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, "true");
+ }
+
+ @computed private get url() {
+ let data = Cast(Doc.GetProto(this.props.Document).data, ImageField);
+ return data ? data.url.href : undefined;
+ }
+
dots(paths: string[]) {
let nativeWidth = FieldValue(this.Document.nativeWidth, 1);
let dist = Math.min(nativeWidth / paths.length, 40);
@@ -229,11 +283,15 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
choosePath(url: URL) {
const lower = url.href.toLowerCase();
- if (url.protocol === "data" || url.href.indexOf(window.location.origin) === -1 || !(lower.endsWith(".png") || lower.endsWith(".jpg") || lower.endsWith(".jpeg"))) {
+ if (url.protocol === "data") {
return url.href;
+ } else if (url.href.indexOf(window.location.origin) === -1) {
+ return Utils.CorsProxy(url.href);
+ } else if (!(lower.endsWith(".png") || lower.endsWith(".jpg") || lower.endsWith(".jpeg"))) {
+ return url.href;//Why is this here
}
let ext = path.extname(url.href);
- const suffix = this.props.renderDepth <= 1 ? "_o" : this._curSuffix;
+ const suffix = this.props.renderDepth < 1 ? "_o" : this._curSuffix;
return url.href.replace(ext, suffix + ext);
}
@@ -254,16 +312,16 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
_curSuffix = "_m";
resize(srcpath: string, layoutdoc: Doc) {
- requestImageSize(window.origin + RouteStore.corsProxy + "/" + srcpath)
+ requestImageSize(srcpath)
.then((size: any) => {
- let aspect = size.height / size.width;
let rotation = NumCast(this.dataDoc.rotation) % 180;
- if (rotation === 90 || rotation === 270) aspect = 1 / aspect;
- if (Math.abs(layoutdoc[HeightSym]() / layoutdoc[WidthSym]() - aspect) > 0.01) {
+ let realsize = rotation === 90 || rotation === 270 ? { height: size.width, width: size.height } : size;
+ let aspect = realsize.height / realsize.width;
+ if (Math.abs(NumCast(layoutdoc.height) - realsize.height) > 1 || Math.abs(NumCast(layoutdoc.width) - realsize.width) > 1) {
setTimeout(action(() => {
layoutdoc.height = layoutdoc[WidthSym]() * aspect;
- layoutdoc.nativeHeight = size.height;
- layoutdoc.nativeWidth = size.width;
+ layoutdoc.nativeHeight = realsize.height;
+ layoutdoc.nativeWidth = realsize.width;
}), 0);
}
})
@@ -272,6 +330,48 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
});
}
+ @observable _audioState = 0;
+
+ @action
+ onPointerEnter = () => {
+ let self = this;
+ let audioAnnos = DocListCast(this.extensionDoc.audioAnnotations);
+ if (audioAnnos.length && this._audioState === 0) {
+ let anno = audioAnnos[Math.floor(Math.random() * audioAnnos.length)];
+ anno.data instanceof AudioField && new Howl({
+ src: [anno.data.url.href],
+ format: ["mp3"],
+ autoplay: true,
+ loop: false,
+ volume: 0.5,
+ onend: function () {
+ runInAction(() => self._audioState = 0);
+ }
+ });
+ this._audioState = 1;
+ }
+ // else {
+ // if (this._audioState === 0) {
+ // this._audioState = 1;
+ // new Howl({
+ // src: ["https://www.kozco.com/tech/piano2-CoolEdit.mp3"],
+ // autoplay: true,
+ // loop: false,
+ // volume: 0.5,
+ // onend: function () {
+ // runInAction(() => self._audioState = 0);
+ // }
+ // });
+ // }
+ // }
+ }
+
+ @action
+ audioDown = () => {
+ this.recordAudioAnnotation();
+ }
+
+
render() {
// let transform = this.props.ScreenToLocalTransform().inverse();
let pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50;
@@ -282,7 +382,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
let id = (this.props as any).id; // bcz: used to set id = "isExpander" in templates.tsx
let nativeWidth = FieldValue(this.Document.nativeWidth, pw);
let nativeHeight = FieldValue(this.Document.nativeHeight, 0);
- let paths: string[] = ["http://www.cs.brown.edu/~bcz/noImage.png"];
+ let paths: string[] = [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")];
// this._curSuffix = "";
// if (w > 20) {
Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
@@ -295,11 +395,11 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
if (field instanceof ImageField) paths = [this.choosePath(field.url)];
paths.push(...altpaths);
// }
- let interactive = InkingControl.Instance.selectedTool ? "" : "-interactive";
+ let interactive = InkingControl.Instance.selectedTool || this.props.Document.isBackground ? "" : "-interactive";
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)];
+ let srcpath = paths[Math.min(paths.length - 1, this.Document.curPage || 0)];
if (!this.props.Document.ignoreAspect && !this.props.leaveNativeSize) this.resize(srcpath, this.props.Document);
@@ -311,12 +411,20 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
key={this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys
src={srcpath}
style={{ transform: `translate(0px, ${shift}px) rotate(${rotation}deg) scale(${aspect})` }}
- // style={{ objectFit: (this.Document.curPage === 0 ? undefined : "contain") }}
width={nativeWidth}
ref={this._imgRef}
onError={this.onError} />
{paths.length > 1 ? this.dots(paths) : (null)}
+ <div className="imageBox-audioBackground"
+ onPointerDown={this.audioDown}
+ onPointerEnter={this.onPointerEnter}
+ style={{ height: `calc(${.1 * nativeHeight / nativeWidth * 100}%)` }}
+ >
+ <FontAwesomeIcon className="imageBox-audioFont"
+ style={{ color: [DocListCast(this.extensionDoc.audioAnnotations).length ? "blue" : "gray", "green", "red"][this._audioState] }} icon={faFileAudio} size="sm" />
+ </div>
{/* {this.lightbox(paths)} */}
+ <FaceRectangles document={this.extensionDoc} color={"#0000FF"} backgroundColor={"#0000FF"} />
</div>);
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index c9dd9a64e..f10079169 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -20,6 +20,8 @@ import { RichTextField } from "../../../new_fields/RichTextField";
import { ImageField } from "../../../new_fields/URLField";
import { SelectionManager } from "../../util/SelectionManager";
import { listSpec } from "../../../new_fields/Schema";
+import { CollectionViewType } from "../collections/CollectionBaseView";
+import { undoBatch } from "../../util/UndoManager";
export type KVPScript = {
script: CompiledScript;
@@ -46,6 +48,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
@action
onEnterKey = (e: React.KeyboardEvent): void => {
if (e.key === 'Enter') {
+ e.stopPropagation();
if (this._keyInput && this._valueInput && this.fieldDocToLayout) {
if (KeyValueBox.SetField(this.fieldDocToLayout, this._keyInput, this._valueInput)) {
this._keyInput = "";
@@ -88,6 +91,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
return false;
}
+ @undoBatch
public static SetField(doc: Doc, key: string, value: string) {
const script = this.CompileKVPScript(value);
if (!script) return false;
@@ -114,7 +118,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
let protos = Doc.GetAllPrototypes(doc);
for (const proto of protos) {
Object.keys(proto).forEach(key => {
- if (!(key in ids)) {
+ if (!(key in ids) && realDoc[key] !== ComputedField.undefined) {
ids[key] = key;
}
});
@@ -153,7 +157,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
<input style={{ width: "100%" }} type="text" value={this._keyInput} placeholder="Key" onChange={this.keyChanged} />
</td>
<td className="keyValueBox-td-value" style={{ width: `${this.splitPercentage}%` }}>
- <input style={{ width: "100%" }} type="text" value={this._valueInput} placeholder="Value" onChange={this.valueChanged} onKeyPress={this.onEnterKey} />
+ <input style={{ width: "100%" }} type="text" value={this._valueInput} placeholder="Value" onChange={this.valueChanged} onKeyDown={this.onEnterKey} />
</td>
</tr>
)
@@ -194,6 +198,9 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
}
let fieldTemplate = await this.inferType(sourceDoc[metaKey], metaKey);
+ if (!fieldTemplate) {
+ return;
+ }
let previousViewType = fieldTemplate.viewType;
Doc.MakeTemplate(fieldTemplate, metaKey, Doc.GetProto(parentStackingDoc));
previousViewType && (fieldTemplate.viewType = previousViewType);
@@ -210,14 +217,17 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
return Docs.Create.StackingDocument([], options);
}
let first = await Cast(data[0], Doc);
- if (!first) {
+ if (!first || !first.data) {
return Docs.Create.StackingDocument([], options);
}
- switch (first.type) {
- case "image":
- return Docs.Create.StackingDocument([], options);
- case "text":
+ switch (first.data.constructor) {
+ case RichTextField:
return Docs.Create.TreeDocument([], options);
+ case ImageField:
+ return Docs.Create.MasonryDocument([], options);
+ default:
+ console.log(`Template for ${first.data.constructor} not supported!`);
+ return undefined;
}
} else if (data instanceof ImageField) {
return Docs.Create.ImageDocument("https://image.flaticon.com/icons/png/512/23/23765.png", options);
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 209782242..534a42efc 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -1,18 +1,19 @@
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, CompiledScript, ScriptOptions } from "../../util/Scripting";
+import { Doc, Field } from '../../../new_fields/Doc';
+import { emptyFunction, returnFalse, returnOne, returnZero } from '../../../Utils';
+import { Docs } from '../../documents/Documents';
import { Transform } from '../../util/Transform';
+import { undoBatch } from '../../util/UndoManager';
+import { CollectionDockingView } from '../collections/CollectionDockingView';
+import { ContextMenu } from '../ContextMenu';
import { EditableView } from "../EditableView";
import { FieldView, FieldViewProps } from './FieldView';
+import { KeyValueBox } from './KeyValueBox';
import "./KeyValueBox.scss";
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
@@ -39,6 +40,16 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
this.isChecked = false;
}
+ onContextMenu = (e: React.MouseEvent) => {
+ const value = this.props.doc[this.props.keyName];
+ if (value instanceof Doc) {
+ e.stopPropagation();
+ e.preventDefault();
+ ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { let kvp = Docs.Create.KVPDocument(value, { width: 300, height: 300 }); CollectionDockingView.Instance.AddRightSplit(kvp, undefined); }, icon: "layer-group" });
+ ContextMenu.Instance.displayMenu(e.clientX, e.clientY);
+ }
+ }
+
render() {
let props: FieldViewProps = {
Document: this.props.doc,
@@ -57,10 +68,21 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
PanelWidth: returnZero,
PanelHeight: returnZero,
addDocTab: returnZero,
+ ContentScaling: returnOne
};
let contents = <FieldView {...props} />;
// let fieldKey = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? props.fieldKey : "(" + props.fieldKey + ")";
- let keyStyle = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? "black" : "blue";
+ let protoCount = 0;
+ let doc: Doc | undefined = props.Document;
+ while (doc) {
+ if (Object.keys(doc).includes(props.fieldKey)) {
+ break;
+ }
+ protoCount++;
+ doc = doc.proto;
+ }
+ const parenCount = Math.max(0, protoCount - 1);
+ let keyStyle = protoCount === 0 ? "black" : "blue";
let hover = { transition: "0.3s ease opacity", opacity: this.isPointerOver || this.isChecked ? 1 : 0 };
@@ -68,12 +90,12 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
<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 style={hover} className="keyValuePair-td-key-delete" onClick={() => {
+ <button style={hover} className="keyValuePair-td-key-delete" onClick={undoBatch(() => {
if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1) {
props.Document[props.fieldKey] = undefined;
}
else props.Document.proto![props.fieldKey] = undefined;
- }}>
+ })}>
X
</button>
<input
@@ -83,10 +105,10 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
onChange={this.handleCheck}
ref={this.checkbox}
/>
- <div className="keyValuePair-keyField" style={{ color: keyStyle }}>{props.fieldKey}</div>
+ <div className="keyValuePair-keyField" style={{ color: keyStyle }}>{"(".repeat(parenCount)}{props.fieldKey}{")".repeat(parenCount)}</div>
</div>
</td>
- <td className="keyValuePair-td-value" style={{ width: `${100 - this.props.keyWidth}%` }}>
+ <td className="keyValuePair-td-value" style={{ width: `${100 - this.props.keyWidth}%` }} onContextMenu={this.onContextMenu}>
<div className="keyValuePair-td-value-container">
<EditableView
contents={contents}
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index afde85b69..ecb3e9db4 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -11,6 +11,7 @@ import { faArrowLeft, faEllipsisV, faTable, faTrash, faCog, faExchangeAlt, faTim
import { library } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SetupDrag } from "../../util/DragManager";
+import { SchemaHeaderField, RandomPastel } from "../../../new_fields/SchemaHeaderField";
library.add(faArrowLeft, faEllipsisV, faTable, faTrash, faCog, faExchangeAlt, faTimes, faPlus);
@@ -289,7 +290,7 @@ export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
let index = keys.indexOf("");
if (index > -1) keys.splice(index, 1);
- let cols = ["anchor1", "anchor2", ...[...keys]];
+ let cols = ["anchor1", "anchor2", ...[...keys]].map(c => new SchemaHeaderField(c, "#f1efeb"));
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>();
diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx
index 1eda7d1fb..1a4af04f8 100644
--- a/src/client/views/nodes/LinkMenu.tsx
+++ b/src/client/views/nodes/LinkMenu.tsx
@@ -19,6 +19,7 @@ import { DocumentType } from "../../documents/Documents";
interface Props {
docView: DocumentView;
changeFlyout: () => void;
+ addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
}
@observer
@@ -39,7 +40,13 @@ export class LinkMenu extends React.Component<Props> {
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)} />
+ <LinkMenuGroup
+ key={groupType}
+ sourceDoc={this.props.docView.props.Document}
+ group={group}
+ groupType={groupType}
+ showEditor={action((linkDoc: Doc) => this._editingLink = linkDoc)}
+ addDocTab={this.props.addDocTab} />
);
});
diff --git a/src/client/views/nodes/LinkMenuGroup.tsx b/src/client/views/nodes/LinkMenuGroup.tsx
index ae97bed2f..e04044266 100644
--- a/src/client/views/nodes/LinkMenuGroup.tsx
+++ b/src/client/views/nodes/LinkMenuGroup.tsx
@@ -14,12 +14,14 @@ import { Docs } from "../../documents/Documents";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { UndoManager } from "../../util/UndoManager";
import { StrCast } from "../../../new_fields/Types";
+import { SchemaHeaderField, RandomPastel } from "../../../new_fields/SchemaHeaderField";
interface LinkMenuGroupProps {
sourceDoc: Doc;
group: Doc[];
groupType: string;
showEditor: (linkDoc: Doc) => void;
+ addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
}
@observer
@@ -70,7 +72,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
let index = keys.indexOf("");
if (index > -1) keys.splice(index, 1);
- let cols = ["anchor1", "anchor2", ...[...keys]];
+ let cols = ["anchor1", "anchor2", ...[...keys]].map(c => new SchemaHeaderField(c, "#f1efeb"));
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>();
@@ -82,6 +84,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
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}
+ addDocTab={this.props.addDocTab}
linkDoc={linkDoc} sourceDoc={this.props.sourceDoc} destinationDoc={destination} showEditor={this.props.showEditor} />;
}
});
diff --git a/src/client/views/nodes/LinkMenuItem.tsx b/src/client/views/nodes/LinkMenuItem.tsx
index 42007018c..1d4fcad69 100644
--- a/src/client/views/nodes/LinkMenuItem.tsx
+++ b/src/client/views/nodes/LinkMenuItem.tsx
@@ -7,11 +7,12 @@ 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 { StrCast, Cast, 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';
+import { SelectionManager } from '../../util/SelectionManager';
library.add(faEye, faEdit, faTimes, faArrowRight, faChevronDown, faChevronUp);
@@ -21,6 +22,7 @@ interface LinkMenuItemProps {
sourceDoc: Doc;
destinationDoc: Doc;
showEditor: (linkDoc: Doc) => void;
+ addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
}
@observer
@@ -42,18 +44,24 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
let targetContext = await Cast(proto.targetContext, Doc);
let sourceContext = await Cast(proto.sourceContext, Doc);
let self = this;
- if (DocumentManager.Instance.getDocumentView(jumpToDoc)) {
+
+
+ let dockingFunc = (document: Doc) => { this.props.addDocTab(document, undefined, "inTab"); SelectionManager.DeselectAll(); };
+ if (e.ctrlKey) {
+ dockingFunc = (document: Doc) => CollectionDockingView.Instance.AddRightSplit(document, undefined);
+ }
+
+ if (this.props.destinationDoc === self.props.linkDoc.anchor2 && targetContext) {
+ DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, document => dockingFunc(targetContext!));
+ }
+ else if (this.props.destinationDoc === self.props.linkDoc.anchor1 && sourceContext) {
+ DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, document => dockingFunc(sourceContext!));
+ }
+ else if (DocumentManager.Instance.getDocumentView(jumpToDoc)) {
DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, undefined, undefined, NumCast((this.props.destinationDoc === self.props.linkDoc.anchor2 ? self.props.linkDoc.anchor2Page : self.props.linkDoc.anchor1Page)));
}
- else if (!((this.props.destinationDoc === self.props.linkDoc.anchor2 && targetContext) || (this.props.destinationDoc === self.props.linkDoc.anchor1 && sourceContext))) {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, document => CollectionDockingView.Instance.AddRightSplit(document, undefined));
- } else {
- if (this.props.destinationDoc === self.props.linkDoc.anchor2 && targetContext) {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, document => CollectionDockingView.Instance.AddRightSplit(targetContext!, undefined));
- }
- else if (this.props.destinationDoc === self.props.linkDoc.anchor1 && sourceContext) {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, document => CllectionDockingView.Instance.AddRightSplit(sourceContext!, undefined));
- }
+ else {
+ DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, dockingFunc);
}
}
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 5a5e6e6dd..a49709e83 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -24,6 +24,8 @@ import { Flyout, anchorPoints } from '../DocumentDecorations';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { ScriptField } from '../../../new_fields/ScriptField';
import { KeyCodes } from '../../northstar/utils/KeyCodes';
+import { Utils } from '../../../Utils';
+import { Id } from '../../../new_fields/FieldSymbols';
type PdfDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>;
const PdfDocument = makeInterface(positionSchema, pageSchema);
@@ -150,9 +152,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
}
scrollTo(y: number) {
- if (this._mainCont.current) {
- this._mainCont.current.scrollTo({ top: y, behavior: "auto" });
- }
+ this._mainCont.current && this._mainCont.current.scrollTo({ top: Math.max(y - (this._mainCont.current.offsetHeight / 2), 0), behavior: "auto" });
}
settingsPanel() {
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 8b8e500c4..704030d85 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -8,7 +8,6 @@ import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
import { VideoField } from "../../../new_fields/URLField";
import { RouteStore } from "../../../server/RouteStore";
import { Utils } from "../../../Utils";
-import { DocServer } from "../../DocServer";
import { Docs, DocUtils } from "../../documents/Documents";
import { ContextMenu } from "../ContextMenu";
import { ContextMenuProps } from "../ContextMenuItem";
@@ -19,10 +18,18 @@ import { positionSchema } from "./DocumentView";
import { FieldView, FieldViewProps } from './FieldView';
import { pageSchema } from "./ImageBox";
import "./VideoBox.scss";
+import { library } from "@fortawesome/fontawesome-svg-core";
+import { faVideo } from "@fortawesome/free-solid-svg-icons";
+import { CompileScript } from "../../util/Scripting";
+import { Doc } from "../../../new_fields/Doc";
+import { ScriptField } from "../../../new_fields/ScriptField";
+var path = require('path');
type VideoDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>;
const VideoDocument = makeInterface(positionSchema, pageSchema);
+library.add(faVideo);
+
@observer
export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoDocument) {
private _reactionDisposer?: IReactionDisposer;
@@ -52,25 +59,27 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
this.Document.nativeHeight = this.Document.nativeWidth / aspect;
this.Document.height = FieldValue(this.Document.width, 0) / aspect;
}
+ if (!this.Document.duration) this.Document.duration = this.player!.duration;
}
@action public Play = (update: boolean = true) => {
this.Playing = true;
update && this.player && this.player.play();
update && this._youtubePlayer && this._youtubePlayer.playVideo();
- !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 500));
+ this._youtubePlayer && !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 5));
this.updateTimecode();
}
@action public Seek(time: number) {
this._youtubePlayer && this._youtubePlayer.seekTo(Math.round(time), true);
+ this.player && (this.player.currentTime = time);
}
@action public Pause = (update: boolean = true) => {
this.Playing = false;
update && this.player && this.player.pause();
update && this._youtubePlayer && this._youtubePlayer.pauseVideo();
- this._playTimer && clearInterval(this._playTimer);
+ this._youtubePlayer && this._playTimer && clearInterval(this._playTimer);
this._playTimer = undefined;
this.updateTimecode();
}
@@ -81,6 +90,63 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
this._youtubePlayer && this.props.addDocTab(this.props.Document, this.props.DataDoc, "inTab");
}
+ choosePath(url: string) {
+ if (url.indexOf(window.location.origin) === -1) {
+ return Utils.CorsProxy(url);
+ }
+ return url;
+ }
+
+ @action public Snapshot() {
+ let width = NumCast(this.props.Document.width);
+ let height = NumCast(this.props.Document.height);
+ 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);
+ }
+
+ if (!this._videoRef) { // can't find a way to take snapshots of videos
+ let b = Docs.Create.ButtonDocument({
+ x: NumCast(this.props.Document.x) + width, y: NumCast(this.props.Document.y),
+ width: 150, height: 50, title: NumCast(this.props.Document.curPage).toString()
+ });
+ const script = CompileScript(`(self as any).curPage = ${NumCast(this.props.Document.curPage)}`, {
+ params: { this: Doc.name },
+ capturedVariables: { self: this.props.Document },
+ typecheck: false,
+ editable: true,
+ });
+ if (script.compiled) {
+ b.onClick = new ScriptField(script);
+ this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.addDocument && this.props.ContainingCollectionView.props.addDocument(b, false);
+ } else {
+ console.log(script.errors.map(error => error.messageText).join("\n"));
+ }
+ } else {
+ //convert to desired file format
+ var dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
+ // if you want to preview the captured image,
+ let filename = encodeURIComponent("snapshot" + this.props.Document.title + "_" + this.props.Document.curPage).replace(/\./g, "");
+ VideoBox.convertDataUri(dataUrl, filename).then(returnedFilename => {
+ if (returnedFilename) {
+ let url = this.choosePath(Utils.prepend(returnedFilename));
+ let imageSummary = Docs.Create.ImageDocument(url, {
+ x: NumCast(this.props.Document.x) + width, y: NumCast(this.props.Document.y),
+ width: 150, height: height / width * 150, title: "--snapshot" + NumCast(this.props.Document.curPage) + " image-"
+ });
+ this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.addDocument && this.props.ContainingCollectionView.props.addDocument(imageSummary, false);
+ DocUtils.MakeLink(imageSummary, this.props.Document);
+ }
+ });
+ }
+ }
+
@action
updateTimecode = () => {
this.player && (this.props.Document.curPage = this.player.currentTime);
@@ -112,6 +178,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
setVideoRef = (vref: HTMLVideoElement | null) => {
this._videoRef = vref;
if (vref) {
+ this._videoRef!.ontimeupdate = this.updateTimecode;
vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen);
if (this._reactionDisposer) this._reactionDisposer();
this._reactionDisposer = reaction(() => this.props.Document.curPage, () =>
@@ -122,7 +189,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
public static async convertDataUri(imageUri: string, returnedFilename: string) {
try {
- let posting = DocServer.prepend(RouteStore.dataUriToImage);
+ let posting = Utils.prepend(RouteStore.dataUriToImage);
const returnedUri = await rp.post(posting, {
body: {
uri: imageUri,
@@ -143,40 +210,8 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
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);
- }
-
- //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 });
+ subitems.push({ description: "Take Snapshot", event: () => this.Snapshot(), icon: "expand-arrows-alt" });
+ ContextMenu.Instance.addItem({ description: "Video Funcs...", subitems: subitems, icon: "video" });
}
}
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index f0a9ec6d8..162ac1d98 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -9,22 +9,6 @@ import React = require("react");
import { InkTool } from "../../../new_fields/InkField";
import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
-export function onYouTubeIframeAPIReady() {
- console.log("player");
- return;
- let player = new YT.Player('player', {
- events: {
- 'onReady': onPlayerReady
- }
- });
-}
-// must cast as any to set property on window
-const _global = (window /* browser */ || global /* node */) as any;
-_global.onYouTubeIframeAPIReady = onYouTubeIframeAPIReady;
-
-function onPlayerReady(event: any) {
- event.target.playVideo();
-}
@observer
export class WebBox extends React.Component<FieldViewProps> {
diff --git a/src/client/views/pdf/Annotation.scss b/src/client/views/pdf/Annotation.scss
new file mode 100644
index 000000000..0ea85d522
--- /dev/null
+++ b/src/client/views/pdf/Annotation.scss
@@ -0,0 +1,4 @@
+.pdfViewer-annotationBox {
+ pointer-events: all;
+ user-select: none;
+} \ No newline at end of file
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index ed7081b1d..a08ff5969 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -1,14 +1,15 @@
import React = require("react");
-import { Doc, DocListCast, WidthSym, HeightSym } from "../../../new_fields/Doc";
-import { AnnotationTypes, Viewer, scale } from "./PDFViewer";
+import { action, IReactionDisposer, observable, reaction } from "mobx";
import { observer } from "mobx-react";
-import { observable, IReactionDisposer, reaction, action } from "mobx";
-import { BoolCast, NumCast, FieldValue, Cast, StrCast } from "../../../new_fields/Types";
+import { Doc, DocListCast, HeightSym, WidthSym } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
import { List } from "../../../new_fields/List";
-import PDFMenu from "./PDFMenu";
+import { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
import { DocumentManager } from "../../util/DocumentManager";
import { PresentationView } from "../presentationview/PresentationView";
+import PDFMenu from "./PDFMenu";
+import "./Annotation.scss";
+import { AnnotationTypes, scale, Viewer } from "./PDFViewer";
interface IAnnotationProps {
anno: Doc;
@@ -110,11 +111,16 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
}
@action
- onPointerDown = (e: React.PointerEvent) => {
+ onPointerDown = async (e: React.PointerEvent) => {
if (e.button === 0) {
- let targetDoc = Cast(this.props.document.target, Doc, null);
+ let targetDoc = await Cast(this.props.document.target, Doc);
if (targetDoc) {
- DocumentManager.Instance.jumpToDocument(targetDoc, false);
+ let context = await Cast(targetDoc.targetContext, Doc);
+ if (context) {
+ DocumentManager.Instance.jumpToDocument(targetDoc, false, false,
+ ((doc) => this.props.parent.props.parent.props.addDocTab(targetDoc!, undefined, e.ctrlKey ? "onRight" : "inTab")),
+ undefined, undefined);
+ }
}
}
if (e.button === 2) {
@@ -145,7 +151,6 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
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>
);
diff --git a/src/client/views/pdf/PDFAnnotationLayer.scss b/src/client/views/pdf/PDFAnnotationLayer.scss
new file mode 100644
index 000000000..733533007
--- /dev/null
+++ b/src/client/views/pdf/PDFAnnotationLayer.scss
@@ -0,0 +1,6 @@
+.pdfAnnotationLayer-cont {
+ width:100%;
+ height:100%;
+ position:relative;
+ top:-200%;
+} \ No newline at end of file
diff --git a/src/client/views/pdf/PDFAnnotationLayer.tsx b/src/client/views/pdf/PDFAnnotationLayer.tsx
index 1f49e0d2f..4f267a5c0 100644
--- a/src/client/views/pdf/PDFAnnotationLayer.tsx
+++ b/src/client/views/pdf/PDFAnnotationLayer.tsx
@@ -1,5 +1,6 @@
import React = require("react");
import { observer } from "mobx-react";
+import "./PDFAnnotationLayer.scss";
interface IAnnotationProps {
@@ -15,10 +16,6 @@ export class PDFAnnotationLayer extends React.Component {
}
render() {
- return (
- <div className="pdfAnnotationLayer-cont" style={{ width: "100%", height: "100%", position: "relative", top: "-200%" }} onPointerDown={this.onPointerDown}>
-
- </div>
- );
+ return <div className="pdfAnnotationLayer-cont" onPointerDown={this.onPointerDown} />;
}
} \ No newline at end of file
diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss
index 0fde764d0..7158aaffa 100644
--- a/src/client/views/pdf/PDFViewer.scss
+++ b/src/client/views/pdf/PDFViewer.scss
@@ -27,9 +27,12 @@
// position: absolute;
// top: 0;
}
-
+.pdfViewere-viewer {
+ pointer-events:inherit;
+}
.pdfViewer-text {
-
+ transform: scale(1.5);
+ transform-origin: top left;
.page {
.canvasWrapper {
display: none;
@@ -120,6 +123,8 @@
.pdfViewer-annotationLayer {
position: absolute;
top: 0;
+ width: 100%;
+ pointer-events: none;
}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 699a1ffd7..6a99cec59 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -4,27 +4,23 @@ import * as Pdfjs from "pdfjs-dist";
import "pdfjs-dist/web/pdf_viewer.css";
import * as rp from "request-promise";
import { Dictionary } from "typescript-collections";
-import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc";
+import { Doc, DocListCast, Opt } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
import { List } from "../../../new_fields/List";
-import { BoolCast, Cast, NumCast, StrCast, FieldValue } from "../../../new_fields/Types";
-import { emptyFunction } from "../../../Utils";
-import { DocServer } from "../../DocServer";
-import { Docs, DocUtils, DocumentOptions } from "../../documents/Documents";
-import { DocumentManager } from "../../util/DocumentManager";
+import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { emptyFunction, Utils } from "../../../Utils";
+import { Docs, DocUtils } from "../../documents/Documents";
import { DragManager } from "../../util/DragManager";
-import { DocumentView } from "../nodes/DocumentView";
-import { PDFBox, handleBackspace } from "../nodes/PDFBox";
+import { PDFBox } from "../nodes/PDFBox";
import Page from "./Page";
import "./PDFViewer.scss";
import React = require("react");
-import PDFMenu from "./PDFMenu";
-import { UndoManager } from "../../util/UndoManager";
-import { CompileScript, CompiledScript, CompileResult } from "../../util/Scripting";
+import { CompileScript, CompileResult } from "../../util/Scripting";
import { ScriptField } from "../../../new_fields/ScriptField";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Annotation from "./Annotation";
import { KeyCodes } from "../../northstar/utils/KeyCodes";
+import { DocServer } from "../../DocServer";
const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer");
export const scale = 2;
@@ -90,16 +86,13 @@ export class Viewer extends React.Component<IViewerProps> {
private _annotationReactionDisposer?: IReactionDisposer;
private _dropDisposer?: DragManager.DragDropDisposer;
private _filterReactionDisposer?: IReactionDisposer;
- private _activeReactionDisposer?: IReactionDisposer;
private _viewer: React.RefObject<HTMLDivElement>;
private _mainCont: React.RefObject<HTMLDivElement>;
private _pdfViewer: any;
// private _textContent: Pdfjs.TextContent[] = [];
private _pdfFindController: any;
private _searchString: string = "";
- private _rendered: boolean = false;
- private _pageIndex: number = -1;
- private _matchIndex: number = 0;
+ private _selectionText: string = "";
constructor(props: IViewerProps) {
super(props);
@@ -110,6 +103,10 @@ export class Viewer extends React.Component<IViewerProps> {
this._mainCont = React.createRef();
}
+ setSelectionText = (text: string) => {
+ this._selectionText = text;
+ }
+
componentDidUpdate = (prevProps: IViewerProps) => {
if (this.scrollY !== prevProps.scrollY) {
this.renderPages();
@@ -135,23 +132,6 @@ export class Viewer extends React.Component<IViewerProps> {
},
{ fireImmediately: true });
- this._activeReactionDisposer = reaction(
- () => this.props.parent.props.active(),
- () => {
- runInAction(() => {
- if (!this.props.parent.props.active()) {
- this._searching = false;
- this._pdfFindController = null;
- if (this._viewer.current) {
- let cns = this._viewer.current.childNodes;
- for (let i = cns.length - 1; i >= 0; i--) {
- cns.item(i).remove();
- }
- }
- }
- });
- }
- );
if (this.props.parent.props.ContainingCollectionView) {
this._filterReactionDisposer = reaction(
@@ -182,6 +162,9 @@ export class Viewer extends React.Component<IViewerProps> {
if (this._mainCont.current) {
this._dropDisposer = this._mainCont.current && DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } });
}
+
+ document.removeEventListener("copy", this.copy);
+ document.addEventListener("copy", this.copy);
}
componentWillUnmount = () => {
@@ -189,11 +172,47 @@ export class Viewer extends React.Component<IViewerProps> {
this._annotationReactionDisposer && this._annotationReactionDisposer();
this._filterReactionDisposer && this._filterReactionDisposer();
this._dropDisposer && this._dropDisposer();
+ document.removeEventListener("copy", this.copy);
+ }
+
+ private copy = (e: ClipboardEvent) => {
+ if (this.props.parent.props.active()) {
+ let text = this._selectionText;
+ if (e.clipboardData) {
+ e.clipboardData.setData("text/plain", text);
+ e.clipboardData.setData("dash/pdfOrigin", this.props.parent.props.Document[Id]);
+ let annoDoc = this.makeAnnotationDocument(undefined, 0, "#0390fc");
+ e.clipboardData.setData("dash/pdfRegion", annoDoc[Id]);
+ e.preventDefault();
+ }
+ }
+ // let targetAnnotations = DocListCast(this.props.parent.fieldExtensionDoc.annotations);
+ // if (targetAnnotations) {
+ // targetAnnotations.push(destDoc);
+ // }
+ }
+
+ paste = (e: ClipboardEvent) => {
+ if (e.clipboardData) {
+ if (e.clipboardData.getData("dash/pdfOrigin") === this.props.parent.props.Document[Id]) {
+ let linkDocId = e.clipboardData.getData("dash/linkDoc");
+ if (linkDocId) {
+ DocServer.GetRefField(linkDocId).then(async (link) => {
+ if (!(link instanceof Doc)) {
+ return;
+ }
+ let proto = Doc.GetProto(link);
+ let source = await Cast(proto.anchor1, Doc);
+ proto.anchor2 = this.makeAnnotationDocument(source, 0, "#0390fc", false);
+ });
+ }
+ }
+ }
}
scrollTo(y: number) {
if (this.props.mainCont.current) {
- this.props.parent.scrollTo(y - this.props.mainCont.current.clientHeight);
+ this.props.parent.scrollTo(y);
}
}
@@ -239,7 +258,7 @@ export class Viewer extends React.Component<IViewerProps> {
}
@action
- makeAnnotationDocument = (sourceDoc: Doc | undefined, s: number, color: string): Doc => {
+ makeAnnotationDocument = (sourceDoc: Doc | undefined, s: number, color: string, createLink: boolean = true): Doc => {
let annoDocs: Doc[] = [];
let mainAnnoDoc = Docs.Create.InstanceFromProto(new Doc(), "", {});
@@ -268,7 +287,7 @@ export class Viewer extends React.Component<IViewerProps> {
mainAnnoDoc.y = Math.max(minY, 0);
mainAnnoDoc.annotations = new List<Doc>(annoDocs);
- if (sourceDoc) {
+ if (sourceDoc && createLink) {
DocUtils.MakeLink(sourceDoc, mainAnnoDoc, undefined, `Annotation from ${StrCast(this.props.parent.Document.title)}`, "", StrCast(this.props.parent.Document.title));
}
this._savedAnnotations.clear();
@@ -277,20 +296,19 @@ export class Viewer extends React.Component<IViewerProps> {
}
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();
- }
+ // 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);
+ // }
+ // 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
@@ -318,6 +336,7 @@ export class Viewer extends React.Component<IViewerProps> {
this._isPage[page] = "page";
this._visibleElements[page] = (
<Page
+ setSelectionText={this.setSelectionText}
size={this._pageSizes[page]}
pdf={this.props.pdf}
page={page}
@@ -346,7 +365,7 @@ export class Viewer extends React.Component<IViewerProps> {
this._isPage[page] = "image";
const address = this.props.url;
try {
- let res = JSON.parse(await rp.get(DocServer.prepend(`/thumbnail${address.substring("files/".length, address.length - ".pdf".length)}-${page + 1}.PNG`)));
+ let res = JSON.parse(await rp.get(Utils.prepend(`/thumbnail${address.substring("files/".length, address.length - ".pdf".length)}-${page + 1}.PNG`)));
runInAction(() => this._visibleElements[page] =
<img key={res.path} src={res.path} onError={handleError}
style={{ width: `${parseInt(res.width) * scale}px`, height: `${parseInt(res.height) * scale}px` }} />);
@@ -476,7 +495,6 @@ export class Viewer extends React.Component<IViewerProps> {
phraseSearch: true,
query: searchString
});
- this._rendered = true;
});
container.addEventListener("pagerendered", () => {
console.log("rendered");
@@ -488,7 +506,6 @@ export class Viewer extends React.Component<IViewerProps> {
phraseSearch: true,
query: searchString
});
- this._rendered = true;
});
}
}
@@ -563,7 +580,6 @@ export class Viewer extends React.Component<IViewerProps> {
});
container.addEventListener("pagerendered", () => {
console.log("rendered");
- this._rendered = true;
});
this._pdfViewer.setDocument(this.props.pdf);
this._pdfFindController = new PDFJSViewer.PDFFindController(this._pdfViewer);
@@ -636,16 +652,12 @@ export class Viewer extends React.Component<IViewerProps> {
render() {
let compiled = this._script;
return (
- <div ref={this._mainCont} style={{ pointerEvents: "all" }} onPointerDown={this.pointerDown}>
+ <div className="pdfViewer-viewer" ref={this._mainCont} 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-text" ref={this._viewer} />
+ <div className="pdfViewer-annotationLayer" style={{ height: this.props.parent.Document.nativeHeight }}>
<div className="pdfViewer-annotationLayer-subCont" ref={this._annotationLayer}>
{this._annotations.filter(anno => {
if (compiled && compiled.compiled) {
@@ -703,32 +715,24 @@ class SimpleLinkService {
externalLinkRel: any = null;
pdf: any = null;
- navigateTo(dest: any) { }
+ navigateTo() { }
- getDestinationHash(dest: any) { return "#"; }
+ getDestinationHash() { return "#"; }
- getAnchorUrl(hash: any) { return "#"; }
+ getAnchorUrl() { return "#"; }
- setHash(hash: any) { }
+ setHash() { }
- executeNamedAction(action: any) { }
+ executeNamedAction() { }
- cachePageRef(pageNum: any, pageRef: any) { }
+ cachePageRef() { }
- get pagesCount() {
- return this.pdf ? this.pdf.numPages : 0;
- }
+ get pagesCount() { return this.pdf ? this.pdf.numPages : 0; }
- get page() {
- return 0;
- }
+ get page() { return 0; }
- setPdf(pdf: any) {
- this.pdf = pdf;
- }
+ setPdf(pdf: any) { this.pdf = pdf; }
- get rotation() {
- return 0;
- }
+ get rotation() { return 0; }
set rotation(value: any) { }
} \ No newline at end of file
diff --git a/src/client/views/pdf/Page.tsx b/src/client/views/pdf/Page.tsx
index c9d442fe5..dea4e0da1 100644
--- a/src/client/views/pdf/Page.tsx
+++ b/src/client/views/pdf/Page.tsx
@@ -1,22 +1,18 @@
+import { action, IReactionDisposer, observable } from "mobx";
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 { Doc, DocListCastAsync, Opt, WidthSym } from "../../../new_fields/Doc";
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 { Cast, NumCast, StrCast, BoolCast } from "../../../new_fields/Types";
+import { Docs, DocUtils } from "../../documents/Documents";
+import { DragManager } from "../../util/DragManager";
+import { PDFBox } from "../nodes/PDFBox";
import PDFMenu from "./PDFMenu";
-import { UndoManager } from "../../util/UndoManager";
-import { copy } from "typescript-collections/dist/lib/arrays";
+import { scale } from "./PDFViewer";
+import "./PDFViewer.scss";
+import React = require("react");
interface IPageProps {
@@ -33,6 +29,7 @@ interface IPageProps {
createAnnotation: (div: HTMLDivElement, page: number) => void;
makeAnnotationDocuments: (doc: Doc | undefined, scale: number, color: string, linkTo: boolean) => Doc;
getScrollFromPage: (page: number) => number;
+ setSelectionText: (text: string) => void;
}
@observer
@@ -162,6 +159,7 @@ export default class Page extends React.Component<IPageProps> {
let targetDoc = Docs.Create.TextDocument({ width: 200, height: 200, title: "New Annotation" });
targetDoc.targetPage = this.props.page;
let annotationDoc = this.highlight(undefined, "red");
+ Doc.GetProto(annotationDoc).annotationOn = thisDoc;
annotationDoc.linkedToDoc = false;
// create dragData and star tdrag
let dragData = new DragManager.AnnotationDragData(thisDoc, annotationDoc, targetDoc);
@@ -169,16 +167,12 @@ export default class Page extends React.Component<IPageProps> {
DragManager.StartAnnotationDrag([ele], dragData, e.pageX, e.pageY, {
handlers: {
dragComplete: async () => {
- if (!(await annotationDoc.linkedToDoc)) {
+ if (!BoolCast(annotationDoc.linkedToDoc)) {
let annotations = await DocListCastAsync(annotationDoc.annotations);
- if (annotations) {
- annotations.forEach(anno => {
- anno.target = targetDoc;
- });
- }
+ 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));
+ DocUtils.MakeLink(annotationDoc, targetDoc, dragData.targetContext, `Annotation from ${StrCast(pdfDoc.title)}`, "", StrCast(pdfDoc.title));
}
}
}
@@ -357,7 +351,8 @@ export default class Page extends React.Component<IPageProps> {
else {
let sel = window.getSelection();
if (sel && sel.type === "Range") {
- this.createTextAnnotation(sel);
+ let selRange = sel.getRangeAt(0);
+ this.createTextAnnotation(sel, selRange);
PDFMenu.Instance.jumpTo(e.clientX, e.clientY);
}
}
@@ -375,8 +370,8 @@ export default class Page extends React.Component<IPageProps> {
}
@action
- createTextAnnotation = (sel: Selection) => {
- let clientRects = sel.getRangeAt(0).getClientRects();
+ createTextAnnotation = (sel: Selection, selRange: Range) => {
+ let clientRects = selRange.getClientRects();
if (this._textLayer.current) {
let boundingRect = this._textLayer.current.getBoundingClientRect();
for (let i = 0; i < clientRects.length; i++) {
@@ -393,6 +388,10 @@ export default class Page extends React.Component<IPageProps> {
}
}
}
+ let text = selRange.extractContents().textContent;
+ if (text) {
+ this.props.setSelectionText(text);
+ }
// clear selection
if (sel.empty) { // Chrome
sel.empty();
diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx
index a16d7bc76..e2d8daea9 100644
--- a/src/client/views/presentationview/PresentationElement.tsx
+++ b/src/client/views/presentationview/PresentationElement.tsx
@@ -1,23 +1,30 @@
-import { observer } from "mobx-react";
-import React = require("react");
-import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc";
-import { NumCast, BoolCast, StrCast, Cast } from "../../../new_fields/Types";
-import { Id } from "../../../new_fields/FieldSymbols";
-import { observable, action, computed, runInAction } from "mobx";
-import "./PresentationView.scss";
-import { Utils } from "../../../Utils";
import { library } from '@fortawesome/fontawesome-svg-core';
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { faFile as fileSolid, faFileDownload, faLocationArrow, faArrowUp, faSearch } from '@fortawesome/free-solid-svg-icons';
import { faFile as fileRegular } from '@fortawesome/free-regular-svg-icons';
+import { faArrowUp, faFile as fileSolid, faFileDownload, faLocationArrow, faSearch, faArrowRight } 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 } 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, StrCast } from "../../../new_fields/Types";
+import { Utils, returnFalse, emptyFunction, returnOne, returnEmptyString } from "../../../Utils";
+import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager";
+import { SelectionManager } from "../../util/SelectionManager";
+import { ContextMenu } from "../ContextMenu";
+import { Transform } from "../../util/Transform";
+import { DocumentView } from "../nodes/DocumentView";
+import { DocumentType } from "../../documents/Documents";
+import React = require("react");
+
library.add(faArrowUp);
library.add(fileSolid);
library.add(faLocationArrow);
library.add(fileRegular as any);
library.add(faSearch);
+library.add(faArrowRight);
interface PresentationElementProps {
mainDocument: Doc;
@@ -30,6 +37,8 @@ interface PresentationElementProps {
presStatus: boolean;
presButtonBackUp: Doc;
presGroupBackUp: Doc;
+ removeDocByRef(doc: Doc): boolean;
+ PresElementsMappings: Map<Doc, PresentationElement>;
}
@@ -42,6 +51,7 @@ export enum buttonIndex {
FadeAfter = 3,
HideAfter = 4,
Group = 5,
+ OpenRight = 6
}
@@ -53,13 +63,25 @@ export enum buttonIndex {
export default class PresentationElement extends React.Component<PresentationElementProps> {
@observable private selectedButtons: boolean[];
+ private header?: HTMLDivElement | undefined;
+ private listdropDisposer?: DragManager.DragDropDisposer;
+ private presElRef: React.RefObject<HTMLDivElement>;
+ private backUpDoc: Doc | undefined;
constructor(props: PresentationElementProps) {
super(props);
- this.selectedButtons = new Array(6);
+ this.selectedButtons = new Array(7);
+
+ this.presElRef = React.createRef();
+ }
+
+
+ componentWillUnmount() {
+ this.listdropDisposer && this.listdropDisposer();
}
+
/**
* Getter to get the status of the buttons.
*/
@@ -71,14 +93,23 @@ export default class PresentationElement extends React.Component<PresentationEle
//Lifecycle function that makes sure that button BackUp is received when mounted.
async componentDidMount() {
this.receiveButtonBackUp();
-
+ if (this.presElRef.current) {
+ this.header = this.presElRef.current;
+ this.createListDropTarget(this.presElRef.current);
+ }
}
//Lifecycle function that makes sure button BackUp is received when not re-mounted bu re-rendered.
async componentDidUpdate() {
- this.receiveButtonBackUp();
+ if (this.presElRef.current) {
+ this.header = this.presElRef.current;
+ this.createListDropTarget(this.presElRef.current);
+ }
}
+ /**
+ * Function that will be called to receive stored backUp for buttons
+ */
receiveButtonBackUp = async () => {
//get the list that stores docs that keep track of buttons
@@ -86,19 +117,32 @@ export default class PresentationElement extends React.Component<PresentationEle
if (!castedList) {
this.props.presButtonBackUp.selectedButtonDocs = castedList = new List<Doc>();
}
+
+ let foundDoc: boolean = false;
+
//if this is the first time this doc mounts, push a doc for it to store
- if (castedList.length <= this.props.index) {
+
+ for (let doc of castedList) {
+ let curDoc = await doc;
+ let curDocId = StrCast(curDoc.docId);
+ if (curDocId === this.props.document[Id]) {
+ let selectedButtonOfDoc = Cast(curDoc.selectedButtons, listSpec("boolean"), null);
+ if (selectedButtonOfDoc !== undefined) {
+ runInAction(() => this.selectedButtons = selectedButtonOfDoc);
+ foundDoc = true;
+ this.backUpDoc = curDoc;
+ break;
+ }
+ }
+ }
+
+ if (!foundDoc) {
let newDoc = new Doc();
- let defaultBooleanArray: boolean[] = new Array(6);
+ let defaultBooleanArray: boolean[] = new Array(7);
newDoc.selectedButtons = new List(defaultBooleanArray);
+ newDoc.docId = this.props.document[Id];
castedList.push(newDoc);
- //otherwise update the selected buttons depending on storage.
- } else {
- let curDoc: Doc = await castedList[this.props.index];
- let selectedButtonOfDoc = Cast(curDoc.selectedButtons, listSpec("boolean"), null);
- if (selectedButtonOfDoc !== undefined) {
- runInAction(() => this.selectedButtons = selectedButtonOfDoc);
- }
+ this.backUpDoc = newDoc;
}
}
@@ -244,9 +288,9 @@ export default class PresentationElement extends React.Component<PresentationEle
*/
@action
autoSaveButtonChange = async (index: buttonIndex) => {
- let castedList = (await DocListCastAsync(this.props.presButtonBackUp.selectedButtonDocs))!;
- castedList[this.props.index].selectedButtons = new List(this.selectedButtons);
-
+ if (this.backUpDoc) {
+ this.backUpDoc.selectedButtons = new List(this.selectedButtons);
+ }
}
/**
@@ -356,6 +400,472 @@ export default class PresentationElement extends React.Component<PresentationEle
}
+ /**
+ * Function that opens up the option to open a element on right when navigated,
+ * instead of openening it as tab as default.
+ */
+ @action
+ onRightTabClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ if (this.selectedButtons[buttonIndex.OpenRight]) {
+ this.selectedButtons[buttonIndex.OpenRight] = false;
+ // action maybe
+ } else {
+ this.selectedButtons[buttonIndex.OpenRight] = true;
+ }
+ this.autoSaveButtonChange(buttonIndex.OpenRight);
+ }
+
+ /**
+ * Creating a drop target for drag and drop when called.
+ */
+ protected createListDropTarget = (ele: HTMLDivElement) => {
+ this.listdropDisposer && this.listdropDisposer();
+ if (ele) {
+ this.listdropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.listDrop.bind(this) } });
+ }
+ }
+
+ /**
+ * Returns a local transformed coordinate array for given coordinates.
+ */
+ ScreenToLocalListTransform = (xCord: number, yCord: number) => {
+ return [xCord, yCord];
+ }
+
+ /**
+ * This method is called when a element is dropped on a already esstablished target.
+ * It makes sure to do appropirate action depending on if the item is dropped before
+ * or after the target.
+ */
+ listDrop = async (e: Event, de: DragManager.DropEvent) => {
+ let x = this.ScreenToLocalListTransform(de.x, de.y);
+ let rect = this.header!.getBoundingClientRect();
+ let bounds = this.ScreenToLocalListTransform(rect.left, rect.top + rect.height / 2);
+ let before = x[1] < bounds[1];
+ if (de.data instanceof DragManager.DocumentDragData) {
+ let addDoc = (doc: Doc) => Doc.AddDocToList(this.props.mainDocument, "data", doc, this.props.document, before);
+ e.stopPropagation();
+ //where does treeViewId come from
+ let movedDocs = (de.data.options === this.props.mainDocument[Id] ? de.data.draggedDocuments : de.data.droppedDocuments);
+ //console.log("How is this causing an issue");
+ let droppedDoc: Doc = de.data.droppedDocuments[0];
+ await this.updateGroupsOnDrop(droppedDoc, de);
+ document.removeEventListener("pointermove", this.onDragMove, true);
+ return (de.data.dropAction || de.data.userDropAction) ?
+ de.data.droppedDocuments.reduce((added: boolean, d: Doc) => Doc.AddDocToList(this.props.mainDocument, "data", d, this.props.document, before) || added, false)
+ : (de.data.moveDocument) ?
+ movedDocs.reduce((added: boolean, d: Doc) => de.data.moveDocument(d, this.props.document, addDoc) || added, false)
+ : de.data.droppedDocuments.reduce((added: boolean, d: Doc) => Doc.AddDocToList(this.props.mainDocument, "data", d, this.props.document, before), false);
+ }
+ document.removeEventListener("pointermove", this.onDragMove, true);
+
+ return false;
+ }
+
+ /**
+ * This method is called to update groups when the user drags and drops an
+ * element to a different place. It follows the default behaviour and reconstructs
+ * the groups in the way they would appear if clicked by user.
+ */
+ updateGroupsOnDrop = async (droppedDoc: Doc, de: DragManager.DropEvent) => {
+
+ let x = this.ScreenToLocalListTransform(de.x, de.y);
+ let rect = this.header!.getBoundingClientRect();
+ let bounds = this.ScreenToLocalListTransform(rect.left, rect.top + rect.height / 2);
+ let before = x[1] < bounds[1];
+
+ let droppedDocIndex = this.props.allListElements.indexOf(droppedDoc);
+
+ let dropIndexDiff = droppedDocIndex - this.props.index;
+
+ //checking if the position it's dropped corresponds to current location with 3 cases.
+ if (droppedDocIndex === this.props.index) {
+ return;
+ }
+
+ if (dropIndexDiff === 1 && !before) {
+ return;
+ }
+ if (dropIndexDiff === -1 && before) {
+ return;
+ }
+
+ let p = this.props;
+ let droppedDocSelectedButtons: boolean[] = await this.getSelectedButtonsOfDoc(droppedDoc);
+ let curDocGuid = StrCast(droppedDoc.presentId, null);
+
+ //Splicing the doc from its current group, since it's moved
+ if (p.groupMappings.has(curDocGuid)) {
+ let groupArray = this.props.groupMappings.get(curDocGuid)!;
+
+ if (droppedDocSelectedButtons[buttonIndex.Group]) {
+ let groupIndexOfDrop = groupArray.indexOf(droppedDoc);
+ let firstPart = groupArray.splice(0, groupIndexOfDrop);
+
+ if (firstPart.length > 1) {
+ let newGroupGuid = Utils.GenerateGuid();
+ firstPart.forEach((doc: Doc) => doc.presentId = newGroupGuid);
+ this.props.groupMappings.set(newGroupGuid, firstPart);
+ }
+ }
+
+ groupArray.splice(groupArray.indexOf(droppedDoc), 1);
+ if (groupArray.length === 0) {
+ this.props.groupMappings.delete(curDocGuid);
+ }
+ droppedDoc.presentId = Utils.GenerateGuid();
+
+ //making sure to correct to groups after splicing, in case the dragged element
+ //had the grouping on.
+ let indexOfBelow = droppedDocIndex + 1;
+ if (indexOfBelow < this.props.allListElements.length && indexOfBelow > 1) {
+ let selectedButtonsOrigBelow: boolean[] = await this.getSelectedButtonsOfDoc(this.props.allListElements[indexOfBelow]);
+ let aboveBelowDoc: Doc = this.props.allListElements[droppedDocIndex - 1];
+ let aboveBelowDocSelectedButtons: boolean[] = await this.getSelectedButtonsOfDoc(aboveBelowDoc);
+ let belowDoc: Doc = this.props.allListElements[indexOfBelow];
+ let belowDocPresId = StrCast(belowDoc.presentId);
+
+ if (selectedButtonsOrigBelow[buttonIndex.Group]) {
+ let belowDocGroup: Doc[] = this.props.groupMappings.get(belowDocPresId)!;
+ if (aboveBelowDocSelectedButtons[buttonIndex.Group]) {
+ let aboveBelowDocPresId = StrCast(aboveBelowDoc.presentId);
+ if (this.props.groupMappings.has(aboveBelowDocPresId)) {
+ let aboveBelowDocGroup: Doc[] = this.props.groupMappings.get(aboveBelowDocPresId)!;
+ aboveBelowDocGroup.push(...belowDocGroup);
+ this.props.groupMappings.delete(belowDocPresId);
+ belowDocGroup.forEach((doc: Doc) => doc.presentId = aboveBelowDocPresId);
+
+ }
+ } else {
+ belowDocGroup.unshift(aboveBelowDoc);
+ aboveBelowDoc.presentId = belowDocPresId;
+ }
+
+
+ }
+ }
+
+ }
+
+ //Case, when the dropped doc had the group button clicked.
+ if (droppedDocSelectedButtons[buttonIndex.Group]) {
+ if (before) {
+ if (this.props.index > 0) {
+ let aboveDoc = this.props.allListElements[this.props.index - 1];
+ let aboveDocGuid = StrCast(aboveDoc.presentId);
+ if (this.props.groupMappings.has(aboveDocGuid)) {
+ this.protectOrderAndPush(aboveDocGuid, aboveDoc, droppedDoc);
+ } else {
+ this.createNewGroup(aboveDoc, droppedDoc, aboveDocGuid);
+ }
+ } else {
+ let propsPresId = StrCast(this.props.document.presentId);
+ if (this.selectedButtons[buttonIndex.Group]) {
+ let propsArray = this.props.groupMappings.get(propsPresId)!;
+ propsArray.unshift(droppedDoc);
+ droppedDoc.presentId = propsPresId;
+ }
+ }
+ } else {
+ let propsDocGuid = StrCast(this.props.document.presentId);
+ if (this.props.groupMappings.has(propsDocGuid)) {
+ this.protectOrderAndPush(propsDocGuid, this.props.document, droppedDoc);
+
+ } else {
+ this.createNewGroup(this.props.document, droppedDoc, propsDocGuid);
+ }
+ }
+
+
+ //if the group button of the element was not clicked.
+ } else {
+ if (before) {
+ if (this.props.index > 0) {
+
+ let aboveDoc = this.props.allListElements[this.props.index - 1];
+ let aboveDocGuid = StrCast(aboveDoc.presentId);
+ let aboveDocSelectedButtons: boolean[] = await this.getSelectedButtonsOfDoc(aboveDoc);
+
+
+ if (this.selectedButtons[buttonIndex.Group]) {
+ if (aboveDocSelectedButtons[buttonIndex.Group]) {
+ let aboveGroupArray = this.props.groupMappings.get(aboveDocGuid)!;
+ let propsDocPresId = StrCast(this.props.document.presentId);
+
+ this.halveGroupArray(aboveDoc, aboveGroupArray, droppedDoc, propsDocPresId);
+
+ } else {
+ let belowPresentId = StrCast(this.props.document.presentId);
+ let belowGroup = this.props.groupMappings.get(belowPresentId)!;
+ belowGroup.splice(belowGroup.indexOf(aboveDoc), 1);
+ belowGroup.unshift(droppedDoc);
+ droppedDoc.presentId = belowPresentId;
+ aboveDoc.presentId = Utils.GenerateGuid();
+ }
+
+
+ }
+ } else {
+ let propsPresId = StrCast(this.props.document.presentId);
+ if (this.selectedButtons[buttonIndex.Group]) {
+ let propsArray = this.props.groupMappings.get(propsPresId)!;
+ propsArray.unshift(droppedDoc);
+ droppedDoc.presentId = propsPresId;
+ }
+ }
+ } else {
+ if (this.props.index < this.props.allListElements.length - 1) {
+ let belowDoc = this.props.allListElements[this.props.index + 1];
+ let belowDocGuid = StrCast(belowDoc.presentId);
+ let belowDocSelectedButtons: boolean[] = await this.getSelectedButtonsOfDoc(belowDoc);
+
+ let propsDocGuid = StrCast(this.props.document.presentId);
+
+ if (belowDocSelectedButtons[buttonIndex.Group]) {
+ let belowGroupArray = this.props.groupMappings.get(belowDocGuid)!;
+ if (this.selectedButtons[buttonIndex.Group]) {
+
+ let propsGroupArray = this.props.groupMappings.get(propsDocGuid)!;
+
+ this.halveGroupArray(this.props.document, propsGroupArray, droppedDoc, belowDocGuid);
+
+ } else {
+ belowGroupArray.splice(belowGroupArray.indexOf(this.props.document), 1);
+ this.props.document.presentId = Utils.GenerateGuid();
+ belowGroupArray.unshift(droppedDoc);
+ droppedDoc.presentId = belowDocGuid;
+ }
+ }
+
+ }
+ }
+ }
+ this.autoSaveGroupChanges();
+
+ }
+
+ /**
+ * This method returns the selectedButtons boolean array of the passed in doc,
+ * retrieving it from the back-up.
+ */
+ getSelectedButtonsOfDoc = async (paramDoc: Doc) => {
+ let castedList = Cast(this.props.presButtonBackUp.selectedButtonDocs, listSpec(Doc));
+ let foundSelectedButtons: boolean[] = new Array(7);
+
+ //if this is the first time this doc mounts, push a doc for it to store
+ for (let doc of castedList!) {
+ let curDoc = await doc;
+ let curDocId = StrCast(curDoc.docId);
+ if (curDocId === paramDoc[Id]) {
+ let selectedButtonOfDoc = Cast(curDoc.selectedButtons, listSpec("boolean"), null);
+ if (selectedButtonOfDoc !== undefined) {
+ return selectedButtonOfDoc;
+ }
+ }
+ }
+
+ return foundSelectedButtons;
+
+ }
+
+ //This is used to add dragging as an event.
+ onPointerEnter = (e: React.PointerEvent): void => {
+ if (e.buttons === 1 && SelectionManager.GetIsDragging()) {
+ let selected = NumCast(this.props.mainDocument.selectedDoc, 0);
+
+ this.header!.className = "presentationView-item";
+
+
+ if (selected === this.props.index) {
+ //this doc is selected
+ this.header!.className = "presentationView-item presentationView-selected";
+ }
+ document.addEventListener("pointermove", this.onDragMove, true);
+ }
+ }
+
+ //This is used to remove the dragging when dropped.
+ onPointerLeave = (e: React.PointerEvent): void => {
+ //to get currently selected presentation doc
+ let selected = NumCast(this.props.mainDocument.selectedDoc, 0);
+
+ this.header!.className = "presentationView-item";
+
+
+ if (selected === this.props.index) {
+ //this doc is selected
+ this.header!.className = "presentationView-item presentationView-selected";
+
+ }
+ document.removeEventListener("pointermove", this.onDragMove, true);
+ }
+
+ /**
+ * This method is passed in to be used when dragging a document.
+ * It makes it possible to show dropping lines on drop targets.
+ */
+ onDragMove = (e: PointerEvent): void => {
+ this.props.document.libraryBrush = false;
+ let x = this.ScreenToLocalListTransform(e.clientX, e.clientY);
+ let rect = this.header!.getBoundingClientRect();
+ let bounds = this.ScreenToLocalListTransform(rect.left, rect.top + rect.height / 2);
+ let before = x[1] < bounds[1];
+ this.header!.className = "presentationView-item";
+ if (before) {
+ this.header!.className += " presentationView-item-above";
+ }
+ else if (!before) {
+ this.header!.className += " presentationView-item-below";
+ }
+ e.stopPropagation();
+ }
+
+ /**
+ * This method is passed in to on down event of presElement, so that drag and
+ * drop can be completed with DragManager functionality.
+ */
+ @action
+ move: DragManager.MoveFunction = (doc: Doc, target: Doc, addDoc) => {
+ return this.props.document !== target && this.props.removeDocByRef(doc) && addDoc(doc);
+ }
+
+ /**
+ * Helper method that gets called to divide a group array into two different groups
+ * including the targetDoc in first part.
+ * @param targetDoc document that is targeted as slicing point
+ * @param propsGroupArray the array that gets divided into 2
+ * @param droppedDoc the dropped document
+ * @param belowDocGuid presentId of the belowGroup
+ */
+ private halveGroupArray(targetDoc: Doc, propsGroupArray: Doc[], droppedDoc: Doc, belowDocGuid: string) {
+ let targetIndex = propsGroupArray.indexOf(targetDoc);
+ let firstPart = propsGroupArray.slice(0, targetIndex + 1);
+ let firstPartNewGuid = Utils.GenerateGuid();
+ firstPart.forEach((doc: Doc) => doc.presentId = firstPartNewGuid);
+ let secondPart = propsGroupArray.slice(targetIndex + 1);
+ secondPart.unshift(droppedDoc);
+ droppedDoc.presentId = belowDocGuid;
+ this.props.groupMappings.set(firstPartNewGuid, firstPart);
+ this.props.groupMappings.set(belowDocGuid, secondPart);
+ }
+
+ /**
+ * Helper method that creates a new group, pushing above document first,
+ * and dropped document second.
+ * @param aboveDoc the document above dropped document
+ * @param droppedDoc the dropped document itself
+ * @param aboveDocGuid above document's presentId
+ */
+ private createNewGroup(aboveDoc: Doc, droppedDoc: Doc, aboveDocGuid: string) {
+ let newGroup: Doc[] = [];
+ newGroup.push(aboveDoc);
+ newGroup.push(droppedDoc);
+ droppedDoc.presentId = aboveDocGuid;
+ this.props.groupMappings.set(aboveDocGuid, newGroup);
+ }
+
+ /**
+ * Helper method that finds the above document's group, and pushes the
+ * dropped document into that group, protecting the visual order of the
+ * presentation elements.
+ * @param aboveDoc the document above dropped document
+ * @param droppedDoc the dropped document itself
+ * @param aboveDocGuid above document's presentId
+ */
+ private protectOrderAndPush(aboveDocGuid: string, aboveDoc: Doc, droppedDoc: Doc) {
+ let groupArray = this.props.groupMappings.get(aboveDocGuid)!;
+ let tempStack: Doc[] = [];
+ while (groupArray[groupArray.length - 1] !== aboveDoc) {
+ tempStack.push(groupArray.pop()!);
+ }
+ groupArray.push(droppedDoc);
+ droppedDoc.presentId = aboveDocGuid;
+ while (tempStack.length !== 0) {
+ groupArray.push(tempStack.pop()!);
+ }
+ }
+ /**
+ * This function is a getter to get if a document is in previewMode.
+ */
+ private get embedInline() {
+ return BoolCast(this.props.document.embedOpen);
+ }
+
+ /**
+ * This function sets document in presentation preview mode as the given value.
+ */
+ private set embedInline(value: boolean) {
+ this.props.document.embedOpen = value;
+ }
+
+ /**
+ * The function that recreates that context menu of presentation elements.
+ */
+ onContextMenu = (e: React.MouseEvent<HTMLDivElement>) => {
+ e.preventDefault();
+ e.stopPropagation();
+ ContextMenu.Instance.addItem({ description: this.embedInline ? "Collapse Inline" : "Expand Inline", event: () => this.embedInline = !this.embedInline, icon: "expand" });
+ ContextMenu.Instance.displayMenu(e.clientX, e.clientY);
+ }
+
+ /**
+ * The function that is responsible for rendering the a preview or not for this
+ * presentation element.
+ */
+ renderEmbeddedInline = () => {
+ if (!this.embedInline) {
+ return (null);
+ }
+
+ let propDocWidth = NumCast(this.props.document.nativeWidth);
+ let propDocHeight = NumCast(this.props.document.nativeHeight);
+ let scale = () => {
+ let newScale = 175 / NumCast(this.props.document.nativeWidth, 175);
+ return newScale;
+ };
+ return (
+ <div style={{
+ position: "relative",
+ height: propDocHeight === 0 ? 100 : propDocHeight * scale(),
+ width: propDocWidth === 0 ? "auto" : propDocWidth * scale(),
+ marginTop: 15
+
+ }}>
+ <DocumentView
+ fitToBox={StrCast(this.props.document.type).indexOf(DocumentType.COL) !== -1}
+ Document={this.props.document}
+ addDocument={returnFalse}
+ removeDocument={returnFalse}
+ ScreenToLocalTransform={Transform.Identity}
+ addDocTab={returnFalse}
+ renderDepth={1}
+ PanelWidth={() => 350}
+ PanelHeight={() => 90}
+ focus={emptyFunction}
+ backgroundColor={returnEmptyString}
+ selectOnLoad={false}
+ parentActive={returnFalse}
+ whenActiveChanged={returnFalse}
+ bringToFront={emptyFunction}
+ zoomToScale={emptyFunction}
+ getScale={returnOne}
+ ContainingCollectionView={undefined}
+ ContentScaling={scale}
+ />
+ <div style={{
+ width: " 100%",
+ height: " 100%",
+ position: "absolute",
+ left: 0,
+ top: 0,
+ background: "transparent",
+ zIndex: 2,
+
+ }}></div>
+ </div>
+ );
+ }
render() {
let p = this.props;
@@ -364,16 +874,18 @@ export default class PresentationElement extends React.Component<PresentationEle
//to get currently selected presentation doc
let selected = NumCast(p.mainDocument.selectedDoc, 0);
- let className = "presentationView-item";
+ let className = " presentationView-item";
if (selected === p.index) {
//this doc is selected
className += " presentationView-selected";
}
- let onEnter = (e: React.PointerEvent) => { p.document.libraryBrush = true; };
- let onLeave = (e: React.PointerEvent) => { p.document.libraryBrush = undefined; };
+ let dropAction = StrCast(this.props.document.dropAction) as dropActionType;
+ let onItemDown = SetupDrag(this.presElRef, () => p.document, this.move, dropAction, this.props.mainDocument[Id], true);
return (
- <div className={className} key={p.document[Id] + p.index}
- onPointerEnter={onEnter} onPointerLeave={onLeave}
+ <div className={className} onContextMenu={this.onContextMenu} key={p.document[Id] + p.index}
+ ref={this.presElRef}
+ onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}
+ onPointerDown={onItemDown}
style={{
outlineColor: "maroon",
outlineStyle: "dashed",
@@ -383,19 +895,22 @@ export default class PresentationElement extends React.Component<PresentationEle
<strong className="presentationView-name">
{`${p.index + 1}. ${title}`}
</strong>
- <button className="presentation-icon" onClick={e => { this.props.deleteDocument(p.index); e.stopPropagation(); }}>X</button>
+ <button className="presentation-icon" onPointerDown={(e) => e.stopPropagation()} onClick={e => { this.props.deleteDocument(p.index); e.stopPropagation(); }}>X</button>
<br></br>
- <button title="Zoom" className={this.selectedButtons[buttonIndex.Show] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onZoomDocumentClick}><FontAwesomeIcon icon={"search"} /></button>
- <button title="Navigate" className={this.selectedButtons[buttonIndex.Navigate] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onNavigateDocumentClick}><FontAwesomeIcon icon={"location-arrow"} /></button>
- <button title="Hide Document Till Presented" className={this.selectedButtons[buttonIndex.HideTillPressed] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onHideDocumentUntilPressClick}><FontAwesomeIcon icon={fileSolid} /></button>
- <button title="Fade Document After Presented" className={this.selectedButtons[buttonIndex.FadeAfter] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} color={"gray"} /></button>
- <button title="Hide Document After Presented" className={this.selectedButtons[buttonIndex.HideAfter] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
- <button title="Group With Up" className={this.selectedButtons[buttonIndex.Group] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={(e) => {
+ <button title="Zoom" className={this.selectedButtons[buttonIndex.Show] ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onZoomDocumentClick}><FontAwesomeIcon icon={"search"} /></button>
+ <button title="Navigate" className={this.selectedButtons[buttonIndex.Navigate] ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onNavigateDocumentClick}><FontAwesomeIcon icon={"location-arrow"} /></button>
+ <button title="Hide Document Till Presented" className={this.selectedButtons[buttonIndex.HideTillPressed] ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onHideDocumentUntilPressClick}><FontAwesomeIcon icon={fileSolid} /></button>
+ <button title="Fade Document After Presented" className={this.selectedButtons[buttonIndex.FadeAfter] ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} color={"gray"} /></button>
+ <button title="Hide Document After Presented" className={this.selectedButtons[buttonIndex.HideAfter] ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
+ <button title="Group With Up" className={this.selectedButtons[buttonIndex.Group] ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={(e) => {
e.stopPropagation();
this.changeGroupStatus();
this.onGroupClick(p.document, p.index, this.selectedButtons[buttonIndex.Group]);
}}> <FontAwesomeIcon icon={"arrow-up"} /> </button>
+ <button title="Open Right" className={this.selectedButtons[buttonIndex.OpenRight] ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onRightTabClick}><FontAwesomeIcon icon={"arrow-right"} /></button>
+ <br />
+ {this.renderEmbeddedInline()}
</div>
);
}
diff --git a/src/client/views/presentationview/PresentationList.tsx b/src/client/views/presentationview/PresentationList.tsx
index 7abd3e366..e853c4070 100644
--- a/src/client/views/presentationview/PresentationList.tsx
+++ b/src/client/views/presentationview/PresentationList.tsx
@@ -7,6 +7,7 @@ import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc";
import { NumCast, StrCast } from "../../../new_fields/Types";
import { Id } from "../../../new_fields/FieldSymbols";
import PresentationElement, { buttonIndex } from "./PresentationElement";
+import "../../../new_fields/Doc";
@@ -16,11 +17,14 @@ interface PresListProps {
deleteDocument(index: number): void;
gotoDocument(index: number, fromDoc: number): Promise<void>;
groupMappings: Map<String, Doc[]>;
- presElementsMappings: Map<Doc, PresentationElement>;
+ PresElementsMappings: Map<Doc, PresentationElement>;
setChildrenDocs: (docList: Doc[]) => void;
presStatus: boolean;
presButtonBackUp: Doc;
presGroupBackUp: Doc;
+ removeDocByRef(doc: Doc): boolean;
+ clearElemMap(): void;
+
}
@@ -79,25 +83,31 @@ export default class PresentationViewList extends React.Component<PresListProps>
this.initializeGroupIds(children);
this.initializeScaleViews(children);
this.props.setChildrenDocs(children);
+ this.props.clearElemMap();
return (
-
- <div className="presentationView-listCont">
- {children.map((doc: Doc, index: number) =>
- <PresentationElement
- ref={(e) => { if (e) { this.props.presElementsMappings.set(doc, e); } }}
- key={doc[Id]}
- mainDocument={this.props.mainDocument}
- document={doc}
- index={index}
- deleteDocument={this.props.deleteDocument}
- gotoDocument={this.props.gotoDocument}
- groupMappings={this.props.groupMappings}
- allListElements={children}
- presStatus={this.props.presStatus}
- presButtonBackUp={this.props.presButtonBackUp}
- presGroupBackUp={this.props.presGroupBackUp}
- />
- )}
+ <div className="presentationView-listCont" >
+ {children.map((doc: Doc, index: number) =>
+ <PresentationElement
+ ref={(e) => {
+ if (e && e !== null) {
+ this.props.PresElementsMappings.set(doc, e);
+ }
+ }}
+ key={doc[Id]}
+ mainDocument={this.props.mainDocument}
+ document={doc}
+ index={index}
+ deleteDocument={this.props.deleteDocument}
+ gotoDocument={this.props.gotoDocument}
+ groupMappings={this.props.groupMappings}
+ allListElements={children}
+ presStatus={this.props.presStatus}
+ presButtonBackUp={this.props.presButtonBackUp}
+ presGroupBackUp={this.props.presGroupBackUp}
+ removeDocByRef={this.props.removeDocByRef}
+ PresElementsMappings={this.props.PresElementsMappings}
+ />
+ )}
</div>
);
}
diff --git a/src/client/views/presentationview/PresentationModeMenu.scss b/src/client/views/presentationview/PresentationModeMenu.scss
new file mode 100644
index 000000000..336f43d20
--- /dev/null
+++ b/src/client/views/presentationview/PresentationModeMenu.scss
@@ -0,0 +1,30 @@
+.presMenu-cont {
+ position: fixed;
+ 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;
+
+ .presMenu-button {
+ background-color: transparent;
+ width: 35px;
+ height: 35px;
+ }
+
+ .presMenu-button:hover {
+ background-color: #121212;
+ }
+
+ .presMenu-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;
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/presentationview/PresentationModeMenu.tsx b/src/client/views/presentationview/PresentationModeMenu.tsx
new file mode 100644
index 000000000..4de8da587
--- /dev/null
+++ b/src/client/views/presentationview/PresentationModeMenu.tsx
@@ -0,0 +1,100 @@
+import React = require("react");
+import { observable, action, runInAction } from "mobx";
+import "./PresentationModeMenu.scss";
+import { observer } from "mobx-react";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+
+export interface PresModeMenuProps {
+ next: () => void;
+ back: () => void;
+ presStatus: boolean;
+ startOrResetPres: () => void;
+ closePresMode: () => void;
+}
+
+/**
+ * This class is responsible for modeling of the Presentation Mode Menu. The menu allows
+ * user to navigate through presentation elements, and start/stop the presentation.
+ */
+@observer
+export default class PresModeMenu extends React.Component<PresModeMenuProps> {
+
+ @observable private _top: number = 20;
+ @observable private _right: number = 0;
+ @observable private _opacity: number = 1;
+ @observable private _transition: string = "opacity 0.5s";
+ @observable private _transitionDelay: string = "";
+
+
+ private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
+
+ /**
+ * The function that changes the coordinates of the menu, depending on the
+ * movement of the mouse when it's being dragged.
+ */
+ @action
+ dragging = (e: PointerEvent) => {
+ this._right -= e.movementX;
+ this._top += e.movementY;
+
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ /**
+ * The function that removes the event listeners that are responsible for
+ * dragging of the menu.
+ */
+ dragEnd = (e: PointerEvent) => {
+ document.removeEventListener("pointermove", this.dragging);
+ document.removeEventListener("pointerup", this.dragEnd);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ /**
+ * The function that starts the dragging of the presentation mode menu. When
+ * the lines on further right are clicked on.
+ */
+ dragStart = (e: React.PointerEvent) => {
+ document.removeEventListener("pointermove", this.dragging);
+ document.addEventListener("pointermove", this.dragging);
+ document.removeEventListener("pointerup", this.dragEnd);
+ document.addEventListener("pointerup", this.dragEnd);
+
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ /**
+ * The function that is responsible for rendering the play or pause button, depending on the
+ * status of the presentation.
+ */
+ renderPlayPauseButton = () => {
+ if (this.props.presStatus) {
+ return <button title="Reset Presentation" className="presMenu-button" onClick={this.props.startOrResetPres}><FontAwesomeIcon icon="stop" /></button>;
+ } else {
+ return <button title="Start Presentation From Start" className="presMenu-button" onClick={this.props.startOrResetPres}><FontAwesomeIcon icon="play" /></button>;
+ }
+ }
+
+ render() {
+ return (
+ <div className="presMenu-cont" ref={this._mainCont}
+ style={{ right: this._right, top: this._top, opacity: this._opacity, transition: this._transition, transitionDelay: this._transitionDelay }}>
+ <button title="Back" className="presMenu-button" onClick={this.props.back}><FontAwesomeIcon icon={"arrow-left"} /></button>
+ {this.renderPlayPauseButton()}
+ <button title="Next" className="presMenu-button" onClick={this.props.next}><FontAwesomeIcon icon={"arrow-right"} /></button>
+ <button className="presMenu-button" title="Close Presentation Menu" onClick={this.props.closePresMode}>
+ <FontAwesomeIcon icon="times" size="lg" />
+ </button>
+ <div className="presMenu-dragger" onPointerDown={this.dragStart} style={{ width: "20px" }} />
+ </div >
+ );
+ }
+
+
+
+
+} \ No newline at end of file
diff --git a/src/client/views/presentationview/PresentationView.scss b/src/client/views/presentationview/PresentationView.scss
index a35a5849b..65b09c833 100644
--- a/src/client/views/presentationview/PresentationView.scss
+++ b/src/client/views/presentationview/PresentationView.scss
@@ -1,11 +1,13 @@
.presentationView-cont {
position: absolute;
background: white;
- z-index: 1;
+ z-index: 2;
box-shadow: #AAAAAA .2vw .2vw .4vw;
right: 0;
top: 0;
bottom: 0;
+ letter-spacing: 2px;
+
}
.presentationView-item {
@@ -19,6 +21,23 @@
-ms-user-select: none;
user-select: none;
transition: all .1s;
+
+
+
+ .documentView-node {
+
+ position: absolute;
+ z-index: 1;
+ }
+
+}
+
+.presentationView-item-above {
+ border-top: black 2px solid;
+}
+
+.presentationView-item-below {
+ border-bottom: black 2px solid;
}
.presentationView-listCont {
@@ -33,10 +52,11 @@
.presentationView-selected {
background: gray;
+ color: black;
}
.presentationView-heading {
- background: lightseagreen;
+ background: gray;
padding: 10px;
display: inline-block;
width: 100%;
@@ -47,7 +67,9 @@
padding-bottom: 3px;
font-size: 25px;
display: inline-block;
- width: calc(100% - 160px);
+ width: calc(100% - 200px);
+ letter-spacing: 2px;
+
}
.presentation-icon {
diff --git a/src/client/views/presentationview/PresentationView.tsx b/src/client/views/presentationview/PresentationView.tsx
index edbbeb8f9..bea70f00b 100644
--- a/src/client/views/presentationview/PresentationView.tsx
+++ b/src/client/views/presentationview/PresentationView.tsx
@@ -1,10 +1,10 @@
import { observer } from "mobx-react";
import React = require("react");
-import { observable, action, runInAction, reaction } from "mobx";
+import { observable, action, runInAction, reaction, autorun } from "mobx";
import "./PresentationView.scss";
import { DocumentManager } from "../../util/DocumentManager";
import { Utils } from "../../../Utils";
-import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc";
+import { Doc, DocListCast, DocListCastAsync, WidthSym } from "../../../new_fields/Doc";
import { listSpec } from "../../../new_fields/Schema";
import { Cast, NumCast, FieldValue, PromiseValue, StrCast, BoolCast } from "../../../new_fields/Types";
import { Id } from "../../../new_fields/FieldSymbols";
@@ -12,10 +12,12 @@ import { List } from "../../../new_fields/List";
import PresentationElement, { buttonIndex } from "./PresentationElement";
import { library } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { faArrowRight, faArrowLeft, faPlay, faStop, faPlus, faTimes, faMinus, faEdit } from '@fortawesome/free-solid-svg-icons';
+import { faArrowRight, faArrowLeft, faPlay, faStop, faPlus, faTimes, faMinus, faEdit, faEye } from '@fortawesome/free-solid-svg-icons';
import { Docs } from "../../documents/Documents";
import { undoBatch, UndoManager } from "../../util/UndoManager";
import PresentationViewList from "./PresentationList";
+import PresModeMenu from "./PresentationModeMenu";
+import { CollectionDockingView } from "../collections/CollectionDockingView";
library.add(faArrowLeft);
library.add(faArrowRight);
@@ -25,12 +27,16 @@ library.add(faPlus);
library.add(faTimes);
library.add(faMinus);
library.add(faEdit);
+library.add(faEye);
export interface PresViewProps {
Documents: List<Doc>;
}
+const expandedWidth = 400;
+const presMinWidth = 300;
+
@observer
export class PresentationView extends React.Component<PresViewProps> {
public static Instance: PresentationView;
@@ -60,6 +66,12 @@ export class PresentationView extends React.Component<PresViewProps> {
//Variable that holds reference to title input, so that new presentations get titles assigned.
@observable titleInputElement: HTMLInputElement | undefined;
@observable PresTitleChangeOpen: boolean = false;
+ @observable presMode: boolean = false;
+
+
+ @observable opacity = 1;
+ @observable persistOpacity = true;
+ @observable labelOpacity = 0;
//initilize class variables
constructor(props: PresViewProps) {
@@ -67,8 +79,18 @@ export class PresentationView extends React.Component<PresViewProps> {
PresentationView.Instance = this;
}
+ @action
+ toggle = (forcedValue: boolean | undefined) => {
+ if (forcedValue !== undefined) {
+ this.curPresentation.width = forcedValue ? expandedWidth : 0;
+ } else {
+ this.curPresentation.width = this.curPresentation.width === expandedWidth ? 0 : expandedWidth;
+ }
+ }
+
//The first lifecycle function that gets called to set up the current presentation.
async componentWillMount() {
+
this.props.Documents.forEach(async (doc, index: number) => {
//For each presentation received from mainContainer, a mapping is created.
@@ -156,7 +178,7 @@ export class PresentationView extends React.Component<PresViewProps> {
//storing the presentation status,ie. whether it was stopped or playing
- let presStatusBackUp = BoolCast(this.curPresentation.presStatus, null);
+ let presStatusBackUp = BoolCast(this.curPresentation.presStatus);
runInAction(() => this.presStatus = presStatusBackUp);
}
@@ -231,6 +253,7 @@ export class PresentationView extends React.Component<PresViewProps> {
//checking if any of the group members had used zooming in
currentsArray.forEach((doc: Doc) => {
+ //let presElem: PresentationElement | undefined = this.presElementsMappings.get(doc);
if (this.presElementsMappings.get(doc)!.selected[buttonIndex.Show]) {
zoomOut = true;
return;
@@ -345,11 +368,16 @@ export class PresentationView extends React.Component<PresViewProps> {
//checking if curDoc has navigation open
let curDocButtons = this.presElementsMappings.get(curDoc)!.selected;
if (curDocButtons[buttonIndex.Navigate]) {
- DocumentManager.Instance.jumpToDocument(curDoc, false);
+ this.jumpToTabOrRight(curDocButtons, curDoc);
} else if (curDocButtons[buttonIndex.Show]) {
let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[fromDoc]);
- //awaiting jump so that new scale can be found, since jumping is async
- await DocumentManager.Instance.jumpToDocument(curDoc, true);
+ if (curDocButtons[buttonIndex.OpenRight]) {
+ //awaiting jump so that new scale can be found, since jumping is async
+ await DocumentManager.Instance.jumpToDocument(curDoc, true);
+ } else {
+ await DocumentManager.Instance.jumpToDocument(curDoc, true, undefined, doc => CollectionDockingView.Instance.AddTab(undefined, doc, undefined));
+ }
+
let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc);
curDoc.viewScale = newScale;
@@ -362,9 +390,15 @@ export class PresentationView extends React.Component<PresViewProps> {
return;
}
let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[fromDoc]);
+ let curDocButtons = this.presElementsMappings.get(docToJump)!.selected;
+
- //awaiting jump so that new scale can be found, since jumping is async
- await DocumentManager.Instance.jumpToDocument(docToJump, willZoom);
+ if (curDocButtons[buttonIndex.OpenRight]) {
+ //awaiting jump so that new scale can be found, since jumping is async
+ await DocumentManager.Instance.jumpToDocument(docToJump, willZoom);
+ } else {
+ await DocumentManager.Instance.jumpToDocument(docToJump, willZoom, undefined, doc => CollectionDockingView.Instance.AddTab(undefined, doc, undefined));
+ }
let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc);
curDoc.viewScale = newScale;
//saving the scale that user was on
@@ -375,6 +409,18 @@ export class PresentationView extends React.Component<PresViewProps> {
}
/**
+ * This function checks if right option is clicked on a presentation element, if not it does open it as a tab
+ * with help of CollectionDockingView.
+ */
+ jumpToTabOrRight = (curDocButtons: boolean[], curDoc: Doc) => {
+ if (curDocButtons[buttonIndex.OpenRight]) {
+ DocumentManager.Instance.jumpToDocument(curDoc, false);
+ } else {
+ DocumentManager.Instance.jumpToDocument(curDoc, false, undefined, doc => CollectionDockingView.Instance.AddTab(undefined, doc, undefined));
+ }
+ }
+
+ /**
* Async function that supposedly return the doc that is located at given index.
*/
getDocAtIndex = async (index: number) => {
@@ -418,10 +464,18 @@ export class PresentationView extends React.Component<PresViewProps> {
}
}
- //removing it from the backUp of selected Buttons
+
let castedList = Cast(this.presButtonBackUp.selectedButtonDocs, listSpec(Doc));
if (castedList) {
- castedList.splice(index, 1);
+ for (let doc of castedList) {
+ let curDoc = await doc;
+ let curDocId = StrCast(curDoc.docId);
+ if (curDocId === removedDoc[Id]) {
+ castedList.splice(castedList.indexOf(curDoc), 1);
+ break;
+
+ }
+ }
}
//removing it from the backup of groups
@@ -447,6 +501,22 @@ export class PresentationView extends React.Component<PresViewProps> {
}
}
+ /**
+ * An alternative remove method that removes a doc from presentation by its actual
+ * reference.
+ */
+ public removeDocByRef = (doc: Doc) => {
+ let indexOfDoc = this.childrenDocs.indexOf(doc);
+ const value = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
+ if (value) {
+ value.splice(indexOfDoc, 1)[0];
+ }
+ if (indexOfDoc !== - 1) {
+ return true;
+ }
+ return false;
+ }
+
//The function that is called when a document is clicked or reached through next or back.
//it'll also execute the necessary actions if presentation is playing.
@action
@@ -505,7 +575,7 @@ export class PresentationView extends React.Component<PresViewProps> {
this.curPresentation.data = new List([doc]);
}
- this.curPresentation.width = 400;
+ this.toggle(true);
}
//Function that sets the store of the children docs.
@@ -526,18 +596,40 @@ export class PresentationView extends React.Component<PresViewProps> {
//The function that starts or resets presentaton functionally, depending on status flag.
@action
- startOrResetPres = () => {
+ startOrResetPres = async () => {
if (this.presStatus) {
this.resetPresentation();
} else {
this.presStatus = true;
- this.startPresentation(0);
+ let startIndex = await this.findStartDocument();
+ this.startPresentation(startIndex);
const current = NumCast(this.curPresentation.selectedDoc);
- this.gotoDocument(0, current);
+ this.gotoDocument(startIndex, current);
}
this.curPresentation.presStatus = this.presStatus;
}
+ /**
+ * This method is called to find the start document of presentation. So
+ * that when user presses on play, the correct presentation element will be
+ * selected.
+ */
+ findStartDocument = async () => {
+ let docAtZero = await this.getDocAtIndex(0);
+ if (docAtZero === undefined) {
+ return 0;
+ }
+ let docAtZeroPresId = StrCast(docAtZero.presentId);
+
+ if (this.groupMappings.has(docAtZeroPresId)) {
+ let group = this.groupMappings.get(docAtZeroPresId)!;
+ let lastDoc = group[group.length - 1];
+ return this.childrenDocs.indexOf(lastDoc);
+ } else {
+ return 0;
+ }
+ }
+
//The function that resets the presentation by removing every action done by it. It also
//stops the presentaton.
@action
@@ -752,42 +844,150 @@ export class PresentationView extends React.Component<PresViewProps> {
this.curPresentation.title = newTitle;
}
+ /**
+ * On pointer down element that is catched on resizer of te
+ * presentation view. Sets up the event listeners to change the size with
+ * mouse move.
+ */
+ _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();
+ }
+ /**
+ * Changes the size of the presentation view, with mouse move.
+ * Minimum size is set to 300, so that every button is visible.
+ */
+ @action
+ onPointerMove = (e: PointerEvent) => {
+
+ this.curPresentation.width = Math.max(window.innerWidth - e.clientX, presMinWidth);
+ }
+
+ /**
+ * The method that is called on pointer up event. It checks if the button is just
+ * clicked so that presentation view will be closed. The way it's done is to check
+ * for minimal pixel change like 4, and accept it as it's just a click on top of the dragger.
+ */
+ @action
+ onPointerUp = (e: PointerEvent) => {
+ if (Math.abs(e.clientX - this._downsize) < 4) {
+ let presWidth = NumCast(this.curPresentation.width);
+ if (presWidth - presMinWidth !== 0) {
+ this.curPresentation.width = 0;
+ }
+ if (presWidth === 0) {
+ this.curPresentation.width = presMinWidth;
+ }
+ }
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ }
+
+ /**
+ * This function is a setter that opens up the
+ * presentation mode, by setting it's render flag
+ * to true. It also closes the presentation view.
+ */
+ @action
+ openPresMode = () => {
+ if (!this.presMode) {
+ this.curPresentation.width = 0;
+ this.presMode = true;
+ }
+ }
+
+ /**
+ * This function closes the presentation mode by setting its
+ * render flag to false. It also opens up the presentation view.
+ * By setting it to it's minimum size.
+ */
+ @action
+ closePresMode = () => {
+ if (this.presMode) {
+ this.presMode = false;
+ this.curPresentation.width = presMinWidth;
+ }
+
+ }
+
+ /**
+ * Function that is called to render the presentation mode, depending on its flag.
+ */
+ renderPresMode = () => {
+ if (this.presMode) {
+ return <PresModeMenu next={this.next} back={this.back} startOrResetPres={this.startOrResetPres} presStatus={this.presStatus} closePresMode={this.closePresMode} />;
+ } else {
+ return (null);
+ }
+
+ }
render() {
let width = NumCast(this.curPresentation.width);
return (
- <div className="presentationView-cont" style={{ width: width, overflow: "hidden" }}>
- <div className="presentationView-heading">
- {this.renderSelectOrPresSelection()}
- <button title="Close Presentation" className='presentation-icon' onClick={this.closePresentation}><FontAwesomeIcon icon={"times"} /></button>
- <button title="Add Presentation" className="presentation-icon" style={{ marginRight: 10 }} onClick={() => {
- runInAction(() => { if (this.PresTitleChangeOpen) { this.PresTitleChangeOpen = false; } });
- runInAction(() => this.PresTitleInputOpen ? this.PresTitleInputOpen = false : this.PresTitleInputOpen = true);
- }}><FontAwesomeIcon icon={"plus"} /></button>
- <button title="Remove Presentation" className='presentation-icon' style={{ marginRight: 10 }} onClick={this.removePresentation}><FontAwesomeIcon icon={"minus"} /></button>
- <button title="Change Presentation Title" className="presentation-icon" style={{ marginRight: 10 }} onClick={() => {
- runInAction(() => { if (this.PresTitleInputOpen) { this.PresTitleInputOpen = false; } });
- runInAction(() => this.PresTitleChangeOpen ? this.PresTitleChangeOpen = false : this.PresTitleChangeOpen = true);
- }}><FontAwesomeIcon icon={"edit"} /></button>
+ <div>
+ <div className="presentationView-cont" onPointerEnter={action(() => !this.persistOpacity && (this.opacity = 1))} onPointerLeave={action(() => !this.persistOpacity && (this.opacity = 0.4))} style={{ width: width, overflowY: "scroll", overflowX: "hidden", opacity: this.opacity, transition: "0.7s opacity ease" }}>
+ <div className="presentationView-heading">
+ {this.renderSelectOrPresSelection()}
+ <button title="Close Presentation" className='presentation-icon' onClick={this.closePresentation}><FontAwesomeIcon icon={"times"} /></button>
+ <button title="Open Presentation Mode" className="presentation-icon" style={{ marginRight: 10 }} onClick={this.openPresMode}><FontAwesomeIcon icon={"eye"} /></button>
+ <button title="Add Presentation" className="presentation-icon" style={{ marginRight: 10 }} onClick={() => {
+ runInAction(() => { if (this.PresTitleChangeOpen) { this.PresTitleChangeOpen = false; } });
+ runInAction(() => this.PresTitleInputOpen ? this.PresTitleInputOpen = false : this.PresTitleInputOpen = true);
+ }}><FontAwesomeIcon icon={"plus"} /></button>
+ <button title="Remove Presentation" className='presentation-icon' style={{ marginRight: 10 }} onClick={this.removePresentation}><FontAwesomeIcon icon={"minus"} /></button>
+ <button title="Change Presentation Title" className="presentation-icon" style={{ marginRight: 10 }} onClick={() => {
+ runInAction(() => { if (this.PresTitleInputOpen) { this.PresTitleInputOpen = false; } });
+ runInAction(() => this.PresTitleChangeOpen ? this.PresTitleChangeOpen = false : this.PresTitleChangeOpen = true);
+ }}><FontAwesomeIcon icon={"edit"} /></button>
+ </div>
+ <div className="presentation-buttons">
+ <button title="Back" className="presentation-button" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></button>
+ {this.renderPlayPauseButton()}
+ <button title="Next" className="presentation-button" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></button>
+ </div>
+
+ <PresentationViewList
+ mainDocument={this.curPresentation}
+ deleteDocument={this.RemoveDoc}
+ gotoDocument={this.gotoDocument}
+ groupMappings={this.groupMappings}
+ PresElementsMappings={this.presElementsMappings}
+ setChildrenDocs={this.setChildrenDocs}
+ presStatus={this.presStatus}
+ presButtonBackUp={this.presButtonBackUp}
+ presGroupBackUp={this.presGroupBackUp}
+ removeDocByRef={this.removeDocByRef}
+ clearElemMap={() => this.presElementsMappings.clear()}
+ />
+ <input
+ type="checkbox"
+ onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
+ this.persistOpacity = e.target.checked;
+ this.opacity = this.persistOpacity ? 1 : 0.4;
+ })}
+ checked={this.persistOpacity}
+ style={{ position: "absolute", bottom: 5, left: 5 }}
+ onPointerEnter={action(() => this.labelOpacity = 1)}
+ onPointerLeave={action(() => this.labelOpacity = 0)}
+ />
+ <p style={{ position: "absolute", bottom: 1, left: 22, opacity: this.labelOpacity, transition: "0.7s opacity ease" }}>opacity {this.persistOpacity ? "persistent" : "on focus"}</p>
</div>
- <div className="presentation-buttons">
- <button title="Back" className="presentation-button" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></button>
- {this.renderPlayPauseButton()}
- <button title="Next" className="presentation-button" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></button>
+ <div className="mainView-libraryHandle"
+ style={{ cursor: "ew-resize", right: `${width - 10}px`, backgroundColor: "white", opacity: this.opacity, transition: "0.7s opacity ease" }}
+ onPointerDown={this.onPointerDown}>
+ <span title="library View Dragger" style={{ width: "100%", height: "100%", position: "absolute" }} />
</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}
- />
+ {this.renderPresMode()}
+
</div>
);
}
diff --git a/src/client/views/search/CheckBox.scss b/src/client/views/search/CheckBox.scss
index af59d5fbf..cc858bec6 100644
--- a/src/client/views/search/CheckBox.scss
+++ b/src/client/views/search/CheckBox.scss
@@ -13,9 +13,9 @@
margin-top: 0px;
.check-container:hover~.check-box {
- background-color: $intermediate-color;
+ background-color: $darker-alt-accent;
}
-
+
.check-container {
width: 40px;
height: 40px;
@@ -27,7 +27,8 @@
position: absolute;
fill-opacity: 0;
stroke-width: 4px;
- stroke: white;
+ // stroke: white;
+ stroke: gray;
}
}
@@ -55,5 +56,4 @@
margin-left: 15px;
}
-}
-
+} \ No newline at end of file
diff --git a/src/client/views/search/FieldFilters.scss b/src/client/views/search/FieldFilters.scss
index ba0926140..e1d0d8df5 100644
--- a/src/client/views/search/FieldFilters.scss
+++ b/src/client/views/search/FieldFilters.scss
@@ -1,5 +1,12 @@
.field-filters {
width: 100%;
display: grid;
- grid-template-columns: 18% 20% 60%;
+ // grid-template-columns: 18% 20% 60%;
+ grid-template-columns: 20% 25% 60%;
+}
+
+.field-filters-required {
+ width: 100%;
+ display: grid;
+ grid-template-columns: 50% 50%;
} \ No newline at end of file
diff --git a/src/client/views/search/FilterBox.scss b/src/client/views/search/FilterBox.scss
index 1eb8963d7..ebb39460d 100644
--- a/src/client/views/search/FilterBox.scss
+++ b/src/client/views/search/FilterBox.scss
@@ -3,22 +3,25 @@
.filter-form {
padding: 25px;
- width: 600px;
- background: $dark-color;
+ width: 440px;
+ background: whitesmoke;
position: relative;
right: 1px;
- color: $light-color;
+ color: grey;
flex-direction: column;
display: inline-block;
transform-origin: top;
overflow: auto;
+ border-radius: 15px;
+ box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw;
+ border: solid #BBBBBBBB 1px;
.top-filter-header {
#header {
text-transform: uppercase;
letter-spacing: 2px;
- font-size: 25;
+ font-size: 13;
width: 80%;
}
@@ -26,13 +29,13 @@
width: 20%;
opacity: .6;
position: relative;
- display: inline-block;
+ display: block;
.line {
display: block;
background: $alt-accent;
- width: $width-line;
- height: $height-line;
+ width: 20;
+ height: 3;
position: absolute;
right: 0;
border-radius: ($height-line / 2);
@@ -69,9 +72,10 @@
display: flex;
align-items: center;
margin-bottom: 10px;
+ letter-spacing: 2px;
.filter-title {
- font-size: 18;
+ font-size: 13;
text-transform: uppercase;
margin-top: 10px;
margin-bottom: 10px;
@@ -96,6 +100,7 @@
-moz-transition: all 0.2s ease-in-out;
-o-transition: all 0.2s ease-in-out;
transition: all 0.2s ease-in-out;
+ text-align: center;
}
}
}
@@ -105,4 +110,72 @@
border-top-style: solid;
padding-top: 10px;
}
+}
+
+.active-filters {
+ display: flex;
+ flex-direction: row-reverse;
+ justify-content: flex-end;
+ width: 100%;
+ margin-right: 30px;
+ position: relative;
+
+ .active-icon {
+ max-width: 40px;
+ flex: initial;
+
+ &.icon{
+ width: 40px;
+ text-align: center;
+ margin-bottom: 5px;
+ position: absolute;
+ }
+
+ &.container {
+ display: flex;
+ flex-direction: column;
+ width: 40px;
+ }
+
+ &.description {
+ text-align: center;
+ top: 40px;
+ position: absolute;
+ width: 40px;
+ font-size: 9px;
+ 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;
+ }
+
+ &.icon:hover + .description {
+ opacity: 1;
+ }
+ }
+
+ .col-icon {
+ height: 35px;
+ margin-left: 5px;
+ width: 35px;
+ background-color: black;
+ color: white;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ .save-filter,
+ .reset-filter,
+ .all-filter {
+ background-color: gray;
+ }
+
+ .save-filter:hover,
+ .reset-filter:hover,
+ .all-filter:hover {
+ background-color: $darker-alt-accent;
+ }
+ }
} \ No newline at end of file
diff --git a/src/client/views/search/FilterBox.tsx b/src/client/views/search/FilterBox.tsx
index 706d1eb7f..3e8582d61 100644
--- a/src/client/views/search/FilterBox.tsx
+++ b/src/client/views/search/FilterBox.tsx
@@ -2,25 +2,26 @@ 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 { faTimes, faCheckCircle, faObjectGroup } 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';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
library.add(faTimes);
+library.add(faCheckCircle);
+library.add(faObjectGroup);
export enum Keys {
TITLE = "title",
@@ -35,11 +36,18 @@ export class FilterBox extends React.Component {
public _allIcons: string[] = [DocumentType.AUDIO, DocumentType.COL, DocumentType.HIST, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.TEXT, DocumentType.VID, DocumentType.WEB];
//if true, any keywords can be used. if false, all keywords are required.
+ //this also serves as an indicator if the word status filter is applied
@observable private _basicWordStatus: boolean = true;
@observable private _filterOpen: boolean = false;
+ //if icons = all icons, then no icon filter is applied
@observable private _icons: string[] = this._allIcons;
+ //if all of these are true, no key filter is applied
+ @observable private _anyKeywordStatus: boolean = true;
+ @observable private _allKeywordStatus: boolean = true;
@observable private _titleFieldStatus: boolean = true;
@observable private _authorFieldStatus: boolean = true;
+ @observable private _dataFieldStatus: boolean = true;
+ //this also serves as an indicator if the collection status filter is applied
@observable public _deletedDocsStatus: boolean = false;
@observable private _collectionStatus = false;
@observable private _collectionSelfStatus = true;
@@ -114,10 +122,9 @@ export class FilterBox extends React.Component {
@action.bound
resetFilters = () => {
- ToggleBar.Instance.resetToggle();
+ this._basicWordStatus = true;
IconBar.Instance.selectAll();
FieldFilters.Instance.resetFieldFilters();
- CollectionFilters.Instance.resetCollectionFilters();
}
basicRequireWords(query: string): string {
@@ -259,6 +266,40 @@ export class FilterBox extends React.Component {
return finalDocs;
}
+ getABCicon() {
+ return (
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.8 87.8" height="35">
+ <path d="M25.4 47.9c-1.3 1.3-1.9 2.8-1.9 4.8 0 3.8 2.3 6.1 6.1 6.1 5.1 0 8-3.3 9-6.2 0.2-0.7 0.4-1.4 0.4-2.1v-6.1c-0.1 0-0.1 0-0.2 0C32.2 44.5 27.7 45.6 25.4 47.9z" />
+ <path d="M64.5 28.6c-2.2 0-4.1 1.5-4.7 3.8l0 0.2c-0.1 0.3-0.1 0.7-0.1 1.1v3.3c0 0.4 0.1 0.8 0.2 1.1 0.6 2.2 2.4 3.6 4.6 3.6 3.2 0 5.2-2.6 5.2-6.7C69.5 31.8 68 28.6 64.5 28.6z" />
+ <path d="M43.9 0C19.7 0 0 19.7 0 43.9s19.7 43.9 43.9 43.9 43.9-19.6 43.9-43.9S68.1 0 43.9 0zM40.1 65.5l-0.5-4c-3 3.1-7.4 4.9-12.1 4.9 -6.8 0-13.6-4.4-13.6-12.8 0-4 1.3-7.4 4-10 4.1-4.1 11.1-6.2 20.8-6.3 0-5.5-2.9-8.4-8.3-8.4 -3.6 0-7.4 1.1-10.2 2.9l-1.1 0.7 -2.4-6.9 0.7-0.4c3.7-2.4 8.9-3.8 14.1-3.8 10.9 0 16.7 6.2 16.7 17.9V54.6c0 4.1 0.2 7.2 0.7 9.7L49 65.5H40.1zM65.5 67.5c1.8 0 3-0.5 4-0.9l0.5-0.2 0.8 3.4 -0.3 0.2c-1 0.5-3 1.1-5.5 1.1 -5.8 0-9.7-4-9.7-9.9 0-6.1 4.3-10.3 10.4-10.3 2.1 0 4 0.5 4.9 1l0.3 0.2 -1 3.5 -0.5-0.3c-0.7-0.4-1.8-0.8-3.7-0.8 -3.7 0-6.1 2.6-6.1 6.6C59.5 64.8 61.9 67.5 65.5 67.5zM65 45.3c-2.5 0-4.5-0.9-5.9-2.7l-0.1 2.3h-3.8l0-0.5c0.1-1.2 0.2-3.1 0.2-4.8V16.7h4.3v10.8c1.4-1.6 3.5-2.5 6-2.5 2.2 0 4.1 0.8 5.5 2.3 1.8 1.8 2.8 4.5 2.8 7.7C73.8 42.1 69.3 45.3 65 45.3z" />
+ </svg>
+ );
+ }
+
+ getTypeIcon() {
+ return (
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.8 87.8" height="35">
+ <path d="M43.9 0C19.7 0 0 19.7 0 43.9s19.7 43.9 43.9 43.9 43.9-19.6 43.9-43.9S68.1 0 43.9 0zM43.9 12.2c4.1 0 7.5 3.4 7.5 7.5 0 4.1-3.4 7.5-7.5 7.5 -4.1 0-7.5-3.4-7.5-7.5C36.4 15.5 39.7 12.2 43.9 12.2zM11.9 50.4l7.5-13 7.5 13H11.9zM47.6 75.7h-7.5l-3.7-6.5 3.8-6.5h7.5l3.8 6.5L47.6 75.7zM70.7 70.7c-0.2 0.2-0.4 0.3-0.7 0.3s-0.5-0.1-0.7-0.3l-25.4-25.4 -25.4 25.4c-0.2 0.2-0.4 0.3-0.7 0.3s-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1 0-1.4l25.4-25.4 -25.4-25.4c-0.4-0.4-0.4-1 0-1.4s1-0.4 1.4 0l25.4 25.4 25.4-25.4c0.4-0.4 1-0.4 1.4 0s0.4 1 0 1.4l-25.4 25.4 25.4 25.4C71.1 69.7 71.1 70.3 70.7 70.7zM61.4 51.4v-15h15v15H61.4z" />
+ </svg>
+ );
+ }
+
+ getKeyIcon() {
+ return (
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.8 87.8" height="35">
+ <path d="M38.5 32.4c0 3.4-2.7 6.1-6.1 6.1 -3.4 0-6.1-2.7-6.1-6.1 0-3.4 2.8-6.1 6.1-6.1C35.8 26.3 38.5 29 38.5 32.4zM87.8 43.9c0 24.2-19.6 43.9-43.9 43.9S0 68.1 0 43.9C0 19.7 19.7 0 43.9 0S87.8 19.7 87.8 43.9zM66.8 60.3L50.2 43.7c-0.5-0.5-0.6-1.2-0.4-1.8 2.4-5.6 1.1-12.1-3.2-16.5 -5.9-5.8-15.4-5.8-21.2 0l0 0c-4.3 4.3-5.6 10.8-3.2 16.5 3.2 7.6 12 11.2 19.7 8 0.6-0.3 1.4-0.1 1.8 0.4l3.1 3.1h3.9c1.2 0 2.2 1 2.2 2.2v3.6h3.6c1.2 0 2.2 1 2.2 2.2v4l1.6 1.6h6.5V60.3z" />
+ </svg>
+ );
+ }
+
+ getColIcon() {
+ return (
+ <div className="col-icon">
+ <FontAwesomeIcon icon={faObjectGroup} size="lg" />
+ </div>
+ );
+ }
+
@action.bound
openFilter = () => {
this._filterOpen = !this._filterOpen;
@@ -268,10 +309,9 @@ export class FilterBox extends React.Component {
//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; }
+ handleWordQueryChange = () => {
+ this._basicWordStatus = !this._basicWordStatus;
+ }
@action.bound
updateIcon(newArray: string[]) { this._icons = newArray; }
@@ -290,16 +330,10 @@ export class FilterBox extends React.Component {
}
@action.bound
- toggleFieldOpen() { this._fieldOpen = !this._fieldOpen; }
-
- @action.bound
- toggleColOpen() { this._colOpen = !this._colOpen; }
-
- @action.bound
- toggleTypeOpen() { this._typeOpen = !this._typeOpen; }
+ updateAnyKeywordStatus(newStat: boolean) { this._anyKeywordStatus = newStat; }
@action.bound
- toggleWordStatusOpen() { this._wordStatusOpen = !this._wordStatusOpen; }
+ updateAllKeywordStatus(newStat: boolean) { this._allKeywordStatus = newStat; }
@action.bound
updateTitleStatus(newStat: boolean) { this._titleFieldStatus = newStat; }
@@ -319,6 +353,8 @@ export class FilterBox extends React.Component {
@action.bound
updateParentCollectionStatus(newStat: boolean) { this._collectionParentStatus = newStat; }
+ getAnyKeywordStatus() { return this._anyKeywordStatus; }
+ getAllKeywordStatus() { return this._allKeywordStatus; }
getCollectionStatus() { return this._collectionStatus; }
getSelfCollectionStatus() { return this._collectionSelfStatus; }
getParentCollectionStatus() { return this._collectionParentStatus; }
@@ -326,6 +362,31 @@ export class FilterBox extends React.Component {
getAuthorStatus() { return this._authorFieldStatus; }
getDataStatus() { return this._deletedDocsStatus; }
+ getActiveFilters() {
+ console.log(this._authorFieldStatus, this._titleFieldStatus, this._dataFieldStatus);
+ return (
+ <div className="active-filters">
+ {!this._basicWordStatus ? <div className="active-icon container">
+ <div className="active-icon icon">{this.getABCicon()}</div>
+ <div className="active-icon description">Required Words Applied</div>
+ </div> : undefined}
+ {!(this._icons.length === 9) ? <div className="active-icon container">
+ <div className="active-icon icon">{this.getTypeIcon()}</div>
+ <div className="active-icon description">Type Filters Applied</div>
+ </div> : undefined}
+ {!(this._authorFieldStatus && this._dataFieldStatus && this._titleFieldStatus) ?
+ <div className="active-icon container">
+ <div className="active-icon icon">{this.getKeyIcon()}</div>
+ <div className="active-icon description">Field Filters Applied</div>
+ </div> : undefined}
+ {this._collectionStatus ? <div className="active-icon container">
+ <div className="active-icon icon">{this.getColIcon()}</div>
+ <div className="active-icon description">Collection Filters Active</div>
+ </div> : undefined}
+ </div>
+ );
+ }
+
// 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
@@ -334,11 +395,13 @@ export class FilterBox extends React.Component {
<div>
<div style={{ display: "flex", flexDirection: "row-reverse" }}>
<SearchBox />
+ {this.getActiveFilters()}
</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 style={{ marginLeft: "auto" }}></div>
<div className="close-icon" onClick={this.closeFilter}>
<span className="line line-1"></span>
<span className="line line-2"></span></div>
@@ -347,33 +410,20 @@ export class FilterBox extends React.Component {
<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"} />
+ <button className="all-filter" onClick={this.handleWordQueryChange}>Include All Keywords</button>
</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}
@@ -381,13 +431,12 @@ export class FilterBox extends React.Component {
</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}
+ ) :
+ undefined}
</div>
);
}
diff --git a/src/client/views/search/IconBar.scss b/src/client/views/search/IconBar.scss
index e384722ce..2555ad271 100644
--- a/src/client/views/search/IconBar.scss
+++ b/src/client/views/search/IconBar.scss
@@ -4,9 +4,8 @@
display: flex;
justify-content: space-evenly;
align-items: center;
- height: 40px;
+ height: 35px;
width: 100%;
flex-wrap: wrap;
margin-bottom: 10px;
-}
-
+} \ No newline at end of file
diff --git a/src/client/views/search/IconButton.scss b/src/client/views/search/IconButton.scss
index 94b294ba5..d1853177e 100644
--- a/src/client/views/search/IconButton.scss
+++ b/src/client/views/search/IconButton.scss
@@ -4,13 +4,15 @@
display: flex;
flex-direction: column;
align-items: center;
- width: 45px;
+ width: 30px;
height: 60px;
.type-icon {
- height: 45px;
- width: 45px;
+ height: 30px;
+ width: 30px;
color: $light-color;
+ // background-color: rgb(194, 194, 197);
+ background-color: gray;
border-radius: 50%;
display: flex;
justify-content: center;
@@ -22,8 +24,8 @@
font-size: 2em;
.fontawesome-icon {
- height: 24px;
- width: 24px;
+ height: 15px;
+ width: 15px
}
}
@@ -44,7 +46,7 @@
transform: scale(1.1);
background-color: $darker-alt-accent;
opacity: 1;
-
+
+.filter-description {
opacity: 1;
}
diff --git a/src/client/views/search/IconButton.tsx b/src/client/views/search/IconButton.tsx
index bfe2c7d0b..5d23f6eeb 100644
--- a/src/client/views/search/IconButton.tsx
+++ b/src/client/views/search/IconButton.tsx
@@ -13,6 +13,7 @@ import { IconBar } from './IconBar';
import { props } from 'bluebird';
import { FilterBox } from './FilterBox';
import { Search } from '../../../server/Search';
+import { gravity } from 'sharp';
library.add(faSearch);
library.add(faObjectGroup);
@@ -123,11 +124,11 @@ export class IconButton extends React.Component<IconButtonProps>{
selected = {
opacity: 1,
- backgroundColor: "#c2c2c5" //$alt-accent
+ backgroundColor: "rgb(128, 128, 128)"
};
notSelected = {
- opacity: 0.6,
+ opacity: 0.2,
};
hoverStyle = {
diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss
index 481ee5789..5ed33a596 100644
--- a/src/client/views/search/SearchBox.scss
+++ b/src/client/views/search/SearchBox.scss
@@ -50,6 +50,11 @@
top: 300px;
display: flex;
flex-direction: column;
+ margin-right: 72px;
+ // height: 560px;
+ height: 100%;
+ // overflow: hidden;
+ // overflow-y: auto;
max-height: 560px;
overflow: hidden;
overflow-y: auto;
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 2e1017d27..4dc409e77 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -11,7 +11,6 @@ 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';
@@ -20,6 +19,7 @@ import { FilterBox } from './FilterBox';
import { ReadStream } from 'fs';
import * as $ from 'jquery';
import { MainView } from '../MainView';
+import { Utils } from '../../../Utils';
library.add(faTimes);
@@ -90,7 +90,7 @@ export class SearchBox extends React.Component {
public static async convertDataUri(imageUri: string, returnedFilename: string) {
try {
- let posting = DocServer.prepend(RouteStore.dataUriToImage);
+ let posting = Utils.prepend(RouteStore.dataUriToImage);
const returnedUri = await rp.post(posting, {
body: {
uri: imageUri,
@@ -161,7 +161,7 @@ export class SearchBox extends React.Component {
const highlighting = res.highlighting || {};
const highlightList = res.docs.map(doc => highlighting[doc[Id]]);
- const docs = await Promise.all(res.docs.map(doc => Cast(doc.extendsDoc, Doc, doc as any)));
+ const docs = await Promise.all(res.docs.map(async doc => (await Cast(doc.extendsDoc, Doc)) || doc));
const highlights: typeof res.highlighting = {};
docs.forEach((doc, index) => highlights[doc[Id]] = highlightList[index]);
let filteredDocs = FilterBox.Instance.filterDocsByType(docs);
@@ -170,7 +170,7 @@ export class SearchBox extends React.Component {
filteredDocs.forEach(doc => {
const index = this._resultsSet.get(doc);
const highlight = highlights[doc[Id]];
- const hlights = highlight ? Object.keys(highlight).map(key => key.substring(0, key.length - 2)) : []
+ const hlights = highlight ? Object.keys(highlight).map(key => key.substring(0, key.length - 2)) : [];
if (index === undefined) {
this._resultsSet.set(doc, this._results.length);
this._results.push([doc, hlights]);
diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx
index 26f00e03e..48eb87251 100644
--- a/src/client/views/search/SearchItem.tsx
+++ b/src/client/views/search/SearchItem.tsx
@@ -1,13 +1,13 @@
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 { faCaretUp, faChartBar, faFilePdf, faFilm, faGlobeAsia, faImage, faLink, faMusic, faObjectGroup, faStickyNote, faFingerprint } 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 { emptyFunction, returnFalse, returnOne, Utils, returnEmptyString } from "../../../Utils";
import { DocumentType } from "../../documents/Documents";
import { DocumentManager } from "../../util/DocumentManager";
import { SetupDrag, DragManager } from "../../util/DragManager";
@@ -21,11 +21,18 @@ import { DocumentView } from "../nodes/DocumentView";
import { SearchBox } from "./SearchBox";
import "./SearchItem.scss";
import "./SelectorContextMenu.scss";
+import { RichTextField } from "../../../new_fields/RichTextField";
+import { FormattedTextBox } from "../nodes/FormattedTextBox";
+import { MarqueeView } from "../collections/collectionFreeForm/MarqueeView";
+import { SelectionManager } from "../../util/SelectionManager";
+import { ObjectField } from "../../../new_fields/ObjectField";
import { ContextMenu } from "../ContextMenu";
import { faFile } from '@fortawesome/free-solid-svg-icons';
+import { DocServer } from "../../DocServer";
export interface SearchItemProps {
doc: Doc;
+ query?: string;
highlighting: string[];
}
@@ -38,7 +45,7 @@ library.add(faFilm);
library.add(faMusic);
library.add(faLink);
library.add(faChartBar);
-library.add(faGlobeAsia);
+library.add(faGlobeAsia, faFingerprint);
@observer
export class SelectorContextMenu extends React.Component<SearchItemProps> {
@@ -86,7 +93,7 @@ export class SelectorContextMenu extends React.Component<SearchItemProps> {
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>
+ <a onClick={this.getOnClick(doc)}>{doc.col.title}</a>
</div>;
})}
</div>
@@ -94,27 +101,115 @@ export class SelectorContextMenu extends React.Component<SearchItemProps> {
}
}
+export interface LinkMenuProps {
+ doc1: Doc;
+ doc2: Doc;
+}
+
+@observer
+export class LinkContextMenu extends React.Component<LinkMenuProps> {
+
+ highlightDoc = (doc: Doc) => {
+ return () => {
+ doc.libraryBrush = true;
+ };
+ }
+
+ unHighlightDoc = (doc: Doc) => {
+ return () => {
+ doc.libraryBrush = false;
+ };
+ }
+
+ getOnClick(col: Doc) {
+ return () => {
+ CollectionDockingView.Instance.AddRightSplit(col, undefined);
+ };
+ }
+
+ render() {
+ return (
+ <div className="parents">
+ <p className="contexts">Anchors:</p>
+ <div className="collection"><a onMouseEnter={this.highlightDoc(this.props.doc1)} onMouseLeave={this.unHighlightDoc(this.props.doc1)} onClick={this.getOnClick(this.props.doc1)}>Doc 1: {this.props.doc2.title}</a></div>
+ <div><a onMouseEnter={this.highlightDoc(this.props.doc2)} onMouseLeave={this.unHighlightDoc(this.props.doc2)} onClick={this.getOnClick(this.props.doc2)}>Doc 2: {this.props.doc1.title}</a></div>
+ </div>
+ );
+ }
+
+}
+
@observer
export class SearchItem extends React.Component<SearchItemProps> {
@observable _selected: boolean = false;
+ private _previewDoc?: Doc;
onClick = () => {
// I dont think this is the best functionality because clicking the name of the collection does that. Change it back if you'd like
- // DocumentManager.Instance.jumpToDocument(this.props.doc, false);
- CollectionDockingView.Instance.AddRightSplit(this.props.doc, undefined);
+ DocumentManager.Instance.jumpToDocument(this.props.doc, false);
+ if (this.props.doc.data instanceof RichTextField) {
+ this.highlightTextBox(this.props.doc);
+ }
+ // CollectionDockingView.Instance.AddRightSplit(this.props.doc, undefined);
}
@observable _useIcons = true;
@observable _displayDim = 50;
- @computed
- public get DocumentIcon() {
+ highlightTextBox = (doc: Doc) => {
+ if (this.props.query) {
+ const fieldkey = 'search_string';
+ if (Object.keys(doc).indexOf(fieldkey) === -1) {
+ doc.search_string = this.props.query;
+ }
+ else {
+ doc.search_string = undefined;
+ }
+
+ }
+ }
+
+ fitToBox = () => {
+ let bounds = Doc.ComputeContentBounds([this.props.doc]);
+ return [(bounds.x + bounds.r) / 2, (bounds.y + bounds.b) / 2, Number(SEARCH_THUMBNAIL_SIZE) / Math.max((bounds.b - bounds.y), (bounds.r - bounds.x)), this._displayDim];
+ }
+
+ componentWillUnmount() {
+ if (this._previewDoc) {
+ DocServer.DeleteDocument(this._previewDoc[Id]);
+ }
+ }
+
+
+ //@computed
+ @action
+ public DocumentIcon() {
+ let layoutresult = StrCast(this.props.doc.type);
if (!this._useIcons) {
+ let renderDoc = this.props.doc;
+ //let box: number[] = [];
+ if (layoutresult.indexOf(DocumentType.COL) !== -1) {
+ renderDoc = Doc.MakeDelegate(renderDoc);
+ let bounds = DocListCast(renderDoc.data).reduce((bounds, doc) => {
+ var [sptX, sptY] = [NumCast(doc.x), NumCast(doc.y)];
+ 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 });
+ let box = () => [(bounds.x + bounds.r) / 2, (bounds.y + bounds.b) / 2, Number(SEARCH_THUMBNAIL_SIZE) / (bounds.r - bounds.x), this._displayDim];
+ }
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); })}
+ let scale = () => returnXDimension() / NumCast(renderDoc.nativeWidth, returnXDimension());
+ let newRenderDoc = Doc.MakeDelegate(renderDoc); /// newRenderDoc -> renderDoc -> render"data"Doc -> TextProt
+ this._previewDoc = newRenderDoc;
+ const docview = <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
@@ -128,6 +223,7 @@ export class SearchItem extends React.Component<SearchItemProps> {
PanelWidth={returnXDimension}
PanelHeight={returnYDimension}
focus={emptyFunction}
+ backgroundColor={returnEmptyString}
selectOnLoad={false}
parentActive={returnFalse}
whenActiveChanged={returnFalse}
@@ -138,9 +234,15 @@ export class SearchItem extends React.Component<SearchItemProps> {
ContentScaling={scale}
/>
</div>;
+ const data = renderDoc.data;
+ if (data instanceof ObjectField) newRenderDoc.data = ObjectField.MakeCopy(data);
+ newRenderDoc.preview = true;
+ newRenderDoc.search_string = this.props.query;
+ return docview;
+ }
+ if (this._previewDoc) {
+ DocServer.DeleteDocument(this._previewDoc[Id]);
}
-
- 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 :
@@ -205,13 +307,13 @@ export class SearchItem extends React.Component<SearchItemProps> {
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);
+ doc1 && (doc1.libraryBrush = false);
+ doc2 && (doc2.libraryBrush = false);
}
} else {
let docViews: DocumentView[] = DocumentManager.Instance.getAllDocumentViews(this.props.doc);
docViews.forEach(element => {
- element.props.Document.libraryBrush = undefined;
+ element.props.Document.libraryBrush = false;
});
}
}
@@ -223,7 +325,8 @@ export class SearchItem extends React.Component<SearchItemProps> {
ContextMenu.Instance.addItem({
description: "Copy ID", event: () => {
Utils.CopyText(this.props.doc[Id]);
- }
+ },
+ icon: "fingerprint"
});
ContextMenu.Instance.displayMenu(e.clientX, e.clientY);
}
@@ -238,6 +341,8 @@ export class SearchItem extends React.Component<SearchItemProps> {
}
render() {
+ const doc1 = Cast(this.props.doc.anchor1, Doc);
+ const doc2 = Cast(this.props.doc.anchor2, Doc);
return (
<div className="search-overview" onPointerDown={this.pointerDown} onContextMenu={this.onContextMenu}>
<div className="search-item" onPointerEnter={this.highlightDoc} onPointerLeave={this.unHighlightDoc} id="result"
@@ -261,7 +366,8 @@ export class SearchItem extends React.Component<SearchItemProps> {
</div>
</div>
<div className="searchBox-instances">
- <SelectorContextMenu {...this.props} />
+ {(doc1 instanceof Doc && doc2 instanceof Doc) ? this.props.doc.type === DocumentType.LINK ? <LinkContextMenu doc1={doc1} doc2={doc2} /> :
+ <SelectorContextMenu {...this.props} /> : null}
</div>
</div>
);
diff --git a/src/client/views/search/SelectorContextMenu.scss b/src/client/views/search/SelectorContextMenu.scss
index 49f77b9bf..48cacc608 100644
--- a/src/client/views/search/SelectorContextMenu.scss
+++ b/src/client/views/search/SelectorContextMenu.scss
@@ -3,6 +3,7 @@
.parents {
background: $lighter-alt-accent;
padding: 10px;
+ // width: 300px;
.contexts {
text-transform: uppercase;
diff --git a/src/client/views/search/ToggleBar.scss b/src/client/views/search/ToggleBar.scss
index 633a194fe..79f866acb 100644
--- a/src/client/views/search/ToggleBar.scss
+++ b/src/client/views/search/ToggleBar.scss
@@ -16,11 +16,15 @@
-moz-transition: all 0.2s ease-in-out;
-o-transition: all 0.2s ease-in-out;
transition: all 0.2s ease-in-out;
+ color: gray;
+ font-size: 13;
}
}
.toggle-bar {
- height: 50px;
+ // height: 50px;
+ height: 30px;
+ width: 100px;
background-color: $alt-accent;
border-radius: 10px;
padding: 5px;
@@ -28,7 +32,8 @@
align-items: center;
.toggle-button {
- width: 275px;
+ // width: 275px;
+ width: 40px;
height: 100%;
border-radius: 10px;
background-color: $light-color;
diff --git a/src/client/views/search/ToggleBar.tsx b/src/client/views/search/ToggleBar.tsx
index 178578c5c..ed5ecd3ba 100644
--- a/src/client/views/search/ToggleBar.tsx
+++ b/src/client/views/search/ToggleBar.tsx
@@ -59,6 +59,7 @@ export class ToggleBar extends React.Component<ToggleBarProps>{
this._forwardTimeline.play();
this._forwardTimeline.reverse();
this.props.handleChange();
+ console.log(this.props.getStatus());
}
@action.bound
diff --git a/src/debug/Test.tsx b/src/debug/Test.tsx
index 0dca4b4b1..79f87f4ac 100644
--- a/src/debug/Test.tsx
+++ b/src/debug/Test.tsx
@@ -1,81 +1,39 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
-import { SerializationHelper } from '../client/util/SerializationHelper';
-import { createSchema, makeInterface, makeStrictInterface, listSpec } from '../new_fields/Schema';
-import { ImageField } from '../new_fields/URLField';
+import { DocServer } from '../client/DocServer';
import { Doc } from '../new_fields/Doc';
-import { List } from '../new_fields/List';
-
-const schema1 = createSchema({
- hello: "number",
- test: "string",
- fields: "boolean",
- url: ImageField,
- testDoc: Doc
-});
-
-type TestDoc = makeInterface<[typeof schema1]>;
-const TestDoc: (doc?: Doc) => TestDoc = makeInterface(schema1);
-
-const schema2 = createSchema({
- hello: ImageField,
- test: "boolean",
- fields: listSpec("number"),
- url: "number",
- testDoc: ImageField
-});
-
-const Test2Doc = makeStrictInterface(schema2);
-type Test2Doc = makeStrictInterface<typeof schema2>;
-
-const assert = (bool: boolean) => {
- if (!bool) throw new Error();
-};
+const protoId = "protoDoc";
+const delegateId = "delegateDoc";
class Test extends React.Component {
- onClick = () => {
- const url = new ImageField(new URL("http://google.com"));
- const doc = new Doc();
- const doc2 = new Doc();
- doc.hello = 5;
- doc.fields = "test";
- doc.test = "hello doc";
- doc.url = url;
- //doc.testDoc = doc2;
-
+ onCreateClick = () => {
+ const proto = new Doc(protoId, true);
+ const delegate = Doc.MakeDelegate(proto, delegateId);
+ }
- const test1: TestDoc = TestDoc(doc);
- assert(test1.hello === 5);
- assert(test1.fields === undefined);
- assert(test1.test === "hello doc");
- assert(test1.url === url);
- assert(test1.testDoc === doc2);
- test1.myField = 20;
- assert(test1.myField === 20);
+ onReadClick = async () => {
+ console.log("reading");
+ const docs = await DocServer.GetRefFields([delegateId, protoId]);
+ console.log("done");
+ console.log(docs);
+ }
- const test2: Test2Doc = Test2Doc(doc);
- assert(test2.hello === undefined);
- // assert(test2.fields === "test");
- assert(test2.test === undefined);
- assert(test2.url === undefined);
- assert(test2.testDoc === undefined);
- test2.url = 35;
- assert(test2.url === 35);
- const l = new List<Doc>();
- //TODO push, and other array functions don't go through the proxy
- l.push(doc2);
- //TODO currently length, and any other string fields will get serialized
- doc.list = l;
- console.log(l.slice());
+ onDeleteClick = () => {
+ DocServer.DeleteDocuments([protoId, delegateId]);
}
render() {
- return <div><button onClick={this.onClick}>Click me</button>
- {/* <input onKeyPress={this.onEnter}></input> */}
- </div>;
+ return (
+ <div>
+ <button onClick={this.onCreateClick}>Create Docs</button>
+ <button onClick={this.onReadClick}>Read Docs</button>
+ <button onClick={this.onDeleteClick}>Delete Docs</button>
+ </div>
+ );
}
}
+DocServer.init(window.location.protocol, window.location.hostname, 4321, "test");
ReactDOM.render(
<Test />,
document.getElementById('root')
diff --git a/src/debug/Viewer.tsx b/src/debug/Viewer.tsx
index 2b3eed154..24db3f934 100644
--- a/src/debug/Viewer.tsx
+++ b/src/debug/Viewer.tsx
@@ -10,6 +10,7 @@ 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 { RichTextField } from '../new_fields/RichTextField';
import { DateField } from '../new_fields/DateField';
import { ScriptField } from '../new_fields/ScriptField';
import CursorField from '../new_fields/CursorField';
@@ -126,6 +127,8 @@ class DebugViewer extends React.Component<{ field: FieldResult, setValue(value:
content = <p>"{field}"</p>;
} else if (typeof field === "number" || typeof field === "boolean") {
content = <p>{field}</p>;
+ } else if (field instanceof RichTextField) {
+ content = <p>RTF: {field.Data}</p>;
} else if (field instanceof URLField) {
content = <p>{field.url.href}</p>;
} else if (field instanceof Promise) {
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index a8f94b746..33a615cbf 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -11,6 +11,7 @@ import { listSpec } from '../new_fields/Schema';
import { List } from '../new_fields/List';
import { observer } from 'mobx-react';
import { observable } from 'mobx';
+import { Utils } from '../Utils';
@@ -57,7 +58,7 @@ class Uploader extends React.Component {
this.status = "getting user document";
- const res = await rp.get(DocServer.prepend(RouteStore.getUserDocumentId));
+ const res = await rp.get(Utils.prepend(RouteStore.getUserDocumentId));
if (!res) {
throw new Error("No user id returned");
}
@@ -104,6 +105,8 @@ class Uploader extends React.Component {
}
+DocServer.init(window.location.protocol, window.location.hostname, 4321, "image upload");
+
ReactDOM.render((
<Uploader />
),
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 1dd721396..c01f4e8cf 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -1,6 +1,6 @@
-import { observable, action } from "mobx";
-import { serializable, primitive, map, alias, list } from "serializr";
-import { autoObject, SerializationHelper, Deserializable } from "../client/util/SerializationHelper";
+import { observable, action, runInAction } from "mobx";
+import { serializable, primitive, map, alias, list, PropSchema, custom } from "serializr";
+import { autoObject, SerializationHelper, Deserializable, afterDocDeserialize } from "../client/util/SerializationHelper";
import { DocServer } from "../client/DocServer";
import { setter, getter, getField, updateFunction, deleteProperty, makeEditable, makeReadOnly } from "./util";
import { Cast, ToConstructor, PromiseValue, FieldValue, NumCast, BoolCast, StrCast } from "./Types";
@@ -10,14 +10,17 @@ import { RefField, FieldId } from "./RefField";
import { ToScriptString, SelfProxy, Parent, OnUpdate, Self, HandleUpdate, Update, Id } from "./FieldSymbols";
import { scriptingGlobal } from "../client/util/Scripting";
import { List } from "./List";
+import { DocumentType } from "../client/documents/Documents";
+import { ComputedField } from "./ScriptField";
+import { PrefetchProxy, ProxyField } from "./Proxy";
export namespace Field {
export function toKeyValueString(doc: Doc, key: string): string {
const onDelegate = Object.keys(doc).includes(key);
- let field = FieldValue(doc[key]);
+ let field = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
if (Field.IsField(field)) {
- return (onDelegate ? "=" : "") + Field.toScriptString(field);
+ return (onDelegate ? "=" : "") + (field instanceof ComputedField ? `:=${field.script.originalScript}` : Field.toScriptString(field));
}
return "";
}
@@ -66,8 +69,17 @@ export function DocListCast(field: FieldResult): Doc[] {
export const WidthSym = Symbol("Width");
export const HeightSym = Symbol("Height");
+function fetchProto(doc: Doc) {
+ const proto = doc.proto;
+ if (proto instanceof Promise) {
+ return proto;
+ }
+}
+
+let updatingFromServer = false;
+
@scriptingGlobal
-@Deserializable("doc").withFields(["id"])
+@Deserializable("Doc", fetchProto).withFields(["id"])
export class Doc extends RefField {
constructor(id?: FieldId, forceSave?: boolean) {
super(id);
@@ -100,7 +112,7 @@ export class Doc extends RefField {
proto: Opt<Doc>;
[key: string]: FieldResult;
- @serializable(alias("fields", map(autoObject())))
+ @serializable(alias("fields", map(autoObject(), { afterDeserialize: afterDocDeserialize })))
private get __fields() {
return this.___fields;
}
@@ -120,6 +132,9 @@ export class Doc extends RefField {
private ___fields: any = {};
private [Update] = (diff: any) => {
+ if (updatingFromServer) {
+ return;
+ }
DocServer.UpdateField(this[Id], diff);
}
@@ -132,16 +147,18 @@ export class Doc extends RefField {
return "invalid";
}
- public [HandleUpdate](diff: any) {
+ public async [HandleUpdate](diff: any) {
const set = diff.$set;
if (set) {
for (const key in set) {
if (!key.startsWith("fields.")) {
continue;
}
- const value = SerializationHelper.Deserialize(set[key]);
+ const value = await SerializationHelper.Deserialize(set[key]);
const fKey = key.substring(7);
+ updatingFromServer = true;
this[fKey] = value;
+ updatingFromServer = false;
}
}
const unset = diff.$unset;
@@ -151,7 +168,9 @@ export class Doc extends RefField {
continue;
}
const fKey = key.substring(7);
+ updatingFromServer = true;
delete this[fKey];
+ updatingFromServer = false;
}
}
}
@@ -178,8 +197,12 @@ export namespace Doc {
}
export function Get(doc: Doc, key: string, ignoreProto: boolean = false): FieldResult {
- const self = doc[Self];
- return getField(self, key, ignoreProto);
+ try {
+ const self = doc[Self];
+ return getField(self, key, ignoreProto);
+ } catch {
+ return doc;
+ }
}
export function GetT<T extends Field>(doc: Doc, key: string, ctor: ToConstructor<T>, ignoreProto: boolean = false): FieldResult<T> {
return Cast(Get(doc, key, ignoreProto), ctor) as FieldResult<T>;
@@ -242,7 +265,7 @@ export namespace Doc {
let r = (doc === other);
let r2 = (doc.proto === other);
let r3 = (other.proto === doc);
- let r4 = (doc.proto === other.proto);
+ let r4 = (doc.proto === other.proto && other.proto !== undefined);
return r || r2 || r3 || r4;
}
@@ -265,21 +288,28 @@ export namespace Doc {
export function AddDocToList(target: Doc, key: string, doc: Doc, relativeTo?: Doc, before?: boolean, first?: boolean, allowDuplicates?: boolean) {
if (target[key] === undefined) {
+ console.log("target key undefined");
Doc.GetProto(target)[key] = new List<Doc>();
}
let list = Cast(target[key], listSpec(Doc));
if (list) {
+ console.log("has 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);
+ if (first) {
+ console.log("is first");
+ list.splice(0, 0, doc);
+ }
else {
+ console.log("not first");
let ind = relativeTo ? list.indexOf(relativeTo) : -1;
if (ind === -1) list.push(doc);
else list.splice(before ? ind : ind + 1, 0, doc);
+ console.log("index", ind);
}
}
return true;
@@ -296,7 +326,7 @@ export namespace Doc {
x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y),
r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b)
};
- }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE });
+ }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: -Number.MAX_VALUE, b: -Number.MAX_VALUE });
return bounds;
}
@@ -311,21 +341,28 @@ export namespace Doc {
return fieldExt && doc[fieldKey + "_ext"] instanceof Doc ? doc[fieldKey + "_ext"] as Doc : doc;
}
+ export function CreateDocumentExtensionForField(doc: Doc, fieldKey: string) {
+ let docExtensionForField = new Doc(doc[Id] + fieldKey, true);
+ docExtensionForField.title = fieldKey + ".ext";
+ docExtensionForField.extendsDoc = doc; // this is used by search to map field matches on the extension doc back to the document it extends.
+ docExtensionForField.type = DocumentType.EXTENSION;
+ let proto: Doc | undefined = doc;
+ while (proto && !Doc.IsPrototype(proto)) {
+ proto = proto.proto;
+ }
+ (proto ? proto : doc)[fieldKey + "_ext"] = new PrefetchProxy(docExtensionForField);
+ return docExtensionForField;
+ }
+
export function UpdateDocumentExtensionForField(doc: Doc, fieldKey: string) {
- let extensionDoc = doc[fieldKey + "_ext"];
- if (extensionDoc === undefined) {
+ let docExtensionForField = doc[fieldKey + "_ext"] as Doc;
+ if (docExtensionForField === 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;
+ CreateDocumentExtensionForField(doc, fieldKey);
}, 0);
- } else if (extensionDoc instanceof Doc && extensionDoc.extendsDoc === undefined) {
- setTimeout(() => (extensionDoc as Doc).extendsDoc = doc, 0);
+ } else if (doc instanceof Doc) { // backward compatibility -- add fields for docs that don't have them already
+ docExtensionForField.extendsDoc === undefined && setTimeout(() => docExtensionForField.extendsDoc = doc, 0);
+ docExtensionForField.type === undefined && setTimeout(() => docExtensionForField.type = DocumentType.EXTENSION, 0);
}
}
export function MakeAlias(doc: Doc) {
@@ -335,33 +372,60 @@ export namespace Doc {
return Doc.MakeDelegate(doc); // bcz?
}
+ //
+ // Determines whether the combination of the layoutDoc and dataDoc represents
+ // a template relationship. If so, the layoutDoc will be expanded into a new
+ // document that inherits the properties of the original layout while allowing
+ // for individual layout properties to be overridden in the expanded layout.
+ //
+ export function WillExpandTemplateLayout(layoutDoc: Doc, dataDoc?: Doc) {
+ return BoolCast(layoutDoc.isTemplate) && dataDoc && layoutDoc !== dataDoc && !(layoutDoc.layout instanceof Doc);
+ }
+
+ //
+ // Returns an expanded template layout for a target data document.
+ // First it checks if an expanded layout already exists -- if so it will be stored on the dataDoc
+ // using the template layout doc's id as the field key.
+ // If it doesn't find the expanded layout, then it makes a delegate of the template layout and
+ // saves it on the data doc indexed by the template layout's id
+ //
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;
- }
+ if (!WillExpandTemplateLayout(templateLayoutDoc, dataDoc) || !dataDoc) return templateLayoutDoc;
// 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]];
+ let expandedTemplateLayout = dataDoc[templateLayoutDoc[Id]];
if (expandedTemplateLayout instanceof Doc) {
return expandedTemplateLayout;
}
- if (expandedTemplateLayout === undefined && BoolCast(templateLayoutDoc.isTemplate)) {
- setTimeout(() => {
- templateLayoutDoc["_expanded_" + dataDoc[Id]] = Doc.MakeDelegate(templateLayoutDoc);
- (templateLayoutDoc["_expanded_" + dataDoc[Id]] as Doc).title = templateLayoutDoc.title + " applied to " + dataDoc.title;
- (templateLayoutDoc["_expanded_" + dataDoc[Id]] as Doc).isExpandedTemplate = templateLayoutDoc;
- }, 0);
+ let expandedLayoutFieldKey = "Layout[" + templateLayoutDoc[Id] + "]";
+ expandedTemplateLayout = dataDoc[expandedLayoutFieldKey];
+ if (expandedTemplateLayout instanceof Doc) {
+ return expandedTemplateLayout;
}
- return templateLayoutDoc;
+ if (expandedTemplateLayout === undefined) {
+ setTimeout(() =>
+ dataDoc[expandedLayoutFieldKey] = Doc.MakeDelegate(templateLayoutDoc, undefined, "[" + templateLayoutDoc.title + "]"), 0);
+ }
+ return templateLayoutDoc; // use the templateLayout when it's not a template or the expandedTemplate is pending.
+ }
+
+ export function GetLayoutDataDocPair(doc: Doc, dataDoc: Doc | undefined, fieldKey: string, childDocLayout: Doc) {
+ let layoutDoc = childDocLayout;
+ let resolvedDataDoc = !doc.isTemplate && dataDoc !== doc ? dataDoc : undefined;
+ if (resolvedDataDoc && Doc.WillExpandTemplateLayout(childDocLayout, resolvedDataDoc)) {
+ Doc.UpdateDocumentExtensionForField(resolvedDataDoc, fieldKey);
+ let fieldExtensionDoc = Doc.resolvedFieldDataDoc(resolvedDataDoc, StrCast(childDocLayout.templateField, StrCast(childDocLayout.title)), "dummy");
+ layoutDoc = Doc.expandTemplateLayout(childDocLayout, fieldExtensionDoc !== resolvedDataDoc ? fieldExtensionDoc : undefined);
+ } else layoutDoc = Doc.expandTemplateLayout(childDocLayout, resolvedDataDoc);
+ return { layout: layoutDoc, data: resolvedDataDoc };
}
export function MakeCopy(doc: Doc, copyProto: boolean = false): Doc {
const copy = new Doc;
Object.keys(doc).forEach(key => {
- const field = doc[key];
+ const field = ProxyField.WithoutProxy(() => doc[key]);
if (key === "proto" && copyProto) {
if (field instanceof Doc) {
copy[key] = Doc.MakeCopy(field);
@@ -372,7 +436,7 @@ export namespace Doc {
} else if (field instanceof ObjectField) {
copy[key] = ObjectField.MakeCopy(field);
} else if (field instanceof Promise) {
- field.then(f => (copy[key] === undefined) && (copy[key] = f)); //TODO what should we do here?
+ debugger; //This shouldn't happend...
} else {
copy[key] = field;
}
@@ -382,18 +446,50 @@ export namespace Doc {
return copy;
}
- export function MakeDelegate(doc: Doc, id?: string): Doc;
- export function MakeDelegate(doc: Opt<Doc>, id?: string): Opt<Doc>;
- export function MakeDelegate(doc: Opt<Doc>, id?: string): Opt<Doc> {
- if (!doc) {
- return undefined;
+ export function MakeDelegate(doc: Doc, id?: string, title?: string): Doc;
+ export function MakeDelegate(doc: Opt<Doc>, id?: string, title?: string): Opt<Doc>;
+ export function MakeDelegate(doc: Opt<Doc>, id?: string, title?: string): Opt<Doc> {
+ if (doc) {
+ const delegate = new Doc(id, true);
+ delegate.proto = doc;
+ title && (delegate.title = title);
+ return delegate;
+ }
+ return undefined;
+ }
+
+ let _applyCount: number = 0;
+ export function ApplyTemplate(templateDoc: Doc) {
+ if (!templateDoc) return undefined;
+ let otherdoc = new Doc();
+ otherdoc.width = templateDoc[WidthSym]();
+ otherdoc.height = templateDoc[HeightSym]();
+ otherdoc.title = templateDoc.title + "(..." + _applyCount++ + ")";
+ otherdoc.layout = Doc.MakeDelegate(templateDoc);
+ otherdoc.miniLayout = StrCast(templateDoc.miniLayout);
+ otherdoc.detailedLayout = otherdoc.layout;
+ otherdoc.type = DocumentType.TEMPLATE;
+ return otherdoc;
+ }
+ export function ApplyTemplateTo(templateDoc: Doc, target: Doc, targetData?: Doc) {
+ let temp = Doc.MakeDelegate(templateDoc);
+ target.nativeWidth = Doc.GetProto(target).nativeWidth = undefined;
+ target.nativeHeight = Doc.GetProto(target).nativeHeight = undefined;
+ target.width = templateDoc.width;
+ target.height = templateDoc.height;
+ Doc.GetProto(target).type = DocumentType.TEMPLATE;
+ if (targetData && targetData.layout === target) {
+ targetData.layout = temp;
+ targetData.miniLayout = StrCast(templateDoc.miniLayout);
+ targetData.detailedLayout = targetData.layout;
+ } else {
+ target.layout = temp;
+ target.miniLayout = StrCast(templateDoc.miniLayout);
+ target.detailedLayout = target.layout;
}
- const delegate = new Doc(id, true);
- delegate.proto = doc;
- return delegate;
}
- export function MakeTemplate(fieldTemplate: Doc, metaKey: string, proto: Doc) {
+ export function MakeTemplate(fieldTemplate: Doc, metaKey: string, templateDataDoc: Doc) {
// move data doc fields to layout doc as needed (nativeWidth/nativeHeight, data, ??)
let backgroundLayout = StrCast(fieldTemplate.backgroundLayout);
let fieldLayoutDoc = fieldTemplate;
@@ -402,22 +498,48 @@ export namespace Doc {
}
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.templateField = metaKey;
fieldTemplate.title = metaKey;
+ fieldTemplate.isTemplate = true;
fieldTemplate.layout = layoutDelegate !== fieldTemplate ? layoutDelegate : layout;
fieldTemplate.backgroundLayout = backgroundLayout;
- fieldTemplate.nativeWidth = nw;
- fieldTemplate.nativeHeight = nh;
- fieldTemplate.isTemplate = true;
+ /* move certain layout properties from the original data doc to the template layout to avoid
+ inheriting them from the template's data doc which may also define these fields for its own use.
+ */
+ fieldTemplate.ignoreAspect = BoolCast(fieldTemplate.ignoreAspect);
+ fieldTemplate.singleColumn = BoolCast(fieldTemplate.singleColumn);
+ fieldTemplate.nativeWidth = Cast(fieldTemplate.nativeWidth, "number");
+ fieldTemplate.nativeHeight = Cast(fieldTemplate.nativeHeight, "number");
fieldTemplate.showTitle = "title";
- fieldTemplate.proto = proto;
+ setTimeout(() => fieldTemplate.proto = templateDataDoc);
+ }
+
+ export function ToggleDetailLayout(d: Doc) {
+ runInAction(async () => {
+ let miniLayout = await PromiseValue(d.miniLayout);
+ let detailLayout = await PromiseValue(d.detailedLayout);
+ d.layout !== miniLayout ? miniLayout && (d.layout = d.miniLayout) : detailLayout && (d.layout = detailLayout);
+ if (d.layout === detailLayout) Doc.GetProto(d).nativeWidth = Doc.GetProto(d).nativeHeight = undefined;
+ });
+ }
+ export function UseDetailLayout(d: Doc) {
+ runInAction(async () => {
+ let detailLayout = await d.detailedLayout;
+ if (detailLayout) {
+ d.layout = detailLayout;
+ d.nativeWidth = d.nativeHeight = undefined;
+ if (detailLayout instanceof Doc) {
+ let delegDetailLayout = Doc.MakeDelegate(detailLayout);
+ d.layout = delegDetailLayout;
+ delegDetailLayout.layout = await delegDetailLayout.detailedLayout;
+ }
+ }
+ });
}
} \ No newline at end of file
diff --git a/src/new_fields/FieldSymbols.ts b/src/new_fields/FieldSymbols.ts
index a436dcf2b..b5b3aa588 100644
--- a/src/new_fields/FieldSymbols.ts
+++ b/src/new_fields/FieldSymbols.ts
@@ -7,4 +7,4 @@ export const Id = Symbol("Id");
export const OnUpdate = Symbol("OnUpdate");
export const Parent = Symbol("Parent");
export const Copy = Symbol("Copy");
-export const ToScriptString = Symbol("Copy"); \ No newline at end of file
+export const ToScriptString = Symbol("ToScriptString"); \ No newline at end of file
diff --git a/src/new_fields/InkField.ts b/src/new_fields/InkField.ts
index 4e3b7abe0..8f64c1c2e 100644
--- a/src/new_fields/InkField.ts
+++ b/src/new_fields/InkField.ts
@@ -2,7 +2,7 @@ import { Deserializable } from "../client/util/SerializationHelper";
import { serializable, custom, createSimpleSchema, list, object, map } from "serializr";
import { ObjectField } from "./ObjectField";
import { Copy, ToScriptString } from "./FieldSymbols";
-import { deepCopy } from "../Utils";
+import { DeepCopy } from "../Utils";
export enum InkTool {
None,
@@ -19,6 +19,8 @@ export interface StrokeData {
page: number;
}
+export type InkData = Map<string, StrokeData>;
+
const pointSchema = createSimpleSchema({
x: true, y: true
});
@@ -31,15 +33,15 @@ const strokeDataSchema = createSimpleSchema({
@Deserializable("ink")
export class InkField extends ObjectField {
@serializable(map(object(strokeDataSchema)))
- readonly inkData: Map<string, StrokeData>;
+ readonly inkData: InkData;
- constructor(data?: Map<string, StrokeData>) {
+ constructor(data?: InkData) {
super();
this.inkData = data || new Map;
}
[Copy]() {
- return new InkField(deepCopy(this.inkData));
+ return new InkField(DeepCopy(this.inkData));
}
[ToScriptString]() {
diff --git a/src/new_fields/List.ts b/src/new_fields/List.ts
index a2133a990..0c7b77fa5 100644
--- a/src/new_fields/List.ts
+++ b/src/new_fields/List.ts
@@ -1,4 +1,4 @@
-import { Deserializable, autoObject } from "../client/util/SerializationHelper";
+import { Deserializable, autoObject, afterDocDeserialize } from "../client/util/SerializationHelper";
import { Field } from "./Doc";
import { setter, getter, deleteProperty, updateFunction } from "./util";
import { serializable, alias, list } from "serializr";
@@ -254,7 +254,7 @@ class ListImpl<T extends Field> extends ObjectField {
[key: number]: T | (T extends RefField ? Promise<T> : never);
- @serializable(alias("fields", list(autoObject())))
+ @serializable(alias("fields", list(autoObject(), { afterDeserialize: afterDocDeserialize })))
private get __fields() {
return this.___fields;
}
diff --git a/src/new_fields/ObjectField.ts b/src/new_fields/ObjectField.ts
index 5f4a6f8fb..65ada91c0 100644
--- a/src/new_fields/ObjectField.ts
+++ b/src/new_fields/ObjectField.ts
@@ -1,6 +1,7 @@
import { Doc } from "./Doc";
import { RefField } from "./RefField";
import { OnUpdate, Parent, Copy, ToScriptString } from "./FieldSymbols";
+import { Scripting } from "../client/util/Scripting";
export abstract class ObjectField {
protected [OnUpdate](diff?: any) { }
@@ -15,3 +16,5 @@ export namespace ObjectField {
return field[Copy]();
}
}
+
+Scripting.addGlobal(ObjectField); \ No newline at end of file
diff --git a/src/new_fields/Proxy.ts b/src/new_fields/Proxy.ts
index 38d874a68..c6292e37c 100644
--- a/src/new_fields/Proxy.ts
+++ b/src/new_fields/Proxy.ts
@@ -6,6 +6,8 @@ import { DocServer } from "../client/DocServer";
import { RefField } from "./RefField";
import { ObjectField } from "./ObjectField";
import { Id, Copy, ToScriptString } from "./FieldSymbols";
+import { scriptingGlobal } from "../client/util/Scripting";
+import { Plugins } from "./util";
@Deserializable("proxy")
export class ProxyField<T extends RefField> extends ObjectField {
@@ -48,7 +50,7 @@ export class ProxyField<T extends RefField> extends ObjectField {
private failed = false;
private promise?: Promise<any>;
- value(): T | undefined | FieldWaiting {
+ value(): T | undefined | FieldWaiting<T> {
if (this.cache) {
return this.cache;
}
@@ -63,6 +65,43 @@ export class ProxyField<T extends RefField> extends ObjectField {
return field;
}));
}
- return this.promise;
+ return this.promise as any;
}
}
+
+export namespace ProxyField {
+ let useProxy = true;
+ export function DisableProxyFields() {
+ useProxy = false;
+ }
+
+ export function EnableProxyFields() {
+ useProxy = true;
+ }
+
+ export function WithoutProxy<T>(fn: () => T) {
+ DisableProxyFields();
+ try {
+ return fn();
+ } finally {
+ EnableProxyFields();
+ }
+ }
+
+ export function initPlugin() {
+ Plugins.addGetterPlugin((doc, _, value) => {
+ if (useProxy && value instanceof ProxyField) {
+ return { value: value.value() };
+ }
+ });
+ }
+}
+
+function prefetchValue(proxy: PrefetchProxy<RefField>) {
+ return proxy.value() as any;
+}
+
+@scriptingGlobal
+@Deserializable("prefetch_proxy", prefetchValue)
+export class PrefetchProxy<T extends RefField> extends ProxyField<T> {
+}
diff --git a/src/new_fields/RefField.ts b/src/new_fields/RefField.ts
index 75ce4287f..f7bea8c94 100644
--- a/src/new_fields/RefField.ts
+++ b/src/new_fields/RefField.ts
@@ -1,10 +1,11 @@
import { serializable, primitive, alias } from "serializr";
import { Utils } from "../Utils";
import { Id, HandleUpdate, ToScriptString } from "./FieldSymbols";
+import { afterDocDeserialize } from "../client/util/SerializationHelper";
export type FieldId = string;
export abstract class RefField {
- @serializable(alias("id", primitive()))
+ @serializable(alias("id", primitive({ afterDeserialize: afterDocDeserialize })))
private __id: FieldId;
readonly [Id]: FieldId;
@@ -13,7 +14,7 @@ export abstract class RefField {
this[Id] = this.__id;
}
- protected [HandleUpdate]?(diff: any): void;
+ protected [HandleUpdate]?(diff: any): void | Promise<void>;
abstract [ToScriptString](): string;
}
diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts
index 78a3a4067..89799b2af 100644
--- a/src/new_fields/RichTextField.ts
+++ b/src/new_fields/RichTextField.ts
@@ -20,6 +20,6 @@ export class RichTextField extends ObjectField {
}
[ToScriptString]() {
- return "invalid";
+ return `new RichTextField("${this.Data}")`;
}
} \ No newline at end of file
diff --git a/src/new_fields/SchemaHeaderField.ts b/src/new_fields/SchemaHeaderField.ts
new file mode 100644
index 000000000..23605cfb0
--- /dev/null
+++ b/src/new_fields/SchemaHeaderField.ts
@@ -0,0 +1,101 @@
+import { Deserializable } from "../client/util/SerializationHelper";
+import { serializable, createSimpleSchema, primitive } from "serializr";
+import { ObjectField } from "./ObjectField";
+import { Copy, ToScriptString, OnUpdate } from "./FieldSymbols";
+import { scriptingGlobal, Scripting } from "../client/util/Scripting";
+import { ColumnType } from "../client/views/collections/CollectionSchemaView";
+
+export const PastelSchemaPalette = new Map<string, string>([
+ // ["pink1", "#FFB4E8"],
+ ["pink2", "#ff9cee"],
+ ["pink3", "#ffccf9"],
+ ["pink4", "#fcc2ff"],
+ ["pink5", "#f6a6ff"],
+ ["purple1", "#b28dff"],
+ ["purple2", "#c5a3ff"],
+ ["purple3", "#d5aaff"],
+ ["purple4", "#ecd4ff"],
+ // ["purple5", "#fb34ff"],
+ ["purple6", "#dcd3ff"],
+ ["purple7", "#a79aff"],
+ ["purple8", "#b5b9ff"],
+ ["purple9", "#97a2ff"],
+ ["bluegreen1", "#afcbff"],
+ ["bluegreen2", "#aff8db"],
+ ["bluegreen3", "#c4faf8"],
+ ["bluegreen4", "#85e3ff"],
+ ["bluegreen5", "#ace7ff"],
+ // ["bluegreen6", "#6eb5ff"],
+ ["bluegreen7", "#bffcc6"],
+ ["bluegreen8", "#dbffd6"],
+ ["yellow1", "#f3ffe3"],
+ ["yellow2", "#e7ffac"],
+ ["yellow3", "#ffffd1"],
+ ["yellow4", "#fff5ba"],
+ // ["red1", "#ffc9de"],
+ ["red2", "#ffabab"],
+ ["red3", "#ffbebc"],
+ ["red4", "#ffcbc1"],
+ ["orange1", "#ffd5b3"],
+]);
+
+export const RandomPastel = () => Array.from(PastelSchemaPalette.values())[Math.floor(Math.random() * PastelSchemaPalette.size)];
+
+@scriptingGlobal
+@Deserializable("schemaheader")
+export class SchemaHeaderField extends ObjectField {
+ @serializable(primitive())
+ heading: string;
+ @serializable(primitive())
+ color: string;
+ @serializable(primitive())
+ type: number;
+ @serializable(primitive())
+ width: number;
+ @serializable(primitive())
+ desc: boolean | undefined; // boolean determines sort order, undefined when no sort
+
+ constructor(heading: string = "", color: string = RandomPastel(), type?: ColumnType, width?: number, desc?: boolean) {
+ super();
+
+ this.heading = heading;
+ this.color = color;
+ this.type = type ? type : 0;
+ this.width = width ? width : -1;
+ this.desc = desc;
+ }
+
+ setHeading(heading: string) {
+ this.heading = heading;
+ this[OnUpdate]();
+ }
+
+ setColor(color: string) {
+ this.color = color;
+ this[OnUpdate]();
+ }
+
+ setType(type: ColumnType) {
+ this.type = type;
+ this[OnUpdate]();
+ }
+
+ setWidth(width: number) {
+ this.width = width;
+ this[OnUpdate]();
+ }
+
+ setDesc(desc: boolean | undefined) {
+ this.desc = desc;
+ this[OnUpdate]();
+ }
+
+ [Copy]() {
+ return new SchemaHeaderField(this.heading, this.color, this.type);
+ }
+
+ [ToScriptString]() {
+ return `invalid`;
+ }
+}
+
diff --git a/src/new_fields/ScriptField.ts b/src/new_fields/ScriptField.ts
index e2994ed70..83fb52d07 100644
--- a/src/new_fields/ScriptField.ts
+++ b/src/new_fields/ScriptField.ts
@@ -2,8 +2,11 @@ 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 { Deserializable, autoObject } from "../client/util/SerializationHelper";
import { Doc } from "../new_fields/Doc";
+import { Plugins } from "./util";
+import { computedFn } from "mobx-utils";
+import { ProxyField } from "./Proxy";
function optional(propSchema: PropSchema) {
return custom(value => {
@@ -23,6 +26,7 @@ const optionsSchema = createSimpleSchema({
requiredType: true,
addReturn: true,
typecheck: true,
+ editable: true,
readonly: true,
params: optional(map(primitive()))
});
@@ -32,7 +36,16 @@ const scriptSchema = createSimpleSchema({
originalScript: true
});
-function deserializeScript(script: ScriptField) {
+async function deserializeScript(script: ScriptField) {
+ const captures: ProxyField<Doc> = (script as any).captures;
+ if (captures) {
+ const doc = (await captures.value())!;
+ const captured: any = {};
+ const keys = Object.keys(doc);
+ const vals = await Promise.all(keys.map(key => doc[key]) as any);
+ keys.forEach((key, i) => captured[key] = vals[i]);
+ (script.script.options as any).capturedVariables = captured;
+ }
const comp = CompileScript(script.script.originalScript, script.script.options);
if (!comp.compiled) {
throw new Error("Couldn't compile loaded script");
@@ -46,9 +59,17 @@ export class ScriptField extends ObjectField {
@serializable(object(scriptSchema))
readonly script: CompiledScript;
+ @serializable(autoObject())
+ private captures?: ProxyField<Doc>;
+
constructor(script: CompiledScript) {
super();
+ if (script && script.options.capturedVariables) {
+ const doc = Doc.assign(new Doc, script.options.capturedVariables);
+ this.captures = new ProxyField(doc);
+ }
+
this.script = script;
}
@@ -86,11 +107,41 @@ export class ScriptField extends ObjectField {
@Deserializable("computed", deserializeScript)
export class ComputedField extends ScriptField {
//TODO maybe add an observable cache based on what is passed in for doc, considering there shouldn't really be that many possible values for doc
- value(doc: Doc) {
+ value = computedFn((doc: Doc) => {
const val = this.script.run({ this: doc });
if (val.success) {
return val.result;
}
return undefined;
+ });
+}
+
+export namespace ComputedField {
+ let useComputed = true;
+ export function DisableComputedFields() {
+ useComputed = false;
+ }
+
+ export function EnableComputedFields() {
+ useComputed = true;
+ }
+
+ export const undefined = "__undefined";
+
+ export function WithoutComputed<T>(fn: () => T) {
+ DisableComputedFields();
+ try {
+ return fn();
+ } finally {
+ EnableComputedFields();
+ }
+ }
+
+ export function initPlugin() {
+ Plugins.addGetterPlugin((doc, _, value) => {
+ if (useComputed && value instanceof ComputedField) {
+ return { value: value.value(doc), shouldReturn: true };
+ }
+ });
}
} \ No newline at end of file
diff --git a/src/new_fields/Types.ts b/src/new_fields/Types.ts
index f8a4a30b4..565ae2ee3 100644
--- a/src/new_fields/Types.ts
+++ b/src/new_fields/Types.ts
@@ -78,7 +78,7 @@ export function StrCast(field: FieldResult, defaultVal: string | null = "") {
return Cast(field, "string", defaultVal);
}
-export function BoolCast(field: FieldResult, defaultVal: boolean | null = null) {
+export function BoolCast(field: FieldResult, defaultVal: boolean | null = false) {
return Cast(field, "boolean", defaultVal);
}
export function DateCast(field: FieldResult) {
diff --git a/src/new_fields/URLField.ts b/src/new_fields/URLField.ts
index d935a61af..b9ad96450 100644
--- a/src/new_fields/URLField.ts
+++ b/src/new_fields/URLField.ts
@@ -42,4 +42,5 @@ export abstract class URLField extends ObjectField {
@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 { } \ No newline at end of file
+@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 47e467041..c6f693f7f 100644
--- a/src/new_fields/util.ts
+++ b/src/new_fields/util.ts
@@ -1,5 +1,5 @@
import { UndoManager } from "../client/util/UndoManager";
-import { Doc, Field } from "./Doc";
+import { Doc, Field, FieldResult } from "./Doc";
import { SerializationHelper } from "../client/util/SerializationHelper";
import { ProxyField } from "./Proxy";
import { RefField } from "./RefField";
@@ -11,6 +11,20 @@ import { ComputedField } from "./ScriptField";
function _readOnlySetter(): never {
throw new Error("Documents can't be modified in read-only mode");
}
+
+export interface GetterResult {
+ value: FieldResult;
+ shouldReturn?: boolean;
+}
+export type GetterPlugin = (receiver: any, prop: string | number, currentValue: any) => GetterResult | undefined;
+const getterPlugins: GetterPlugin[] = [];
+
+export namespace Plugins {
+ export function addGetterPlugin(plugin: GetterPlugin) {
+ getterPlugins.push(plugin);
+ }
+}
+
const _setterImpl = action(function (target: any, prop: string | symbol | number, value: any, receiver: any): boolean {
//console.log("-set " + target[SelfProxy].title + "(" + target[SelfProxy][prop] + ")." + prop.toString() + " = " + value);
if (SerializationHelper.IsSerializing()) {
@@ -74,6 +88,9 @@ export function setter(target: any, prop: string | symbol | number, value: any,
}
export function getter(target: any, prop: string | symbol | number, receiver: any): any {
+ if (prop === "then") {//If we're being awaited
+ return undefined;
+ }
if (typeof prop === "symbol") {
return target.__fields[prop] || target[prop];
}
@@ -85,12 +102,15 @@ export function getter(target: any, prop: string | symbol | number, receiver: an
function getFieldImpl(target: any, prop: string | number, receiver: any, ignoreProto: boolean = false): any {
receiver = receiver || target[SelfProxy];
- const field = target.__fields[prop];
- if (field instanceof ProxyField) {
- return field.value();
- }
- if (field instanceof ComputedField) {
- return field.value(receiver);
+ let field = target.__fields[prop];
+ for (const plugin of getterPlugins) {
+ const res = plugin(receiver, prop, field);
+ if (res === undefined) continue;
+ if (res.shouldReturn) {
+ return res.value;
+ } else {
+ field = res.value;
+ }
}
if (field === undefined && !ignoreProto && prop !== "proto") {
const proto = getFieldImpl(target, "proto", receiver, true);//TODO tfs: instead of receiver we could use target[SelfProxy]... I don't which semantics we want or if it really matters
diff --git a/src/scraping/acm/chromedriver b/src/scraping/acm/chromedriver
index 9e9b16717..9e9b16717 100755..100644
--- a/src/scraping/acm/chromedriver
+++ b/src/scraping/acm/chromedriver
Binary files differ
diff --git a/src/scraping/buxton/scraper.py b/src/scraping/buxton/scraper.py
index 700269727..f0f45d8f9 100644
--- a/src/scraping/buxton/scraper.py
+++ b/src/scraping/buxton/scraper.py
@@ -1,4 +1,5 @@
import os
+from shutil import copyfile
import docx2txt
from docx import Document
from docx.opc.constants import RELATIONSHIP_TYPE as RT
@@ -15,7 +16,9 @@ source = "./source"
dist = "../../server/public/files"
db = MongoClient("localhost", 27017)["Dash"]
+target_collection = db.newDocuments
schema_guids = []
+common_proto_id = ""
def extract_links(fileName):
@@ -84,7 +87,7 @@ def write_schema(parse_results, display_fields, storage_key):
"height": 600,
"panX": 0,
"panY": 0,
- "zoomBasis": 0.5,
+ "zoomBasis": 1,
"zIndex": 2,
"libraryBrush": False,
"viewType": 2
@@ -92,7 +95,7 @@ def write_schema(parse_results, display_fields, storage_key):
"__type": "Doc"
}
- fields["proto"] = protofy("collectionProto")
+ fields["proto"] = protofy(common_proto_id)
fields[storage_key] = listify(proxify_guids(view_guids))
fields["schemaColumns"] = listify(display_fields)
fields["backgroundColor"] = "white"
@@ -106,8 +109,8 @@ def write_schema(parse_results, display_fields, storage_key):
fields["isPrototype"] = True
fields["page"] = -1
- db.newDocuments.insert_one(data_doc)
- db.newDocuments.insert_one(view_doc)
+ target_collection.insert_one(data_doc)
+ target_collection.insert_one(view_doc)
data_doc_guid = data_doc["_id"]
print(f"inserted view document ({view_doc_guid})")
@@ -158,8 +161,8 @@ def write_text_doc(content):
"__type": "Doc"
}
- db.newDocuments.insert_one(view_doc)
- db.newDocuments.insert_one(data_doc)
+ target_collection.insert_one(view_doc)
+ target_collection.insert_one(data_doc)
return view_doc_guid
@@ -209,8 +212,8 @@ def write_image(folder, name):
"__type": "Doc"
}
- db.newDocuments.insert_one(view_doc)
- db.newDocuments.insert_one(data_doc)
+ target_collection.insert_one(view_doc)
+ target_collection.insert_one(data_doc)
return view_doc_guid
@@ -231,7 +234,9 @@ def parse_document(file_name: str):
for image in os.listdir(dir_path):
count += 1
view_guids.append(write_image(pure_name, image))
- os.rename(dir_path + "/" + image, dir_path +
+ copyfile(dir_path + "/" + image, dir_path +
+ "/" + image.replace(".", "_o.", 1))
+ copyfile(dir_path + "/" + image, dir_path +
"/" + image.replace(".", "_m.", 1))
print(f"extracted {count} images...")
@@ -347,6 +352,22 @@ def proxify_guids(guids):
return list(map(lambda guid: {"fieldId": guid, "__type": "proxy"}, guids))
+def write_common_proto():
+ id = guid()
+ common_proto = {
+ "_id": id,
+ "fields": {
+ "proto": protofy("collectionProto"),
+ "title": "Common Import Proto",
+ },
+ "__type": "Doc"
+ }
+
+ target_collection.insert_one(common_proto)
+
+ return id
+
+
if os.path.exists(dist):
shutil.rmtree(dist)
while os.path.exists(dist):
@@ -354,6 +375,8 @@ while os.path.exists(dist):
os.mkdir(dist)
mkdir_if_absent(source)
+common_proto_id = write_common_proto()
+
candidates = 0
for file_name in os.listdir(source):
if file_name.endswith('.docx'):
@@ -372,7 +395,7 @@ parent_guid = write_schema({
}, ["title", "short_description", "original_price"], "data")
print("appending parent schema to main workspace...\n")
-db.newDocuments.update_one(
+target_collection.update_one(
{"fields.title": "WS collection 1"},
{"$push": {"fields.data.fields": {"fieldId": parent_guid, "__type": "proxy"}}}
)
diff --git a/src/scraping/buxton/source/Bill_Notes_3_button_optical_mouse.docx b/src/scraping/buxton/source/Bill_Notes_3_button_optical_mouse.docx
new file mode 100644
index 000000000..a2ab04b78
--- /dev/null
+++ b/src/scraping/buxton/source/Bill_Notes_3_button_optical_mouse.docx
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_Amazon_Kindle_Keyboard.docx b/src/scraping/buxton/source/Bill_Notes_Amazon_Kindle_Keyboard.docx
new file mode 100644
index 000000000..e4375ebeb
--- /dev/null
+++ b/src/scraping/buxton/source/Bill_Notes_Amazon_Kindle_Keyboard.docx
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_Apple_Adj_Keyboard.docx b/src/scraping/buxton/source/Bill_Notes_Apple_Adj_Keyboard.docx
new file mode 100644
index 000000000..99f7ad19d
--- /dev/null
+++ b/src/scraping/buxton/source/Bill_Notes_Apple_Adj_Keyboard.docx
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_BAT.docx b/src/scraping/buxton/source/Bill_Notes_BAT.docx
new file mode 100644
index 000000000..0e3368611
--- /dev/null
+++ b/src/scraping/buxton/source/Bill_Notes_BAT.docx
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_Bill_Notes_CyKey.docx b/src/scraping/buxton/source/Bill_Notes_Bill_Notes_CyKey.docx
index 06094b4d3..649d636e3 100644
--- a/src/scraping/buxton/source/Bill_Notes_Bill_Notes_CyKey.docx
+++ 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
index 356697092..b00080e08 100644
--- a/src/scraping/buxton/source/Bill_Notes_Braun_T3.docx
+++ 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
index cd89fb97b..510a006e0 100644
--- a/src/scraping/buxton/source/Bill_Notes_CasioC801.docx
+++ b/src/scraping/buxton/source/Bill_Notes_CasioC801.docx
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_Casio_CZ-101.docx b/src/scraping/buxton/source/Bill_Notes_Casio_CZ-101.docx
new file mode 100644
index 000000000..c8d3943c0
--- /dev/null
+++ b/src/scraping/buxton/source/Bill_Notes_Casio_CZ-101.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
index a503cddfc..cea9e7b69 100644
--- a/src/scraping/buxton/source/Bill_Notes_Casio_Mini.docx
+++ 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
index 4d13a8cf5..f53402a06 100644
--- a/src/scraping/buxton/source/Bill_Notes_FingerWorks_Prototype.docx
+++ 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
index 578a1be08..0eec89949 100644
--- a/src/scraping/buxton/source/Bill_Notes_Fingerworks_TouchStream.docx
+++ 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
index d01e1bf5c..ba80c1959 100644
--- a/src/scraping/buxton/source/Bill_Notes_FrogPad.docx
+++ 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
index 7bd28b376..8558a4e13 100644
--- a/src/scraping/buxton/source/Bill_Notes_Gavilan_SC.docx
+++ 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
index 0615c4953..09e17f971 100644
--- a/src/scraping/buxton/source/Bill_Notes_Grandjean_Stenotype.docx
+++ b/src/scraping/buxton/source/Bill_Notes_Grandjean_Stenotype.docx
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_Kindle_3G_lighted_cover.docx b/src/scraping/buxton/source/Bill_Notes_Kindle_3G_lighted_cover.docx
new file mode 100644
index 000000000..f00fcb772
--- /dev/null
+++ b/src/scraping/buxton/source/Bill_Notes_Kindle_3G_lighted_cover.docx
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_Matias.docx b/src/scraping/buxton/source/Bill_Notes_Matias.docx
index 547603256..d2d014bbe 100644
--- a/src/scraping/buxton/source/Bill_Notes_Matias.docx
+++ b/src/scraping/buxton/source/Bill_Notes_Matias.docx
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_Microwriter.docx b/src/scraping/buxton/source/Bill_Notes_Microwriter.docx
new file mode 100644
index 000000000..3ac272e42
--- /dev/null
+++ b/src/scraping/buxton/source/Bill_Notes_Microwriter.docx
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_MousePen.docx b/src/scraping/buxton/source/Bill_Notes_MousePen.docx
index 4e1056636..cd0b3eab3 100644
--- a/src/scraping/buxton/source/Bill_Notes_MousePen.docx
+++ 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
index a514926d2..2f4a04e81 100644
--- a/src/scraping/buxton/source/Bill_Notes_NewO.docx
+++ 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
index bfca0a9bb..7a636e2d6 100644
--- a/src/scraping/buxton/source/Bill_Notes_OLPC.docx
+++ 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
index c0cf6ba9a..3038de363 100644
--- a/src/scraping/buxton/source/Bill_Notes_PARCkbd.docx
+++ 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
index ad06903f3..af72fa662 100644
--- a/src/scraping/buxton/source/Bill_Notes_Philco_Mystery_Control.docx
+++ 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
index e4c659de9..5c2eb8d7f 100644
--- a/src/scraping/buxton/source/Bill_Notes_TASA_Kbd.docx
+++ 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
index 8ceebc71e..c9ee2eaea 100644
--- a/src/scraping/buxton/source/Bill_Notes_The_Tap.docx
+++ b/src/scraping/buxton/source/Bill_Notes_The_Tap.docx
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_Twiddler.docx b/src/scraping/buxton/source/Bill_Notes_Twiddler.docx
new file mode 100644
index 000000000..27b4acc85
--- /dev/null
+++ b/src/scraping/buxton/source/Bill_Notes_Twiddler.docx
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_orbiTouch.doc b/src/scraping/buxton/source/Bill_Notes_orbiTouch.doc
new file mode 100644
index 000000000..6bd71f20e
--- /dev/null
+++ b/src/scraping/buxton/source/Bill_Notes_orbiTouch.doc
Binary files differ
diff --git a/src/server/Message.ts b/src/server/Message.ts
index 19e0a48aa..aaee143e8 100644
--- a/src/server/Message.ts
+++ b/src/server/Message.ts
@@ -24,6 +24,16 @@ export interface Transferable {
readonly data?: any;
}
+export enum YoutubeQueryTypes {
+ Channels, SearchVideo, VideoDetails
+}
+
+export interface YoutubeQueryInput {
+ readonly type: YoutubeQueryTypes;
+ readonly userInput?: string;
+ readonly videoIds?: string;
+}
+
export interface Reference {
readonly id: string;
}
@@ -45,6 +55,7 @@ export namespace MessageStore {
export const GetRefFields = new Message<string[]>("Get Ref Fields");
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 5c13495ff..e30015e39 100644
--- a/src/server/RouteStore.ts
+++ b/src/server/RouteStore.ts
@@ -29,4 +29,7 @@ export enum RouteStore {
forgot = "/forgotpassword",
reset = "/reset/:token",
+ // APIS
+ cognitiveServices = "/cognitiveservices"
+
} \ No newline at end of file
diff --git a/src/server/authentication/controllers/user_controller.ts b/src/server/authentication/controllers/user_controller.ts
index 0e431f1e6..f5c6e1610 100644
--- a/src/server/authentication/controllers/user_controller.ts
+++ b/src/server/authentication/controllers/user_controller.ts
@@ -77,8 +77,9 @@ export let postSignup = (req: Request, res: Response, next: NextFunction) => {
let tryRedirectToTarget = (req: Request, res: Response) => {
if (req.session && req.session.target) {
- res.redirect(req.session.target);
+ let target = req.session.target;
req.session.target = undefined;
+ res.redirect(target);
} else {
res.redirect(RouteStore.home);
}
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index e796ccb43..91d7ba87d 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -12,6 +12,7 @@ import { List } from "../../../new_fields/List";
import { listSpec } from "../../../new_fields/Schema";
import { Cast, FieldValue, StrCast } from "../../../new_fields/Types";
import { RouteStore } from "../../RouteStore";
+import { Utils } from "../../../Utils";
export class CurrentUserUtils {
private static curr_email: string;
@@ -70,11 +71,11 @@ export class CurrentUserUtils {
doc.sidebar = sidebar;
}
StrCast(doc.title).indexOf("@") !== -1 && (doc.title = StrCast(doc.title).split("@")[0] + "'s Library");
-
+ doc.width = 100;
}
public static loadCurrentUser() {
- return rp.get(DocServer.prepend(RouteStore.getCurrUser)).then(response => {
+ return rp.get(Utils.prepend(RouteStore.getCurrUser)).then(response => {
if (response) {
const result: { id: string, email: string } = JSON.parse(response);
return result;
@@ -87,7 +88,7 @@ export class CurrentUserUtils {
public static async loadUserDocument({ id, email }: { id: string, email: string }) {
this.curr_id = id;
this.curr_email = email;
- await rp.get(DocServer.prepend(RouteStore.getUserDocumentId)).then(id => {
+ await rp.get(Utils.prepend(RouteStore.getUserDocumentId)).then(id => {
if (id) {
return DocServer.GetRefField(id).then(async field => {
if (field instanceof Doc) {
diff --git a/src/server/database.ts b/src/server/database.ts
index 7f5331998..a7254fb0c 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -17,7 +17,7 @@ export class Database {
});
}
- public update(id: string, value: any, callback: () => void, upsert = true, collectionName = Database.DocumentsCollection) {
+ public update(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, upsert = true, collectionName = Database.DocumentsCollection) {
if (this.db) {
let collection = this.db.collection(collectionName);
const prom = this.currentWrites[id];
@@ -30,7 +30,7 @@ export class Database {
delete this.currentWrites[id];
}
resolve();
- callback();
+ callback(err, res);
});
});
};
@@ -41,6 +41,30 @@ export class Database {
}
}
+ public replace(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, upsert = true, collectionName = Database.DocumentsCollection) {
+ if (this.db) {
+ let collection = this.db.collection(collectionName);
+ const prom = this.currentWrites[id];
+ let newProm: Promise<void>;
+ const run = (): Promise<void> => {
+ return new Promise<void>(resolve => {
+ collection.replaceOne({ _id: id }, value, { upsert }
+ , (err, res) => {
+ if (this.currentWrites[id] === newProm) {
+ delete this.currentWrites[id];
+ }
+ resolve();
+ callback(err, res);
+ });
+ });
+ };
+ newProm = prom ? prom.then(run) : run();
+ this.currentWrites[id] = newProm;
+ } else {
+ this.onConnect.push(() => this.replace(id, value, callback, upsert, collectionName));
+ }
+ }
+
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) {
@@ -126,6 +150,34 @@ export class Database {
}
}
+ public async visit(ids: string[], fn: (result: any) => string[], collectionName = "newDocuments"): Promise<void> {
+ if (this.db) {
+ const visited = new Set<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;
+ visited.add(id);
+ ids.push(...fn(doc));
+ }
+ }
+
+ } else {
+ return new Promise(res => {
+ this.onConnect.push(() => {
+ this.visit(ids, fn, collectionName);
+ res();
+ });
+ });
+ }
+ }
+
public query(query: { [key: string]: any }, projection?: { [key: string]: 0 | 1 }, collectionName = "newDocuments"): Promise<mongodb.Cursor> {
if (this.db) {
let cursor = this.db.collection(collectionName).find(query);
diff --git a/src/server/index.ts b/src/server/index.ts
index 51fafbdc3..10a84c823 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -25,8 +25,9 @@ import { getForgot, getLogin, getLogout, getReset, getSignup, postForgot, postLo
import { DashUserModel } from './authentication/models/user_model';
import { Client } from './Client';
import { Database } from './database';
-import { MessageStore, Transferable, Types, Diff, Message } from "./Message";
+import { MessageStore, Transferable, Types, Diff, YoutubeQueryTypes as YoutubeQueryType, YoutubeQueryInput } from "./Message";
import { RouteStore } from './RouteStore';
+import v4 = require('uuid/v4');
const app = express();
const config = require('../../webpack.config');
import { createCanvas, loadImage, Canvas } from "canvas";
@@ -39,12 +40,20 @@ import c = require("crypto");
import { Search } from './Search';
import { debug } from 'util';
import _ = require('lodash');
+import * as Archiver from 'archiver';
+import * as AdmZip from 'adm-zip';
+import * as YoutubeApi from './youtubeApi/youtubeApiSample.js';
import { Response } from 'express-serve-static-core';
+import { DocComponent } from '../client/views/DocComponent';
const MongoStore = require('connect-mongo')(session);
const mongoose = require('mongoose');
const probe = require("probe-image-size");
+var SolrNode = require('solr-node');
+var shell = require('shelljs');
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") {
@@ -110,7 +119,7 @@ function addSecureRoute(method: Method,
if (req.user) {
handler(req.user, res, req);
} else {
- req.session!.target = `${req.headers.host}${req.originalUrl}`;
+ req.session!.target = req.originalUrl;
onRejection(res, req);
}
};
@@ -139,7 +148,18 @@ app.get("/pull", (req, res) =>
res.redirect("/");
}));
+app.get("/version", (req, res) => {
+ exec('"C:\\Program Files\\Git\\bin\\git.exe" rev-parse HEAD', (err, stdout, stderr) => {
+ if (err) {
+ res.send(err.message);
+ return;
+ }
+ res.send(stdout);
+ });
+});
+
// SEARCH
+const solrURL = "http://localhost:8983/solr/#/dash";
// GETTERS
@@ -167,6 +187,186 @@ function msToTime(duration: number) {
return hoursS + ":" + minutesS + ":" + secondsS + "." + milliseconds;
}
+async function getDocs(id: string) {
+ const files = new Set<string>();
+ const docs: { [id: string]: any } = {};
+ const fn = (doc: any): string[] => {
+ const id = doc.id;
+ if (typeof id === "string" && id.endsWith("Proto")) {
+ //Skip protos
+ return [];
+ }
+ const ids: string[] = [];
+ for (const key in doc.fields) {
+ if (!doc.fields.hasOwnProperty(key)) {
+ continue;
+ }
+ const field = doc.fields[key];
+ if (field === undefined || field === null) {
+ continue;
+ }
+
+ if (field.__type === "proxy" || field.__type === "prefetch_proxy") {
+ ids.push(field.fieldId);
+ } else if (field.__type === "script" || field.__type === "computed") {
+ if (field.captures) {
+ ids.push(field.captures.fieldId);
+ }
+ } else if (field.__type === "list") {
+ ids.push(...fn(field));
+ } 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;
+ files.add(pathname);
+ }
+ } else if (["audio", "image", "video", "pdf", "web"].includes(field.__type)) {
+ const url = new URL(field.url);
+ const pathname = url.pathname;
+ files.add(pathname);
+ }
+ }
+
+ if (doc.id) {
+ docs[doc.id] = doc;
+ }
+ return ids;
+ };
+ await Database.Instance.visit([id], fn);
+ return { id, docs, files };
+}
+app.get("/serializeDoc/:docId", async (req, res) => {
+ const { docs, files } = await getDocs(req.params.docId);
+ res.send({ docs, files: Array.from(files) });
+});
+
+app.get("/downloadId/:docId", async (req, res) => {
+ res.set('Content-disposition', `attachment;`);
+ res.set('Content-Type', "application/zip");
+ const { id, docs, files } = await getDocs(req.params.docId);
+ const docString = JSON.stringify({ id, docs });
+ const zip = Archiver('zip');
+ zip.pipe(res);
+ zip.append(docString, { name: "doc.json" });
+ files.forEach(val => {
+ zip.file(__dirname + RouteStore.public + val, { name: val.substring(1) });
+ });
+ zip.finalize();
+});
+
+app.post("/uploadDoc", (req, res) => {
+ let form = new formidable.IncomingForm();
+ form.keepExtensions = true;
+ // let path = req.body.path;
+ const ids: { [id: string]: string } = {};
+ let remap = true;
+ const getId = (id: string): string => {
+ if (!remap) return id;
+ if (id.endsWith("Proto")) return id;
+ if (id in ids) {
+ return ids[id];
+ } else {
+ return ids[id] = v4();
+ }
+ };
+ const mapFn = (doc: any) => {
+ if (doc.id) {
+ doc.id = getId(doc.id);
+ }
+ for (const key in doc.fields) {
+ if (!doc.fields.hasOwnProperty(key)) {
+ continue;
+ }
+ const field = doc.fields[key];
+ if (field === undefined || field === null) {
+ continue;
+ }
+
+ if (field.__type === "proxy" || field.__type === "prefetch_proxy") {
+ field.fieldId = getId(field.fieldId);
+ } else if (field.__type === "script" || field.__type === "computed") {
+ if (field.captures) {
+ field.captures.fieldId = getId(field.captures.fieldId);
+ }
+ } else if (field.__type === "list") {
+ mapFn(field);
+ } else if (typeof field === "string") {
+ const re = /("(?:dataD|d)ocumentId"\s*:\s*")([\w\-]*)"/g;
+ doc.fields[key] = (field as any).replace(re, (match: any, p1: string, p2: string) => {
+ return `${p1}${getId(p2)}"`;
+ });
+ } else if (field.__type === "RichTextField") {
+ const re = /("href"\s*:\s*")(.*?)"/g;
+ field.Data = field.Data.replace(re, (match: any, p1: string, p2: string) => {
+ return `${p1}${getId(p2)}"`;
+ });
+ }
+ }
+ };
+ form.parse(req, async (err, fields, files) => {
+ remap = fields.remap !== "false";
+ let id: string = "";
+ try {
+ for (const name in files) {
+ const path_2 = files[name].path;
+ const zip = new AdmZip(path_2);
+ zip.getEntries().forEach(entry => {
+ if (!entry.entryName.startsWith("files/")) return;
+ let dirname = path.dirname(entry.entryName) + "/";
+ let extname = path.extname(entry.entryName);
+ let basename = path.basename(entry.entryName).split(".")[0];
+ // zip.extractEntryTo(dirname + basename + "_o" + extname, __dirname + RouteStore.public, true, false);
+ // zip.extractEntryTo(dirname + basename + "_s" + extname, __dirname + RouteStore.public, true, false);
+ // zip.extractEntryTo(dirname + basename + "_m" + extname, __dirname + RouteStore.public, true, false);
+ // zip.extractEntryTo(dirname + basename + "_l" + extname, __dirname + RouteStore.public, true, false);
+ zip.extractEntryTo(entry.entryName, __dirname + RouteStore.public, true, false);
+ dirname = "/" + dirname;
+
+ fs.createReadStream(__dirname + RouteStore.public + dirname + basename + extname).pipe(fs.createWriteStream(__dirname + RouteStore.public + dirname + basename + "_o" + extname));
+ fs.createReadStream(__dirname + RouteStore.public + dirname + basename + extname).pipe(fs.createWriteStream(__dirname + RouteStore.public + dirname + basename + "_s" + extname));
+ fs.createReadStream(__dirname + RouteStore.public + dirname + basename + extname).pipe(fs.createWriteStream(__dirname + RouteStore.public + dirname + basename + "_m" + extname));
+ fs.createReadStream(__dirname + RouteStore.public + dirname + basename + extname).pipe(fs.createWriteStream(__dirname + RouteStore.public + dirname + basename + "_l" + extname));
+ });
+ const json = zip.getEntry("doc.json");
+ let docs: any;
+ try {
+ let data = JSON.parse(json.getData().toString("utf8"));
+ docs = data.docs;
+ id = data.id;
+ docs = Object.keys(docs).map(key => docs[key]);
+ docs.forEach(mapFn);
+ await Promise.all(docs.map((doc: any) => new Promise(res => Database.Instance.replace(doc.id, doc, (err, r) => {
+ err && console.log(err);
+ res();
+ }, true, "newDocuments"))));
+ } catch (e) { console.log(e); }
+ fs.unlink(path_2, () => { });
+ }
+ if (id) {
+ res.send(JSON.stringify(getId(id)));
+ } else {
+ res.send(JSON.stringify("error"));
+ }
+ } catch (e) { console.log(e); }
+ });
+});
+
app.get("/whosOnline", (req, res) => {
let users: any = { active: {}, inactive: {} };
const now = Date.now();
@@ -179,7 +379,6 @@ app.get("/whosOnline", (req, res) => {
res.send(users);
});
-
app.get("/thumbnail/:filename", (req, res) => {
let filename = req.params.filename;
let noExt = filename.substring(0, filename.length - ".png".length);
@@ -285,6 +484,17 @@ addSecureRoute(
RouteStore.getCurrUser
);
+const ServicesApiKeyMap = new Map<string, string | undefined>([
+ ["face", process.env.FACE],
+ ["vision", process.env.VISION],
+ ["handwriting", process.env.HANDWRITING]
+]);
+
+addSecureRoute(Method.GET, (user, res, req) => {
+ let service = req.params.requestedservice;
+ res.send(ServicesApiKeyMap.get(service));
+}, undefined, `${RouteStore.cognitiveServices}/:requestedservice`);
+
class NodeCanvasFactory {
create = (width: number, height: number) => {
var canvas = createCanvas(width, height);
@@ -345,38 +555,6 @@ app.post(
});
isImage = true;
}
- else if (pdfTypes.includes(ext)) {
- // Pdfjs.getDocument(uploadDir + file).promise
- // .then((pdf: Pdfjs.PDFDocumentProxy) => {
- // let numPages = pdf.numPages;
- // let factory = new NodeCanvasFactory();
- // for (let pageNum = 0; pageNum < numPages; pageNum++) {
- // console.log(pageNum);
- // pdf.getPage(pageNum + 1).then((page: Pdfjs.PDFPageProxy) => {
- // console.log("reading " + pageNum);
- // let viewport = page.getViewport(1);
- // let canvasAndContext = factory.create(viewport.width, viewport.height);
- // let renderContext = {
- // canvasContext: canvasAndContext.context,
- // viewport: viewport,
- // canvasFactory: factory
- // }
- // console.log("read " + pageNum);
-
- // page.render(renderContext).promise
- // .then(() => {
- // console.log("saving " + pageNum);
- // let stream = canvasAndContext.canvas.createPNGStream();
- // let out = fs.createWriteStream(uploadDir + file.substring(0, file.length - ext.length) + `-${pageNum + 1}.PNG`);
- // stream.pipe(out);
- // out.on("finish", () => console.log(`Success! Saved to ${uploadDir + file.substring(0, file.length - ext.length) + `-${pageNum + 1}.PNG`}`));
- // }, (reason: string) => {
- // console.error(reason + ` ${pageNum}`);
- // });
- // });
- // }
- // });
- }
if (isImage) {
resizers.forEach(resizer => {
fs.createReadStream(uploadDir + file).pipe(resizer.resizer).pipe(fs.createWriteStream(uploadDir + file.substring(0, file.length - ext.length) + resizer.suffix + ext));
@@ -449,8 +627,22 @@ app.post(RouteStore.forgot, postForgot);
app.get(RouteStore.reset, getReset);
app.post(RouteStore.reset, postReset);
-app.use(RouteStore.corsProxy, (req, res) =>
- req.pipe(request(req.url.substring(1))).pipe(res));
+const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
+app.use(RouteStore.corsProxy, (req, res) => {
+ req.pipe(request(decodeURIComponent(req.url.substring(1)))).on("response", res => {
+ const headers = Object.keys(res.headers);
+ headers.forEach(headerName => {
+ const header = res.headers[headerName];
+ if (Array.isArray(header)) {
+ res.headers[headerName] = header.filter(h => !headerCharRegex.test(h));
+ } else if (header) {
+ if (headerCharRegex.test(header as any)) {
+ delete res.headers[headerName];
+ }
+ }
+ });
+ }).pipe(res);
+});
app.get(RouteStore.delete, (req, res) => {
if (release) {
@@ -505,6 +697,7 @@ server.on("connection", function (socket: Socket) {
}
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));
@@ -559,6 +752,17 @@ function GetRefFields([ids, callback]: [string[], (result?: Transferable[]) => v
Database.Instance.getDocuments(ids, callback, "newDocuments");
}
+function HandleYoutubeQuery([query, callback]: [YoutubeQueryInput, (result?: any[]) => void]) {
+ switch (query.type) {
+ case YoutubeQueryType.Channels:
+ YoutubeApi.authorizedGetChannel(youtubeApiKey);
+ break;
+ case YoutubeQueryType.SearchVideo:
+ YoutubeApi.authorizedGetVideos(youtubeApiKey, query.userInput, callback);
+ case YoutubeQueryType.VideoDetails:
+ YoutubeApi.authorizedGetVideoDetails(youtubeApiKey, query.videoIds, callback);
+ }
+}
const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = {
"number": "_n",
@@ -658,4 +862,5 @@ function CreateField(newValue: any) {
}
server.listen(serverPort);
-console.log(`listening on port ${serverPort}`); \ No newline at end of file
+console.log(`listening on port ${serverPort}`);
+
diff --git a/src/server/remapUrl.ts b/src/server/remapUrl.ts
index 6f4d6642f..69c766d56 100644
--- a/src/server/remapUrl.ts
+++ b/src/server/remapUrl.ts
@@ -6,7 +6,8 @@ const suffixMap: { [type: string]: true } = {
"video": true,
"pdf": true,
"audio": true,
- "web": true
+ "web": true,
+ "image": true
};
async function update() {
@@ -30,10 +31,10 @@ async function update() {
const value = fields[key];
if (value && value.__type && suffixMap[value.__type]) {
const url = new URL(value.url);
- if (url.href.includes("azure")) {
+ if (url.href.includes("localhost") && url.href.includes("Bill")) {
dynfield = true;
- update.$set = { ["fields." + key + ".url"]: `${url.protocol}//localhost:1050${url.pathname}` };
+ update.$set = { ["fields." + key + ".url"]: `${url.protocol}//dash-web.eastus2.cloudapp.azure.com:1050${url.pathname}` };
}
}
}
diff --git a/src/server/youtubeApi/youtubeApiSample.d.ts b/src/server/youtubeApi/youtubeApiSample.d.ts
new file mode 100644
index 000000000..427f54608
--- /dev/null
+++ b/src/server/youtubeApi/youtubeApiSample.d.ts
@@ -0,0 +1,2 @@
+declare const YoutubeApi: any;
+export = YoutubeApi; \ No newline at end of file
diff --git a/src/server/youtubeApi/youtubeApiSample.js b/src/server/youtubeApi/youtubeApiSample.js
new file mode 100644
index 000000000..50b3c7b38
--- /dev/null
+++ b/src/server/youtubeApi/youtubeApiSample.js
@@ -0,0 +1,179 @@
+const fs = require('fs');
+const readline = require('readline');
+const { google } = require('googleapis');
+const OAuth2 = google.auth.OAuth2;
+
+
+// If modifying these scopes, delete your previously saved credentials
+// at ~/.credentials/youtube-nodejs-quickstart.json
+let SCOPES = ['https://www.googleapis.com/auth/youtube.readonly'];
+let TOKEN_DIR = (process.env.HOME || process.env.HOMEPATH ||
+ process.env.USERPROFILE) + '/.credentials/';
+let TOKEN_PATH = TOKEN_DIR + 'youtube-nodejs-quickstart.json';
+
+module.exports.readApiKey = (callback) => {
+ fs.readFile('client_secret.json', function processClientSecrets(err, content) {
+ if (err) {
+ console.log('Error loading client secret file: ' + err);
+ return;
+ }
+ callback(content);
+ });
+}
+
+module.exports.authorizedGetChannel = (apiKey) => {
+ //this didnt get called
+ // Authorize a client with the loaded credentials, then call the YouTube API.
+ authorize(JSON.parse(apiKey), getChannel);
+}
+
+module.exports.authorizedGetVideos = (apiKey, userInput, callBack) => {
+ authorize(JSON.parse(apiKey), getVideos, { userInput: userInput, callBack: callBack });
+}
+
+module.exports.authorizedGetVideoDetails = (apiKey, videoIds, callBack) => {
+ authorize(JSON.parse(apiKey), getVideoDetails, { videoIds: videoIds, callBack: callBack });
+}
+
+
+/**
+ * Create an OAuth2 client with the given credentials, and then execute the
+ * given callback function.
+ *
+ * @param {Object} credentials The authorization client credentials.
+ * @param {function} callback The callback to call with the authorized client.
+ */
+function authorize(credentials, callback, args = {}) {
+ let clientSecret = credentials.installed.client_secret;
+ let clientId = credentials.installed.client_id;
+ let redirectUrl = credentials.installed.redirect_uris[0];
+ let oauth2Client = new OAuth2(clientId, clientSecret, redirectUrl);
+
+ // Check if we have previously stored a token.
+ fs.readFile(TOKEN_PATH, function (err, token) {
+ if (err) {
+ getNewToken(oauth2Client, callback);
+ } else {
+ oauth2Client.credentials = JSON.parse(token);
+ callback(oauth2Client, args);
+ }
+ });
+}
+
+/**
+ * Get and store new token after prompting for user authorization, and then
+ * execute the given callback with the authorized OAuth2 client.
+ *
+ * @param {google.auth.OAuth2} oauth2Client The OAuth2 client to get token for.
+ * @param {getEventsCallback} callback The callback to call with the authorized
+ * client.
+ */
+function getNewToken(oauth2Client, callback) {
+ var authUrl = oauth2Client.generateAuthUrl({
+ access_type: 'offline',
+ scope: SCOPES
+ });
+ console.log('Authorize this app by visiting this url: ', authUrl);
+ var rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout
+ });
+ rl.question('Enter the code from that page here: ', function (code) {
+ rl.close();
+ oauth2Client.getToken(code, function (err, token) {
+ if (err) {
+ console.log('Error while trying to retrieve access token', err);
+ return;
+ }
+ oauth2Client.credentials = token;
+ storeToken(token);
+ callback(oauth2Client);
+ });
+ });
+}
+
+/**
+ * Store token to disk be used in later program executions.
+ *
+ * @param {Object} token The token to store to disk.
+ */
+function storeToken(token) {
+ try {
+ fs.mkdirSync(TOKEN_DIR);
+ } catch (err) {
+ if (err.code != 'EEXIST') {
+ throw err;
+ }
+ }
+ fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) => {
+ if (err) throw err;
+ console.log('Token stored to ' + TOKEN_PATH);
+ });
+ console.log('Token stored to ' + TOKEN_PATH);
+}
+
+/**
+ * Lists the names and IDs of up to 10 files.
+ *
+ * @param {google.auth.OAuth2} auth An authorized OAuth2 client.
+ */
+function getChannel(auth) {
+ var service = google.youtube('v3');
+ service.channels.list({
+ auth: auth,
+ part: 'snippet,contentDetails,statistics',
+ forUsername: 'GoogleDevelopers'
+ }, function (err, response) {
+ if (err) {
+ console.log('The API returned an error: ' + err);
+ return;
+ }
+ var channels = response.data.items;
+ if (channels.length == 0) {
+ console.log('No channel found.');
+ } else {
+ console.log('This channel\'s ID is %s. Its title is \'%s\', and ' +
+ 'it has %s views.',
+ channels[0].id,
+ channels[0].snippet.title,
+ channels[0].statistics.viewCount);
+ }
+ });
+}
+
+function getVideos(auth, args) {
+ let service = google.youtube('v3');
+ service.search.list({
+ auth: auth,
+ part: 'id, snippet',
+ type: 'video',
+ q: args.userInput,
+ maxResults: 10
+ }, function (err, response) {
+ if (err) {
+ console.log('The API returned an error: ' + err);
+ return;
+ }
+ let videos = response.data.items;
+ args.callBack(videos);
+ });
+}
+
+function getVideoDetails(auth, args) {
+ if (args.videoIds === undefined) {
+ return;
+ }
+ let service = google.youtube('v3');
+ service.videos.list({
+ auth: auth,
+ part: 'contentDetails, statistics',
+ id: args.videoIds
+ }, function (err, response) {
+ if (err) {
+ console.log('The API returned an error from details: ' + err);
+ return;
+ }
+ let videoDetails = response.data.items;
+ args.callBack(videoDetails);
+ });
+} \ No newline at end of file