aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/ClientRecommender.tsx2
-rw-r--r--src/client/DocServer.ts122
-rw-r--r--src/client/apis/GoogleAuthenticationManager.tsx6
-rw-r--r--src/client/apis/HypothesisAuthenticationManager.scss26
-rw-r--r--src/client/apis/HypothesisAuthenticationManager.tsx160
-rw-r--r--src/client/apis/IBM_Recommender.ts2
-rw-r--r--src/client/apis/google_docs/GooglePhotosClientUtils.ts2
-rw-r--r--src/client/cognitive_services/CognitiveServices.ts2
-rw-r--r--src/client/documents/DocumentTypes.ts1
-rw-r--r--src/client/documents/Documents.ts113
-rw-r--r--src/client/util/CurrentUserUtils.ts325
-rw-r--r--src/client/util/DictationManager.ts2
-rw-r--r--src/client/util/DocumentManager.ts50
-rw-r--r--src/client/util/DragManager.ts66
-rw-r--r--src/client/util/DropConverter.ts2
-rw-r--r--src/client/util/GroupManager.scss116
-rw-r--r--src/client/util/GroupManager.tsx264
-rw-r--r--src/client/util/GroupMemberView.scss58
-rw-r--r--src/client/util/GroupMemberView.tsx49
-rw-r--r--src/client/util/InteractionUtils.tsx75
-rw-r--r--src/client/util/LinkManager.ts19
-rw-r--r--src/client/util/ScriptManager.ts10
-rw-r--r--src/client/util/Scripting.ts10
-rw-r--r--src/client/util/SelectionManager.ts2
-rw-r--r--src/client/util/SettingsManager.scss25
-rw-r--r--src/client/util/SettingsManager.tsx40
-rw-r--r--src/client/util/SharingManager.scss180
-rw-r--r--src/client/util/SharingManager.tsx521
-rw-r--r--src/client/views/AntimodeMenu.scss31
-rw-r--r--src/client/views/AntimodeMenu.tsx26
-rw-r--r--src/client/views/ContextMenu.scss16
-rw-r--r--src/client/views/ContextMenuItem.tsx10
-rw-r--r--src/client/views/DictationOverlay.tsx1
-rw-r--r--src/client/views/DocComponent.tsx30
-rw-r--r--src/client/views/DocumentButtonBar.tsx184
-rw-r--r--src/client/views/DocumentDecorations.scss5
-rw-r--r--src/client/views/DocumentDecorations.tsx138
-rw-r--r--src/client/views/EditableView.tsx45
-rw-r--r--src/client/views/GestureOverlay.scss2
-rw-r--r--src/client/views/GestureOverlay.tsx233
-rw-r--r--src/client/views/GlobalKeyHandler.ts21
-rw-r--r--src/client/views/InkingStroke.tsx134
-rw-r--r--src/client/views/KeyphraseQueryView.tsx1
-rw-r--r--src/client/views/MainView.scss4
-rw-r--r--src/client/views/MainView.tsx143
-rw-r--r--src/client/views/MainViewModal.scss5
-rw-r--r--src/client/views/MainViewModal.tsx7
-rw-r--r--src/client/views/MetadataEntryMenu.tsx100
-rw-r--r--src/client/views/OverlayView.scss2
-rw-r--r--src/client/views/OverlayView.tsx4
-rw-r--r--src/client/views/PreviewCursor.tsx4
-rw-r--r--src/client/views/RecommendationsBox.scss69
-rw-r--r--src/client/views/RecommendationsBox.tsx201
-rw-r--r--src/client/views/ScriptBox.tsx2
-rw-r--r--src/client/views/SearchDocBox.tsx9
-rw-r--r--src/client/views/TemplateMenu.tsx16
-rw-r--r--src/client/views/Touchable.tsx5
-rw-r--r--src/client/views/animationtimeline/Keyframe.tsx4
-rw-r--r--src/client/views/animationtimeline/Timeline.tsx5
-rw-r--r--src/client/views/animationtimeline/Track.tsx4
-rw-r--r--src/client/views/collections/CollectionCarousel3DView.tsx26
-rw-r--r--src/client/views/collections/CollectionCarouselView.tsx23
-rw-r--r--src/client/views/collections/CollectionDockingView.scss18
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx151
-rw-r--r--src/client/views/collections/CollectionLinearView.scss76
-rw-r--r--src/client/views/collections/CollectionLinearView.tsx74
-rw-r--r--src/client/views/collections/CollectionMasonryViewFieldRow.tsx1
-rw-r--r--src/client/views/collections/CollectionMenu.scss (renamed from src/client/views/collections/CollectionViewChromes.scss)117
-rw-r--r--src/client/views/collections/CollectionMenu.tsx974
-rw-r--r--src/client/views/collections/CollectionPileView.tsx28
-rw-r--r--src/client/views/collections/CollectionSchemaCells.tsx11
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx1
-rw-r--r--src/client/views/collections/CollectionStackingView.scss14
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx12
-rw-r--r--src/client/views/collections/CollectionStackingViewFieldColumn.tsx68
-rw-r--r--src/client/views/collections/CollectionSubView.tsx109
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx39
-rw-r--r--src/client/views/collections/CollectionView.tsx185
-rw-r--r--src/client/views/collections/CollectionViewChromes.tsx800
-rw-r--r--src/client/views/collections/ParentDocumentSelector.tsx22
-rw-r--r--src/client/views/collections/SchemaTable.tsx5
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx40
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss1
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx80
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx218
-rw-r--r--src/client/views/collections/collectionFreeForm/FormatShapePane.scss (renamed from src/client/views/collections/collectionFreeForm/InkOptionsMenu.scss)29
-rw-r--r--src/client/views/collections/collectionFreeForm/FormatShapePane.tsx487
-rw-r--r--src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx328
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx39
-rw-r--r--src/client/views/collections/collectionGrid/CollectionGridView.scss3
-rw-r--r--src/client/views/collections/collectionGrid/CollectionGridView.tsx60
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx7
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx6
-rw-r--r--src/client/views/linking/LinkEditor.scss170
-rw-r--r--src/client/views/linking/LinkEditor.tsx152
-rw-r--r--src/client/views/linking/LinkMenu.scss81
-rw-r--r--src/client/views/linking/LinkMenu.tsx42
-rw-r--r--src/client/views/linking/LinkMenuGroup.tsx16
-rw-r--r--src/client/views/linking/LinkMenuItem.scss120
-rw-r--r--src/client/views/linking/LinkMenuItem.tsx128
-rw-r--r--src/client/views/nodes/AudioBox.scss154
-rw-r--r--src/client/views/nodes/AudioBox.tsx7
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx13
-rw-r--r--src/client/views/nodes/ColorBox.tsx15
-rw-r--r--src/client/views/nodes/ComparisonBox.scss2
-rw-r--r--src/client/views/nodes/ComparisonBox.tsx2
-rw-r--r--src/client/views/nodes/ContentFittingDocumentView.tsx2
-rw-r--r--src/client/views/nodes/DocHolderBox.tsx2
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx12
-rw-r--r--src/client/views/nodes/DocumentLinksButton.scss17
-rw-r--r--src/client/views/nodes/DocumentLinksButton.tsx225
-rw-r--r--src/client/views/nodes/DocumentView.tsx577
-rw-r--r--src/client/views/nodes/FieldView.tsx6
-rw-r--r--src/client/views/nodes/FontIconBox.scss4
-rw-r--r--src/client/views/nodes/FontIconBox.tsx11
-rw-r--r--src/client/views/nodes/ImageBox.scss2
-rw-r--r--src/client/views/nodes/ImageBox.tsx68
-rw-r--r--src/client/views/nodes/LabelBox.tsx11
-rw-r--r--src/client/views/nodes/LinkAnchorBox.scss7
-rw-r--r--src/client/views/nodes/LinkAnchorBox.tsx5
-rw-r--r--src/client/views/nodes/LinkDescriptionPopup.scss77
-rw-r--r--src/client/views/nodes/LinkDescriptionPopup.tsx65
-rw-r--r--src/client/views/nodes/LinkDocPreview.tsx47
-rw-r--r--src/client/views/nodes/PDFBox.scss325
-rw-r--r--src/client/views/nodes/PDFBox.tsx11
-rw-r--r--src/client/views/nodes/PresBox.scss65
-rw-r--r--src/client/views/nodes/PresBox.tsx1
-rw-r--r--src/client/views/nodes/RadialMenu.tsx7
-rw-r--r--src/client/views/nodes/ScreenshotBox.tsx2
-rw-r--r--src/client/views/nodes/ScriptingBox.tsx3
-rw-r--r--src/client/views/nodes/SliderBox.tsx2
-rw-r--r--src/client/views/nodes/TaskCompletedBox.scss20
-rw-r--r--src/client/views/nodes/TaskCompletedBox.tsx32
-rw-r--r--src/client/views/nodes/VideoBox.tsx2
-rw-r--r--src/client/views/nodes/WebBox.scss222
-rw-r--r--src/client/views/nodes/WebBox.tsx407
-rw-r--r--src/client/views/nodes/formattedText/DashDocView.tsx2
-rw-r--r--src/client/views/nodes/formattedText/DashFieldView.tsx4
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.scss320
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx224
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBoxComment.scss92
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx159
-rw-r--r--src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts13
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx258
-rw-r--r--src/client/views/nodes/formattedText/RichTextRules.ts9
-rw-r--r--src/client/views/nodes/formattedText/RichTextSchema.tsx12
-rw-r--r--src/client/views/nodes/formattedText/marks_rts.ts12
-rw-r--r--src/client/views/nodes/formattedText/nodes_rts.ts27
-rw-r--r--src/client/views/pdf/PDFMenu.tsx9
-rw-r--r--src/client/views/pdf/PDFViewer.tsx28
-rw-r--r--src/client/views/presentationview/PresElementBox.scss64
-rw-r--r--src/client/views/presentationview/PresElementBox.tsx6
-rw-r--r--src/client/views/search/FilterBox.tsx1
-rw-r--r--src/client/views/search/SearchBox.tsx4
-rw-r--r--src/client/views/search/ToggleBar.tsx1
-rw-r--r--src/fields/Doc.ts218
-rw-r--r--src/fields/Proxy.ts8
-rw-r--r--src/fields/Schema.ts4
-rw-r--r--src/fields/ScriptField.ts26
-rw-r--r--src/fields/documentSchemas.ts10
-rw-r--r--src/fields/util.ts136
-rw-r--r--src/mobile/AudioUpload.scss61
-rw-r--r--src/mobile/AudioUpload.tsx153
-rw-r--r--src/mobile/ImageUpload.scss130
-rw-r--r--src/mobile/ImageUpload.tsx228
-rw-r--r--src/mobile/MobileInkOverlay.tsx4
-rw-r--r--src/mobile/MobileInterface.scss444
-rw-r--r--src/mobile/MobileInterface.tsx871
-rw-r--r--src/mobile/MobileMain.tsx25
-rw-r--r--src/mobile/MobileMenu.scss0
-rw-r--r--src/pen-gestures/ndollar.ts2
-rw-r--r--src/server/ApiManagers/DownloadManager.ts12
-rw-r--r--src/server/ApiManagers/HypothesisManager.ts44
-rw-r--r--src/server/ApiManagers/PDFManager.ts2
-rw-r--r--src/server/ApiManagers/UploadManager.ts19
-rw-r--r--src/server/Recommender.ts4
-rw-r--r--src/server/apis/google/GoogleApiServerUtils.ts3
-rw-r--r--src/server/downsize.ts2
-rw-r--r--src/server/index.ts4
-rw-r--r--src/server/websocket.ts9
180 files changed, 9611 insertions, 4955 deletions
diff --git a/src/client/ClientRecommender.tsx b/src/client/ClientRecommender.tsx
index d18669b02..3f875057e 100644
--- a/src/client/ClientRecommender.tsx
+++ b/src/client/ClientRecommender.tsx
@@ -50,7 +50,6 @@ export class ClientRecommender extends React.Component<RecommenderProps> {
@observable private corr_matrix = [[0, 0], [0, 0]]; // for testing
constructor(props: RecommenderProps) {
- //console.log("creating client recommender...");
super(props);
if (!ClientRecommender.Instance) ClientRecommender.Instance = this;
ClientRecommender.Instance.docVectors = new Set();
@@ -383,7 +382,6 @@ export class ClientRecommender extends React.Component<RecommenderProps> {
case 200:
const title_vals: string[] = [];
const url_vals: string[] = [];
- //console.log(result);
if (xml) {
const titles = xml.getElementsByTagName("title");
let counter = 1;
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index 2a7a7c59a..f5ee6da3c 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -1,13 +1,15 @@
import * as io from 'socket.io-client';
import { MessageStore, YoutubeQueryTypes, GestureContent, MobileInkOverlayContent, UpdateMobileInkOverlayPositionContent, MobileDocumentUploadContent } from "./../server/Message";
-import { Opt, Doc, fetchProto } from '../fields/Doc';
+import { Opt, Doc, fetchProto, FieldsSym, UpdatingFromServer } from '../fields/Doc';
import { Utils, emptyFunction } from '../Utils';
import { SerializationHelper } from './util/SerializationHelper';
import { RefField } from '../fields/RefField';
-import { Id, HandleUpdate } from '../fields/FieldSymbols';
+import { Id, HandleUpdate, Parent } from '../fields/FieldSymbols';
import GestureOverlay from './views/GestureOverlay';
import MobileInkOverlay from '../mobile/MobileInkOverlay';
import { runInAction } from 'mobx';
+import { ObjectField } from '../fields/ObjectField';
+import { getPlaygroundMode } from '../fields/util';
/**
* This class encapsulates the transfer and cross-client synchronization of
@@ -31,7 +33,7 @@ export namespace DocServer {
export enum WriteMode {
Default = 0, //Anything goes
- Playground = 1, //Playground (write own/no read)
+ Playground = 1, //Playground (write own/no read other updates)
LiveReadonly = 2,//Live Readonly (no write/read others)
LivePlayground = 3,//Live Playground (write own/read others)
}
@@ -39,9 +41,9 @@ export namespace DocServer {
const docsWithUpdates: { [field: string]: Set<Doc> } = {};
export var PlaygroundFields: string[];
- export function setPlaygroundFields(livePlayougroundFields: string[]) {
- DocServer.PlaygroundFields = livePlayougroundFields;
- livePlayougroundFields.forEach(f => DocServer.setFieldWriteMode(f, DocServer.WriteMode.LivePlayground));
+ export function setPlaygroundFields(livePlaygroundFields: string[]) {
+ DocServer.PlaygroundFields = livePlaygroundFields;
+ livePlaygroundFields.forEach(f => DocServer.setFieldWriteMode(f, DocServer.WriteMode.LivePlayground));
}
export function setFieldWriteMode(field: string, writeMode: WriteMode) {
@@ -155,23 +157,23 @@ export namespace DocServer {
let _isReadOnly = false;
export function makeReadOnly() {
- if (_isReadOnly) return;
- _isReadOnly = true;
- _CreateField = field => {
- _cache[field[Id]] = field;
- };
- _UpdateField = emptyFunction;
- _RespondToUpdate = emptyFunction;
+ if (!_isReadOnly) {
+ _isReadOnly = true;
+ _CreateField = field => _cache[field[Id]] = field;
+ _UpdateField = emptyFunction;
+ _RespondToUpdate = emptyFunction;
+ }
}
export function makeEditable() {
- if (!_isReadOnly) return;
- location.reload();
- // _isReadOnly = false;
- // _CreateField = _CreateFieldImpl;
- // _UpdateField = _UpdateFieldImpl;
- // _respondToUpdate = _respondToUpdateImpl;
- // _cache = {};
+ if (_isReadOnly) {
+ location.reload();
+ // _isReadOnly = false;
+ // _CreateField = _CreateFieldImpl;
+ // _UpdateField = _UpdateFieldImpl;
+ // _respondToUpdate = _respondToUpdateImpl;
+ // _cache = {};
+ }
}
export function isReadOnly() { return _isReadOnly; }
@@ -207,12 +209,12 @@ export namespace DocServer {
* the server if the document has not been cached.
* @param id the id of the requested document
*/
- const _GetRefFieldImpl = (id: string): Promise<Opt<RefField>> => {
+ const _GetRefFieldImpl = (id: string, force: boolean = false): Promise<Opt<RefField>> => {
// an initial pass through the cache to determine whether the document needs to be fetched,
// is already in the process of being fetched or already exists in the
// cache
const cached = _cache[id];
- if (cached === undefined) {
+ if (cached === undefined || force) {
// NOT CACHED => we'll have to send a request to the server
// synchronously, we emit a single callback to the server requesting the serialized (i.e. represented by a string)
@@ -226,7 +228,19 @@ export namespace DocServer {
const deserializeField = getSerializedField.then(async fieldJson => {
// deserialize
const field = await SerializationHelper.Deserialize(fieldJson);
- if (field !== undefined) {
+ if (force && field instanceof Doc && cached instanceof Doc) {
+ cached[UpdatingFromServer] = true;
+ Array.from(Object.keys(field)).forEach(key => {
+ const fieldval = field[key];
+ if (fieldval instanceof ObjectField) {
+ fieldval[Parent] = undefined;
+ }
+ cached[key] = field[key];
+ });
+ cached[UpdatingFromServer] = false;
+ return cached;
+ }
+ else if (field !== undefined) {
_cache[id] = field;
} else {
delete _cache[id];
@@ -238,8 +252,8 @@ export namespace DocServer {
});
// here, indicate that the document associated with this id is currently
// being retrieved and cached
- _cache[id] = deserializeField;
- return deserializeField;
+ !force && (_cache[id] = deserializeField);
+ return force ? cached as any : deserializeField;
} else if (cached instanceof Promise) {
// BEING RETRIEVED AND CACHED => some other caller previously (likely recently) called GetRefField(s),
// and requested the document I'm looking for. Shouldn't fetch again, just
@@ -260,11 +274,11 @@ export namespace DocServer {
}
};
- let _GetRefField: (id: string) => Promise<Opt<RefField>> = errorFunc;
+ let _GetRefField: (id: string, force: boolean) => Promise<Opt<RefField>> = errorFunc;
let _GetCachedRefField: (id: string) => Opt<RefField> = errorFunc;
- export function GetRefField(id: string): Promise<Opt<RefField>> {
- return _GetRefField(id);
+ export function GetRefField(id: string, force = false): Promise<Opt<RefField>> {
+ return _GetRefField(id, force);
}
export function GetCachedRefField(id: string): Opt<RefField> {
return _GetCachedRefField(id);
@@ -330,29 +344,35 @@ export namespace DocServer {
const proms: Promise<void>[] = [];
runInAction(() => {
for (const field of fields) {
- if (field !== undefined && field !== null) {
+ if (field !== undefined && field !== null && !_cache[field.id]) {
// deserialize
- const 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
- proms.push(prom);
+ const cached = _cache[field.id];
+ if (!cached) {
+ const 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
+ proms.push(prom);
+ } else if (cached instanceof Promise) {
+ proms.push(cached as any);
+ }
}
}
});
@@ -432,7 +452,7 @@ export namespace DocServer {
}
function _UpdateFieldImpl(id: string, diff: any) {
- Utils.Emit(_socket, MessageStore.UpdateField, { id, diff });
+ (!getPlaygroundMode()) && Utils.Emit(_socket, MessageStore.UpdateField, { id, diff });
}
let _UpdateField: (id: string, diff: any) => void = errorFunc;
diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx
index bf4469aeb..117d1fa1e 100644
--- a/src/client/apis/GoogleAuthenticationManager.tsx
+++ b/src/client/apis/GoogleAuthenticationManager.tsx
@@ -146,7 +146,7 @@ export default class GoogleAuthenticationManager extends React.Component<{}> {
private get dialogueBoxStyle() {
const borderColor = this.success === undefined ? "black" : this.success ? "green" : "red";
- return { borderColor, transition: "0.2s borderColor ease" };
+ return { borderColor, transition: "0.2s borderColor ease", zIndex: 1002 };
}
render() {
@@ -155,8 +155,10 @@ export default class GoogleAuthenticationManager extends React.Component<{}> {
isDisplayed={this.openState}
interactive={true}
contents={this.renderPrompt}
- overlayDisplayedOpacity={0.9}
+ // overlayDisplayedOpacity={0.9}
dialogueBoxStyle={this.dialogueBoxStyle}
+ overlayStyle={{ zIndex: 1001 }}
+ closeOnExternalClick={action(() => this.isOpen = false)}
/>
);
}
diff --git a/src/client/apis/HypothesisAuthenticationManager.scss b/src/client/apis/HypothesisAuthenticationManager.scss
new file mode 100644
index 000000000..bd30dd94f
--- /dev/null
+++ b/src/client/apis/HypothesisAuthenticationManager.scss
@@ -0,0 +1,26 @@
+.authorize-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ .paste-target {
+ padding: 5px;
+ width: 100%;
+ }
+
+ .avatar {
+ border-radius: 50%;
+ }
+
+ .welcome {
+ font-style: italic;
+ margin-top: 15px;
+ }
+
+ .disconnect {
+ font-size: 10px;
+ margin-top: 20px;
+ color: red;
+ cursor: grab;
+ }
+} \ No newline at end of file
diff --git a/src/client/apis/HypothesisAuthenticationManager.tsx b/src/client/apis/HypothesisAuthenticationManager.tsx
new file mode 100644
index 000000000..c3e8d2fff
--- /dev/null
+++ b/src/client/apis/HypothesisAuthenticationManager.tsx
@@ -0,0 +1,160 @@
+import { observable, action, reaction, runInAction, IReactionDisposer } from "mobx";
+import { observer } from "mobx-react";
+import * as React from "react";
+import MainViewModal from "../views/MainViewModal";
+import { Opt } from "../../fields/Doc";
+import { Networking } from "../Network";
+import "./HypothesisAuthenticationManager.scss";
+import { Scripting } from "../util/Scripting";
+
+const prompt = "Paste authorization code here...";
+
+@observer
+export default class HypothesisAuthenticationManager extends React.Component<{}> {
+ public static Instance: HypothesisAuthenticationManager;
+ private authenticationLink: Opt<string> = undefined;
+ @observable private openState = false;
+ @observable private authenticationCode: Opt<string> = undefined;
+ @observable private showPasteTargetState = false;
+ @observable private success: Opt<boolean> = undefined;
+ @observable private displayLauncher = true;
+ @observable private credentials: string;
+ private disposer: Opt<IReactionDisposer>;
+
+ private set isOpen(value: boolean) {
+ runInAction(() => this.openState = value);
+ }
+
+ private set shouldShowPasteTarget(value: boolean) {
+ runInAction(() => this.showPasteTargetState = value);
+ }
+
+ public cancel() {
+ this.openState && this.resetState(0, 0);
+ }
+
+ public fetchAccessToken = async (displayIfFound = false) => {
+ const response: any = await Networking.FetchFromServer("/readHypothesisAccessToken");
+ // if this is an authentication url, activate the UI to register the new access token
+ if (!response) { // new RegExp(AuthenticationUrl).test(response)) {
+ this.isOpen = true;
+ this.authenticationLink = response;
+ return new Promise<string>(async resolve => {
+ this.disposer?.();
+ this.disposer = reaction(
+ () => this.authenticationCode,
+ async authenticationCode => {
+ if (authenticationCode) {
+ this.disposer?.();
+ Networking.PostToServer("/writeHypothesisAccessToken", { authenticationCode });
+ runInAction(() => {
+ this.success = true;
+ this.credentials = response;
+ });
+ this.resetState();
+ resolve(authenticationCode);
+ }
+ }
+ );
+ });
+ }
+
+ if (displayIfFound) {
+ runInAction(() => {
+ this.success = true;
+ this.credentials = response;
+ });
+ this.resetState(-1, -1);
+ this.isOpen = true;
+ }
+ return response.access_token;
+ }
+
+ resetState = action((visibleForMS: number = 3000, fadesOutInMS: number = 500) => {
+ if (!visibleForMS && !fadesOutInMS) {
+ runInAction(() => {
+ this.isOpen = false;
+ this.success = undefined;
+ this.displayLauncher = true;
+ this.credentials = "";
+ this.shouldShowPasteTarget = false;
+ this.authenticationCode = undefined;
+ });
+ return;
+ }
+ this.authenticationCode = undefined;
+ this.displayLauncher = false;
+ this.shouldShowPasteTarget = false;
+ if (visibleForMS > 0 && fadesOutInMS > 0) {
+ setTimeout(action(() => {
+ this.isOpen = false;
+ setTimeout(action(() => {
+ this.success = undefined;
+ this.displayLauncher = true;
+ this.credentials = "";
+ }), fadesOutInMS);
+ }), visibleForMS);
+ }
+ });
+
+ constructor(props: {}) {
+ super(props);
+ HypothesisAuthenticationManager.Instance = this;
+ }
+
+ private get renderPrompt() {
+ return (
+ <div className={'authorize-container'}>
+
+ {this.displayLauncher ? <button
+ className={"dispatch"}
+ onClick={() => {
+ this.shouldShowPasteTarget = true;
+ }}
+ style={{ marginBottom: this.showPasteTargetState ? 15 : 0 }}
+ >Authorize a Hypothesis account...</button> : (null)}
+ {this.showPasteTargetState ? <input
+ className={'paste-target'}
+ onChange={action(e => this.authenticationCode = e.currentTarget.value)}
+ placeholder={prompt}
+ /> : (null)}
+ {this.credentials ?
+ <>
+ <span
+ className={'welcome'}
+ >Welcome to Dash, {this.credentials}
+ </span>
+ <div
+ className={'disconnect'}
+ onClick={async () => {
+ await Networking.FetchFromServer("/revokeHypothesisAccessToken");
+ this.resetState(0, 0);
+ }}
+ >Disconnect Account</div>
+ </> : (null)}
+ </div>
+ );
+ }
+
+ private get dialogueBoxStyle() {
+ const borderColor = this.success === undefined ? "black" : this.success ? "green" : "red";
+ return { borderColor, transition: "0.2s borderColor ease", zIndex: 1002 };
+ }
+
+ render() {
+ return (
+ <MainViewModal
+ isDisplayed={this.openState}
+ interactive={true}
+ contents={this.renderPrompt}
+ // overlayDisplayedOpacity={0.9}
+ dialogueBoxStyle={this.dialogueBoxStyle}
+ overlayStyle={{ zIndex: 1001 }}
+ closeOnExternalClick={action(() => this.isOpen = false)}
+ />
+ );
+ }
+
+}
+
+Scripting.addGlobal("HypothesisAuthenticationManager", HypothesisAuthenticationManager); \ No newline at end of file
diff --git a/src/client/apis/IBM_Recommender.ts b/src/client/apis/IBM_Recommender.ts
index 480b9cb1c..e6265fcb5 100644
--- a/src/client/apis/IBM_Recommender.ts
+++ b/src/client/apis/IBM_Recommender.ts
@@ -29,10 +29,8 @@
// export const analyze = async (_parameters: any): Promise<Opt<string>> => {
// try {
// const response = await naturalLanguageUnderstanding.analyze(_parameters);
-// console.log(response);
// return (JSON.stringify(response, null, 2));
// } catch (err) {
-// console.log('error: ', err);
// return undefined;
// }
// };
diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index 13bfb3a91..92eaf2e73 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -156,8 +156,6 @@ export namespace GooglePhotos {
const values = Object.values(ContentCategories).filter(value => value !== ContentCategories.NONE);
for (const value of values) {
const searched = (await ContentSearch({ included: [value] }))?.mediaItems?.map(({ id }) => id);
- console.log("Searching " + value);
- console.log(searched);
searched?.forEach(async id => {
const image = await Cast(idMapping[id], Doc);
if (image) {
diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts
index 6b0b3e029..80961af14 100644
--- a/src/client/cognitive_services/CognitiveServices.ts
+++ b/src/client/cognitive_services/CognitiveServices.ts
@@ -377,7 +377,6 @@ export namespace CognitiveServices {
console.log("successful vectorization!");
const vectorValues = new List<number>();
indices.forEach((ind: any) => {
- //console.log(wordvec.word);
vectorValues.push(wordvecs[ind]);
});
ClientRecommender.Instance.processVector(vectorValues, dataDoc, mainDoc);
@@ -385,7 +384,6 @@ export namespace CognitiveServices {
else {
console.log("unsuccessful :( word(s) not in vocabulary");
}
- //console.log(vectorValues.size);
}
);
}
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 86d8c6db7..71d6c2ccc 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -37,6 +37,5 @@ export enum DocumentType {
LINKDB = "linkdb", // database of links ??? why do we have this
SCRIPTDB = "scriptdb", // database of scripts
- RECOMMENDATION = "recommendation", // view of a recommendation
GROUPDB = "groupdb" // database of groups
} \ No newline at end of file
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 5a2d746d6..a71450319 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1,4 +1,4 @@
-import { runInAction } from "mobx";
+import { runInAction, action } from "mobx";
import { extname, basename } from "path";
import { DateField } from "../../fields/DateField";
import { Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym } from "../../fields/Doc";
@@ -47,7 +47,6 @@ import { SliderBox } from "../views/nodes/SliderBox";
import { VideoBox } from "../views/nodes/VideoBox";
import { WebBox } from "../views/nodes/WebBox";
import { PresElementBox } from "../views/presentationview/PresElementBox";
-import { RecommendationsBox } from "../views/RecommendationsBox";
import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo";
import { DocumentType } from "./DocumentTypes";
import { Networking } from "../Network";
@@ -71,6 +70,7 @@ export interface DocumentOptions {
_showTitle?: string; // which field to display in the title area. leave empty to have no title
_showCaption?: string; // which field to display in the caption area. leave empty to have no caption
_scrollTop?: number; // scroll location for pdfs
+ _noAutoscroll?: boolean;// whether collections autoscroll when this item is dragged
_chromeStatus?: string;
_viewType?: string; // sub type of a collection
_gridGap?: number; // gap between items in masonry view
@@ -93,9 +93,11 @@ export interface DocumentOptions {
layoutKey?: string;
type?: string;
title?: string;
- label?: string; // short form of title for use as an icon label
+ label?: string;
+ toolTip?: string; // tooltip to display on hover
style?: string;
page?: number;
+ description?: string; // added for links
_viewScale?: number;
isDisplayPanel?: boolean; // whether the panel functions as GoldenLayout "stack" used to display documents
forceActive?: boolean;
@@ -103,6 +105,8 @@ export interface DocumentOptions {
childLayoutTemplate?: Doc; // template for collection to use to render its children (see PresBox or Buxton layout in tree view)
childLayoutString?: string; // template string for collection to use to render its children
hideFilterView?: boolean; // whether to hide the filter popout on collections
+ hideLinkButton?: boolean; // whether the blue link counter button should be hidden
+ hideAllLinks?: boolean; // whether all individual blue anchor dots should be hidden
_columnsHideIfEmpty?: boolean; // whether stacking view column headings should be hidden
isTemplateForField?: string; // the field key for which the containing document is a rendering template
isTemplateDoc?: boolean;
@@ -124,7 +128,7 @@ export interface DocumentOptions {
isBackground?: boolean;
isLinkButton?: boolean;
_columnWidth?: number;
- _fontSize?: number;
+ _fontSize?: string;
_fontFamily?: string;
curPage?: number;
currentTimecode?: number; // the current timecode of a time-based document (e.g., current time of a video) value is in seconds
@@ -137,6 +141,8 @@ export interface DocumentOptions {
dontRegisterChildViews?: boolean;
lookupField?: ScriptField; // script that returns the value of a field. This script is passed the rootDoc, layoutDoc, field, and container of the document. see PresBox.
"onDoubleClick-rawScript"?: string; // onDoubleClick script in raw text form
+ "onChildDoubleClick-rawScript"?: string; // onChildDoubleClick script in raw text form
+ "onChildClick-rawScript"?: string; // on ChildClick script in raw text form
"onClick-rawScript"?: string; // onClick script in raw text form
"onCheckedClick-rawScript"?: string; // onChecked script in raw text form
"onCheckedClick-params"?: List<string>; // parameter list for onChecked treeview functions
@@ -262,7 +268,7 @@ export namespace Docs {
}],
[DocumentType.LINK, {
layout: { view: LinkBox, dataField: defaultDataKey },
- options: { _height: 150 }
+ options: { _height: 150, description: "" }
}],
[DocumentType.LINKDB, {
data: new List<Doc>(),
@@ -297,10 +303,6 @@ export namespace Docs {
layout: { view: FontIconBox, dataField: defaultDataKey },
options: { _width: 40, _height: 40, borderRounding: "100%" },
}],
- [DocumentType.RECOMMENDATION, {
- layout: { view: RecommendationsBox, dataField: defaultDataKey },
- options: { _width: 200, _height: 200 },
- }],
[DocumentType.WEBCAM, {
layout: { view: DashWebRTCVideo, dataField: defaultDataKey }
}],
@@ -494,7 +496,7 @@ export namespace Docs {
Doc.Get.FromJson({ data: device, appendToExisting: { targetDoc: Doc.GetProto(doc) } });
Doc.AddDocToList(parentProto, "data", doc);
} else if (errors) {
- console.log(errors);
+ console.log("Documents:" + errors);
} else {
alert("A Buxton document import was completely empty (??)");
}
@@ -550,6 +552,11 @@ export namespace Docs {
const dataDoc = MakeDataDelegate(proto, protoProps, data, fieldKey);
const viewDoc = Doc.MakeDelegate(dataDoc, delegId);
+ // so that the list of annotations is already initialised, prevents issues in addonly.
+ // without this, if a doc has no annotations but the user has AddOnly privileges, they won't be able to add an annotation because they would have needed to create the field's list which they don't have permissions to do.
+
+ dataDoc[fieldKey + "-annotations"] = new List<Doc>();
+
proto.links = ComputedField.MakeFunction("links(self)");
viewDoc.author = Doc.CurrentUserEmail;
@@ -676,12 +683,12 @@ export namespace Docs {
I.type = DocumentType.INK;
I.layout = InkingStroke.LayoutString("data");
I.color = color;
- I.strokeWidth = strokeWidth;
- I.strokeBezier = strokeBezier;
I.fillColor = fillColor;
- I.arrowStart = arrowStart;
- I.arrowEnd = arrowEnd;
- I.dash = dash;
+ I.strokeWidth = Number(strokeWidth);
+ I.strokeBezier = strokeBezier;
+ I.strokeStartMarker = arrowStart;
+ I.strokeEndMarker = arrowEnd;
+ I.strokeDash = dash;
I.tool = tool;
I.title = "ink";
I.x = options.x;
@@ -690,14 +697,9 @@ export namespace Docs {
I._width = options._width;
I._height = options._height;
I.author = Doc.CurrentUserEmail;
+ I.rotation = 0;
I.data = new InkField(points);
return I;
- // return I;
- // const doc = InstanceFromProto(Prototypes.get(DocumentType.INK), new InkField(points), options);
- // doc.color = color;
- // doc.strokeWidth = strokeWidth;
- // doc.tool = tool;
- // return doc;
}
export function PdfDocument(url: string, options: DocumentOptions = {}) {
@@ -721,11 +723,11 @@ export namespace Docs {
}
export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", ...options, _viewType: CollectionViewType.Freeform }, id);
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", dontRegisterChildViews: true, ...options, _viewType: CollectionViewType.Freeform }, id);
}
export function PileDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", backgroundColor: "black", ...options, _viewType: CollectionViewType.Pile }, id);
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", backgroundColor: "black", hideFilterView: true, forceActive: true, ...options, _viewType: CollectionViewType.Pile }, id);
}
export function LinearDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
@@ -749,11 +751,11 @@ export namespace Docs {
}
export function TreeDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", ...options, _viewType: CollectionViewType.Tree }, id);
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", dontRegisterChildViews: true, ...options, _viewType: CollectionViewType.Tree }, id);
}
export function StackingDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", ...options, _viewType: CollectionViewType.Stacking }, id);
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", dontRegisterChildViews: true, ...options, _viewType: CollectionViewType.Stacking }, id);
}
export function MulticolumnDocument(documents: Array<Doc>, options: DocumentOptions) {
@@ -765,7 +767,7 @@ export namespace Docs {
export function MasonryDocument(documents: Array<Doc>, options: DocumentOptions) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", ...options, _viewType: CollectionViewType.Masonry });
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", dontRegisterChildViews: true, ...options, _viewType: CollectionViewType.Masonry });
}
export function LabelDocument(options?: DocumentOptions) {
@@ -787,7 +789,7 @@ export namespace Docs {
export function FontIconDocument(options?: DocumentOptions) {
- return InstanceFromProto(Prototypes.get(DocumentType.FONTICON), undefined, { ...(options || {}) });
+ return InstanceFromProto(Prototypes.get(DocumentType.FONTICON), undefined, { hideLinkButton: true, ...(options || {}) });
}
export function PresElementBoxDocument(options?: DocumentOptions) {
@@ -808,10 +810,6 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.IMPORT), new List<Doc>(), options);
}
- export function RecommendationsDocument(data: Doc[], options: DocumentOptions = {}) {
- return InstanceFromProto(Prototypes.get(DocumentType.RECOMMENDATION), new List<Doc>(data), options);
- }
-
export type DocConfig = {
doc: Doc,
initialWidth?: number,
@@ -839,6 +837,47 @@ export namespace Docs {
}
export namespace DocUtils {
+ export function FilterDocs(docs: Doc[], docFilters: string[], docRangeFilters: string[], viewSpecScript?: ScriptField) {
+ const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;
+
+ const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields
+ for (let i = 0; i < docFilters.length; i += 3) {
+ const [key, value, modifiers] = docFilters.slice(i, i + 3);
+ if (!filterFacets[key]) {
+ filterFacets[key] = {};
+ }
+ filterFacets[key][value] = modifiers;
+ }
+
+ const filteredDocs = docFilters.length ? childDocs.filter(d => {
+ for (const facetKey of Object.keys(filterFacets)) {
+ const facet = filterFacets[facetKey];
+ const satisfiesFacet = Object.keys(facet).some(value => {
+ if (facet[value] === "match") {
+ return d[facetKey] === undefined || Field.toString(d[facetKey] as Field).includes(value);
+ }
+ return (facet[value] === "x") !== Doc.matchFieldValue(d, facetKey, value);
+ });
+ if (!satisfiesFacet) {
+ return false;
+ }
+ }
+ return true;
+ }) : childDocs;
+ const rangeFilteredDocs = filteredDocs.filter(d => {
+ for (let i = 0; i < docRangeFilters.length; i += 3) {
+ const key = docRangeFilters[i];
+ const min = Number(docRangeFilters[i + 1]);
+ const max = Number(docRangeFilters[i + 2]);
+ const val = Cast(d[key], "number", null);
+ if (val !== undefined && (val < min || val > max)) {
+ return false;
+ }
+ }
+ return true;
+ });
+ return rangeFilteredDocs;
+ }
export function Publish(promoteDoc: Doc, targetID: string, addDoc: any, remDoc: any) {
targetID = targetID.replace(/^-/, "").replace(/\([0-9]*\)$/, "");
@@ -876,17 +915,18 @@ export namespace DocUtils {
export function MakeLinkToActiveAudio(doc: Doc) {
DocUtils.ActiveRecordings.map(d => DocUtils.MakeLink({ doc: doc }, { doc: d }, "audio link", "audio timeline"));
}
- export function MakeLink(source: { doc: Doc }, target: { doc: Doc }, linkRelationship: string = "", id?: string) {
+
+ export function MakeLink(source: { doc: Doc }, target: { doc: Doc }, linkRelationship: string = "", description: string = "", id?: string) {
const sv = DocumentManager.Instance.getDocumentView(source.doc);
if (sv && sv.props.ContainingCollectionDoc === target.doc) return;
if (target.doc === Doc.UserDoc()) return undefined;
- const linkDoc = Docs.Create.LinkDocument(source, target, { linkRelationship, layoutKey: "layout_linkView" }, id);
+ const linkDoc = Docs.Create.LinkDocument(source, target, { linkRelationship, layoutKey: "layout_linkView", description }, id);
+ linkDoc.linkDisplay = true;
+ linkDoc.hidden = true;
linkDoc.layout_linkView = Cast(Cast(Doc.UserDoc()["template-button-link"], Doc, null).dragFactory, Doc, null);
Doc.GetProto(linkDoc).title = ComputedField.MakeFunction('self.anchor1?.title +" (" + (self.linkRelationship||"to") +") " + self.anchor2?.title');
- Doc.GetProto(source.doc).links = ComputedField.MakeFunction("links(self)");
- Doc.GetProto(target.doc).links = ComputedField.MakeFunction("links(self)");
return linkDoc;
}
@@ -944,6 +984,7 @@ export namespace DocUtils {
}
if (type.indexOf("pdf") !== -1) {
ctor = Docs.Create.PdfDocument;
+ if (!options._fitWidth) options._fitWidth = true;
if (!options._width) options._width = 400;
if (!options._height) options._height = options._width * 1200 / 927;
}
@@ -994,6 +1035,7 @@ export namespace DocUtils {
event: (args: { x: number, y: number }) => {
const newDoc = Doc.ApplyTemplate(dragDoc);
if (newDoc) {
+ newDoc.author = Doc.CurrentUserEmail;
newDoc.x = x;
newDoc.y = y;
docAdder(newDoc);
@@ -1013,6 +1055,7 @@ export namespace DocUtils {
}
});
batch.end();
+ return doc;
}
export function findTemplate(templateName: string, type: string, signature: string) {
let docLayoutTemplate: Opt<Doc>;
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index c7cdf8545..2d95d081e 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -4,7 +4,7 @@ import { Utils } from "../../Utils";
import { DocServer } from "../DocServer";
import { Docs, DocumentOptions, DocUtils } from "../documents/Documents";
import { UndoManager } from "./UndoManager";
-import { Doc, DocListCast, DocListCastAsync } from "../../fields/Doc";
+import { Doc, DocListCast, DocListCastAsync, DataSym } from "../../fields/Doc";
import { List } from "../../fields/List";
import { listSpec } from "../../fields/Schema";
import { ScriptField, ComputedField } from "../../fields/ScriptField";
@@ -22,6 +22,7 @@ import { DocumentType } from "../documents/DocumentTypes";
import { SchemaHeaderField } from "../../fields/SchemaHeaderField";
import { DimUnit } from "../views/collections/collectionMulticolumn/CollectionMulticolumnView";
import { LabelBox } from "../views/nodes/LabelBox";
+import { LinkManager } from "./LinkManager";
export class CurrentUserUtils {
private static curr_id: string;
@@ -43,7 +44,7 @@ export class CurrentUserUtils {
const queryTemplate = Docs.Create.MulticolumnDocument(
[
Docs.Create.SearchDocument({ _viewType: CollectionViewType.Schema, ignoreClick: true, forceActive: true, lockedPosition: true, title: "query", _height: 200 }),
- Docs.Create.FreeformDocument([], { title: "data", _height: 100, _LODdisable: true })
+ Docs.Create.FreeformDocument([], { title: "data", _height: 100 })
],
{ _width: 400, _height: 300, title: "queryView", _chromeStatus: "disabled", _xMargin: 3, _yMargin: 3, hideFilterView: true }
);
@@ -54,6 +55,25 @@ export class CurrentUserUtils {
removeDropProperties: new List<string>(["dropAction"]), title: "query view", icon: "question-circle"
});
}
+ // Prototype for mobile button (not sure if 'Advanced Item Prototypes' is ideal location)
+ if (doc["template-mobile-button"] === undefined) {
+ const queryTemplate = this.mobileButton({
+ title: "NEW MOBILE BUTTON",
+ onClick: undefined,
+ },
+ [this.ficon({
+ ignoreClick: true,
+ icon: "mobile",
+ backgroundColor: "rgba(0,0,0,0)"
+ }),
+ this.mobileTextContainer({},
+ [this.mobileButtonText({}, "NEW MOBILE BUTTON"), this.mobileButtonInfo({}, "You can customize this button and make it your own.")])]);
+ doc["template-mobile-button"] = CurrentUserUtils.ficon({
+ onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'),
+ dragFactory: new PrefetchProxy(queryTemplate) as any as Doc,
+ removeDropProperties: new List<string>(["dropAction"]), title: "mobile button", icon: "mobile"
+ });
+ }
if (doc["template-button-slides"] === undefined) {
const slideTemplate = Docs.Create.MultirowDocument(
@@ -72,29 +92,29 @@ export class CurrentUserUtils {
}
if (doc["template-button-description"] === undefined) {
- const descriptionTemplate = Docs.Create.TextDocument(" ", { title: "header", _height: 100 }, "header"); // text needs to be a space to allow templateText to be created
- Doc.GetProto(descriptionTemplate).layout =
+ const descriptionTemplate = Doc.MakeDelegate(Docs.Create.TextDocument(" ", { title: "header", _height: 100 }, "header")); // text needs to be a space to allow templateText to be created
+ descriptionTemplate[DataSym].layout =
"<div>" +
" <FormattedTextBox {...props} height='{this._headerHeight||75}px' background='{this._headerColor||`orange`}' fieldKey={'header'}/>" +
" <FormattedTextBox {...props} position='absolute' top='{(this._headerHeight||75)*scale}px' height='calc({100/scale}% - {this._headerHeight||75}px)' fieldKey={'text'}/>" +
"</div>";
- descriptionTemplate.isTemplateDoc = makeTemplate(descriptionTemplate, true, "descriptionView");
+ (descriptionTemplate.proto as Doc).isTemplateDoc = makeTemplate(descriptionTemplate.proto as Doc, true, "descriptionView");
doc["template-button-description"] = CurrentUserUtils.ficon({
- onDragStart: ScriptField.MakeFunction('makeDelegate(this.dragFactory)'),
+ onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'),
dragFactory: new PrefetchProxy(descriptionTemplate) as any as Doc,
removeDropProperties: new List<string>(["dropAction"]), title: "description view", icon: "window-maximize"
});
}
if (doc["template-button-link"] === undefined) { // set _backgroundColor to transparent to prevent link dot from obscuring document it's attached to.
- const linkTemplate = Docs.Create.TextDocument(" ", { title: "header", _height: 100 }, "header"); // text needs to be a space to allow templateText to be created
+ const linkTemplate = Doc.MakeDelegate(Docs.Create.TextDocument(" ", { title: "header", _height: 100 }, "header")); // text needs to be a space to allow templateText to be created
Doc.GetProto(linkTemplate).layout =
"<div>" +
" <FormattedTextBox {...props} height='{this._headerHeight||75}px' background='{this._headerColor||`lightGray`}' fieldKey={'header'}/>" +
" <FormattedTextBox {...props} position='absolute' top='{(this._headerHeight||75)*scale}px' height='calc({100/scale}% - {this._headerHeight||75}px)' fieldKey={'text'}/>" +
"</div>";
- linkTemplate.isTemplateDoc = makeTemplate(linkTemplate, true, "linkView");
+ (linkTemplate.proto as Doc).isTemplateDoc = makeTemplate(linkTemplate.proto as Doc, true, "linkView");
const rtf2 = {
doc: {
@@ -127,7 +147,7 @@ export class CurrentUserUtils {
linkTemplate.header = new RichTextField(JSON.stringify(rtf2), "");
doc["template-button-link"] = CurrentUserUtils.ficon({
- onDragStart: ScriptField.MakeFunction('makeDelegate(this.dragFactory)'),
+ onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'),
dragFactory: new PrefetchProxy(linkTemplate) as any as Doc,
removeDropProperties: new List<string>(["dropAction"]), title: "link view", icon: "window-maximize"
});
@@ -192,7 +212,7 @@ export class CurrentUserUtils {
details.text = new RichTextField(JSON.stringify(detailedTemplate), buxtonFieldKeys.join(" "));
const shared = { _chromeStatus: "disabled", _autoHeight: true, _xMargin: 0 };
- const detailViewOpts = { title: "detailView", _width: 300, _fontFamily: "Arial", _fontSize: 12 };
+ const detailViewOpts = { title: "detailView", _width: 300, _fontFamily: "Arial", _fontSize: "12pt" };
const descriptionWrapperOpts = { title: "descriptions", _height: 300, _columnWidth: -1, treeViewHideTitle: true, _pivotField: "title" };
const descriptionWrapper = MasonryDocument([details, short, long], { ...shared, ...descriptionWrapperOpts });
@@ -219,6 +239,7 @@ export class CurrentUserUtils {
doc["template-button-slides"] as Doc,
doc["template-button-description"] as Doc,
doc["template-button-query"] as Doc,
+ doc["template-mobile-button"] as Doc,
doc["template-button-detail"] as Doc,
doc["template-button-link"] as Doc,
doc["template-button-switch"] as Doc];
@@ -325,6 +346,14 @@ export class CurrentUserUtils {
iconRtfView.isTemplateDoc = makeTemplate(iconRtfView, true, "icon_" + DocumentType.RTF);
doc["template-icon-view-rtf"] = new PrefetchProxy(iconRtfView);
}
+ if (doc["template-icon-view-button"] === undefined) {
+ const iconBtnView = Docs.Create.FontIconDocument({
+ title: "icon_" + DocumentType.BUTTON, _nativeHeight: 30, _nativeWidth: 30,
+ _width: 30, _height: 30, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)")
+ });
+ iconBtnView.isTemplateDoc = makeTemplate(iconBtnView, true, "icon_" + DocumentType.BUTTON);
+ doc["template-icon-view-button"] = new PrefetchProxy(iconBtnView);
+ }
if (doc["template-icon-view-img"] === undefined) {
const iconImageView = Docs.Create.ImageDocument("http://www.cs.brown.edu/~bcz/face.gif", {
title: "data", _width: 50, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)")
@@ -338,12 +367,12 @@ export class CurrentUserUtils {
doc["template-icon-view-col"] = new PrefetchProxy(iconColView);
}
if (doc["template-icons"] === undefined) {
- doc["template-icons"] = new PrefetchProxy(Docs.Create.TreeDocument([doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc,
+ doc["template-icons"] = new PrefetchProxy(Docs.Create.TreeDocument([doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc, doc["template-icon-view-button"] as Doc,
doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc, doc["template-icon-view-pdf"] as Doc], { title: "icon templates", _height: 75 }));
} else {
const templateIconsDoc = Cast(doc["template-icons"], Doc, null);
- const requiredTypes = [doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc,
- doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc, doc["template-icon-view-pdf"] as Doc];
+ const requiredTypes = [doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc, doc["template-icon-view-button"] as Doc,
+ doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc];
DocListCastAsync(templateIconsDoc.data).then(async curIcons => {
await Promise.all(curIcons!);
requiredTypes.map(ntype => Doc.AddDocToList(templateIconsDoc, "data", ntype));
@@ -353,7 +382,7 @@ export class CurrentUserUtils {
}
static creatorBtnDescriptors(doc: Doc): {
- title: string, label: string, icon: string, drag?: string, ignoreClick?: boolean,
+ title: string, toolTip: string, icon: string, drag?: string, ignoreClick?: boolean,
click?: string, ischecked?: string, activeInkPen?: Doc, backgroundColor?: string, dragFactory?: Doc
}[] {
if (doc.emptyPresentation === undefined) {
@@ -370,37 +399,51 @@ export class CurrentUserUtils {
if (doc.emptyScript === undefined) {
doc.emptyScript = Docs.Create.ScriptingDocument(undefined, { _width: 200, _height: 250, title: "script" });
}
+ if (doc.emptyScreenshot === undefined) {
+ doc.emptyScreenshot = Docs.Create.ScreenshotDocument("", { _width: 400, _height: 200, title: "screen snapshot" });
+ }
+ if (doc.emptyAudio === undefined) {
+ doc.emptyAudio = Docs.Create.AudioDocument(nullAudio, { _width: 200, title: "ready to record audio" });
+ }
+ if (doc.emptyImage === undefined) {
+ doc.emptyImage = Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { _width: 250, _nativeWidth: 250, title: "an image of a cat" });
+ }
+ if (doc.emptyButton === undefined) {
+ doc.emptyButton = Docs.Create.ButtonDocument({ _width: 150, _height: 50, _xPadding: 10, _yPadding: 10, title: "Button" });
+ }
if (doc.emptyDocHolder === undefined) {
- doc.emptyDocHolder = Docs.Create.DocumentDocument(
- ComputedField.MakeFunction("selectedDocs(this,this.excludeCollections,[_last_])?.[0]") as any,
- { _width: 250, _height: 250, title: "container" });
+ // doc.emptyDocHolder = Docs.Create.DocumentDocument(
+ // ComputedField.MakeFunction("selectedDocs(this,this.excludeCollections,[_last_])?.[0]") as any,
+ // { _width: 250, _height: 250, title: "container" });
}
if (doc.emptyWebpage === undefined) {
doc.emptyWebpage = Docs.Create.WebDocument("", { title: "webpage", _nativeWidth: 850, _nativeHeight: 962, _width: 600, UseCors: true });
}
+ if (doc.activeMobileMenu === undefined) {
+ this.setupActiveMobileMenu(doc);
+ }
return [
- { title: "Drag a comparison box", label: "Comp", icon: "columns", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyComparison as Doc },
- { title: "Drag a collection", label: "Col", icon: "folder", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyCollection as Doc },
- { title: "Drag a web page", label: "Web", icon: "globe-asia", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyWebpage as Doc },
- { title: "Drag a cat image", label: "Img", icon: "cat", ignoreClick: true, drag: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { _width: 250, _nativeWidth:250, title: "an image of a cat" })' },
- { title: "Drag a screenshot", label: "Grab", icon: "photo-video", ignoreClick: true, drag: 'Docs.Create.ScreenshotDocument("", { _width: 400, _height: 200, title: "screen snapshot" })' },
- { title: "Drag a webcam", label: "Cam", icon: "video", ignoreClick: true, drag: 'Docs.Create.WebCamDocument("", { _width: 400, _height: 400, title: "a test cam" })' },
- { title: "Drag a audio recorder", label: "Audio", icon: "microphone", ignoreClick: true, drag: `Docs.Create.AudioDocument("${nullAudio}", { _width: 200, title: "ready to record audio" })` },
- { title: "Drag a clickable button", label: "Btn", icon: "bolt", ignoreClick: true, drag: 'Docs.Create.ButtonDocument({ _width: 150, _height: 50, _xPadding:10, _yPadding: 10, title: "Button" })' },
- { title: "Drag a presentation view", label: "Prezi", icon: "tv", click: 'openOnRight(Doc.UserDoc().activePresentation = getCopy(this.dragFactory, true))', drag: `Doc.UserDoc().activePresentation = getCopy(this.dragFactory,true)`, dragFactory: doc.emptyPresentation as Doc },
- { title: "Drag a search box", label: "Query", icon: "search", ignoreClick: true, drag: 'Docs.Create.SearchDocument({ _width: 200, title: "an image of a cat" })' },
- { title: "Drag a scripting box", label: "Script", icon: "terminal", ignoreClick: true, drag: 'Docs.Create.ScriptingDocument(undefined, { _width: 200, _height: 250 title: "untitled script" })' },
- { title: "Drag an import folder", label: "Load", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", _width: 400, _height: 400 })' },
- { title: "Drag a mobile view", label: "Phone", icon: "phone", ignoreClick: true, drag: 'Doc.UserDoc().activeMobile' },
- { title: "Drag an instance of the device collection", label: "Buxton", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.Buxton()' },
+ { title: "Drag a comparison box", toolTip: "columns", icon: "columns", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyComparison as Doc },
+ { title: "Drag a collection", toolTip: "Col", icon: "folder", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyCollection as Doc },
+ { title: "Drag a web page", toolTip: "Web", icon: "globe-asia", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyWebpage as Doc },
+ { title: "Drag a cat image", toolTip: "Img", icon: "cat", ignoreClick: true, drag: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { _width: 250, _nativeWidth:250, title: "an image of a cat" })' },
+ { title: "Drag a screenshot", toolTip: "Grab", icon: "photo-video", ignoreClick: true, drag: 'Docs.Create.ScreenshotDocument("", { _width: 400, _height: 200, title: "screen snapshot" })' },
+ { title: "Drag a webcam", toolTip: "Cam", icon: "video", ignoreClick: true, drag: 'Docs.Create.WebCamDocument("", { _width: 400, _height: 400, title: "a test cam" })' },
+ { title: "Drag a audio recorder", toolTip: "Audio", icon: "microphone", ignoreClick: true, drag: `Docs.Create.AudioDocument("${nullAudio}", { _width: 200, title: "ready to record audio" })` },
+ { title: "Drag a clickable button", toolTip: "Btn", icon: "bolt", ignoreClick: true, drag: 'Docs.Create.ButtonDocument({ _width: 150, _height: 50, _xPadding:10, _yPadding: 10, title: "Button" })' },
+ { title: "Drag a presentation view", toolTip: "Prezi", icon: "tv", click: 'openOnRight(Doc.UserDoc().activePresentation = getCopy(this.dragFactory, true))', drag: `Doc.UserDoc().activePresentation = getCopy(this.dragFactory,true)`, dragFactory: doc.emptyPresentation as Doc },
+ { title: "Drag a search box", toolTip: "Query", icon: "search", ignoreClick: true, drag: 'Docs.Create.SearchDocument({ _width: 200, title: "an image of a cat" })' },
+ { title: "Drag a scripting box", toolTip: "Script", icon: "terminal", ignoreClick: true, drag: 'Docs.Create.ScriptingDocument(undefined, { _width: 200, _height: 250 title: "untitled script" })' },
+ { title: "Drag an import folder", toolTip: "Load", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", _width: 400, _height: 400 })' },
+ { title: "Drag a mobile view", toolTip: "Phone", icon: "phone", ignoreClick: true, drag: 'Doc.UserDoc().activeMobile' },
+ { title: "Drag an instance of the device collection", toolTip: "Buxton", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.Buxton()' },
// { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this)', backgroundColor: "blue", ischecked: `sameDocs(this.activeInkPen, this)`, activeInkPen: doc },
// { title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activeInkPen, this)`, activeInkPen: doc },
// { title: "use stamp", icon: "stamp", click: 'activateStamp(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this)', backgroundColor: "orange", ischecked: `sameDocs(this.activeInkPen, this)`, activeInkPen: doc },
// { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this);', ischecked: `sameDocs(this.activeInkPen, this)`, backgroundColor: "pink", activeInkPen: doc },
// { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activeInkPen = this;', ischecked: `sameDocs(this.activeInkPen, this)`, backgroundColor: "white", activeInkPen: doc },
- { title: "Drag a document previewer", label: "Prev", icon: "expand", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory,true)', dragFactory: doc.emptyDocHolder as Doc },
- { title: "Toggle a Calculator REPL", label: "repl", icon: "calculator", click: 'addOverlayWindow("ScriptingRepl", { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" })' },
- { title: "Connect a Google Account", label: "Google Account", icon: "external-link-alt", click: 'GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(true)' },
+ { toolTip: "Drag a document previewer", title: "Prev", icon: "expand", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyDocHolder as Doc },
+ { toolTip: "Toggle a Calculator REPL", title: "repl", icon: "calculator", click: 'addOverlayWindow("ScriptingRepl", { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" })' },
];
}
@@ -417,11 +460,11 @@ export class CurrentUserUtils {
}
}
const buttons = CurrentUserUtils.creatorBtnDescriptors(doc).filter(d => !alreadyCreatedButtons?.includes(d.title));
- const creatorBtns = buttons.map(({ title, label, icon, ignoreClick, drag, click, ischecked, activeInkPen, backgroundColor, dragFactory }) => Docs.Create.FontIconDocument({
+ const creatorBtns = buttons.map(({ title, toolTip, icon, ignoreClick, drag, click, ischecked, activeInkPen, backgroundColor, dragFactory }) => Docs.Create.FontIconDocument({
_nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100,
icon,
title,
- label,
+ toolTip,
ignoreClick,
dropAction: "copy",
onDragStart: drag ? ScriptField.MakeFunction(drag) : undefined,
@@ -445,25 +488,71 @@ export class CurrentUserUtils {
return doc.myItemCreators as Doc;
}
- static setupMobileButtons(doc: Doc, buttons?: string[]) {
- const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, ischecked?: string, activeInkPen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
- { title: "record", icon: "microphone", ignoreClick: true, click: "FILL" },
- { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this)', backgroundColor: "blue", ischecked: `sameDocs(this.activeInkPen, this)`, activeInkPen: doc },
- { title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activeInkPen, this)`, activeInkPen: doc },
- { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this);', ischecked: `sameDocs(this.activeInkPen, this)`, backgroundColor: "pink", activeInkPen: doc },
- { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activeInkPen = this;', ischecked: `sameDocs(this.activeInkPen, this)`, backgroundColor: "white", activeInkPen: doc },
- // { title: "draw", icon: "pen-nib", click: 'switchMobileView(setupMobileInkingDoc, renderMobileInking, onSwitchMobileInking);', ischecked: `sameDocs(this.activeInkPen, this)`, backgroundColor: "red", activeInkPen: doc },
- { title: "upload", icon: "upload", click: 'switchMobileView(setupMobileUploadDoc, renderMobileUpload, onSwitchMobileUpload);', backgroundColor: "orange" },
- // { title: "upload", icon: "upload", click: 'uploadImageMobile();', backgroundColor: "cyan" },
- ];
- return docProtoData.filter(d => !buttons || !buttons.includes(d.title)).map(data => Docs.Create.FontIconDocument({
- _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, dropAction: data.click ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
- onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, onClick: data.click ? ScriptField.MakeScript(data.click) : undefined,
- ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, activeInkPen: data.activeInkPen,
- backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), dragFactory: data.dragFactory,
+ // Sets up mobile menu if it is undefined creates a new one, otherwise returns existing menu
+ static setupActiveMobileMenu(doc: Doc) {
+ if (doc.activeMobileMenu === undefined) {
+ doc.activeMobileMenu = this.setupMobileMenu();
+ }
+ return doc.activeMobileMenu as Doc;
+ }
+
+ // Sets up mobileMenu stacking document
+ static setupMobileMenu() {
+ const menu = new PrefetchProxy(Docs.Create.StackingDocument(this.setupMobileButtons(), {
+ _width: 980, ignoreClick: true, lockedPosition: false, _chromeStatus: "disabled", title: "home", _yMargin: 100
}));
+ return menu;
}
+ // SEts up mobile buttons for inside mobile menu
+ static setupMobileButtons(doc?: Doc, buttons?: string[]) {
+ const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, ischecked?: string, activePen?: Doc, backgroundColor?: string, info: string, dragFactory?: Doc }[] = [
+ { title: "WORKSPACES", icon: "bars", click: 'switchToMobileLibrary()', backgroundColor: "lightgrey", info: "Access your Workspaces from your mobile, and navigate through all of your documents. " },
+ { title: "UPLOAD", icon: "upload", click: 'openMobileUploads()', backgroundColor: "lightgrey", info: "Upload files from your mobile device so they can be accessed on Dash Web." },
+ { title: "MOBILE UPLOAD", icon: "mobile", click: 'switchToMobileUploadCollection()', backgroundColor: "lightgrey", info: "Access the collection of your mobile uploads." },
+ { title: "RECORD", icon: "microphone", click: 'openMobileAudio()', backgroundColor: "lightgrey", info: "Use your phone to record, dictate and then upload audio onto Dash Web." },
+ { title: "PRESENTATION", icon: "desktop", click: 'switchToMobilePresentation()', backgroundColor: "lightgrey", info: "Use your phone as a remote for you presentation." },
+ { title: "SETTINGS", icon: "cog", click: 'openMobileSettings()', backgroundColor: "lightgrey", info: "Change your password, log out, or manage your account security." }
+ ];
+ // returns a list of mobile buttons
+ return docProtoData.filter(d => !buttons || !buttons.includes(d.title)).map(data =>
+ this.mobileButton({
+ title: data.title,
+ lockedPosition: true,
+ onClick: data.click ? ScriptField.MakeScript(data.click) : undefined,
+ _backgroundColor: data.backgroundColor
+ },
+ [this.ficon({ ignoreClick: true, icon: data.icon, backgroundColor: "rgba(0,0,0,0)" }), this.mobileTextContainer({}, [this.mobileButtonText({}, data.title), this.mobileButtonInfo({}, data.info)])])
+ );
+ }
+
+ // sets up the main document for the mobile button
+ static mobileButton = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.MulticolumnDocument(docs, {
+ ...opts,
+ dropAction: undefined, removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 900, _nativeHeight: 250, _width: 900, _height: 250, _yMargin: 15,
+ borderRounding: "5px", boxShadow: "0 0", _chromeStatus: "disabled"
+ }) as any as Doc
+
+ // sets up the text container for the information contained within the mobile button
+ static mobileTextContainer = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.MultirowDocument(docs, {
+ ...opts,
+ dropAction: undefined, removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 450, _nativeHeight: 250, _width: 450, _height: 250, _yMargin: 25,
+ backgroundColor: "rgba(0,0,0,0)", borderRounding: "0", boxShadow: "0 0", _chromeStatus: "disabled", ignoreClick: true
+ }) as any as Doc
+
+ // Sets up the title of the button
+ static mobileButtonText = (opts: DocumentOptions, buttonTitle: string) => Docs.Create.TextDocument(buttonTitle, {
+ ...opts,
+ dropAction: undefined, title: buttonTitle, _fontSize: "37pt", _xMargin: 0, _yMargin: 0, ignoreClick: true, _chromeStatus: "disabled", backgroundColor: "rgba(0,0,0,0)"
+ }) as any as Doc
+
+ // Sets up the description of the button
+ static mobileButtonInfo = (opts: DocumentOptions, buttonInfo: string) => Docs.Create.TextDocument(buttonInfo, {
+ ...opts,
+ dropAction: undefined, title: "info", _fontSize: "25pt", _xMargin: 0, _yMargin: 0, ignoreClick: true, _chromeStatus: "disabled", backgroundColor: "rgba(0,0,0,0)", _dimMagnitude: 2,
+ }) as any as Doc
+
+
static setupThumbButtons(doc: Doc) {
const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, pointerDown?: string, pointerUp?: string, ischecked?: string, clipboard?: Doc, activeInkPen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
{ title: "use pen", icon: "pen-nib", pointerUp: "resetPen()", pointerDown: 'setPen(2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activeInkPen, this)`, activeInkPen: doc },
@@ -497,31 +586,12 @@ export class CurrentUserUtils {
return Cast(userDoc.thumbDoc, Doc);
}
- static setupMobileDoc(userDoc: Doc) {
- return userDoc.activeMoble ?? Docs.Create.MasonryDocument(CurrentUserUtils.setupMobileButtons(userDoc), {
- _columnWidth: 100, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled", title: "buttons", _autoHeight: true, _yMargin: 5
- });
- }
-
- static setupMobileInkingDoc(userDoc: Doc) {
- return Docs.Create.FreeformDocument([], { title: "Mobile Inking", backgroundColor: "white" });
+ static setupLibrary(userDoc: Doc) {
+ return CurrentUserUtils.setupWorkspaces(userDoc);
}
- static setupMobileUploadDoc(userDoc: Doc) {
- // const addButton = Docs.Create.FontIconDocument({ onDragStart: ScriptField.MakeScript('addWebToMobileUpload()'), title: "Add Web Doc to Upload Collection", icon: "plus", backgroundColor: "black" })
- const webDoc = Docs.Create.WebDocument("https://www.britannica.com/biography/Miles-Davis", {
- title: "Upload Images From the Web", _chromeStatus: "enabled", lockedPosition: true
- });
- const uploadDoc = Docs.Create.StackingDocument([], {
- title: "Mobile Upload Collection", backgroundColor: "white", lockedPosition: true
- });
- return Docs.Create.StackingDocument([webDoc, uploadDoc], {
- _width: screen.width, lockedPosition: true, _chromeStatus: "disabled", title: "Upload", _autoHeight: true, _yMargin: 80, backgroundColor: "lightgray"
- });
- }
-
- // setup the Creator button which will display the creator panel. This panel will include the drag creators and the color picker.
- // when clicked, this panel will be displayed in the target container (ie, sidebarContainer)
+ // setup the Creator button which will display the creator panel. This panel will include the drag creators and the color picker.
+ // when clicked, this panel will be displayed in the target container (ie, sidebarContainer)
static async setupToolsBtnPanel(doc: Doc, sidebarContainer: Doc) {
// setup a masonry view of all he creators
const creatorBtns = await CurrentUserUtils.setupCreatorButtons(doc);
@@ -546,10 +616,10 @@ export class CurrentUserUtils {
_width: 500, lockedPosition: true, _chromeStatus: "disabled", title: "tools stack", forceActive: true
})) as any as Doc;
doc["tabs-button-tools"] = new PrefetchProxy(Docs.Create.ButtonDocument({
- _width: 35, _height: 25, title: "Tools", _fontSize: 10,
+ _width: 35, _height: 25, title: "Tools", _fontSize: "10pt",
letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)",
sourcePanel: toolsStack,
- onDragStart: ScriptField.MakeFunction('getAlias(this.dragFactory, true)'),
+ onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'),
dragFactory: toolsStack,
removeDropProperties: new List<string>(["lockedPosition"]),
stayInCollection: true,
@@ -611,25 +681,25 @@ export class CurrentUserUtils {
lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same"
})) as any as Doc;
doc["tabs-button-library"] = new PrefetchProxy(Docs.Create.ButtonDocument({
- _width: 50, _height: 25, title: "Library", _fontSize: 10, targetDropAction: "same",
+ _width: 50, _height: 25, title: "Library", _fontSize: "10pt", targetDropAction: "same",
letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)",
sourcePanel: libraryStack,
- onDragStart: ScriptField.MakeFunction('getAlias(this.dragFactory, true)'),
+ onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'),
dragFactory: libraryStack,
removeDropProperties: new List<string>(["lockedPosition"]),
stayInCollection: true,
targetContainer: new PrefetchProxy(sidebarContainer) as any as Doc,
- onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel;")
+ onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel")
}));
}
return doc["tabs-button-library"] as Doc;
}
- // setup the Search button which will display the search panel.
+ // setup the Search button which will display the search panel.
static setupSearchBtnPanel(doc: Doc, sidebarContainer: Doc) {
if (doc["tabs-button-search"] === undefined) {
doc["tabs-button-search"] = new PrefetchProxy(Docs.Create.ButtonDocument({
- _width: 50, _height: 25, title: "Search", _fontSize: 10,
+ _width: 50, _height: 25, title: "Search", _fontSize: "10pt",
letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)",
sourcePanel: new PrefetchProxy(Docs.Create.SearchDocument({ ignoreClick: true, childDropAction: "alias", lockedPosition: true, _viewType: CollectionViewType.Schema, title: "sidebar search stack", })) as any as Doc,
searchFileTypes: new List<string>([DocumentType.RTF, DocumentType.IMG, DocumentType.PDF, DocumentType.VID, DocumentType.WEB, DocumentType.SCRIPTING]),
@@ -658,8 +728,8 @@ export class CurrentUserUtils {
const libraryBtn = CurrentUserUtils.setupLibraryPanel(doc, sidebarContainer);
const searchBtn = CurrentUserUtils.setupSearchBtnPanel(doc, sidebarContainer);
if (doc["search-panel"] === undefined) {
- doc["search-panel"] = new PrefetchProxy(Docs.Create.SearchDocument({_width: 500, _height: 400, backgroundColor: "dimGray", ignoreClick: true, childDropAction: "alias", lockedPosition: true, _viewType: CollectionViewType.Schema, _chromeStatus: "disabled", title: "sidebar search stack", })) as any as Doc;
- }
+ doc["search-panel"] = new PrefetchProxy(Docs.Create.SearchDocument({ _width: 500, _height: 400, backgroundColor: "dimGray", ignoreClick: true, childDropAction: "alias", lockedPosition: true, _viewType: CollectionViewType.Schema, _chromeStatus: "disabled", title: "sidebar search stack", })) as any as Doc;
+ }
// Finally, setup the list of buttons to display in the sidebar
if (doc["tabs-buttons"] === undefined) {
doc["tabs-buttons"] = new PrefetchProxy(Docs.Create.StackingDocument([libraryBtn, searchBtn, toolsBtn], {
@@ -668,45 +738,37 @@ export class CurrentUserUtils {
}));
(toolsBtn.onClick as ScriptField).script.run({ this: toolsBtn });
}
-
-
- // new PrefetchProxy(Docs.Create.StackingDocument([libraryBtn, searchBtn, toolsBtn], {
- // _width: 500, _height: 80, boxShadow: "0 0", _pivotField: "title", _columnsHideIfEmpty: true, ignoreClick: true, _chromeStatus: "view-mode",
- // title: "sidebar btn row stack", backgroundColor: "dimGray",
- // }));
-
+
+
+ // new PrefetchProxy(Docs.Create.StackingDocument([libraryBtn, searchBtn, toolsBtn], {
+ // _width: 500, _height: 80, boxShadow: "0 0", _pivotField: "title", _columnsHideIfEmpty: true, ignoreClick: true, _chromeStatus: "view-mode",
+ // title: "sidebar btn row stack", backgroundColor: "dimGray",
+ // }));
+
}
static blist = (opts: DocumentOptions, docs: Doc[]) => new PrefetchProxy(Docs.Create.LinearDocument(docs, {
- ...opts,
- _gridGap: 5, _xMargin: 5, _yMargin: 5, _height: 42, _width: 100, boxShadow: "0 0", forceActive: true,
+ ...opts, _gridGap: 5, _xMargin: 5, _yMargin: 5, _height: 42, _width: 100, boxShadow: "0 0", forceActive: true,
dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }),
backgroundColor: "black", treeViewPreventOpen: true, lockedPosition: true, _chromeStatus: "disabled", linearViewIsExpanded: true
})) as any as Doc
static ficon = (opts: DocumentOptions) => new PrefetchProxy(Docs.Create.FontIconDocument({
- ...opts,
- dropAction: "alias", removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100
+ ...opts, dropAction: "alias", removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100
})) as any as Doc
/// sets up the default list of buttons to be shown in the expanding button menu at the bottom of the Dash window
static setupDockedButtons(doc: Doc) {
- if (doc["dockedBtn-pen"] === undefined) {
- doc["dockedBtn-pen"] = CurrentUserUtils.ficon({
- onClick: ScriptField.MakeScript("activatePen(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this)"),
- author: "systemTemplates", title: "ink mode", icon: "pen-nib", ischecked: ComputedField.MakeFunction(`sameDocs(this.activeInkPen, this)`), activeInkPen: doc
- });
- }
if (doc["dockedBtn-undo"] === undefined) {
- doc["dockedBtn-undo"] = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("undo()"), title: "undo button", icon: "undo-alt" });
+ doc["dockedBtn-undo"] = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("undo()"), toolTip: "click to undo", title: "undo", icon: "undo-alt" });
}
if (doc["dockedBtn-redo"] === undefined) {
- doc["dockedBtn-redo"] = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("redo()"), title: "redo button", icon: "redo-alt" });
+ doc["dockedBtn-redo"] = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("redo()"), toolTip: "click to redo", title: "redo", icon: "redo-alt" });
}
if (doc.dockedBtns === undefined) {
- doc.dockedBtns = CurrentUserUtils.blist({ title: "docked buttons", ignoreClick: true }, [doc["dockedBtn-undo"] as Doc, doc["dockedBtn-redo"] as Doc, doc["dockedBtn-pen"] as Doc]);
+ doc.dockedBtns = CurrentUserUtils.blist({ title: "docked buttons", ignoreClick: true }, [doc["dockedBtn-undo"] as Doc, doc["dockedBtn-redo"] as Doc]);
}
}
// sets up the default set of documents to be shown in the Overlay layer
@@ -731,25 +793,27 @@ export class CurrentUserUtils {
}
}
+ // Right sidebar is where mobile uploads are contained
static setupRightSidebar(doc: Doc) {
if (doc.rightSidebarCollection === undefined) {
- doc.rightSidebarCollection = new PrefetchProxy(Docs.Create.StackingDocument([], { title: "Right Sidebar" }));
+ doc.rightSidebarCollection = new PrefetchProxy(Docs.Create.StackingDocument([], { title: "Mobile Uploads" }));
}
}
+
static setupClickEditorTemplates(doc: Doc) {
if (doc["clickFuncs-child"] === undefined) {
+ // to use this function, select it from the context menu of a collection. then edit the onChildClick script. Add two Doc variables: 'target' and 'thisContainer', then assign 'target' to some target collection. After that, clicking on any document in the initial collection will open it in the target
const openInTarget = Docs.Create.ScriptingDocument(ScriptField.MakeScript(
- "docCast(thisContainer.target).then((target) => {" +
- " target && docCast(this.source).then((source) => { " +
- " target.proto.data = new List([source || this]); " +
- " }); " +
- "})",
- { target: Doc.name }), { title: "Click to open in target", _width: 300, _height: 200, targetScriptKey: "onChildClick" });
+ "docCast(thisContainer.target).then((target) => target && (target.proto.data = new List([self]))) ",
+ { thisContainer: Doc.name }), {
+ title: "Click to open in target", _width: 300, _height: 200,
+ targetScriptKey: "onChildClick",
+ });
const openDetail = Docs.Create.ScriptingDocument(ScriptField.MakeScript(
"openOnRight(self.doubleClickView)",
- { target: Doc.name }), { title: "Double click to open doubleClickView", _width: 300, _height: 200, targetScriptKey: "onChildDoubleClick" });
+ {}), { title: "Double click to open doubleClickView", _width: 300, _height: 200, targetScriptKey: "onChildDoubleClick" });
doc["clickFuncs-child"] = Docs.Create.TreeDocument([openInTarget, openDetail], { title: "on Child Click function templates" });
}
@@ -761,14 +825,22 @@ export class CurrentUserUtils {
title: "onClick", "onClick-rawScript": "console.log('click')",
isTemplateDoc: true, isTemplateForField: "onClick", _width: 300, _height: 200
}, "onClick");
+ const onChildClick = Docs.Create.ScriptingDocument(undefined, {
+ title: "onChildClick", "onChildClick-rawScript": "console.log('child click')",
+ isTemplateDoc: true, isTemplateForField: "onChildClick", _width: 300, _height: 200
+ }, "onChildClick");
const onDoubleClick = Docs.Create.ScriptingDocument(undefined, {
title: "onDoubleClick", "onDoubleClick-rawScript": "console.log('double click')",
isTemplateDoc: true, isTemplateForField: "onDoubleClick", _width: 300, _height: 200
}, "onDoubleClick");
+ const onChildDoubleClick = Docs.Create.ScriptingDocument(undefined, {
+ title: "onChildDoubleClick", "onChildDoubleClick-rawScript": "console.log('child double click')",
+ isTemplateDoc: true, isTemplateForField: "onChildDoubleClick", _width: 300, _height: 200
+ }, "onChildDoubleClick");
const onCheckedClick = Docs.Create.ScriptingDocument(undefined, {
title: "onCheckedClick", "onCheckedClick-rawScript": "console.log(heading + checked + containingTreeView)", "onCheckedClick-params": new List<string>(["heading", "checked", "containingTreeView"]), isTemplateDoc: true, isTemplateForField: "onCheckedClick", _width: 300, _height: 200
}, "onCheckedClick");
- doc.clickFuncs = Docs.Create.TreeDocument([onClick, onDoubleClick, onCheckedClick], { title: "onClick funcs" });
+ doc.clickFuncs = Docs.Create.TreeDocument([onClick, onChildClick, onDoubleClick, onCheckedClick], { title: "onClick funcs" });
}
PromiseValue(Cast(doc.clickFuncs, Doc)).then(func => func && PromiseValue(func.data).then(DocListCast));
@@ -782,18 +854,20 @@ export class CurrentUserUtils {
doc.activeInkColor = StrCast(doc.activeInkColor, "rgb(0, 0, 0)");
doc.activeInkWidth = StrCast(doc.activeInkWidth, "1");
doc.activeInkBezier = StrCast(doc.activeInkBezier, "0");
- doc.activeFillColor = StrCast(doc.activeFillColor, "none");
- doc.activeArrowStart = StrCast(doc.activeArrowStart, "none");
- doc.activeArrowEnd = StrCast(doc.activeArrowEnd, "none");
+ doc.activeFillColor = StrCast(doc.activeFillColor, "");
+ doc.activeArrowStart = StrCast(doc.activeArrowStart, "");
+ doc.activeArrowEnd = StrCast(doc.activeArrowEnd, "");
doc.activeDash = StrCast(doc.activeDash, "0");
- doc.fontSize = NumCast(doc.fontSize, 12);
- doc["constants-snapThreshold"] = NumCast(doc["constants-snapThreshold"], 10); //
- doc["constants-dragThreshold"] = NumCast(doc["constants-dragThreshold"], 4); //
+ doc.fontSize = StrCast(doc.fontSize, "12pt");
+ doc.fontFamily = StrCast(doc.fontFamily, "Arial");
+ doc["constants-snapThreshold"] = NumCast(doc["constants-snapThreshold"], 10); //
+ doc["constants-dragThreshold"] = NumCast(doc["constants-dragThreshold"], 4); //
Utils.DRAG_THRESHOLD = NumCast(doc["constants-dragThreshold"]);
this.setupDefaultIconTemplates(doc); // creates a set of icon templates triggered by the document deoration icon
this.setupDocTemplates(doc); // sets up the template menu of templates
this.setupRightSidebar(doc); // sets up the right sidebar collection for mobile upload documents and sharing
- this.setupOverlays(doc); // documents in overlay layer
+ this.setupActiveMobileMenu(doc); // sets up the current mobile menu for Dash Mobile
+ this.setupOverlays(doc); // documents in overlay layer
this.setupDockedButtons(doc); // the bottom bar of font icons
this.setupDefaultPresentation(doc); // presentation that's initially triggered
await this.setupSidebarButtons(doc); // the pop-out left sidebar of tools/panels
@@ -832,9 +906,10 @@ export class CurrentUserUtils {
}
}
-Scripting.addGlobal(function setupMobileInkingDoc(userDoc: Doc) { return CurrentUserUtils.setupMobileInkingDoc(userDoc); },
- "initializes the Mobile inking document", "(userDoc: Doc)");
-Scripting.addGlobal(function setupMobileUploadDoc(userDoc: Doc) { return CurrentUserUtils.setupMobileUploadDoc(userDoc); },
- "initializes the Mobile upload document", "(userDoc: Doc)");
Scripting.addGlobal(function createNewWorkspace() { return MainView.Instance.createNewWorkspace(); },
- "creates a new workspace when called"); \ No newline at end of file
+ "creates a new workspace when called");
+
+Scripting.addGlobal(function links(doc: any) { return new List(LinkManager.Instance.getAllRelatedLinks(doc)); },
+ "returns all the links to the document or its annotations", "(doc: any)");
+Scripting.addGlobal(function directLinks(doc: any) { return new List(LinkManager.Instance.getAllDirectLinks(doc)); },
+ "returns all the links directly to the document", "(doc: any)");
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index 28b1ca6cf..540540642 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -121,7 +121,7 @@ export namespace DictationManager {
const listenImpl = (options?: Partial<ListeningOptions>) => {
if (!recognizer) {
- console.log(unsupported);
+ console.log("DictationManager:" + unsupported);
return unsupported;
}
if (isListening) {
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 1fa5faeb3..523dbfca0 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -9,6 +9,7 @@ import { LinkManager } from './LinkManager';
import { Scripting } from './Scripting';
import { SelectionManager } from './SelectionManager';
import { DocumentType } from '../documents/DocumentTypes';
+import { TraceMobx } from '../../fields/util';
export type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => void) => void;
@@ -104,8 +105,9 @@ export class DocumentManager {
@computed
public get LinkedDocumentViews() {
+ TraceMobx();
const pairs = DocumentManager.Instance.DocumentViews.reduce((pairs, dv) => {
- const linksList = LinkManager.Instance.getAllRelatedLinks(dv.props.Document);
+ const linksList = DocListCast(dv.props.Document.links);
pairs.push(...linksList.reduce((pairs, link) => {
const linkToDoc = link && LinkManager.Instance.getOppositeAnchor(link, dv.props.Document);
linkToDoc && DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => {
@@ -146,22 +148,22 @@ export class DocumentManager {
};
const docView = getFirstDocView(targetDoc, originatingDoc);
let annotatedDoc = await Cast(targetDoc.annotationOn, Doc);
- if (annotatedDoc && !linkDoc?.isPushpin) {
+ if (annotatedDoc && !targetDoc?.isPushpin) {
const first = getFirstDocView(annotatedDoc);
if (first) {
annotatedDoc = first.props.Document;
- if (docView) {
- docView.props.focus(annotatedDoc, false);
- }
+ docView?.props.focus(annotatedDoc, false);
}
}
if (docView) { // we have a docView already and aren't forced to create a new one ... just focus on the document. TODO move into view if necessary otherwise just highlight?
- if (linkDoc?.isPushpin) docView.props.Document.hidden = !docView.props.Document.hidden;
+ if (originatingDoc?.isPushpin) {
+ docView.props.Document.hidden = !docView.props.Document.hidden;
+ }
else {
docView.props.Document.hidden && (docView.props.Document.hidden = undefined);
docView.props.focus(docView.props.Document, willZoom, undefined, focusAndFinish);
+ highlight();
}
- highlight();
} else {
const contextDocs = docContext ? await DocListCastAsync(docContext.data) : undefined;
const contextDoc = contextDocs?.find(doc => Doc.AreProtosEqual(doc, targetDoc)) ? docContext : undefined;
@@ -216,25 +218,27 @@ export class DocumentManager {
const backLinkWithoutTargetView = secondDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor1 as Doc).length === 0);
const linkWithoutTargetDoc = traverseBacklink === undefined ? fwdLinkWithoutTargetView || backLinkWithoutTargetView : traverseBacklink ? backLinkWithoutTargetView : fwdLinkWithoutTargetView;
const linkDocList = linkWithoutTargetDoc ? [linkWithoutTargetDoc] : (traverseBacklink === undefined ? firstDocs.concat(secondDocs) : traverseBacklink ? secondDocs : firstDocs);
- const linkDoc = linkDocList.length && linkDocList[0];
- if (linkDoc) {
- const target = (doc === linkDoc.anchor1 ? linkDoc.anchor2 : doc === linkDoc.anchor2 ? linkDoc.anchor1 :
- (Doc.AreProtosEqual(doc, linkDoc.anchor1 as Doc) ? linkDoc.anchor2 : linkDoc.anchor1)) as Doc;
- const targetTimecode = (doc === linkDoc.anchor1 ? Cast(linkDoc.anchor2_timecode, "number") :
- doc === linkDoc.anchor2 ? Cast(linkDoc.anchor1_timecode, "number") :
- (Doc.AreProtosEqual(doc, linkDoc.anchor1 as Doc) ? Cast(linkDoc.anchor2_timecode, "number") : Cast(linkDoc.anchor1_timecode, "number")));
- if (target) {
- const containerDoc = (await Cast(target.annotationOn, Doc)) || target;
- containerDoc.currentTimecode = targetTimecode;
- const targetContext = await target?.context as Doc;
- const targetNavContext = !Doc.AreProtosEqual(targetContext, currentContext) ? targetContext : undefined;
- DocumentManager.Instance.jumpToDocument(target, zoom, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, "onRight"), finished), targetNavContext, linkDoc, undefined, doc, finished);
+ const followLinks = linkDocList.length ? (doc.isPushpin ? linkDocList : [linkDocList[0]]) : [];
+ followLinks.forEach(async linkDoc => {
+ if (linkDoc) {
+ const target = (doc === linkDoc.anchor1 ? linkDoc.anchor2 : doc === linkDoc.anchor2 ? linkDoc.anchor1 :
+ (Doc.AreProtosEqual(doc, linkDoc.anchor1 as Doc) ? linkDoc.anchor2 : linkDoc.anchor1)) as Doc;
+ const targetTimecode = (doc === linkDoc.anchor1 ? Cast(linkDoc.anchor2_timecode, "number") :
+ doc === linkDoc.anchor2 ? Cast(linkDoc.anchor1_timecode, "number") :
+ (Doc.AreProtosEqual(doc, linkDoc.anchor1 as Doc) ? Cast(linkDoc.anchor2_timecode, "number") : Cast(linkDoc.anchor1_timecode, "number")));
+ if (target) {
+ const containerDoc = (await Cast(target.annotationOn, Doc)) || target;
+ containerDoc.currentTimecode = targetTimecode;
+ const targetContext = await target?.context as Doc;
+ const targetNavContext = !Doc.AreProtosEqual(targetContext, currentContext) ? targetContext : undefined;
+ DocumentManager.Instance.jumpToDocument(target, zoom, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, "onRight"), finished), targetNavContext, linkDoc, undefined, doc, finished);
+ } else {
+ finished?.();
+ }
} else {
finished?.();
}
- } else {
- finished?.();
- }
+ });
}
}
Scripting.addGlobal(function DocFocus(doc: any) { DocumentManager.Instance.getDocumentViews(Doc.GetProto(doc)).map(view => view.props.focus(doc, true)); }); \ No newline at end of file
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 2ceafff30..4b1860b5c 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -7,7 +7,7 @@ import { listSpec } from "../../fields/Schema";
import { SchemaHeaderField } from "../../fields/SchemaHeaderField";
import { ScriptField } from "../../fields/ScriptField";
import { Cast, NumCast, ScriptCast, StrCast } from "../../fields/Types";
-import { emptyFunction } from "../../Utils";
+import { emptyFunction, returnTrue } from "../../Utils";
import { Docs, DocUtils } from "../documents/Documents";
import * as globalCssVariables from "../views/globalCssVariables.scss";
import { UndoManager } from "./UndoManager";
@@ -83,6 +83,7 @@ export namespace DragManager {
hideSource?: boolean; // hide source document during drag
offsetX?: number; // offset of top left of source drag visual from cursor
offsetY?: number;
+ noAutoscroll?: boolean;
}
// event called when the drag operation results in a drop action
@@ -223,15 +224,18 @@ export namespace DragManager {
}
// drag a button template and drop a new button
- export function StartButtonDrag(eles: HTMLElement[], script: string, title: string, vars: { [name: string]: Field }, params: string[], initialize: (button: Doc) => void, downX: number, downY: number, options?: DragOptions) {
+ export function
+ StartButtonDrag(eles: HTMLElement[], script: string, title: string, vars: { [name: string]: Field }, params: string[], initialize: (button: Doc) => void, downX: number, downY: number, options?: DragOptions) {
const finishDrag = (e: DragCompleteEvent) => {
- const bd = Docs.Create.ButtonDocument({ _width: 150, _height: 50, title, onClick: ScriptField.MakeScript(script) });
+ const bd = Docs.Create.ButtonDocument({ toolTip: title, z: 1, _width: 150, _height: 50, title, onClick: ScriptField.MakeScript(script) });
params.map(p => Object.keys(vars).indexOf(p) !== -1 && (Doc.GetProto(bd)[p] = new PrefetchProxy(vars[p] as Doc))); // copy all "captured" arguments into document parameterfields
initialize?.(bd);
Doc.GetProto(bd)["onClick-paramFieldKeys"] = new List<string>(params);
e.docDragData && (e.docDragData.droppedDocuments = [bd]);
return e;
};
+ options = options ?? {};
+ options.noAutoscroll = true; // these buttons are being dragged on the overlay layer, so scrollin the underlay is not appropriate
StartDrag(eles, new DragManager.DocumentDragData([]), downX, downY, options, finishDrag);
}
@@ -323,7 +327,7 @@ export namespace DragManager {
dragLabel = document.createElement("div");
dragLabel.className = "dragManager-dragLabel";
dragLabel.style.zIndex = "100001";
- dragLabel.style.fontSize = "10";
+ dragLabel.style.fontSize = "10pt";
dragLabel.style.position = "absolute";
// dragLabel.innerText = "press 'a' to embed on drop"; // bcz: need to move this to a status bar
dragDiv.appendChild(dragLabel);
@@ -407,6 +411,7 @@ export namespace DragManager {
const yFromTop = downY - elesCont.top;
const xFromRight = elesCont.right - downX;
const yFromBottom = elesCont.bottom - downY;
+ let scrollAwaiter: Opt<NodeJS.Timeout>;
const moveHandler = (e: PointerEvent) => {
e.preventDefault(); // required or dragging text menu link item ends up dragging the link button as native drag/drop
if (dragData instanceof DocumentDragData) {
@@ -427,6 +432,59 @@ export namespace DragManager {
}, dragData.droppedDocuments);
}
+ const target = document.elementFromPoint(e.x, e.y);
+
+ if (target && !options?.noAutoscroll && !dragData.draggedDocuments?.some((d: any) => d._noAutoscroll)) {
+ const autoScrollHandler = () => {
+ target.dispatchEvent(
+ new CustomEvent<React.DragEvent>("dashDragAutoScroll", {
+ bubbles: true,
+ detail: {
+ shiftKey: e.shiftKey,
+ altKey: e.altKey,
+ metaKey: e.metaKey,
+ ctrlKey: e.ctrlKey,
+ clientX: e.clientX,
+ clientY: e.clientY,
+ dataTransfer: new DataTransfer,
+ button: e.button,
+ buttons: e.buttons,
+ getModifierState: e.getModifierState,
+ movementX: e.movementX,
+ movementY: e.movementY,
+ pageX: e.pageX,
+ pageY: e.pageY,
+ relatedTarget: e.relatedTarget,
+ screenX: e.screenX,
+ screenY: e.screenY,
+ detail: e.detail,
+ view: e.view ? e.view : new Window,
+ nativeEvent: new DragEvent("dashDragAutoScroll"),
+ currentTarget: target,
+ target: target,
+ bubbles: true,
+ cancelable: true,
+ defaultPrevented: true,
+ eventPhase: e.eventPhase,
+ isTrusted: true,
+ preventDefault: () => "not implemented for this event" ? false : false,
+ isDefaultPrevented: () => "not implemented for this event" ? false : false,
+ stopPropagation: () => "not implemented for this event" ? false : false,
+ isPropagationStopped: () => "not implemented for this event" ? false : false,
+ persist: emptyFunction,
+ timeStamp: e.timeStamp,
+ type: "dashDragAutoScroll"
+ }
+ })
+ );
+
+ scrollAwaiter && clearTimeout(scrollAwaiter);
+ SnappingManager.GetIsDragging() && (scrollAwaiter = setTimeout(autoScrollHandler, 25));
+ };
+ scrollAwaiter && clearTimeout(scrollAwaiter);
+ scrollAwaiter = setTimeout(autoScrollHandler, 250);
+ }
+
const { thisX, thisY } = snapDrag(e, xFromLeft, yFromTop, xFromRight, yFromBottom);
const moveX = thisX - lastX;
diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
index f9837298d..d0acf14c3 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -57,7 +57,7 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
data?.draggedDocuments.map((doc, i) => {
let dbox = doc;
// bcz: isButtonBar is intended to allow a collection of linear buttons to be dropped and nested into another collection of buttons... it's not being used yet, and isn't very elegant
- if (doc.type === DocumentType.FONTICON) {
+ if (doc.type === DocumentType.FONTICON || StrCast(Doc.Layout(doc).layout).includes("FontIconBox")) {
dbox = Doc.MakeAlias(doc);
} else if (!doc.onDragStart && !doc.isButtonBar) {
const layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc;
diff --git a/src/client/util/GroupManager.scss b/src/client/util/GroupManager.scss
index 544a79e98..51e4fa9e2 100644
--- a/src/client/util/GroupManager.scss
+++ b/src/client/util/GroupManager.scss
@@ -1,23 +1,47 @@
-@import "../views/globalCssVariables";
-
.group-interface {
- background-color: whitesmoke !important;
- color: grey;
- width: 450px;
+ width: 380px;
height: 300px;
.dialogue-box {
- width: 450;
- height: 300;
+ .group-create {
+ display: flex;
+ flex-direction: column;
+ height: 90%;
+ justify-content: space-between;
+ margin-left: 5px;
+
+ input {
+ border-radius: 5px;
+ padding: 8px;
+ min-width: 100%;
+ border: 1px solid hsl(0, 0%, 80%);
+ outline: none;
+ height: 30;
+
+ &:focus {
+ border: 2.5px solid #2684FF;
+ }
+ }
+
+ p {
+ font-size: 20px;
+ text-align: left;
+ color: black;
+ }
+
+ button {
+ align-self: flex-end;
+ }
+ }
}
+
button {
- background: $lighter-alt-accent;
+ align-self: center;
outline: none;
border-radius: 5px;
border: 0px;
- color: #fcfbf7;
- text-transform: uppercase;
+ text-transform: none;
letter-spacing: 2px;
font-size: 75%;
padding: 10px;
@@ -36,12 +60,6 @@
border-radius: 10px;
}
- button {
- width: 100%;
- align-self: center;
- background: $darker-alt-accent;
- }
-
.delete-button {
background: rgb(227, 86, 86);
}
@@ -55,33 +73,33 @@
}
.group-heading {
- letter-spacing: .5em;
- }
-
-
- .group-body {
display: flex;
- justify-content: space-between;
- max-height: 80%;
+ align-items: center;
+ margin-bottom: 25px;
- .group-create {
- display: flex;
- flex-direction: column;
- flex-basis: 30%;
- margin-left: 5px;
+ p {
+ font-size: 20px;
+ text-align: left;
+ margin-right: 15px;
+ color: black;
+ }
+ }
- input {
- border-radius: 5px;
- border: none;
- padding: 4px;
- min-width: 100%;
- margin: 4px 0 4px 0;
- }
+ .main-container {
+ display: flex;
+ flex-direction: column;
+ .sort-groups {
+ text-align: left;
+ margin-left: 5;
+ cursor: pointer;
}
- .group-content {
- padding-left: 1em;
+ .group-body {
+ justify-content: space-between;
+ height: 220;
+ background-color: #e8e8e8;
+
padding-right: 1em;
justify-content: space-around;
text-align: left;
@@ -91,17 +109,18 @@
.group-row {
display: flex;
- position: relative;
margin-bottom: 5px;
- min-height: 40px;
- border: 1px solid;
- border-radius: 10px;
+ min-height: 30px;
align-items: center;
.group-name {
- position: relative;
max-width: 65%;
- left: 10;
+ margin: 0 10;
+ color: black;
+ }
+
+ .group-info {
+ cursor: pointer;
}
button {
@@ -112,10 +131,6 @@
}
}
- ::placeholder {
- color: $intermediate-color;
- }
-
input {
border-radius: 5px;
border: none;
@@ -126,11 +141,4 @@
}
}
-
- h1 {
- color: $dark-color;
- text-transform: uppercase;
- letter-spacing: 2px;
- font-size: 120%;
- }
} \ No newline at end of file
diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx
index 7c68fc2a0..72fba5c1b 100644
--- a/src/client/util/GroupManager.tsx
+++ b/src/client/util/GroupManager.tsx
@@ -3,7 +3,7 @@ import { observable, action, runInAction, computed } from "mobx";
import { SelectionManager } from "./SelectionManager";
import MainViewModal from "../views/MainViewModal";
import { observer } from "mobx-react";
-import { Doc, DocListCast, Opt } from "../../fields/Doc";
+import { Doc, DocListCast, Opt, DocListCastAsync } from "../../fields/Doc";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as fa from '@fortawesome/free-solid-svg-icons';
import { library } from "@fortawesome/fontawesome-svg-core";
@@ -12,10 +12,13 @@ import { Utils } from "../../Utils";
import * as RequestPromise from "request-promise";
import Select from 'react-select';
import "./GroupManager.scss";
-import { StrCast } from "../../fields/Types";
+import { StrCast, Cast } from "../../fields/Types";
import GroupMemberView from "./GroupMemberView";
+import { setGroups } from "../../fields/util";
+import { DocServer } from "../DocServer";
+import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox";
-library.add(fa.faWindowClose);
+library.add(fa.faPlus, fa.faTimes, fa.faInfoCircle);
export interface UserOptions {
label: string;
@@ -32,26 +35,58 @@ export default class GroupManager extends React.Component<{}> {
@observable private users: string[] = []; // list of users populated from the database.
@observable private selectedUsers: UserOptions[] | null = null; // list of users selected in the "Select users" dropdown.
@observable currentGroup: Opt<Doc>; // the currently selected group.
+ @observable private createGroupModalOpen: boolean = false;
private inputRef: React.RefObject<HTMLInputElement> = React.createRef(); // the ref for the input box.
+ private createGroupButtonRef: React.RefObject<HTMLButtonElement> = React.createRef();
+ private currentUserGroups: string[] = [];
+ @observable private buttonColour: "#979797" | "black" = "#979797";
+ @observable private groupSort: "ascending" | "descending" | "none" = "none";
+
+
constructor(props: Readonly<{}>) {
super(props);
GroupManager.Instance = this;
}
- // sets up the list of users
componentDidMount() {
- this.populateUsers().then(resolved => runInAction(() => this.users = resolved));
+ this.populateUsers();
+ this.populateGroups();
}
/**
- * Fetches the list of users stored on the database and @returns a list of the emails.
+ * Fetches the list of users stored on the database.
*/
populateUsers = async () => {
- const userList: User[] = JSON.parse(await RequestPromise.get(Utils.prepend("/getUsers")));
- const currentUserIndex = userList.findIndex(user => user.email === Doc.CurrentUserEmail);
- currentUserIndex !== -1 && userList.splice(currentUserIndex, 1);
- return userList.map(user => user.email);
+ runInAction(() => this.users = []);
+ const userList = await RequestPromise.get(Utils.prepend("/getUsers"));
+ const raw = JSON.parse(userList) as User[];
+ const evaluating = raw.map(async user => {
+ // const isCandidate = user.email !== Doc.CurrentUserEmail;
+ // if (isCandidate) {
+ const userDocument = await DocServer.GetRefField(user.userDocumentId);
+ if (userDocument instanceof Doc) {
+ const notificationDoc = await Cast(userDocument.rightSidebarCollection, Doc);
+ runInAction(() => {
+ if (notificationDoc instanceof Doc) {
+ this.users.push(user.email);
+ }
+ });
+ }
+ // }
+ });
+ return Promise.all(evaluating);
+ }
+
+ populateGroups = () => {
+ DocListCastAsync(this.GroupManagerDoc?.data).then(groups => {
+ groups?.forEach(group => {
+ const members: string[] = JSON.parse(StrCast(group.members));
+ if (members.includes(Doc.CurrentUserEmail)) this.currentUserGroups.push(StrCast(group.groupName));
+ });
+
+ setGroups(this.currentUserGroups);
+ });
}
/**
@@ -68,6 +103,8 @@ export default class GroupManager extends React.Component<{}> {
open = () => {
SelectionManager.DeselectAll();
this.isOpen = true;
+ this.populateUsers();
+ this.populateGroups();
}
/**
@@ -77,6 +114,8 @@ export default class GroupManager extends React.Component<{}> {
close = () => {
this.isOpen = false;
this.currentGroup = undefined;
+ // this.users = [];
+ this.createGroupModalOpen = false;
}
/**
@@ -89,7 +128,8 @@ export default class GroupManager extends React.Component<{}> {
/**
* @returns a list of all group documents.
*/
- private getAllGroups(): Doc[] {
+ // private ?
+ getAllGroups(): Doc[] {
const groupDoc = this.GroupManagerDoc;
return groupDoc ? DocListCast(groupDoc.data) : [];
}
@@ -98,7 +138,8 @@ export default class GroupManager extends React.Component<{}> {
* @returns a group document based on the group name.
* @param groupName
*/
- private getGroup(groupName: string): Doc | undefined {
+ // private?
+ getGroup(groupName: string): Doc | undefined {
const groupDoc = this.getAllGroups().find(group => group.groupName === groupName);
return groupDoc;
}
@@ -123,6 +164,11 @@ export default class GroupManager extends React.Component<{}> {
);
}
+ getGroupMembers(group: string | Doc): string[] {
+ if (group instanceof Doc) return JSON.parse(StrCast(group.members)) as string[];
+ else return JSON.parse(StrCast(this.getGroup(group)!.members)) as string[];
+ }
+
/**
* @returns the members of the admin group.
*/
@@ -150,6 +196,10 @@ export default class GroupManager extends React.Component<{}> {
groupDoc.groupName = groupName;
groupDoc.owners = JSON.stringify([Doc.CurrentUserEmail]);
groupDoc.members = JSON.stringify(memberEmails);
+ if (memberEmails.includes(Doc.CurrentUserEmail)) {
+ this.currentUserGroups.push(groupName);
+ setGroups(this.currentUserGroups);
+ }
this.addGroup(groupDoc);
}
@@ -172,8 +222,16 @@ export default class GroupManager extends React.Component<{}> {
deleteGroup(group: Doc): boolean {
if (group) {
if (this.GroupManagerDoc && this.hasEditAccess(group)) {
+ // TODO look at this later
+ // SharingManager.Instance.setInternalGroupSharing(group, "Not Shared");
Doc.RemoveDocFromList(this.GroupManagerDoc, "data", group);
- SharingManager.Instance.setInternalGroupSharing(group, "Not Shared");
+ SharingManager.Instance.removeGroup(group);
+ const members: string[] = JSON.parse(StrCast(group.members));
+ if (members.includes(Doc.CurrentUserEmail)) {
+ const index = this.currentUserGroups.findIndex(groupName => groupName === group.groupName);
+ index !== -1 && this.currentUserGroups.splice(index, 1);
+ setGroups(this.currentUserGroups);
+ }
if (group === this.currentGroup) {
runInAction(() => this.currentGroup = undefined);
}
@@ -193,6 +251,7 @@ export default class GroupManager extends React.Component<{}> {
const memberList: string[] = JSON.parse(StrCast(groupDoc.members));
!memberList.includes(email) && memberList.push(email);
groupDoc.members = JSON.stringify(memberList);
+ SharingManager.Instance.shareWithAddedMember(groupDoc, email);
}
}
@@ -205,8 +264,11 @@ export default class GroupManager extends React.Component<{}> {
if (this.hasEditAccess(groupDoc)) {
const memberList: string[] = JSON.parse(StrCast(groupDoc.members));
const index = memberList.indexOf(email);
- index !== -1 && memberList.splice(index, 1);
- groupDoc.members = JSON.stringify(memberList);
+ if (index !== -1) {
+ const user = memberList.splice(index, 1)[0];
+ groupDoc.members = JSON.stringify(memberList);
+ SharingManager.Instance.removeMember(groupDoc, email);
+ }
}
}
@@ -243,104 +305,133 @@ export default class GroupManager extends React.Component<{}> {
this.createGroupDoc(this.inputRef.current.value, this.selectedUsers?.map(user => user.value));
this.selectedUsers = null;
this.inputRef.current.value = "";
+ this.buttonColour = "#979797";
+
+ const { left, width, top } = this.createGroupButtonRef.current!.getBoundingClientRect();
+ TaskCompletionBox.popupX = left - 2 * width;
+ TaskCompletionBox.popupY = top;
+ TaskCompletionBox.textDisplayed = "Group created!";
+ TaskCompletionBox.taskCompleted = true;
+ setTimeout(action(() => TaskCompletionBox.taskCompleted = false), 2000);
+
}
- /**
- * A getter that @returns the interface rendered to view an individual group.
- */
- private get editingInterface() {
- const members: string[] = this.currentGroup ? JSON.parse(StrCast(this.currentGroup.members)) : [];
- const options: UserOptions[] = this.currentGroup ? this.options.filter(option => !(JSON.parse(StrCast(this.currentGroup!.members)) as string[]).includes(option.value)) : [];
- return (!this.currentGroup ? null :
- <div className="editing-interface">
- <div className="editing-header">
- <b>{this.currentGroup.groupName}</b>
- <div className={"close-button"} onClick={action(() => this.currentGroup = undefined)}>
- <FontAwesomeIcon icon={fa.faWindowClose} size={"lg"} />
+ private get groupCreationModal() {
+ const contents = (
+ <div className="group-create">
+ <div className="group-heading" style={{ marginBottom: 0 }}>
+ <p><b>New Group</b></p>
+ <div className={"close-button"} onClick={action(() => this.createGroupModalOpen = false)}>
+ <FontAwesomeIcon icon={fa.faTimes} color={"black"} size={"lg"} />
</div>
-
- {this.hasEditAccess(this.currentGroup) ?
- <div className="group-buttons">
- <div className="add-member-dropdown">
- <Select
- // isMulti={true}
- isSearchable={true}
- options={options}
- onChange={selectedOption => this.addMemberToGroup(this.currentGroup!, (selectedOption as UserOptions).value)}
- placeholder={"Add members"}
- value={null}
- closeMenuOnSelect={true}
- />
- </div>
- <button onClick={() => this.deleteGroup(this.currentGroup!)}>Delete group</button>
- </div> :
- null}
- </div>
- <div className="editing-contents">
- {members.map(member => (
- <div className="editing-row">
- <div className="user-email">
- {member}
- </div>
- {this.hasEditAccess(this.currentGroup!) ? <button onClick={() => this.removeMemberFromGroup(this.currentGroup!, member)}> Remove </button> : null}
- </div>
- ))}
</div>
+ <input
+ className="group-input"
+ ref={this.inputRef}
+ onKeyDown={this.handleKeyDown}
+ type="text"
+ placeholder="Group name"
+ onChange={action(() => this.buttonColour = this.inputRef.current?.value ? "black" : "#979797")} />
+ <Select
+ isMulti={true}
+ isSearchable={true}
+ options={this.options}
+ onChange={this.handleChange}
+ placeholder={"Select users"}
+ value={this.selectedUsers}
+ closeMenuOnSelect={false}
+ styles={{
+ dropdownIndicator: (base, state) => ({
+ ...base,
+ transition: '0.5s all ease',
+ transform: state.selectProps.menuIsOpen ? 'rotate(180deg)' : undefined
+ }),
+ multiValue: (base) => ({
+ ...base,
+ maxWidth: "50%",
+
+ '&:hover': {
+ maxWidth: "unset"
+ }
+ })
+ }}
+ />
+ <button
+ ref={this.createGroupButtonRef}
+ onClick={this.createGroup}
+ style={{ background: this.buttonColour }}
+ disabled={this.buttonColour === "#979797"}
+ >
+ Create
+ </button>
</div>
);
+ return (
+ <MainViewModal
+ isDisplayed={this.createGroupModalOpen}
+ interactive={true}
+ contents={contents}
+ dialogueBoxStyle={{ width: "90%", height: "70%" }}
+ closeOnExternalClick={action(() => this.createGroupModalOpen = false)}
+ />
+ );
}
/**
* A getter that @returns the main interface for the GroupManager.
*/
private get groupInterface() {
+
+ const sortGroups = (d1: Doc, d2: Doc) => {
+ const g1 = StrCast(d1.groupName);
+ const g2 = StrCast(d2.groupName);
+
+ return g1 < g2 ? -1 : g1 === g2 ? 0 : 1;
+ };
+
+ let groups = this.getAllGroups();
+ groups = this.groupSort === "ascending" ? groups.sort(sortGroups) : this.groupSort === "descending" ? groups.sort(sortGroups).reverse() : groups;
+
return (
<div className="group-interface">
- {/* <MainViewModal
- contents={this.editingInterface}
- isDisplayed={this.currentGroup ? true : false}
- interactive={true}
- dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity}
- overlayDisplayedOpacity={this.overlayOpacity}
- /> */}
+ {this.groupCreationModal}
{this.currentGroup ?
<GroupMemberView
group={this.currentGroup}
- onCloseButtonClick={() => this.currentGroup = undefined}
+ onCloseButtonClick={action(() => this.currentGroup = undefined)}
/>
: null}
<div className="group-heading">
- <h1>Groups</h1>
+ <p><b>Manage Groups</b></p>
+ <button onClick={action(() => this.createGroupModalOpen = true)}>
+ <FontAwesomeIcon icon={fa.faPlus} size={"sm"} /> Create Group
+ </button>
<div className={"close-button"} onClick={this.close}>
- <FontAwesomeIcon icon={fa.faWindowClose} size={"lg"} />
+ <FontAwesomeIcon icon={fa.faTimes} color={"black"} size={"lg"} />
</div>
</div>
- <div className="group-body">
- <div className="group-create">
- <button onClick={this.createGroup}>Create group</button>
- <input ref={this.inputRef} onKeyDown={this.handleKeyDown} type="text" placeholder="Group name" />
- <Select
- isMulti={true}
- isSearchable={true}
- options={this.options}
- onChange={this.handleChange}
- placeholder={"Select users"}
- value={this.selectedUsers}
- closeMenuOnSelect={false}
- />
+ <div className="main-container">
+ <div
+ className="sort-groups"
+ onClick={action(() => this.groupSort = this.groupSort === "ascending" ? "descending" : this.groupSort === "descending" ? "none" : "ascending")}>
+ Name {this.groupSort === "ascending" ? "↑" : this.groupSort === "descending" ? "↓" : ""} {/* → */}
</div>
- <div className="group-content">
- {this.getAllGroups().map(group =>
- <div className="group-row">
- <div className="group-name">{group.groupName}</div>
- <button onClick={action(() => this.currentGroup = group)}>
- {this.hasEditAccess(group) ? "Edit" : "View"}
- </button>
+ <div className="group-body">
+ {groups.map(group =>
+ <div
+ className="group-row"
+ key={StrCast(group.groupName)}
+ >
+ <div className="group-name" >{group.groupName}</div>
+ <div className="group-info" onClick={action(() => this.currentGroup = group)}>
+ <FontAwesomeIcon icon={fa.faInfoCircle} color={"#e8e8e8"} size={"sm"} style={{ backgroundColor: "#1e89d7", borderRadius: "100%", border: "1px solid #1e89d7" }} />
+ </div>
</div>
)}
</div>
</div>
+
</div>
);
}
@@ -351,8 +442,9 @@ export default class GroupManager extends React.Component<{}> {
contents={this.groupInterface}
isDisplayed={this.isOpen}
interactive={true}
- dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity}
- overlayDisplayedOpacity={this.overlayOpacity}
+ dialogueBoxStyle={{ zIndex: 1002 }}
+ overlayStyle={{ zIndex: 1001 }}
+ closeOnExternalClick={this.close}
/>
);
}
diff --git a/src/client/util/GroupMemberView.scss b/src/client/util/GroupMemberView.scss
index 7833c485f..c609c5c7b 100644
--- a/src/client/util/GroupMemberView.scss
+++ b/src/client/util/GroupMemberView.scss
@@ -1,18 +1,17 @@
-@import "../views/globalCssVariables";
-
.editing-interface {
- background-color: whitesmoke !important;
- color: grey;
width: 100%;
height: 100%;
+ hr {
+ margin-top: 20;
+ }
+
button {
- background: $darker-alt-accent;
outline: none;
border-radius: 5px;
border: 0px;
color: #fcfbf7;
- text-transform: uppercase;
+ text-transform: none;
letter-spacing: 2px;
font-size: 75%;
padding: 10px;
@@ -32,34 +31,69 @@
.editing-header {
margin-bottom: 5;
+ .group-title {
+ font-weight: bold;
+ font-size: 15;
+ text-align: center;
+ border: none;
+ outline: none;
+ color: black;
+ margin-top: -5;
+ height: 20;
+ text-overflow: ellipsis;
+
+ &:hover {
+ text-overflow: visible;
+ overflow-x: auto;
+ }
+ }
+
+ .sort-emails {
+ float: left;
+ margin: -18 0 0 5;
+ cursor: pointer;
+ }
+
.group-buttons {
display: flex;
margin-top: 5;
+ margin-bottom: 25;
.add-member-dropdown {
- width: 100%;
+ width: 65%;
margin: 0 5;
+
+ input {
+ height: 30;
+ }
}
}
}
.editing-contents {
overflow-y: auto;
- // max-height: 67%;
- height: 67%;
+ height: 65%;
width: 100%;
+ color: black;
+ margin-top: -15px;
.editing-row {
display: flex;
align-items: center;
- // border: 1px solid;
- // border-radius: 10px;
+ margin-bottom: 10px;
+ position: relative;
.user-email {
- // position: relative;
min-width: 65%;
word-break: break-all;
padding: 0 5;
+ text-align: left;
+ }
+
+ .remove-button {
+ position: absolute;
+ right: 10;
+ cursor: pointer;
}
}
}
diff --git a/src/client/util/GroupMemberView.tsx b/src/client/util/GroupMemberView.tsx
index b2d75158e..f20670c4e 100644
--- a/src/client/util/GroupMemberView.tsx
+++ b/src/client/util/GroupMemberView.tsx
@@ -4,14 +4,14 @@ import { observer } from "mobx-react";
import GroupManager, { UserOptions } from "./GroupManager";
import { library } from "@fortawesome/fontawesome-svg-core";
import { StrCast } from "../../fields/Types";
-import { action } from "mobx";
+import { action, observable } from "mobx";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as fa from '@fortawesome/free-solid-svg-icons';
import Select from "react-select";
-import { Doc, Opt } from "../../fields/Doc";
+import { Doc } from "../../fields/Doc";
import "./GroupMemberView.scss";
-library.add(fa.faWindowClose);
+library.add(fa.faTimes, fa.faTrashAlt);
interface GroupMemberViewProps {
group: Doc;
@@ -21,17 +21,26 @@ interface GroupMemberViewProps {
@observer
export default class GroupMemberView extends React.Component<GroupMemberViewProps> {
+ @observable private memberSort: "ascending" | "descending" | "none" = "none";
+
private get editingInterface() {
- const members: string[] = this.props.group ? JSON.parse(StrCast(this.props.group.members)) : [];
+ let members: string[] = this.props.group ? JSON.parse(StrCast(this.props.group.members)) : [];
+ members = this.memberSort === "ascending" ? members.sort() : this.memberSort === "descending" ? members.sort().reverse() : members;
+
const options: UserOptions[] = this.props.group ? GroupManager.Instance.options.filter(option => !(JSON.parse(StrCast(this.props.group.members)) as string[]).includes(option.value)) : [];
+
return (!this.props.group ? null :
<div className="editing-interface">
<div className="editing-header">
- <b>{this.props.group.groupName}</b>
+ <input
+ className="group-title"
+ value={StrCast(this.props.group.groupName)}
+ onChange={e => this.props.group.groupName = e.currentTarget.value}
+ >
+ </input>
<div className={"memberView-closeButton"} onClick={action(this.props.onCloseButtonClick)}>
- <FontAwesomeIcon icon={fa.faWindowClose} size={"lg"} />
+ <FontAwesomeIcon icon={fa.faTimes} color={"black"} size={"lg"} />
</div>
-
{GroupManager.Instance.hasEditAccess(this.props.group) ?
<div className="group-buttons">
<div className="add-member-dropdown">
@@ -42,19 +51,39 @@ export default class GroupMemberView extends React.Component<GroupMemberViewProp
placeholder={"Add members"}
value={null}
closeMenuOnSelect={true}
+ styles={{
+ dropdownIndicator: (base, state) => ({
+ ...base,
+ transition: '0.5s all ease',
+ transform: state.selectProps.menuIsOpen ? 'rotate(180deg)' : undefined
+ })
+ }}
/>
</div>
<button onClick={() => GroupManager.Instance.deleteGroup(this.props.group)}>Delete group</button>
</div> :
null}
+ <div
+ className="sort-emails"
+ onClick={action(() => this.memberSort = this.memberSort === "ascending" ? "descending" : this.memberSort === "descending" ? "none" : "ascending")}>
+ Emails {this.memberSort === "ascending" ? "↑" : this.memberSort === "descending" ? "↓" : ""} {/* → */}
+ </div>
</div>
+ <hr />
<div className="editing-contents">
{members.map(member => (
- <div className="editing-row">
+ <div
+ className="editing-row"
+ key={member}
+ >
<div className="user-email">
{member}
</div>
- {GroupManager.Instance.hasEditAccess(this.props.group) ? <button onClick={() => GroupManager.Instance.removeMemberFromGroup(this.props.group, member)}> Remove </button> : null}
+ {GroupManager.Instance.hasEditAccess(this.props.group) ?
+ <div className={"remove-button"} onClick={() => GroupManager.Instance.removeMemberFromGroup(this.props.group, member)}>
+ <FontAwesomeIcon icon={fa.faTrashAlt} size={"sm"} />
+ </div>
+ : null}
</div>
))}
</div>
@@ -68,6 +97,8 @@ export default class GroupMemberView extends React.Component<GroupMemberViewProp
isDisplayed={true}
interactive={true}
contents={this.editingInterface}
+ dialogueBoxStyle={{ width: 400, height: 250 }}
+ closeOnExternalClick={this.props.onCloseButtonClick}
/>;
}
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index edeb461e0..04a750f93 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -27,7 +27,7 @@ export namespace InteractionUtils {
export interface MultiTouchEventDisposer { (): void; }
/**
- *
+ *
* @param element - element to turn into a touch target
* @param startFunc - event handler, typically Touchable.onTouchStart (classes that inherit touchable can pass in this.onTouchStart)
*/
@@ -94,10 +94,20 @@ export namespace InteractionUtils {
export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number,
color: string, width: number, strokeWidth: number, bezier: string, fill: string, arrowStart: string, arrowEnd: string,
dash: string, scalex: number, scaley: number, shape: string, pevents: string, drawHalo: boolean, nodefs: boolean) {
+
let pts: { X: number; Y: number; }[] = [];
if (shape) { //if any of the shape are true
pts = makePolygon(shape, points);
}
+ else if (points.length >= 5 && points[3].X === points[4].X) {
+ for (var i = 0; i < points.length - 3; i += 4) {
+ const array = [[points[i].X, points[i].Y], [points[i + 1].X, points[i + 1].Y], [points[i + 2].X, points[i + 2].Y], [points[i + 3].X, points[i + 3].Y]];
+ for (var t = 0; t < 1; t += 0.01) {
+ const point = beziercurve(t, array);
+ pts.push({ X: point[0], Y: point[1] });
+ }
+ }
+ }
else if (points.length > 1 && points[points.length - 1].X === points[0].X && points[points.length - 1].Y === points[0].Y) {
//pointer is up (first and last points are the same)
const newPoints = points.reduce((p, pts) => { p.push([pts.X, pts.Y]); return p; }, [] as number[][]);
@@ -111,7 +121,17 @@ export namespace InteractionUtils {
}
}
} else {
- pts = points;
+ pts = points.slice();
+ // bcz: Ugh... this is ugly, but shapes apprently have an extra point added that is = (p[0].x,p[0].y+1) as some sort of flag. need to remove it here.
+ if (pts.length > 2 && pts[pts.length - 2].X === pts[0].X && pts[pts.length - 2].Y === pts[0].Y) {
+ pts.pop();
+ }
+ }
+ if (isNaN(scalex)) {
+ scalex = 1;
+ }
+ if (isNaN(scaley)) {
+ scaley = 1;
}
const strpts = pts.reduce((acc: string, pt: { X: number, Y: number }) => acc +
`${(pt.X - left - width / 2) * scalex + width / 2},
@@ -119,24 +139,23 @@ export namespace InteractionUtils {
const dashArray = String(Number(width) * Number(dash));
const defGuid = Utils.GenerateGuid();
const arrowDim = Math.max(0.5, 8 / Math.log(Math.max(2, strokeWidth)));
- return (<svg fill={fill === "none" ? color : fill}> {/* setting the svg fill sets the arrowhead fill */}
+ return (<svg fill={color}> {/* setting the svg fill sets the arrowStart fill */}
{nodefs ? (null) : <defs>
{arrowStart !== "dot" && arrowEnd !== "dot" ? (null) : <marker id={`dot${defGuid}`} orient="auto" overflow="visible">
<circle r={1} fill="context-stroke" />
</marker>}
- {arrowStart !== "arrowHead" && arrowEnd !== "arrowHead" ? (null) : <marker id={`arrowHead${defGuid}`} orient="auto" overflow="visible" refX="1.6" refY="0" markerWidth="10" markerHeight="7">
+ {arrowStart !== "arrow" && arrowEnd !== "arrow" ? (null) : <marker id={`arrowStart${defGuid}`} orient="auto" overflow="visible" refX="1.6" refY="0" markerWidth="10" markerHeight="7">
<polygon points={`${arrowDim} ${-Math.max(1, arrowDim / 2)}, ${arrowDim} ${Math.max(1, arrowDim / 2)}, -1 0`} />
</marker>}
- {arrowStart !== "arrowEnd" && arrowEnd !== "arrowEnd" ? (null) : <marker id={`arrowEnd${defGuid}`} orient="auto" overflow="visible" refX="1.6" refY="0" markerWidth="10" markerHeight="7">
+ {arrowStart !== "arrow" && arrowEnd !== "arrow" ? (null) : <marker id={`arrowEnd${defGuid}`} orient="auto" overflow="visible" refX="1.6" refY="0" markerWidth="10" markerHeight="7">
<polygon points={`${2 - arrowDim} ${-Math.max(1, arrowDim / 2)}, ${2 - arrowDim} ${Math.max(1, arrowDim / 2)}, 3 0`} />
</marker>}
</defs>}
-
<polyline
points={strpts}
style={{
filter: drawHalo ? "url(#inkSelectionHalo)" : undefined,
- fill,
+ fill: fill ? fill : "transparent",
opacity: strokeWidth !== width ? 0.5 : undefined,
pointerEvents: pevents as any,
stroke: color ?? "rgb(0, 0, 0)",
@@ -145,25 +164,13 @@ export namespace InteractionUtils {
strokeLinecap: "round",
strokeDasharray: dashArray
}}
- markerStart={`url(#${arrowStart + defGuid})`}
- markerEnd={`url(#${arrowEnd + defGuid})`}
+ markerStart={`url(#${arrowStart + "Start" + defGuid})`}
+ markerEnd={`url(#${arrowEnd + "End" + defGuid})`}
/>
</svg>);
}
- // export function makeArrow() {
- // return (
- // InkOptionsMenu.Instance.getColors().map(color => {
- // const id1 = "arrowHeadTest" + color;
- // console.log(color);
- // <marker id={id1} orient="auto" overflow="visible" refX="0" refY="1" markerWidth="10" markerHeight="7">
- // <polygon points="0 0, 3 1, 0 2" fill={"#" + color} />
- // </marker>;
- // })
- // );
- // }
-
export function makePolygon(shape: string, points: { X: number, Y: number }[]) {
if (points.length > 1 && points[points.length - 1].X === points[0].X && points[points.length - 1].Y + 1 === points[0].Y) {
//pointer is up (first and last points are the same)
@@ -213,10 +220,28 @@ export namespace InteractionUtils {
points.push({ X: left, Y: top });
return points;
case "triangle":
+ // points.push({ X: left, Y: bottom });
+ // points.push({ X: right, Y: bottom });
+ // points.push({ X: (right + left) / 2, Y: top });
+ // points.push({ X: left, Y: bottom });
+
+ points.push({ X: left, Y: bottom });
points.push({ X: left, Y: bottom });
+
+ points.push({ X: right, Y: bottom });
+ points.push({ X: right, Y: bottom });
+ points.push({ X: right, Y: bottom });
points.push({ X: right, Y: bottom });
+
+ points.push({ X: (right + left) / 2, Y: top });
+ points.push({ X: (right + left) / 2, Y: top });
points.push({ X: (right + left) / 2, Y: top });
+ points.push({ X: (right + left) / 2, Y: top });
+
+ points.push({ X: left, Y: bottom });
points.push({ X: left, Y: bottom });
+
+
return points;
case "circle":
const centerX = (right + left) / 2;
@@ -252,6 +277,7 @@ export namespace InteractionUtils {
// points.push({ X: x2, Y: y2 });
// return points;
case "line":
+
points.push({ X: left, Y: top });
points.push({ X: right, Y: bottom });
return points;
@@ -278,8 +304,8 @@ export namespace InteractionUtils {
/**
* Returns euclidean distance between two points
- * @param pt1
- * @param pt2
+ * @param pt1
+ * @param pt2
*/
export function TwoPointEuclidist(pt1: React.Touch, pt2: React.Touch): number {
return Math.sqrt(Math.pow(pt1.clientX - pt2.clientX, 2) + Math.pow(pt1.clientY - pt2.clientY, 2));
@@ -378,7 +404,6 @@ export namespace InteractionUtils {
// let dist12 = TwoPointEuclidist(pt1, pt2);
// let dist23 = TwoPointEuclidist(pt2, pt3);
// let dist13 = TwoPointEuclidist(pt1, pt3);
- // console.log(`distances: ${dist12}, ${dist23}, ${dist13}`);
// let dist12close = dist12 < leniency;
// let dist23close = dist23 < leniency;
// let dist13close = dist13 < leniency;
@@ -410,4 +435,4 @@ export namespace InteractionUtils {
// }
// }
}
-} \ No newline at end of file
+}
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 749fabfcc..223f0e7ef 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -1,8 +1,7 @@
-import { Doc, DocListCast } from "../../fields/Doc";
+import { Doc, DocListCast, Opt } from "../../fields/Doc";
import { List } from "../../fields/List";
import { listSpec } from "../../fields/Schema";
import { Cast, StrCast } from "../../fields/Types";
-import { Scripting } from "./Scripting";
/*
* link doc:
@@ -23,9 +22,13 @@ import { Scripting } from "./Scripting";
export class LinkManager {
private static _instance: LinkManager;
+
+ public static currentLink: Opt<Doc>;
+
public static get Instance(): LinkManager {
return this._instance || (this._instance = new this());
}
+
private constructor() {
}
@@ -57,12 +60,17 @@ export class LinkManager {
}
// finds all links that contain the given anchor
- public getAllRelatedLinks(anchor: Doc): Doc[] {
+ public getAllDirectLinks(anchor: Doc): Doc[] {
const related = LinkManager.Instance.getAllLinks().filter(link => {
const protomatch1 = Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, null));
const protomatch2 = Doc.AreProtosEqual(anchor, Cast(link.anchor2, Doc, null));
return protomatch1 || protomatch2 || Doc.AreProtosEqual(link, anchor);
});
+ return related;
+ }
+ // finds all links that contain the given anchor
+ public getAllRelatedLinks(anchor: Doc): Doc[] {
+ const related = LinkManager.Instance.getAllDirectLinks(anchor);
DocListCast(anchor[Doc.LayoutFieldKey(anchor) + "-annotations"]).map(anno => {
related.push(...LinkManager.Instance.getAllRelatedLinks(anno));
});
@@ -199,7 +207,4 @@ export class LinkManager {
if (Doc.AreProtosEqual(anchor, a2)) return a1;
if (Doc.AreProtosEqual(anchor, linkDoc)) return linkDoc;
}
-}
-
-Scripting.addGlobal(function links(doc: any) { return new List(LinkManager.Instance.getAllRelatedLinks(doc)); },
- "creates a link to inputted document", "(doc: any)"); \ No newline at end of file
+} \ No newline at end of file
diff --git a/src/client/util/ScriptManager.ts b/src/client/util/ScriptManager.ts
index 785e63d9a..94806a7ba 100644
--- a/src/client/util/ScriptManager.ts
+++ b/src/client/util/ScriptManager.ts
@@ -32,24 +32,17 @@ export class ScriptManager {
}
public addScript(scriptDoc: Doc): boolean {
-
- console.log("in add script method");
-
const scriptList = this.getAllScripts();
scriptList.push(scriptDoc);
if (ScriptManager.Instance.ScriptManagerDoc) {
ScriptManager.Instance.ScriptManagerDoc.data = new List<Doc>(scriptList);
ScriptManager.addScriptToGlobals(scriptDoc);
- console.log("script added");
return true;
}
return false;
}
public deleteScript(scriptDoc: Doc): boolean {
-
- console.log("in delete script method");
-
if (scriptDoc.name) {
Scripting.removeGlobal(StrCast(scriptDoc.name));
}
@@ -70,7 +63,6 @@ export class ScriptManager {
Scripting.removeGlobal(StrCast(scriptDoc.name));
const params = Cast(scriptDoc["data-params"], listSpec("string"), []);
- console.log(params);
const paramNames = params.reduce((o: string, p: string) => {
if (params.indexOf(p) === params.length - 1) {
o = o + p.split(":")[0].trim();
@@ -82,8 +74,6 @@ export class ScriptManager {
const f = new Function(paramNames, StrCast(scriptDoc.script));
- console.log(scriptDoc.script);
-
Object.defineProperty(f, 'name', { value: StrCast(scriptDoc.name), writable: false });
let parameters = "(";
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index 09e2aa56b..cb0a4bea0 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -10,8 +10,6 @@ export { ts };
// @ts-ignore
import * as typescriptlib from '!!raw-loader!./type_decls.d';
import { Doc, Field } from '../../fields/Doc';
-import { Cast } from "../../fields/Types";
-import { listSpec } from "../../fields/Schema";
export interface ScriptSucccess {
success: true;
@@ -95,10 +93,10 @@ export namespace Scripting {
export function removeGlobal(name: string) {
if (getGlobals().includes(name)) {
delete _scriptingGlobals[name];
- if (_scriptingDescriptions[name]){
+ if (_scriptingDescriptions[name]) {
delete _scriptingDescriptions[name];
}
- if (_scriptingParams[name]){
+ if (_scriptingParams[name]) {
delete _scriptingParams[name];
}
return true;
@@ -123,11 +121,11 @@ export namespace Scripting {
return _scriptingGlobals;
}
- export function getDescriptions(){
+ export function getDescriptions() {
return _scriptingDescriptions;
}
- export function getParameters(){
+ export function getParameters() {
return _scriptingParams;
}
}
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 024532f90..20d881961 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -14,6 +14,7 @@ export namespace SelectionManager {
SelectedDocuments: ObservableMap<DocumentView, boolean> = new ObservableMap();
@action
SelectDoc(docView: DocumentView, ctrlPressed: boolean): void {
+
// if doc is not in SelectedDocuments, add it
if (!manager.SelectedDocuments.get(docView)) {
if (!ctrlPressed) {
@@ -21,7 +22,6 @@ export namespace SelectionManager {
}
manager.SelectedDocuments.set(docView, true);
- // console.log(manager.SelectedDocuments);
docView.props.whenActiveChanged(true);
} else if (!ctrlPressed && Array.from(manager.SelectedDocuments.entries()).length > 1) {
Array.from(manager.SelectedDocuments.keys()).map(dv => dv !== docView && dv.props.whenActiveChanged(false));
diff --git a/src/client/util/SettingsManager.scss b/src/client/util/SettingsManager.scss
index fa2609ca2..c1627e69f 100644
--- a/src/client/util/SettingsManager.scss
+++ b/src/client/util/SettingsManager.scss
@@ -52,11 +52,12 @@
.settings-body {
display: flex;
justify-content: space-between;
+ margin-top: -10;
.settings-type {
display: flex;
flex-direction: column;
- flex-basis: 30%;
+ flex-basis: 45%;
}
@@ -105,6 +106,7 @@
text-transform: uppercase;
letter-spacing: 2px;
font-size: 120%;
+ margin-top: 0;
}
.container {
@@ -134,4 +136,25 @@
}
+}
+
+@media only screen and (max-device-width: 480px) {
+ .settings-interface {
+ width: 80vw;
+ height: 400px;
+ }
+
+ .settings-interface .settings-body .settings-content input {
+ font-size: 30;
+ }
+
+ .settings-interface button {
+ width: 100%;
+ font-size: 30px;
+ background: #b2cef8;
+ }
+
+ .settings-interface .settings-heading {
+ font-size: 25;
+ }
} \ No newline at end of file
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index 9888cce48..fbdceb43e 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -9,20 +9,27 @@ import "./SettingsManager.scss";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Networking } from "../Network";
import { CurrentUserUtils } from "./CurrentUserUtils";
-import { Utils } from "../../Utils";
+import { Utils, addStyleSheet, addStyleSheetRule, removeStyleSheetRule } from "../../Utils";
import { Doc } from "../../fields/Doc";
+import GroupManager from "./GroupManager";
+import HypothesisAuthenticationManager from "../apis/HypothesisAuthenticationManager";
+import GoogleAuthenticationManager from "../apis/GoogleAuthenticationManager";
+import { togglePlaygroundMode } from "../../fields/util";
+import { DocServer } from "../DocServer";
-library.add(fa.faWindowClose);
+library.add(fa.faTimes);
@observer
export default class SettingsManager extends React.Component<{}> {
public static Instance: SettingsManager;
+ static _settingsStyle = addStyleSheet();
@observable private isOpen = false;
@observable private dialogueBoxOpacity = 1;
@observable private overlayOpacity = 0.4;
@observable private settingsContent = "password";
@observable private errorText = "";
@observable private successText = "";
+ @observable private playgroundMode = false;
private curr_password_ref = React.createRef<HTMLInputElement>();
private new_password_ref = React.createRef<HTMLInputElement>();
private new_confirm_ref = React.createRef<HTMLInputElement>();
@@ -83,6 +90,24 @@ export default class SettingsManager extends React.Component<{}> {
noviceToggle = (event: any) => {
Doc.UserDoc().noviceMode = !Doc.UserDoc().noviceMode;
}
+ @action
+ googleAuthorize = (event: any) => {
+ GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(true);
+ }
+ @action
+ hypothesisAuthorize = (event: any) => {
+ HypothesisAuthenticationManager.Instance.fetchAccessToken(true);
+ }
+
+ @action
+ togglePlaygroundMode = () => {
+ //togglePlaygroundMode();
+ this.playgroundMode = !this.playgroundMode;
+ if (this.playgroundMode) DocServer.Control.makeReadOnly();
+ else DocServer.Control.makeEditable();
+
+ addStyleSheetRule(SettingsManager._settingsStyle, "lm_header", { background: "pink !important" });
+ }
private get settingsInterface() {
return (
@@ -90,13 +115,17 @@ export default class SettingsManager extends React.Component<{}> {
<div className="settings-heading">
<h1>settings</h1>
<div className={"close-button"} onClick={this.close}>
- <FontAwesomeIcon icon={fa.faWindowClose} size={"lg"} />
+ <FontAwesomeIcon icon={fa.faTimes} color="black" size={"lg"} />
</div>
</div>
<div className="settings-body">
<div className="settings-type">
<button onClick={this.onClick} value="password">reset password</button>
- <button onClick={this.noviceToggle} value="data">{`toggle ${Doc.UserDoc().noviceMode ? "developer" : "novice"} mode`}</button>
+ <button onClick={this.noviceToggle} value="data">{`Set ${Doc.UserDoc().noviceMode ? "developer" : "novice"} mode`}</button>
+ <button onClick={this.togglePlaygroundMode}>{`${this.playgroundMode ? "Disable" : "Enable"} playground mode`}</button>
+ <button onClick={this.googleAuthorize} value="data">{`Link to Google`}</button>
+ <button onClick={this.hypothesisAuthorize} value="data">{`Link to Hypothes.is`}</button>
+ <button onClick={() => GroupManager.Instance.open()}>Manage groups</button>
<button onClick={() => window.location.assign(Utils.prepend("/logout"))}>
{CurrentUserUtils.GuestWorkspace ? "Exit" : "Log Out"}
</button>
@@ -132,8 +161,7 @@ export default class SettingsManager extends React.Component<{}> {
contents={this.settingsInterface}
isDisplayed={this.isOpen}
interactive={true}
- dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity}
- overlayDisplayedOpacity={this.overlayOpacity}
+ closeOnExternalClick={this.close}
/>
);
}
diff --git a/src/client/util/SharingManager.scss b/src/client/util/SharingManager.scss
index fcbc05f8a..130785672 100644
--- a/src/client/util/SharingManager.scss
+++ b/src/client/util/SharingManager.scss
@@ -1,29 +1,112 @@
-@import "../views/globalCssVariables";
-
.sharing-interface {
- display: flex;
- flex-direction: column;
- width: 730px;
-
- .dialogue-box {
- width: 450;
- height: 300;
- }
+ width: 600px;
+ height: 360px;
.overlay {
transform: translate(-20px, -20px);
}
+ select {
+ text-align: justify;
+ text-align-last: end
+ }
+
.sharing-contents {
display: flex;
+ flex-direction: column;
+
+ .close-button {
+ position: absolute;
+ right: 1em;
+ top: 1em;
+ cursor: pointer;
+ z-index: 999;
+ }
+
+ .share-setup {
+ display: flex;
+ margin-bottom: 20px;
+ align-items: center;
+ height: 36;
+
+ .user-search {
+ width: 90%;
+
+ input {
+ height: 30;
+ }
+ }
+
+ .permissions-select {
+ z-index: 1;
+ margin-left: -100;
+ border: none;
+ outline: none;
+ text-align: justify; // for Edge
+ text-align-last: end;
+ }
+
+ .share-button {
+ height: 105%;
+ margin-left: 2%;
+ background-color: #979797;
+ }
+ }
+
+ .main-container {
+ display: flex;
+ margin-top: -10px;
+
+ .individual-container,
+ .group-container {
+ width: 50%;
+ display: flex;
+ flex-direction: column;
+
+ .user-sort {
+ text-align: left;
+ margin-left: 10;
+ cursor: pointer;
+ }
+
+ .share-title {
+ margin-top: 20px;
+ margin-bottom: 20px;
+ }
+
+ .groups-list,
+ .users-list {
+ font-style: italic;
+ background: #e8e8e8;
+ padding-left: 10px;
+ padding-right: 10px;
+ overflow-y: scroll;
+ overflow-x: hidden;
+ text-align: left;
+ display: flex;
+ align-content: center;
+ align-items: center;
+ text-align: center;
+ justify-content: center;
+ color: black;
+ height: 250px;
+ margin: 0 2;
+
+
+ .none {
+ font-style: italic;
+
+ }
+ }
+ }
+ }
button {
- background: $darker-alt-accent;
outline: none;
border-radius: 5px;
border: 0px;
color: #fcfbf7;
- text-transform: uppercase;
+ text-transform: none;
letter-spacing: 2px;
font-size: 75%;
padding: 0 10;
@@ -31,37 +114,6 @@
transition: transform 0.2s;
height: 25;
}
-
- .individual-container,
- .group-container {
- width: 50%;
-
- .share-groups,
- .share-individual {
- margin-top: 20px;
- margin-bottom: 20px;
- }
-
- .groups-list,
- .users-list {
- font-style: italic;
- background: white;
- border: 1px solid black;
- padding-left: 10px;
- padding-right: 10px;
- overflow-y: scroll;
- overflow-x: hidden;
- text-align: left;
- display: flex;
- align-content: center;
- align-items: center;
- text-align: center;
- justify-content: center;
- color: red;
- height: 150px;
- margin: 0 2;
- }
- }
}
.focus-span {
@@ -69,11 +121,10 @@
}
p {
- font-size: 15px;
+ font-size: 20px;
text-align: left;
- font-style: italic;
- padding: 0;
margin: 0 0 20px 0;
+ color: black;
}
.hr-substitute {
@@ -108,30 +159,41 @@
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
- width: 700px;
- min-width: 700px;
- max-width: 700px;
+ width: 100%;
text-align: left;
font-style: normal;
font-size: 14;
font-weight: normal;
padding: 0;
- align-items: baseline;
+ align-items: center;
+
+ .group-info {
+ cursor: pointer;
+ }
+
+ &:hover .padding {
+ white-space: unset;
+ }
.padding {
padding: 0 10px 0 0;
color: black;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ max-width: 40%;
}
.permissions-dropdown {
- outline: none;
+ border: none;
height: 25;
+ background-color: #e8e8e8;
}
.edit-actions {
display: flex;
position: absolute;
- right: 51.5%;
+ right: -10;
}
}
@@ -171,18 +233,4 @@
padding-top: 12px;
}
}
-
- .close-button {
- border-radius: 5px;
- margin-top: 20px;
- padding: 10px 0;
- background: aliceblue;
- transition: 0.5s ease all;
- border: 1px solid;
- border-color: aliceblue;
- }
-
- .close-button:hover {
- border-color: black;
- }
} \ No newline at end of file
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index 127ee33ce..0d8b33fbe 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -1,62 +1,43 @@
import { observable, runInAction, action } from "mobx";
import * as React from "react";
import MainViewModal from "../views/MainViewModal";
-import { Doc, Opt, DocCastAsync } from "../../fields/Doc";
+import { Doc, Opt, DocListCastAsync } from "../../fields/Doc";
import { DocServer } from "../DocServer";
import { Cast, StrCast } from "../../fields/Types";
import * as RequestPromise from "request-promise";
import { Utils } from "../../Utils";
import "./SharingManager.scss";
-import { Id } from "../../fields/FieldSymbols";
import { observer } from "mobx-react";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { library } from '@fortawesome/fontawesome-svg-core';
import * as fa from '@fortawesome/free-solid-svg-icons';
import { DocumentView } from "../views/nodes/DocumentView";
import { SelectionManager } from "./SelectionManager";
import { DocumentManager } from "./DocumentManager";
import { CollectionView } from "../views/collections/CollectionView";
import { DictationOverlay } from "../views/DictationOverlay";
-import GroupManager from "./GroupManager";
+import GroupManager, { UserOptions } from "./GroupManager";
import GroupMemberView from "./GroupMemberView";
-
-library.add(fa.faCopy);
+import Select from "react-select";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { List } from "../../fields/List";
+import { distributeAcls, SharingPermissions } from "../../fields/util";
+import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox";
export interface User {
email: string;
userDocumentId: string;
}
-export enum SharingPermissions {
- None = "Not Shared",
- View = "Can View",
- Add = "Can Add",
- Edit = "Can Edit"
+interface GroupOptions {
+ label: string;
+ options: UserOptions[];
}
-const ColorMapping = new Map<string, string>([
- [SharingPermissions.None, "red"],
- [SharingPermissions.View, "maroon"],
- [SharingPermissions.Add, "blue"],
- [SharingPermissions.Edit, "green"]
-]);
-
-const HierarchyMapping = new Map<string, string>([
- [SharingPermissions.None, "0"],
- [SharingPermissions.View, "1"],
- [SharingPermissions.Add, "2"],
- [SharingPermissions.Edit, "3"],
-
- ["0", SharingPermissions.None],
- ["1", SharingPermissions.View],
- ["2", SharingPermissions.Add],
- ["3", SharingPermissions.Edit]
-
-]);
+// const SharingKey = "sharingPermissions";
+// const PublicKey = "publicLinkPermissions";
+// const DefaultColor = "black";
-const SharingKey = "sharingPermissions";
-const PublicKey = "publicLinkPermissions";
-const DefaultColor = "black";
+const groupType = "!groupType/";
+const indType = "!indType/";
interface ValidatedUser {
user: User;
@@ -70,17 +51,22 @@ export default class SharingManager extends React.Component<{}> {
public static Instance: SharingManager;
@observable private isOpen = false;
@observable private users: ValidatedUser[] = [];
- @observable private groups: Doc[] = [];
@observable private targetDoc: Doc | undefined;
@observable private targetDocView: DocumentView | undefined;
- @observable private copied = false;
+ // @observable private copied = false;
@observable private dialogueBoxOpacity = 1;
@observable private overlayOpacity = 0.4;
- @observable private groupToView: Opt<Doc>;
+ @observable private selectedUsers: UserOptions[] | null = null;
+ @observable private permissions: SharingPermissions = SharingPermissions.Edit;
+ @observable private individualSort: "ascending" | "descending" | "none" = "none";
+ @observable private groupSort: "ascending" | "descending" | "none" = "none";
+ private shareDocumentButtonRef: React.RefObject<HTMLButtonElement> = React.createRef();
- private get linkVisible() {
- return this.sharingDoc ? this.sharingDoc[PublicKey] !== SharingPermissions.None : false;
- }
+
+
+ // private get linkVisible() {
+ // return this.sharingDoc ? this.sharingDoc[PublicKey] !== SharingPermissions.None : false;
+ // }
public open = (target: DocumentView) => {
SelectionManager.DeselectAll();
@@ -89,32 +75,23 @@ export default class SharingManager extends React.Component<{}> {
this.targetDoc = target.props.Document;
DictationOverlay.Instance.hasActiveModal = true;
this.isOpen = true;
- if (!this.sharingDoc) {
- this.sharingDoc = new Doc;
- }
+ this.permissions = SharingPermissions.Edit;
}));
- runInAction(() => this.groups = GroupManager.Instance.getAllGroupsCopy());
}
public close = action(() => {
this.isOpen = false;
this.users = [];
+ this.selectedUsers = null;
+
setTimeout(action(() => {
- this.copied = false;
+ // this.copied = false;
DictationOverlay.Instance.hasActiveModal = false;
this.targetDoc = undefined;
}), 500);
});
- private get sharingDoc() {
- return this.targetDoc ? Cast(this.targetDoc[SharingKey], Doc) as Doc : undefined;
- }
-
- private set sharingDoc(value: Doc | undefined) {
- this.targetDoc && (this.targetDoc[SharingKey] = value);
- }
-
constructor(props: {}) {
super(props);
SharingManager.Instance = this;
@@ -142,98 +119,118 @@ export default class SharingManager extends React.Component<{}> {
setInternalGroupSharing = (group: Doc, permission: string) => {
const members: string[] = JSON.parse(StrCast(group.members));
- const users: ValidatedUser[] = this.users.filter(user => members.includes(user.user.email));
+ const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email));
- const sharingDoc = this.sharingDoc!;
- if (permission === SharingPermissions.None) {
- const metadata = sharingDoc[StrCast(group.groupName)];
- if (metadata) sharingDoc[StrCast(group.groupName)] = undefined;
- }
- else {
- sharingDoc[StrCast(group.groupName)] = permission;
- }
+ const target = this.targetDoc!;
+ const ACL = `ACL-${StrCast(group.groupName)}`;
+ // fix this - not needed (here and setinternalsharing and removegroup)
+ // target[ACL] = permission;
+ // Doc.GetProto(target)[ACL] = permission;
+
+ distributeAcls(ACL, permission as SharingPermissions, this.targetDoc!);
+
+ group.docsShared ? DocListCastAsync(group.docsShared).then(resolved => Doc.IndexOf(target, resolved!) === -1 && (group.docsShared as List<Doc>).push(target)) : group.docsShared = new List<Doc>([target]);
- users.forEach(user => {
- this.setInternalSharing(user, permission, group);
+ users.forEach(({ notificationDoc }) => {
+ DocListCastAsync(notificationDoc[storage]).then(resolved => {
+ if (permission !== SharingPermissions.None) Doc.IndexOf(target, resolved!) === -1 && Doc.AddDocToList(notificationDoc, storage, target);
+ else Doc.IndexOf(target, resolved!) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, target);
+ });
});
}
- setInternalSharing = async (recipient: ValidatedUser, state: string, group: Opt<Doc>) => {
- const { user, notificationDoc } = recipient;
- const target = this.targetDoc!;
- const manager = this.sharingDoc!;
- const key = user.userDocumentId;
-
- let metadata = await DocCastAsync(manager[key]);
- const permissions: { [key: string]: number } = metadata?.permissions ? JSON.parse(StrCast(metadata.permissions)) : {};
- permissions[StrCast(group ? group.groupName : Doc.CurrentUserEmail)] = parseInt(HierarchyMapping.get(state)!);
- const max = Math.max(...Object.values(permissions));
-
- // let max = 0;
- // const keys: string[] = [];
- // for (const [key, value] of Object.entries(permissions)) {
- // if (value === max && max !== 0) {
- // keys.push(key);
- // }
- // else if (value > max) {
- // keys.splice(0, keys.length);
- // keys.push(key);
- // max = value;
- // }
- // }
-
- switch (max) {
- case 0:
- if (metadata) {
- const sharedAlias = (await DocCastAsync(metadata.sharedAlias))!;
- Doc.RemoveDocFromList(notificationDoc, storage, sharedAlias);
- manager[key] = undefined;
- }
- break;
-
- case 1: case 2: case 3:
- if (!metadata) {
- metadata = new Doc;
- const sharedAlias = Doc.MakeAlias(target);
- Doc.AddDocToList(notificationDoc, storage, sharedAlias);
- metadata.sharedAlias = sharedAlias;
- manager[key] = metadata;
- }
- metadata.permissions = JSON.stringify(permissions);
- // metadata.usersShared = JSON.stringify(keys);
- break;
- }
+ shareWithAddedMember = (group: Doc, emailId: string) => {
+ const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!;
- if (metadata) metadata.maxPermission = HierarchyMapping.get(`${max}`);
+ if (group.docsShared) {
+ DocListCastAsync(group.docsShared).then(docsShared => {
+ docsShared?.forEach(doc => {
+ DocListCastAsync(user.notificationDoc[storage]).then(resolved => Doc.IndexOf(doc, resolved!) === -1 && Doc.AddDocToList(user.notificationDoc, storage, doc));
+ });
+ });
+ }
}
- private setExternalSharing = (state: string) => {
- const sharingDoc = this.sharingDoc;
- if (!sharingDoc) {
- return;
+ removeMember = (group: Doc, emailId: string) => {
+ const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!;
+
+ if (group.docsShared) {
+ DocListCastAsync(group.docsShared).then(docsShared => {
+ docsShared?.forEach(doc => {
+ DocListCastAsync(user.notificationDoc[storage]).then(resolved => Doc.IndexOf(doc, resolved!) !== -1 && Doc.RemoveDocFromList(user.notificationDoc, storage, doc));
+ });
+ });
}
- sharingDoc[PublicKey] = state;
}
- private get sharingUrl() {
- if (!this.targetDoc) {
- return undefined;
+ removeGroup = (group: Doc) => {
+ if (group.docsShared) {
+ DocListCastAsync(group.docsShared).then(resolved => {
+ resolved?.forEach(doc => {
+ const ACL = `ACL-${StrCast(group.groupName)}`;
+ // doc[ACL] = doc[DataSym][ACL] = "Not Shared";
+
+ distributeAcls(ACL, SharingPermissions.None, doc);
+
+ const members: string[] = JSON.parse(StrCast(group.members));
+ const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email));
+
+ users.forEach(({ notificationDoc }) => Doc.RemoveDocFromList(notificationDoc, storage, doc));
+ });
+
+ });
}
- const baseUrl = Utils.prepend("/doc/" + this.targetDoc[Id]);
- return `${baseUrl}?sharing=true`;
}
- copy = action(() => {
- if (this.sharingUrl) {
- Utils.CopyText(this.sharingUrl);
- this.copied = true;
+ // @action
+ setInternalSharing = (recipient: ValidatedUser, permission: string) => {
+ const { user, notificationDoc } = recipient;
+ const target = this.targetDoc!;
+ const key = user.email.replace('.', '_');
+ const ACL = `ACL-${key}`;
+
+ distributeAcls(ACL, permission as SharingPermissions, this.targetDoc!);
+
+ if (permission !== SharingPermissions.None) {
+ DocListCastAsync(notificationDoc[storage]).then(resolved => {
+ Doc.IndexOf(target, resolved!) === -1 && Doc.AddDocToList(notificationDoc, storage, target);
+ });
}
- });
+ else {
+ DocListCastAsync(notificationDoc[storage]).then(resolved => {
+ Doc.IndexOf(target, resolved!) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, target);
+ });
+ }
+ }
+
+
+ // private setExternalSharing = (permission: string) => {
+ // const sharingDoc = this.sharingDoc;
+ // if (!sharingDoc) {
+ // return;
+ // }
+ // sharingDoc[PublicKey] = permission;
+ // }
+
+ // private get sharingUrl() {
+ // if (!this.targetDoc) {
+ // return undefined;
+ // }
+ // const baseUrl = Utils.prepend("/doc/" + this.targetDoc[Id]);
+ // return `${baseUrl}?sharing=true`;
+ // }
+
+ // copy = action(() => {
+ // if (this.sharingUrl) {
+ // Utils.CopyText(this.sharingUrl);
+ // this.copied = true;
+ // }
+ // });
private get sharingOptions() {
return Object.values(SharingPermissions).map(permission => {
return (
- <option key={permission} value={permission}>
+ <option key={permission} value={permission} selected={permission === SharingPermissions.Edit}>
{permission}
</option>
);
@@ -270,33 +267,151 @@ export default class SharingManager extends React.Component<{}> {
);
}
- private computePermissions = (userKey: string) => {
- const sharingDoc = this.sharingDoc;
- if (!sharingDoc) {
- return SharingPermissions.None;
- }
- const metadata = sharingDoc[userKey] as Doc | string;
- if (!metadata) {
- return SharingPermissions.None;
+ @action
+ handleUsersChange = (selectedOptions: any) => {
+ this.selectedUsers = selectedOptions as UserOptions[];
+ }
+
+ @action
+ handlePermissionsChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
+ this.permissions = event.currentTarget.value as SharingPermissions;
+ }
+
+ @action
+ share = () => {
+ if (this.selectedUsers) {
+ this.selectedUsers.forEach(user => {
+ if (user.value.includes(indType)) {
+ this.setInternalSharing(this.users.find(u => u.user.email === user.label)!, this.permissions);
+ }
+ else {
+ this.setInternalGroupSharing(GroupManager.Instance.getGroup(user.label)!, this.permissions);
+ }
+ });
+
+ const { left, width, top, height } = this.shareDocumentButtonRef.current!.getBoundingClientRect();
+ TaskCompletionBox.popupX = left - 1.5 * width;
+ TaskCompletionBox.popupY = top - height;
+ TaskCompletionBox.textDisplayed = "Document shared!";
+ TaskCompletionBox.taskCompleted = true;
+ setTimeout(action(() => TaskCompletionBox.taskCompleted = false), 2000);
+
+ this.selectedUsers = null;
}
- return StrCast(metadata instanceof Doc ? metadata.maxPermission : metadata, SharingPermissions.None);
+ }
+
+ sortUsers = (u1: ValidatedUser, u2: ValidatedUser) => {
+ const { email: e1 } = u1.user;
+ const { email: e2 } = u2.user;
+ return e1 < e2 ? -1 : e1 === e2 ? 0 : 1;
+ }
+
+ sortGroups = (group1: Doc, group2: Doc) => {
+ const g1 = StrCast(group1.groupName);
+ const g2 = StrCast(group2.groupName);
+ return g1 < g2 ? -1 : g1 === g2 ? 0 : 1;
}
private get sharingInterface() {
- const existOtherUsers = this.users.length > 0;
- const existGroups = this.groups.length > 0;
+ const groupList = GroupManager.Instance?.getAllGroups() || [];
+
+ const sortedUsers = this.users.sort(this.sortUsers)
+ .map(({ user: { email } }) => ({ label: email, value: indType + email }));
+ const sortedGroups = groupList.sort(this.sortGroups)
+ .map(({ groupName }) => ({ label: StrCast(groupName), value: groupType + StrCast(groupName) }));
+
+ const options: GroupOptions[] = GroupManager.Instance ?
+ [
+ {
+ label: 'Individuals',
+ options: sortedUsers
+ },
+ {
+ label: 'Groups',
+ options: sortedGroups
+ }
+ ]
+ : [];
+
+ const users = this.individualSort === "ascending" ? this.users.sort(this.sortUsers) : this.individualSort === "descending" ? this.users.sort(this.sortUsers).reverse() : this.users;
+ const groups = this.groupSort === "ascending" ? groupList.sort(this.sortGroups) : this.groupSort === "descending" ? groupList.sort(this.sortGroups).reverse() : groupList;
+
+ const userListContents: (JSX.Element | null)[] = users.map(({ user, notificationDoc }) => {
+ const userKey = user.email.replace('.', '_');
+ const permissions = StrCast(this.targetDoc?.[`ACL-${userKey}`], SharingPermissions.None);
+
+ return permissions === SharingPermissions.None || user.email === this.targetDoc?.author ? null : (
+ <div
+ key={userKey}
+ className={"container"}
+ >
+ <span className={"padding"}>{user.email}</span>
+ <div className="edit-actions">
+ <select
+ className={"permissions-dropdown"}
+ value={permissions}
+ onChange={e => this.setInternalSharing({ user, notificationDoc }, e.currentTarget.value)}
+ >
+ {this.sharingOptions}
+ </select>
+ </div>
+ </div>
+ );
+ });
- // const manager = this.sharingDoc!;
+ userListContents.unshift(
+ (
+ <div
+ key={"owner"}
+ className={"container"}
+ >
+ <span className={"padding"}>{this.targetDoc?.author}</span>
+ <div className="edit-actions">
+ <div className={"permissions-dropdown"}>
+ Owner
+ </div>
+ </div>
+ </div>
+ )
+ );
+
+ const groupListContents = groups.map(group => {
+ const permissions = StrCast(this.targetDoc?.[`ACL-${StrCast(group.groupName)}`], SharingPermissions.None);
+
+ return permissions === SharingPermissions.None ? null : (
+ <div
+ key={StrCast(group.groupName)}
+ className={"container"}
+ >
+ <div className={"padding"}>{group.groupName}</div>
+ <div className="group-info" onClick={action(() => GroupManager.Instance.currentGroup = group)}>
+ <FontAwesomeIcon icon={fa.faInfoCircle} color={"#e8e8e8"} size={"sm"} style={{ backgroundColor: "#1e89d7", borderRadius: "100%", border: "1px solid #1e89d7" }} />
+ </div>
+ <div className="edit-actions">
+ <select
+ className={"permissions-dropdown"}
+ value={permissions}
+ onChange={e => this.setInternalGroupSharing(group, e.currentTarget.value)}
+ >
+ {this.sharingOptions}
+ </select>
+ </div>
+ </div>
+ );
+ });
+
+ const displayUserList = !userListContents?.every(user => user === null);
+ const displayGroupList = !groupListContents?.every(group => group === null);
return (
<div className={"sharing-interface"}>
- {this.groupToView ?
+ {GroupManager.Instance?.currentGroup ?
<GroupMemberView
- group={this.groupToView}
- onCloseButtonClick={action(() => this.groupToView = undefined)}
+ group={GroupManager.Instance.currentGroup}
+ onCloseButtonClick={action(() => GroupManager.Instance.currentGroup = undefined)}
/> :
null}
- <p className={"share-link"}>Manage the public link to {this.focusOn("this document...")}</p>
+ {/* <p className={"share-link"}>Manage the public link to {this.focusOn("this document...")}</p>
{!this.linkVisible ? (null) :
<div className={"link-container"}>
<div className={"link-box"} onClick={this.copy}>{this.sharingUrl}</div>
@@ -325,86 +440,81 @@ export default class SharingManager extends React.Component<{}> {
{this.sharingOptions}
</select>
</div>
- <div className={"hr-substitute"} />
+ <div className={"hr-substitute"} /> */}
<div className="sharing-contents">
- <div className={"individual-container"}>
- <p className={"share-individual"}>Privately share {this.focusOn("this document")} with an individual...</p>
- <div className={"users-list"} style={{ display: existOtherUsers ? "block" : "flex", minHeight: existOtherUsers ? undefined : 150 }}>{/*200*/}
- {!existOtherUsers ? "There are no other users in your database." :
- this.users.map(({ user, notificationDoc }) => { // can't use async here
- const userKey = user.userDocumentId;
- const permissions = this.computePermissions(userKey);
- const color = ColorMapping.get(permissions);
-
- // console.log(manager);
- // const metadata = manager[userKey] as Doc;
- // const usersShared = StrCast(metadata?.usersShared, "");
- // console.log(usersShared)
-
-
- return (
+ <p className={"share-title"}><b>Share </b>{this.focusOn(StrCast(this.targetDoc?.title, "this document"))}</p>
+ <div className={"close-button"} onClick={this.close}>
+ <FontAwesomeIcon icon={"times"} color={"black"} size={"lg"} />
+ </div>
+ {this.targetDoc?.author !== Doc.CurrentUserEmail ? null
+ :
+ <div className="share-setup">
+ <Select
+ className={"user-search"}
+ placeholder={"Enter user or group name..."}
+ isMulti
+ closeMenuOnSelect={false}
+ options={options}
+ onChange={this.handleUsersChange}
+ value={this.selectedUsers}
+ />
+ <select className="permissions-select" onChange={this.handlePermissionsChange}>
+ {this.sharingOptions}
+ </select>
+ <button ref={this.shareDocumentButtonRef} className="share-button" onClick={this.share}>
+ Share
+ </button>
+ </div>
+ }
+ <div className="main-container">
+ <div className={"individual-container"}>
+ <div
+ className="user-sort"
+ onClick={action(() => this.individualSort = this.individualSort === "ascending" ? "descending" : this.individualSort === "descending" ? "none" : "ascending")}>
+ Individuals {this.individualSort === "ascending" ? "↑" : this.individualSort === "descending" ? "↓" : ""} {/* → */}
+ </div>
+ <div className={"users-list"} style={{ display: !displayUserList ? "flex" : "block" }}>{/*200*/}
+ {
+ !displayUserList ?
<div
- key={userKey}
- className={"container"}
+ className={"none"}
>
- <span className={"padding"}>{user.email}</span>
- {/* <div className={"shared-by"}>{usersShared}</div> */}
- <div className="edit-actions">
- <select
- className={"permissions-dropdown"}
- value={permissions}
- style={{ color, borderColor: color }}
- onChange={e => this.setInternalSharing({ user, notificationDoc }, e.currentTarget.value, undefined)}
- >
- {this.sharingOptions}
- </select>
- </div>
+ There are no users this document has been shared with.
</div>
- );
- })
- }
+ :
+ userListContents
+ }
+ </div>
</div>
- </div>
- <div className={"group-container"}>
- <p className={"share-groups"}>Privately share {this.focusOn("this document")} with a group...</p>
- <div className={"groups-list"} style={{ display: existGroups ? "block" : "flex", minHeight: existOtherUsers ? undefined : 150 }}>{/*200*/}
- {!existGroups ? "There are no groups in your database." :
- this.groups.map(group => {
- const permissions = this.computePermissions(StrCast(group.groupName));
- const color = ColorMapping.get(permissions);
- return (
+ <div className={"group-container"}>
+ <div
+ className="user-sort"
+ onClick={action(() => this.groupSort = this.groupSort === "ascending" ? "descending" : this.groupSort === "descending" ? "none" : "ascending")}>
+ Groups {this.groupSort === "ascending" ? "↑" : this.groupSort === "descending" ? "↓" : ""} {/* → */}
+
+ </div>
+ <div className={"groups-list"} style={{ display: !displayGroupList ? "flex" : "block" }}>{/*200*/}
+ {
+ !displayGroupList ?
<div
- key={StrCast(group.groupName)}
- className={"container"}
+ className={"none"}
>
- <span className={"padding"}>{group.groupName}</span>
- <div className="edit-actions">
- <select
- className={"permissions-dropdown"}
- value={permissions}
- style={{ color, borderColor: color }}
- onChange={e => this.setInternalGroupSharing(group, e.currentTarget.value)}
- >
- {this.sharingOptions}
- </select>
- <button onClick={action(() => this.groupToView = group)}>Edit</button>
+ There are no groups this document has been shared with.
</div>
- </div>
- );
- })
-
- }
+ :
+ groupListContents
+ }
+ </div>
</div>
</div>
+
</div>
- <div className={"close-button"} onClick={this.close}>Done</div>
</div>
);
}
render() {
- // console.log(this.sharingDoc);
return (
<MainViewModal
contents={this.sharingInterface}
@@ -412,6 +522,7 @@ export default class SharingManager extends React.Component<{}> {
interactive={true}
dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity}
overlayDisplayedOpacity={this.overlayOpacity}
+ closeOnExternalClick={this.close}
/>
);
}
diff --git a/src/client/views/AntimodeMenu.scss b/src/client/views/AntimodeMenu.scss
index e56574bb7..be21cec12 100644
--- a/src/client/views/AntimodeMenu.scss
+++ b/src/client/views/AntimodeMenu.scss
@@ -42,4 +42,35 @@
background-repeat: no-repeat;
background-position: left center;
}
+}
+
+@media only screen and (max-device-width: 480px) {
+ .antimodeMenu-cont {
+ height: 100px;
+ width: 100vw;
+
+ &.with-rows {
+ flex-direction: column-reverse;
+ }
+
+ .antimodeMenu-row {
+ display: flex;
+ height: 100%;
+ width: 100%;
+ }
+
+ .antimodeMenu-button {
+ background-color: transparent;
+ width: 100px;
+ height: 100px;
+
+ &.active {
+ background-color: #121212;
+ }
+ }
+
+ .antimodeMenu-button:hover {
+ background-color: #121212;
+ }
+ }
} \ No newline at end of file
diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx
index a4634103c..68ccefcb5 100644
--- a/src/client/views/AntimodeMenu.tsx
+++ b/src/client/views/AntimodeMenu.tsx
@@ -128,19 +128,41 @@ export default abstract class AntimodeMenu extends React.Component {
}
protected getDragger = () => {
- return <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: "20px" }} />;
+ return <div className="antimodeMenu-dragger" key="dragger" onPointerDown={this.dragStart} style={{ width: "20px" }} />;
}
protected getElement(buttons: JSX.Element[]) {
return (
<div className="antimodeMenu-cont" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu}
- style={{ left: this._left, top: this._top, opacity: this._opacity, transitionProperty: this._transitionProperty, transitionDuration: this._transitionDuration, transitionDelay: this._transitionDelay }}>
+ style={{
+ left: this._left, top: this._top, opacity: this._opacity, transitionProperty: this._transitionProperty, transitionDuration: this._transitionDuration, transitionDelay: this._transitionDelay,
+ position: this.Pinned ? "unset" : undefined
+ }}>
<div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: "20px" }} />
{buttons}
</div>
);
}
+ protected getElementVert(buttons: JSX.Element[]) {
+ return (
+ <div className="antimodeMenu-cont" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu}
+ style={{
+ left: this.Pinned ? undefined : this._left,
+ top: this.Pinned ? 0 : this._top,
+ right: this.Pinned ? 0 : undefined,
+ height: "inherit",
+ width: 200,
+ opacity: this._opacity, transitionProperty: this._transitionProperty, transitionDuration: this._transitionDuration, transitionDelay: this._transitionDelay,
+ position: this.Pinned ? "absolute" : undefined
+ }}>
+ {buttons}
+ </div>
+ );
+ }
+
+
+
protected getElementWithRows(rows: JSX.Element[], numRows: number, hasDragger: boolean = true) {
return (
<div className="antimodeMenu-cont with-rows" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu}
diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss
index 1bf242d93..86e0a568a 100644
--- a/src/client/views/ContextMenu.scss
+++ b/src/client/views/ContextMenu.scss
@@ -42,7 +42,7 @@
.contextMenu-item {
// width: 11vw; //10vw
- height: 30px; //2vh
+ height: 25px; //2vh
background: whitesmoke;
display: flex; //comment out to allow search icon to be inline with search text
justify-content: left;
@@ -70,9 +70,6 @@
text-align: center;
font-size: 20px;
margin-left: 5px;
- margin-top: 5px;
- margin-bottom: 5px;
- height: 20px;
}
}
.contextMenu-description {
@@ -90,14 +87,11 @@
border-style: none;
// padding: 10px 0px 10px 0px;
white-space: nowrap;
- font-size: 13px;
+ font-size: 10px;
color: grey;
- letter-spacing: 2px;
+ letter-spacing: 1px;
text-transform: uppercase;
padding-right: 30px;
- margin-top: 5px;
- height: 20px;
- margin-bottom: 5px;
}
.contextMenu-item:hover {
@@ -138,6 +132,10 @@
padding-left: 5px;
}
+.contextMenu-inlineMenu {
+ border-top: solid 1px;
+}
+
.contextMenu-item:hover {
transition: all 0.1s ease;
background: $lighter-alt-accent;
diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index 99840047f..81432968d 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -19,6 +19,8 @@ export interface OriginalMenuProps {
export interface SubmenuProps {
description: string;
subitems: ContextMenuProps[];
+ noexpand?: boolean;
+ addDivider?: boolean;
icon: IconProp; //maybe should be optional (icon?)
closeMenu?: () => void;
}
@@ -96,8 +98,14 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select
<div className="contextMenu-subMenu-cont" style={{ marginLeft: "25%", left: "0px", marginTop }}>
{this._items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} />)}
</div>;
+ if (!("noexpand" in this.props)) {
+ return <div className="contextMenu-inlineMenu">
+ {this._items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} />)}
+ </div>;
+ }
return (
- <div className={"contextMenu-item" + (this.props.selected ? " contextMenu-itemSelected" : "")} style={{ alignItems: where }}
+ <div className={"contextMenu-item" + (this.props.selected ? " contextMenu-itemSelected" : "")}
+ style={{ alignItems: where, borderTop: this.props.addDivider ? "solid 1px" : undefined }}
onMouseLeave={this.onPointerLeave} onMouseEnter={this.onPointerEnter}>
{this.props.icon ? (
<span className="icon-background" onMouseEnter={this.onPointerLeave} style={{ alignItems: "center" }}>
diff --git a/src/client/views/DictationOverlay.tsx b/src/client/views/DictationOverlay.tsx
index 65770c0bb..9ed14509f 100644
--- a/src/client/views/DictationOverlay.tsx
+++ b/src/client/views/DictationOverlay.tsx
@@ -66,6 +66,7 @@ export class DictationOverlay extends React.Component {
interactive={false}
dialogueBoxStyle={dialogueBoxStyle}
overlayStyle={overlayStyle}
+ closeOnExternalClick={this.initiateDictationFade}
/>);
}
} \ No newline at end of file
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 106250af4..177c1b5ee 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -1,4 +1,4 @@
-import { Doc, Opt, DataSym, DocListCast, AclSym, AclReadonly, AclAddonly } from '../../fields/Doc';
+import { Doc, Opt, DataSym, AclReadonly, AclAddonly, AclPrivate, AclEdit, AclSym, DocListCastAsync, DocListCast } from '../../fields/Doc';
import { Touchable } from './Touchable';
import { computed, action, observable } from 'mobx';
import { Cast, BoolCast, ScriptCast } from '../../fields/Types';
@@ -7,6 +7,7 @@ import { InteractionUtils } from '../util/InteractionUtils';
import { List } from '../../fields/List';
import { DateField } from '../../fields/DateField';
import { ScriptField } from '../../fields/ScriptField';
+import { GetEffectiveAcl, getPlaygroundMode, SharingPermissions } from '../../fields/util';
/// DocComponent returns a generic React base class used by views that don't have 'fieldKey' props (e.g.,CollectionFreeFormDocumentView, DocumentView)
@@ -25,7 +26,7 @@ export function DocComponent<P extends DocComponentProps, T>(schemaCtor: (doc: D
// This is the data part of a document -- ie, the data that is constant across all views of the document
@computed get dataDoc() { return this.props.Document[DataSym] as Doc; }
- protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
+ protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
}
return Component;
}
@@ -58,7 +59,7 @@ export function ViewBoxBaseComponent<P extends ViewBoxBaseProps, T>(schemaCtor:
lookupField = (field: string) => ScriptCast(this.layoutDoc.lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field, container: this.props.ContainingCollectionDoc }).result;
active = (outsideReaction?: boolean) => !this.props.Document.isBackground && (this.props.rootSelected(outsideReaction) || this.props.isSelected(outsideReaction) || this.props.renderDepth === 0 || this.layoutDoc.forceActive);// && !Doc.SelectedTool(); // bcz: inking state shouldn't affect static tools
- protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
+ protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
}
return Component;
}
@@ -91,6 +92,13 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
// key where data is stored
@computed get fieldKey() { return this.props.fieldKey; }
+ private AclMap = new Map<symbol, string>([
+ [AclPrivate, SharingPermissions.None],
+ [AclReadonly, SharingPermissions.View],
+ [AclAddonly, SharingPermissions.Add],
+ [AclEdit, SharingPermissions.Edit]
+ ]);
+
lookupField = (field: string) => ScriptCast((this.layoutDoc as any).lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field }).result;
styleFromLayoutString = (scale: number) => {
@@ -106,7 +114,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
return style;
}
- protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
+ protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
_annotationKey: string = "annotations";
public get annotationKey() { return this.fieldKey + "-" + this._annotationKey; }
@@ -114,14 +122,16 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
@action.bound
removeDocument(doc: Doc | Doc[]): boolean {
const docs = doc instanceof Doc ? [doc] : doc;
- docs.map(doc => doc.annotationOn = undefined);
+ docs.map(doc => doc.isPushpin = doc.annotationOn = undefined);
const targetDataDoc = this.dataDoc;
const value = DocListCast(targetDataDoc[this.annotationKey]);
- const result = value.filter(v => !docs.includes(v));
- if (result.length !== value.length) {
- targetDataDoc[this.annotationKey] = new List<Doc>(result);
+ const toRemove = value.filter(v => docs.includes(v));
+ // can't assign new List<Doc>(result) to this because you can't assign new values in addonly
+ if (toRemove.length !== 0) {
+ toRemove.forEach(doc => Doc.RemoveDocFromList(targetDataDoc, this.annotationKey, doc));
return true;
}
+
return false;
}
// if the moved document is already in this overlay collection nothing needs to be done.
@@ -137,8 +147,10 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
const targetDataDoc = this.props.Document[DataSym];
const docList = DocListCast(targetDataDoc[this.annotationKey]);
const added = docs.filter(d => !docList.includes(d));
+ const effectiveAcl = GetEffectiveAcl(this.dataDoc);
+
if (added.length) {
- if (this.dataDoc[AclSym] === AclReadonly) {
+ if (effectiveAcl === AclPrivate || (effectiveAcl === AclReadonly && !getPlaygroundMode())) {
return false;
} else if (this.dataDoc[AclSym] === AclAddonly) {
added.map(doc => Doc.AddDocToList(targetDataDoc, this.annotationKey, doc));
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index c05ca33fb..6b85616c2 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -11,7 +11,6 @@ import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager';
import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils';
import { Docs, DocUtils } from '../documents/Documents';
import { DragManager } from '../util/DragManager';
-import { UndoManager } from "../util/UndoManager";
import { CollectionDockingView, DockedFrameRenderer } from './collections/CollectionDockingView';
import { ParentDocSelector } from './collections/ParentDocumentSelector';
import './collections/ParentDocumentSelector.scss';
@@ -23,6 +22,7 @@ import { TemplateMenu } from "./TemplateMenu";
import { Template, Templates } from "./Templates";
import React = require("react");
import { DocumentLinksButton } from './nodes/DocumentLinksButton';
+import { Tooltip } from '@material-ui/core';
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -51,7 +51,6 @@ enum UtilityButtonState {
@observer
export class DocumentButtonBar extends React.Component<{ views: () => (DocumentView | undefined)[], stack?: any }, {}> {
- private _linkButton = React.createRef<HTMLDivElement>();
private _dragRef = React.createRef<HTMLDivElement>();
private _pullAnimating = false;
private _pushAnimating = false;
@@ -118,18 +117,18 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
const targetDoc = this.view0?.props.Document;
const published = targetDoc && Doc.GetProto(targetDoc)[GoogleRef] !== undefined;
const animation = this.isAnimatingPulse ? "shadow-pulse 1s linear infinite" : "none";
- return !targetDoc ? (null) : <div
- title={`${published ? "Push" : "Publish"} to Google Docs`}
- className="documentButtonBar-linker"
- style={{ animation }}
- onClick={async () => {
- await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
- !published && runInAction(() => this.isAnimatingPulse = true);
- DocumentButtonBar.hasPushedHack = false;
- targetDoc[Pushes] = NumCast(targetDoc[Pushes]) + 1;
- }}>
- <FontAwesomeIcon className="documentdecorations-icon" icon={published ? (this.pushIcon as any) : cloud} size={published ? "sm" : "xs"} />
- </div>;
+ return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{`${published ? "Push" : "Publish"} to Google Docs`}</div></>}>
+ <div
+ className="documentButtonBar-linker"
+ style={{ animation }}
+ onClick={async () => {
+ await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
+ !published && runInAction(() => this.isAnimatingPulse = true);
+ DocumentButtonBar.hasPushedHack = false;
+ targetDoc[Pushes] = NumCast(targetDoc[Pushes]) + 1;
+ }}>
+ <FontAwesomeIcon className="documentdecorations-icon" icon={published ? (this.pushIcon as any) : cloud} size={published ? "sm" : "xs"} />
+ </div></Tooltip>;
}
@computed
@@ -137,82 +136,87 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
const targetDoc = this.view0?.props.Document;
const dataDoc = targetDoc && Doc.GetProto(targetDoc);
const animation = this.isAnimatingFetch ? "spin 0.5s linear infinite" : "none";
- return !targetDoc || !dataDoc || !dataDoc[GoogleRef] ? (null) : <div className="documentButtonBar-linker"
- title={(() => {
- switch (this.openHover) {
- default:
- case UtilityButtonState.Default: return `${!dataDoc.unchanged ? "Pull from" : "Fetch"} Google Docs`;
- case UtilityButtonState.OpenRight: return "Open in Right Split";
- case UtilityButtonState.OpenExternally: return "Open in new Browser Tab";
- }
- })()}
- style={{ backgroundColor: this.pullColor }}
- onPointerEnter={action(e => {
- if (e.altKey) {
- this.openHover = UtilityButtonState.OpenExternally;
- } else if (e.shiftKey) {
- this.openHover = UtilityButtonState.OpenRight;
- }
- })}
- onPointerLeave={action(() => this.openHover = UtilityButtonState.Default)}
- onClick={async e => {
- const googleDocUrl = `https://docs.google.com/document/d/${dataDoc[GoogleRef]}/edit`;
- if (e.shiftKey) {
- e.preventDefault();
- let googleDoc = await Cast(dataDoc.googleDoc, Doc);
- if (!googleDoc) {
- const options = { _width: 600, _nativeWidth: 960, _nativeHeight: 800, isAnnotating: false, UseCors: false };
- googleDoc = Docs.Create.WebDocument(googleDocUrl, options);
- dataDoc.googleDoc = googleDoc;
+
+ const title = (() => {
+ switch (this.openHover) {
+ default:
+ case UtilityButtonState.Default: return `${!dataDoc?.unchanged ? "Pull from" : "Fetch"} Google Docs`;
+ case UtilityButtonState.OpenRight: return "Open in Right Split";
+ case UtilityButtonState.OpenExternally: return "Open in new Browser Tab";
+ }
+ })();
+
+ return !targetDoc || !dataDoc || !dataDoc[GoogleRef] ? (null) : <Tooltip
+ title={<><div className="dash-tooltip">{title}</div></>}>
+ <div className="documentButtonBar-linker"
+ style={{ backgroundColor: this.pullColor }}
+ onPointerEnter={action(e => {
+ if (e.altKey) {
+ this.openHover = UtilityButtonState.OpenExternally;
+ } else if (e.shiftKey) {
+ this.openHover = UtilityButtonState.OpenRight;
}
- CollectionDockingView.AddRightSplit(googleDoc);
- } else if (e.altKey) {
- e.preventDefault();
- window.open(googleDocUrl);
- } else {
- this.clearPullColor();
- DocumentButtonBar.hasPulledHack = false;
- targetDoc[Pulls] = NumCast(targetDoc[Pulls]) + 1;
- dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true);
- }
- }}>
- <FontAwesomeIcon className="documentdecorations-icon" size="sm"
- style={{ WebkitAnimation: animation, MozAnimation: animation }}
- icon={(() => {
- switch (this.openHover) {
- default:
- case UtilityButtonState.Default: return dataDoc.unchanged === false ? (this.pullIcon as any) : fetch;
- case UtilityButtonState.OpenRight: return "arrow-alt-circle-right";
- case UtilityButtonState.OpenExternally: return "share";
+ })}
+ onPointerLeave={action(() => this.openHover = UtilityButtonState.Default)}
+ onClick={async e => {
+ const googleDocUrl = `https://docs.google.com/document/d/${dataDoc[GoogleRef]}/edit`;
+ if (e.shiftKey) {
+ e.preventDefault();
+ let googleDoc = await Cast(dataDoc.googleDoc, Doc);
+ if (!googleDoc) {
+ const options = { _width: 600, _nativeWidth: 960, _nativeHeight: 800, isAnnotating: false, UseCors: false };
+ googleDoc = Docs.Create.WebDocument(googleDocUrl, options);
+ dataDoc.googleDoc = googleDoc;
+ }
+ CollectionDockingView.AddRightSplit(googleDoc);
+ } else if (e.altKey) {
+ e.preventDefault();
+ window.open(googleDocUrl);
+ } else {
+ this.clearPullColor();
+ DocumentButtonBar.hasPulledHack = false;
+ targetDoc[Pulls] = NumCast(targetDoc[Pulls]) + 1;
+ dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true);
}
- })()}
- />
- </div>;
+ }}>
+ <FontAwesomeIcon className="documentdecorations-icon" size="sm"
+ style={{ WebkitAnimation: animation, MozAnimation: animation }}
+ icon={(() => {
+ switch (this.openHover) {
+ default:
+ case UtilityButtonState.Default: return dataDoc.unchanged === false ? (this.pullIcon as any) : fetch;
+ case UtilityButtonState.OpenRight: return "arrow-alt-circle-right";
+ case UtilityButtonState.OpenExternally: return "share";
+ }
+ })()}
+ />
+ </div></Tooltip>;
}
@computed
get pinButton() {
const targetDoc = this.view0?.props.Document;
const isPinned = targetDoc && Doc.isDocPinned(targetDoc);
- return !targetDoc ? (null) : <div className="documentButtonBar-linker"
- title={Doc.isDocPinned(targetDoc) ? "Unpin from presentation" : "Pin to presentation"}
- style={{ backgroundColor: isPinned ? "black" : "white", color: isPinned ? "white" : "black" }}
- onClick={e => DockedFrameRenderer.PinDoc(targetDoc, isPinned)}>
- <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin"
- />
- </div>;
+ return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{Doc.isDocPinned(targetDoc) ? "Unpin from presentation" : "Pin to presentation"}</div></>}>
+ <div className="documentButtonBar-linker"
+ style={{ backgroundColor: isPinned ? "black" : "white", color: isPinned ? "white" : "black" }}
+ onClick={e => DockedFrameRenderer.PinDoc(targetDoc, isPinned)}>
+ <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin"
+ />
+ </div></Tooltip>;
}
@computed
get metadataButton() {
const view0 = this.view0;
- return !view0 ? (null) : <div title="Show metadata panel" className="documentButtonBar-linkFlyout">
- <Flyout anchorPoint={anchorPoints.LEFT_TOP}
- content={<MetadataEntryMenu docs={() => this.props.views().filter(dv => dv).map(dv => dv!.props.Document)} suggestWithFunction /> /* tfs: @bcz This might need to be the data document? */}>
- <div className={"documentButtonBar-linkButton-" + "empty"} onPointerDown={e => e.stopPropagation()} >
- {<FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="sm" />}
- </div>
- </Flyout>
- </div>;
+ return !view0 ? (null) : <Tooltip title={<><div className="dash-tooltip">Show metadata panel</div></>}>
+ <div className="documentButtonBar-linkFlyout">
+ <Flyout anchorPoint={anchorPoints.LEFT_TOP}
+ content={<MetadataEntryMenu docs={this.props.views().filter(dv => dv).map(dv => dv!.props.Document)} suggestWithFunction /> /* tfs: @bcz This might need to be the data document? */}>
+ <div className={"documentButtonBar-linkButton-" + "empty"} onPointerDown={e => e.stopPropagation()} >
+ {<FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="sm" />}
+ </div>
+ </Flyout>
+ </div></Tooltip>;
}
@computed
@@ -253,14 +257,15 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
Array.from(Object.values(Templates.TemplateList)).map(template =>
templates.set(template, views.reduce((checked, doc) => checked || doc?.props.Document["_show" + template.Name] ? true : false, false as boolean)));
return !view0 ? (null) :
- <div title="Tap: Customize layout. Drag: Create alias" className="documentButtonBar-linkFlyout" ref={this._dragRef}>
- <Flyout anchorPoint={anchorPoints.LEFT_TOP} onOpen={action(() => this._aliasDown = true)} onClose={action(() => this._aliasDown = false)}
- content={!this._aliasDown ? (null) : <TemplateMenu docViews={views.filter(v => v).map(v => v as DocumentView)} templates={templates} />}>
- <div className={"documentButtonBar-linkButton-empty"} ref={this._dragRef} onPointerDown={this.onAliasButtonDown} >
- {<FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" />}
- </div>
- </Flyout>
- </div>;
+ <Tooltip title={<><div className="dash-tooltip">Tap: Customize layout. Drag: Create alias</div></>}>
+ <div className="documentButtonBar-linkFlyout" ref={this._dragRef}>
+ <Flyout anchorPoint={anchorPoints.LEFT_TOP} onOpen={action(() => this._aliasDown = true)} onClose={action(() => this._aliasDown = false)}
+ content={!this._aliasDown ? (null) : <TemplateMenu docViews={views.filter(v => v).map(v => v as DocumentView)} templates={templates} />}>
+ <div className={"documentButtonBar-linkButton-empty"} ref={this._dragRef} onPointerDown={this.onAliasButtonDown} >
+ {<FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" />}
+ </div>
+ </Flyout>
+ </div></Tooltip>;
}
render() {
@@ -271,8 +276,11 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
const considerPush = isText && this.considerGoogleDocsPush;
return <div className="documentButtonBar">
<div className="documentButtonBar-button">
- <DocumentLinksButton View={this.view0} AlwaysOn={true} />
+ <DocumentLinksButton links={this.view0.allLinks} View={this.view0} AlwaysOn={true} InMenu={true} StartLink={true} />
</div>
+ {DocumentLinksButton.StartLink ? <div className="documentButtonBar-button">
+ <DocumentLinksButton links={this.view0.allLinks} View={this.view0} AlwaysOn={true} InMenu={true} StartLink={false} />
+ </div> : null}
<div className="documentButtonBar-button">
{this.templateButton}
</div>
@@ -293,4 +301,4 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
</div>
</div>;
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 5948ada88..424a06431 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -157,6 +157,7 @@ $linkGap : 3px;
grid-column-end: 2;
pointer-events: all;
padding-left: 5px;
+ cursor: pointer;
}
.documentDecorations-title {
@@ -204,7 +205,7 @@ $linkGap : 3px;
width: 20px;
}
-.documentDecorations-closeButton {
+.documentDecorations-openInTab {
opacity: 1;
grid-column-start: 4;
grid-column-end: 5;
@@ -216,7 +217,7 @@ $linkGap : 3px;
margin-top: auto;
}
-.documentDecorations-minimizeButton {
+.documentDecorations-closeButton {
opacity: 1;
grid-column-start: 1;
grid-column-end: 3;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index a45ef8862..51325ae1b 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -17,14 +17,12 @@ import { DocumentButtonBar } from './DocumentButtonBar';
import './DocumentDecorations.scss';
import { DocumentView } from "./nodes/DocumentView";
import React = require("react");
-import { Id, Copy, Update } from '../../fields/FieldSymbols';
import e = require('express');
import { CollectionDockingView } from './collections/CollectionDockingView';
import { SnappingManager } from '../util/SnappingManager';
import { HtmlField } from '../../fields/HtmlField';
-import { InkData, InkField, InkTool } from "../../fields/InkField";
-import { update } from 'serializr';
-import { Transform } from "../util/Transform";
+import { InkField } from "../../fields/InkField";
+import { Tooltip } from '@material-ui/core';
library.add(faCaretUp);
library.add(faObjectGroup);
@@ -61,6 +59,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
private _prevX = 0;
private _prevY = 0;
private _centerPoints: { X: number, Y: number }[] = [];
+ private _inkDocs: { x: number, y: number, width: number, height: number }[] = [];
@observable private _accumulatedTitle = "";
@observable private _titleControlString: string = "#title";
@@ -83,14 +82,15 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
return SelectionManager.SelectedDocuments().reduce((bounds, documentView) => {
if (documentView.props.renderDepth === 0 ||
documentView.props.treeViewDoc ||
+ !documentView.ContentDiv ||
Doc.AreProtosEqual(documentView.props.Document, Doc.UserDoc())) {
return bounds;
}
const transform = (documentView.props.ScreenToLocalTransform().scale(documentView.props.ContentScaling())).inverse();
var [sptX, sptY] = transform.transformPoint(0, 0);
let [bptX, bptY] = transform.transformPoint(documentView.props.PanelWidth(), documentView.props.PanelHeight());
- if (documentView.props.Document.type === DocumentType.LINK) {
- const docuBox = documentView.ContentDiv!.getElementsByClassName("linkAnchorBox-cont");
+ if (documentView.props.LayoutTemplateString?.includes("LinkAnchorBox")) {
+ const docuBox = documentView.ContentDiv.getElementsByClassName("linkAnchorBox-cont");
if (docuBox.length) {
const rect = docuBox[0].getBoundingClientRect();
sptX = rect.left;
@@ -152,8 +152,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
if (e.button === 0 && !e.altKey && !e.ctrlKey) {
let child = SelectionManager.SelectedDocuments()[0].ContentDiv!.children[0];
while (child.children.length) {
- const next = Array.from(child.children).find(c => !c.className.includes("collectionViewChrome"));
- if (next?.className.includes("documentView-node")) break;
+ const next = Array.from(child.children).find(c => typeof (c.className) !== "string");
+ if (typeof (next?.className) === "string" && next?.className.includes("documentView-node")) break;
if (next) child = next;
else break;
}
@@ -293,13 +293,13 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
SelectionManager.SelectedDocuments().forEach(action((element: DocumentView) => {
const doc = Document(element.rootDoc);
if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) {
- const ink = Cast(doc.data, InkField)?.inkData;
- if (ink) {
-
+ doc.rotation = Number(doc.rotation) + Number(angle);
+ const inks = Cast(doc.data, InkField)?.inkData;
+ if (inks) {
const newPoints: { X: number, Y: number }[] = [];
- for (var i = 0; i < ink.length; i++) {
- const newX = Math.cos(angle) * (ink[i].X - this._centerPoints[index].X) - Math.sin(angle) * (ink[i].Y - this._centerPoints[index].Y) + this._centerPoints[index].X;
- const newY = Math.sin(angle) * (ink[i].X - this._centerPoints[index].X) + Math.cos(angle) * (ink[i].Y - this._centerPoints[index].Y) + this._centerPoints[index].Y;
+ for (const ink of inks) {
+ const newX = Math.cos(angle) * (ink.X - this._centerPoints[index].X) - Math.sin(angle) * (ink.Y - this._centerPoints[index].Y) + this._centerPoints[index].X;
+ const newY = Math.sin(angle) * (ink.X - this._centerPoints[index].X) + Math.cos(angle) * (ink.Y - this._centerPoints[index].Y) + this._centerPoints[index].Y;
newPoints.push({ X: newX, Y: newY });
}
doc.data = new InkField(newPoints);
@@ -310,8 +310,10 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
const right = Math.max(...xs);
const bottom = Math.max(...ys);
- doc._height = (bottom - top) * element.props.ScreenToLocalTransform().Scale;
- doc._width = (right - left) * element.props.ScreenToLocalTransform().Scale;
+ // doc._height = (bottom - top) * element.props.ScreenToLocalTransform().Scale;
+ // doc._width = (right - left) * element.props.ScreenToLocalTransform().Scale;
+ doc._height = (bottom - top);
+ doc._width = (right - left);
}
index++;
@@ -330,6 +332,16 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
_dragHeights = new Map<Doc, number>();
@action
onPointerDown = (e: React.PointerEvent): void => {
+
+ this._inkDocs = [];
+ SelectionManager.SelectedDocuments().forEach(action((element: DocumentView) => {
+ const doc = Document(element.rootDoc);
+ if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height) {
+ this._inkDocs.push({ x: doc.x, y: doc.y, width: doc._width, height: doc._height });
+ }
+
+ }));
+
setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, (e) => { });
if (e.button === 0) {
this._resizeHdlId = e.currentTarget.id;
@@ -375,7 +387,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
move[1] = thisPt.thisY - this._snapY;
this._snapX = thisPt.thisX;
this._snapY = thisPt.thisY;
-
+ let dragBottom = false;
let dX = 0, dY = 0, dW = 0, dH = 0;
const unfreeze = () =>
SelectionManager.SelectedDocuments().forEach(action((element: DocumentView) =>
@@ -413,6 +425,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
case "documentDecorations-bottomResizer":
unfreeze();
dH = move[1];
+ dragBottom = true;
break;
case "documentDecorations-leftResizer":
unfreeze();
@@ -433,13 +446,16 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
let nheight = doc._nativeHeight || 0;
const width = (doc._width || 0);
let height = (doc._height || (nheight / nwidth * width));
+ height = !height || isNaN(height) ? 20 : height;
const scale = element.props.ScreenToLocalTransform().Scale * element.props.ContentScaling();
if (nwidth && nheight) {
if (nwidth / nheight !== width / height) {
height = nheight / nwidth * width;
}
- if (Math.abs(dW) > Math.abs(dH)) dH = dW * nheight / nwidth;
- else dW = dH * nwidth / nheight;
+ if (!e.ctrlKey && (!dragBottom || !element.layoutDoc._fitWidth)) { // ctrl key enables modification of the nativeWidth or nativeHeight durin the interaction
+ if (Math.abs(dW) > Math.abs(dH)) dH = dW * nheight / nwidth;
+ else dW = dH * nwidth / nheight;
+ }
}
const actualdW = Math.max(width + (dW * scale), 20);
const actualdH = Math.max(height + (dH * scale), 20);
@@ -463,20 +479,20 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
}
else if (nwidth > 0 && nheight > 0) {
if (Math.abs(dW) > Math.abs(dH)) {
- if (!fixedAspect) {
+ if (!fixedAspect || e.ctrlKey) {
doc._nativeWidth = actualdW / (doc._width || 1) * (doc._nativeWidth || 0);
}
doc._width = actualdW;
if (fixedAspect && !doc._fitWidth) doc._height = nheight / nwidth * doc._width;
- else doc._height = actualdH;
+ else if (!fixedAspect || !e.ctrlKey) doc._height = actualdH;
}
else {
- if (!fixedAspect) {
+ if (!fixedAspect || e.ctrlKey || (dragBottom && element.layoutDoc._fitWidth)) {
doc._nativeHeight = actualdH / (doc._height || 1) * (doc._nativeHeight || 0);
}
doc._height = actualdH;
if (fixedAspect && !doc._fitWidth) doc._width = nwidth / nheight * doc._height;
- else doc._width = actualdW;
+ else if (!fixedAspect || !e.ctrlKey) doc._width = actualdW;
}
} else {
dW && (doc._width = actualdW);
@@ -502,6 +518,27 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
(e.button === 0) && this._resizeUndo?.end();
this._resizeUndo = undefined;
SnappingManager.clearSnapLines();
+
+
+ //need to change points for resize, or else rotation/control points will fail.
+ SelectionManager.SelectedDocuments().forEach(action((element: DocumentView, index) => {
+ const doc = Document(element.rootDoc);
+ if (doc.type === DocumentType.INK && doc.x && doc.y && doc._height && doc._width) {
+ const ink = Cast(doc.data, InkField)?.inkData;
+ if (ink) {
+ const newPoints: { X: number, Y: number }[] = [];
+ for (var i = 0; i < ink.length; i++) {
+ // (new x — oldx) + (oldxpoint * newWidt)/oldWidth
+ const newX = (doc.x - this._inkDocs[index].x) + (ink[i].X * doc._width) / this._inkDocs[index].width;
+ const newY = (doc.y - this._inkDocs[index].y) + (ink[i].Y * doc._height) / this._inkDocs[index].height;
+ newPoints.push({ X: newX, Y: newY });
+ }
+ doc.data = new InkField(newPoints);
+
+ }
+
+ }
+ }));
}
@computed
@@ -545,13 +582,15 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
}
const minimal = bounds.r - bounds.x < 100 ? true : false;
const maximizeIcon = minimal ? (
- <div className="documentDecorations-contextMenu" title="Show context menu" onPointerDown={this.onSettingsDown}>
- <FontAwesomeIcon size="lg" icon="cog" />
- </div>) : (
- <div className="documentDecorations-minimizeButton" title="Iconify" onClick={this.onCloseClick}>
- {/* Currently, this is set to be enabled if there is no ink selected. It might be interesting to think about minimizing ink if it's useful? -syip2*/}
- <FontAwesomeIcon className="documentdecorations-times" icon={faTimes} size="lg" />
- </div>);
+ <Tooltip title={<><div className="dash-tooltip">Show context menu</div></>} placement="top">
+ <div className="documentDecorations-contextMenu" onPointerDown={this.onSettingsDown}>
+ <FontAwesomeIcon size="lg" icon="cog" />
+ </div></Tooltip>) : (
+ <Tooltip title={<><div className="dash-tooltip">Delete</div></>} placement="top">
+ <div className="documentDecorations-closeButton" onClick={this.onCloseClick}>
+ {/* Currently, this is set to be enabled if there is no ink selected. It might be interesting to think about minimizing ink if it's useful? -syip2*/}
+ <FontAwesomeIcon className="documentdecorations-times" icon={faTimes} size="lg" />
+ </div></Tooltip>);
const titleArea = this._edtingTitle ?
<>
@@ -571,11 +610,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
</div>}
</> :
<>
- {minimal ? (null) : <div className="documentDecorations-contextMenu" key="menu" title="Show context menu" onPointerDown={this.onSettingsDown}>
+ {minimal ? (null) : <Tooltip title={<><div className="dash-tooltip">Show context menu</div></>} placement="top"><div className="documentDecorations-contextMenu" key="menu" onPointerDown={this.onSettingsDown}>
<FontAwesomeIcon size="lg" icon="cog" />
- </div>}
+ </div></Tooltip>}
<div className="documentDecorations-title" key="title" onPointerDown={this.onTitleDown} >
- <span style={{ width: "100%", display: "inline-block" }}>{`${this.selectionTitle}`}</span>
+ <span style={{ width: "100%", display: "inline-block", cursor: "move" }}>{`${this.selectionTitle}`}</span>
</div>
</>;
@@ -590,6 +629,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
if (bounds.y > bounds.b) {
bounds.y = bounds.b - (this._resizeBorderWidth + this._linkBoxHeight + this._titleHeight);
}
+ var offset = 0;
+ //make offset larger for ink to edit points
+ if (seldoc.rootDoc.type === DocumentType.INK) {
+ offset = 20;
+ }
return (<div className="documentDecorations" style={{ background: darkScheme }} >
<div className="documentDecorations-background" style={{
width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px",
@@ -602,20 +646,21 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
</div>
{bounds.r - bounds.x < 15 && bounds.b - bounds.y < 15 ? (null) : <>
<div className="documentDecorations-container" key="container" ref={this.setTextBar} style={{
- width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px",
- height: (bounds.b - bounds.y + this._resizeBorderWidth + this._titleHeight) + "px",
- left: bounds.x - this._resizeBorderWidth / 2,
- top: bounds.y - this._resizeBorderWidth / 2 - this._titleHeight,
+ width: (bounds.r - bounds.x + this._resizeBorderWidth + offset) + "px",
+ height: (bounds.b - bounds.y + this._resizeBorderWidth + this._titleHeight + offset) + "px",
+ left: bounds.x - this._resizeBorderWidth / 2 - offset / 2,
+ top: bounds.y - this._resizeBorderWidth / 2 - this._titleHeight - offset / 2,
}}>
{maximizeIcon}
{titleArea}
{SelectionManager.SelectedDocuments().length !== 1 || seldoc.Document.type === DocumentType.INK ? (null) :
- <div className="documentDecorations-iconifyButton" title={`${seldoc.finalLayoutKey.includes("icon") ? "De" : ""}Iconify Document`} onPointerDown={this.onIconifyDown}>
- {"_"}
- </div>}
- <div className="documentDecorations-closeButton" title="Open Document in Tab" onPointerDown={this.onMaximizeDown}>
+ <Tooltip title={<><div className="dash-tooltip">{`${seldoc.finalLayoutKey.includes("icon") ? "De" : ""}Iconify Document`}</div></>} placement="top">
+ <div className="documentDecorations-iconifyButton" onPointerDown={this.onIconifyDown}>
+ {"_"}
+ </div></Tooltip>}
+ <Tooltip title={<><div className="dash-tooltip">Open Document In Tab</div></>} placement="top"><div className="documentDecorations-openInTab" onPointerDown={this.onMaximizeDown}>
{SelectionManager.SelectedDocuments().length === 1 ? DocumentDecorations.DocumentIcon(StrCast(seldoc.props.Document.layout, "...")) : "..."}
- </div>
+ </div></Tooltip>
<div id="documentDecorations-rotation" className="documentDecorations-rotation"
onPointerDown={this.onRotateDown}> ⟲ </div>
<div id="documentDecorations-topLeftResizer" className="documentDecorations-resizer"
@@ -636,10 +681,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
<div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer"
onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
{seldoc.props.renderDepth <= 1 || !seldoc.props.ContainingCollectionView ? (null) :
- <div id="documentDecorations-levelSelector" className="documentDecorations-selector"
- title="tap to select containing document" onPointerDown={this.onSelectorUp} onContextMenu={e => e.preventDefault()}>
- <FontAwesomeIcon className="documentdecorations-times" icon={faArrowAltCircleUp} size="lg" />
- </div>}
+ <Tooltip title={<><div className="dash-tooltip">tap to select containing document</div></>} placement="top">
+ <div id="documentDecorations-levelSelector" className="documentDecorations-selector"
+ onPointerDown={this.onSelectorUp} onContextMenu={e => e.preventDefault()}>
+ <FontAwesomeIcon className="documentdecorations-times" icon={faArrowAltCircleUp} size="lg" />
+ </div></Tooltip>}
<div id="documentDecorations-borderRadius" className="documentDecorations-radius"
onPointerDown={this.onRadiusDown} onContextMenu={(e) => e.preventDefault()}></div>
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index b78ed6fee..0435e70c4 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -57,7 +57,7 @@ export interface EditableProps {
highlight?: boolean;
positions?: number[];
search?: string;
- bing?: () => string|undefined;
+ bing?: () => string | undefined;
}
/**
@@ -79,7 +79,6 @@ export class EditableView extends React.Component<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
- // console.log("props editing = " + nextProps.editing);
// if (nextProps.editing && nextProps.editing !== this._editing) {
// this._editing = nextProps.editing;
// EditableView.loadId = "";
@@ -191,27 +190,27 @@ export class EditableView extends React.Component<EditableProps> {
let results = [];
let contents = this.props.bing!();
- if (contents!== undefined){
- if (this.props.positions!==undefined){
- let positions = this.props.positions;
- let length = this.props.search!.length;
-
- // contents = String(this.props.contents.valueOf());
-
- results.push(<span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize, color: this.props.contents ? "black" : "grey" }}>{contents ? contents.slice(0, this.props.positions![0]) : this.props.placeholder?.valueOf()}</span>);
- positions.forEach((num, cur) => {
- results.push(<span style={{ backgroundColor: "#FFFF00", fontStyle: this.props.fontStyle, fontSize: this.props.fontSize, color: this.props.contents ? "black" : "grey" }}>{contents ? contents.slice(num, num + length) : this.props.placeholder?.valueOf()}</span>);
- let end = 0;
- cur === positions.length-1? end = contents.length: end = positions[cur + 1];
- results.push(<span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize, color: this.props.contents ? "black" : "grey" }}>{contents ? contents.slice(num + length, end) : this.props.placeholder?.valueOf()}</span>);
+ if (contents !== undefined) {
+ if (this.props.positions !== undefined) {
+ let positions = this.props.positions;
+ let length = this.props.search!.length;
+
+ // contents = String(this.props.contents.valueOf());
+
+ results.push(<span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize, color: this.props.contents ? "black" : "grey" }}>{contents ? contents.slice(0, this.props.positions![0]) : this.props.placeholder?.valueOf()}</span>);
+ positions.forEach((num, cur) => {
+ results.push(<span style={{ backgroundColor: "#FFFF00", fontStyle: this.props.fontStyle, fontSize: this.props.fontSize, color: this.props.contents ? "black" : "grey" }}>{contents ? contents.slice(num, num + length) : this.props.placeholder?.valueOf()}</span>);
+ let end = 0;
+ cur === positions.length - 1 ? end = contents.length : end = positions[cur + 1];
+ results.push(<span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize, color: this.props.contents ? "black" : "grey" }}>{contents ? contents.slice(num + length, end) : this.props.placeholder?.valueOf()}</span>);
+ }
+ )
+ }
+ return results;
+ }
+ else {
+ return <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize, color: this.props.contents ? "black" : "grey" }}>{this.props.contents ? this.props.contents?.valueOf() : this.props.placeholder?.valueOf()}</span>;
}
- )
- }
- return results;
-}
-else{
- return <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize, color: this.props.contents ? "black" : "grey" }}>{this.props.contents ? this.props.contents?.valueOf() : this.props.placeholder?.valueOf()}</span>;
-}
}
render() {
@@ -228,7 +227,7 @@ else{
ref={this._ref}
style={{ display: this.props.display, minHeight: "20px", height: `${this.props.height ? this.props.height : "auto"}`, maxHeight: `${this.props.maxHeight}` }}
onClick={this.onClick} placeholder={this.props.placeholder}>
- {this.props.highlight === undefined || this.props.positions===undefined || this.props.bing===undefined? <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize, color: this.props.contents ? "black" : "grey" }}>{this.props.contents ? this.props.contents?.valueOf() : this.props.placeholder?.valueOf()}</span>
+ {this.props.highlight === undefined || this.props.positions === undefined || this.props.bing === undefined ? <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize, color: this.props.contents ? "black" : "grey" }}>{this.props.contents ? this.props.contents?.valueOf() : this.props.placeholder?.valueOf()}</span>
: this.returnHighlights()}
</div>
);
diff --git a/src/client/views/GestureOverlay.scss b/src/client/views/GestureOverlay.scss
index 107077792..c9d78890e 100644
--- a/src/client/views/GestureOverlay.scss
+++ b/src/client/views/GestureOverlay.scss
@@ -1,7 +1,7 @@
.gestureOverlay-cont {
width: 100vw;
height: 100vh;
- position: absolute;
+ position: relative;
top: 0;
left: 0;
touch-action: none;
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 85695bbac..7c0a8635e 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -5,12 +5,10 @@ import { Doc } from "../../fields/Doc";
import { InkData, InkTool } from "../../fields/InkField";
import { Cast, FieldValue, NumCast } from "../../fields/Types";
import MobileInkOverlay from "../../mobile/MobileInkOverlay";
-import MobileInterface from "../../mobile/MobileInterface";
import { GestureUtils } from "../../pen-gestures/GestureUtils";
import { MobileInkOverlayContent } from "../../server/Message";
-import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero, returnEmptyFilter } from "../../Utils";
+import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero, returnEmptyFilter, setupMoveUpEvents } from "../../Utils";
import { CognitiveServices } from "../cognitive_services/CognitiveServices";
-import { DocServer } from "../DocServer";
import { DocUtils } from "../documents/Documents";
import { CurrentUserUtils } from "../util/CurrentUserUtils";
import { InteractionUtils } from "../util/InteractionUtils";
@@ -24,7 +22,8 @@ import { RadialMenu } from "./nodes/RadialMenu";
import HorizontalPalette from "./Palette";
import { Touchable } from "./Touchable";
import TouchScrollableMenu, { TouchScrollableMenuItem } from "./TouchScrollableMenu";
-import HeightLabel from "./collections/collectionMulticolumn/MultirowHeightLabel";
+import * as fitCurve from 'fit-curve';
+import { CollectionFreeFormViewChrome } from "./collections/CollectionMenu";
@observer
export default class GestureOverlay extends Touchable {
@@ -56,6 +55,7 @@ export default class GestureOverlay extends Touchable {
@observable private showMobileInkOverlay: boolean = false;
+ private _overlayRef = React.createRef<HTMLDivElement>();
private _d1: Doc | undefined;
private _inkToTextDoc: Doc | undefined;
private _thumbDoc: Doc | undefined;
@@ -64,7 +64,7 @@ export default class GestureOverlay extends Touchable {
private _hands: Map<number, React.Touch[]> = new Map<number, React.Touch[]>();
private _holdTimer: NodeJS.Timeout | undefined;
- protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
+ protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
constructor(props: Readonly<{}>) {
super(props);
@@ -112,7 +112,7 @@ export default class GestureOverlay extends Touchable {
onReactTouchStart = (te: React.TouchEvent) => {
document.removeEventListener("touchmove", this.onReactHoldTouchMove);
document.removeEventListener("touchend", this.onReactHoldTouchEnd);
- if (RadialMenu.Instance._display === true) {
+ if (RadialMenu.Instance?._display === true) {
te.preventDefault();
te.stopPropagation();
RadialMenu.Instance.closeMenu();
@@ -161,9 +161,8 @@ export default class GestureOverlay extends Touchable {
if (nts.nt.length === 1) {
// -- radial menu code --
this._holdTimer = setTimeout(() => {
- console.log("hold");
- const target = document.elementFromPoint(te.changedTouches.item(0).clientX, te.changedTouches.item(0).clientY);
- const pt: any = te.touches[te.touches.length - 1];
+ const target = document.elementFromPoint(te.changedTouches?.item(0).clientX, te.changedTouches?.item(0).clientY);
+ const pt: any = te.touches[te.touches?.length - 1];
if (nts.nt.length === 1 && pt.radiusX > 1 && pt.radiusY > 1) {
target?.dispatchEvent(
new CustomEvent<InteractionUtils.MultiTouchEvent<React.TouchEvent>>("dashOnTouchHoldStart",
@@ -498,39 +497,26 @@ export default class GestureOverlay extends Touchable {
onPointerDown = (e: React.PointerEvent) => {
if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.GetSelectedTool() === InkTool.Highlighter || Doc.GetSelectedTool() === InkTool.Pen)) {
this._points.push({ X: e.clientX, Y: e.clientY });
- e.stopPropagation();
- e.preventDefault();
-
- document.removeEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- document.addEventListener("pointermove", this.onPointerMove);
- document.addEventListener("pointerup", this.onPointerUp);
+ setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction);
}
}
@action
onPointerMove = (e: PointerEvent) => {
- if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.GetSelectedTool() === InkTool.Highlighter || Doc.GetSelectedTool() === InkTool.Pen)) {
- this._points.push({ X: e.clientX, Y: e.clientY });
- e.stopPropagation();
- e.preventDefault();
-
-
- if (this._points.length > 1) {
- const B = this.svgBounds;
- const initialPoint = this._points[0.];
- const xInGlass = initialPoint.X > (this._thumbX ?? Number.MAX_SAFE_INTEGER) && initialPoint.X < (this._thumbX ?? Number.MAX_SAFE_INTEGER) + this.height;
- const yInGlass = initialPoint.Y > (this._thumbY ?? Number.MAX_SAFE_INTEGER) - this.height && initialPoint.Y < (this._thumbY ?? Number.MAX_SAFE_INTEGER);
- if (this.Tool !== ToolglassTools.None && xInGlass && yInGlass) {
- switch (this.Tool) {
- case ToolglassTools.RadialMenu:
- document.removeEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- //this.handle1PointerHoldStart(e);
- }
+ this._points.push({ X: e.clientX, Y: e.clientY });
+
+ if (this._points.length > 1) {
+ const B = this.svgBounds;
+ const initialPoint = this._points[0.];
+ const xInGlass = initialPoint.X > (this._thumbX ?? Number.MAX_SAFE_INTEGER) && initialPoint.X < (this._thumbX ?? Number.MAX_SAFE_INTEGER) + this.height;
+ const yInGlass = initialPoint.Y > (this._thumbY ?? Number.MAX_SAFE_INTEGER) - this.height && initialPoint.Y < (this._thumbY ?? Number.MAX_SAFE_INTEGER);
+ if (this.Tool !== ToolglassTools.None && xInGlass && yInGlass) {
+ switch (this.Tool) {
+ case ToolglassTools.RadialMenu: return true;
}
}
}
+ return false;
}
handleLineGesture = (): boolean => {
@@ -552,7 +538,7 @@ export default class GestureOverlay extends Touchable {
else if (this._d1 !== doc && !LinkManager.Instance.doesLinkExist(this._d1, doc)) {
// we don't want to create a link between ink strokes (doing so makes drawing a t very hard)
if (this._d1.type !== "ink" && doc.type !== "ink") {
- DocUtils.MakeLink({ doc: this._d1 }, { doc: doc }, "gestural link");
+ DocUtils.MakeLink({ doc: this._d1 }, { doc: doc }, "gestural link", "");
actionPerformed = true;
}
}
@@ -580,14 +566,6 @@ export default class GestureOverlay extends Touchable {
const points = this._points.map(p => ({ X: p.X - B.left, Y: p.Y - B.top }));
//push first points to so interactionUtil knows pointer is up
this._points.push({ X: this._points[0].X, Y: this._points[0].Y });
- if (MobileInterface.Instance && MobileInterface.Instance.drawingInk) {
- DocServer.Mobile.dispatchGesturePoints({
- points: this._points,
- bounds: B,
- color: ActiveInkColor(),
- width: ActiveInkWidth()
- });
- }
const initialPoint = this._points[0.];
const xInGlass = initialPoint.X > (this._thumbX ?? Number.MAX_SAFE_INTEGER) && initialPoint.X < (this._thumbX ?? Number.MAX_SAFE_INTEGER) + (this.height);
@@ -597,8 +575,6 @@ export default class GestureOverlay extends Touchable {
if (this.Tool !== ToolglassTools.None && xInGlass && yInGlass) {
switch (this.Tool) {
case ToolglassTools.InkToText:
- document.removeEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
this._strokes.push(new Array(...this._points));
this._points = [];
CognitiveServices.Inking.Appliers.InterpretStrokes(this._strokes).then((results) => {
@@ -627,12 +603,12 @@ export default class GestureOverlay extends Touchable {
break;
}
}
- //if any of the shape is activated in the InkOptionsMenu
+ //if any of the shape is activated in the CollectionFreeFormViewChrome
else if (this.InkShape) {
this.makePolygon(this.InkShape, false);
this.dispatchGesture(GestureUtils.Gestures.Stroke);
this._points = [];
- if (this.InkShape !== "noRec") {
+ if (!CollectionFreeFormViewChrome.Instance._keepMode) {
this.InkShape = "";
}
}
@@ -642,57 +618,58 @@ export default class GestureOverlay extends Touchable {
let actionPerformed = false;
if (result && result.Score > 0.7) {
switch (result.Name) {
- case GestureUtils.Gestures.Box:
- this.dispatchGesture(GestureUtils.Gestures.Box);
- actionPerformed = true;
- break;
- case GestureUtils.Gestures.StartBracket:
- this.dispatchGesture(GestureUtils.Gestures.StartBracket);
- actionPerformed = true;
- break;
- case GestureUtils.Gestures.EndBracket:
- this.dispatchGesture("endbracket");
- actionPerformed = true;
- break;
- case GestureUtils.Gestures.Line:
- actionPerformed = this.handleLineGesture();
- break;
- case GestureUtils.Gestures.Triangle:
- this.makePolygon("triangle", true);
- break;
- case GestureUtils.Gestures.Circle:
- this.makePolygon("circle", true);
- break;
- case GestureUtils.Gestures.Rectangle:
- this.makePolygon("rectangle", true);
- break;
- case GestureUtils.Gestures.Scribble:
- console.log("scribble");
- break;
- }
- if (actionPerformed) {
- this._points = [];
+ case GestureUtils.Gestures.Box: actionPerformed = this.dispatchGesture(GestureUtils.Gestures.Box); break;
+ case GestureUtils.Gestures.StartBracket: actionPerformed = this.dispatchGesture(GestureUtils.Gestures.StartBracket); break;
+ case GestureUtils.Gestures.EndBracket: actionPerformed = this.dispatchGesture("endbracket"); break;
+ case GestureUtils.Gestures.Line: actionPerformed = this.handleLineGesture(); break;
+ case GestureUtils.Gestures.Triangle: actionPerformed = this.makePolygon("triangle", true); break;
+ case GestureUtils.Gestures.Circle: actionPerformed = this.makePolygon("circle", true); break;
+ case GestureUtils.Gestures.Rectangle: actionPerformed = this.makePolygon("rectangle", true); break;
+ case GestureUtils.Gestures.Scribble: console.log("scribble"); break;
}
}
// if no gesture (or if the gesture was unsuccessful), "dry" the stroke into an ink document
if (!actionPerformed) {
+ const newPoints = this._points.reduce((p, pts) => { p.push([pts.X, pts.Y]); return p; }, [] as number[][]);
+ newPoints.pop();
+ const controlPoints: { X: number, Y: number }[] = [];
+
+ const bezierCurves = fitCurve(newPoints, 10);
+ for (const curve of bezierCurves) {
+
+ controlPoints.push({ X: curve[0][0], Y: curve[0][1] });
+ controlPoints.push({ X: curve[1][0], Y: curve[1][1] });
+ controlPoints.push({ X: curve[2][0], Y: curve[2][1] });
+ controlPoints.push({ X: curve[3][0], Y: curve[3][1] });
+
+
+ }
+ this._points = controlPoints;
+
this.dispatchGesture(GestureUtils.Gestures.Stroke);
- this._points = [];
}
+ this._points = [];
}
} else {
this._points = [];
}
- SetActiveArrowStart("none");
- GestureOverlay.Instance.SavedArrowStart = ActiveArrowStart();
- SetActiveArrowEnd("none");
- GestureOverlay.Instance.SavedArrowEnd = ActiveArrowEnd();
- document.removeEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
+ //get out of ink mode after each stroke=
+ if (!CollectionFreeFormViewChrome.Instance._keepMode) {
+ Doc.SetSelectedTool(InkTool.None);
+ CollectionFreeFormViewChrome.Instance._selected = CollectionFreeFormViewChrome.Instance._shapesNum;
+ SetActiveArrowStart("none");
+ GestureOverlay.Instance.SavedArrowStart = ActiveArrowStart();
+ SetActiveArrowEnd("none");
+ GestureOverlay.Instance.SavedArrowEnd = ActiveArrowEnd();
+ }
}
makePolygon = (shape: string, gesture: boolean) => {
+ //take off gesture recognition for now
+ if (gesture) {
+ return false;
+ }
const xs = this._points.map(p => p.X);
const ys = this._points.map(p => p.Y);
var right = Math.max(...xs);
@@ -700,7 +677,7 @@ export default class GestureOverlay extends Touchable {
var bottom = Math.max(...ys);
var top = Math.min(...ys);
if (shape === "noRec") {
- return;
+ return false;
}
if (!gesture) {
//if shape options is activated in inkOptionMenu
@@ -725,21 +702,56 @@ export default class GestureOverlay extends Touchable {
this._points = [];
switch (shape) {
//must push an extra point in the end so InteractionUtils knows pointer is up.
- //must be (points[0].X,points[0]-1)
+ //must be (points[0].X,points[0]-1)
case "rectangle":
this._points.push({ X: left, Y: top });
+ this._points.push({ X: left, Y: top });
+
+ this._points.push({ X: right, Y: top });
this._points.push({ X: right, Y: top });
+ this._points.push({ X: right, Y: top });
+ this._points.push({ X: right, Y: top });
+
+ this._points.push({ X: right, Y: bottom });
this._points.push({ X: right, Y: bottom });
+ this._points.push({ X: right, Y: bottom });
+ this._points.push({ X: right, Y: bottom });
+
+ this._points.push({ X: left, Y: bottom });
+ this._points.push({ X: left, Y: bottom });
this._points.push({ X: left, Y: bottom });
+ this._points.push({ X: left, Y: bottom });
+
this._points.push({ X: left, Y: top });
- this._points.push({ X: left, Y: top - 1 });
+ this._points.push({ X: left, Y: top });
+ // this._points.push({ X: left, Y: top });
+ // this._points.push({ X: left, Y: top });
+
+ // this._points.push({ X: left, Y: top - 1 });
break;
case "triangle":
+ // this._points.push({ X: left, Y: bottom });
+ // this._points.push({ X: right, Y: bottom });
+ // this._points.push({ X: (right + left) / 2, Y: top });
+ // this._points.push({ X: left, Y: bottom });
+ // this._points.push({ X: left, Y: bottom - 1 });
this._points.push({ X: left, Y: bottom });
+ this._points.push({ X: left, Y: bottom });
+
+ this._points.push({ X: right, Y: bottom });
+ this._points.push({ X: right, Y: bottom });
this._points.push({ X: right, Y: bottom });
+ this._points.push({ X: right, Y: bottom });
+
+ this._points.push({ X: (right + left) / 2, Y: top });
+ this._points.push({ X: (right + left) / 2, Y: top });
this._points.push({ X: (right + left) / 2, Y: top });
+ this._points.push({ X: (right + left) / 2, Y: top });
+
+ this._points.push({ X: left, Y: bottom });
this._points.push({ X: left, Y: bottom });
- this._points.push({ X: left, Y: bottom - 1 });
+
+
break;
case "circle":
const centerX = (right + left) / 2;
@@ -756,11 +768,37 @@ export default class GestureOverlay extends Touchable {
}
this._points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((top - centerY), 2))) + centerX, Y: top });
this._points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((top - centerY), 2))) + centerX, Y: top - 1 });
+ // this._points.push({ X: centerX, Y: top });
+ // this._points.push({ X: centerX + radius / 2, Y: top });
+
+ // this._points.push({ X: right, Y: top + radius / 2 });
+ // this._points.push({ X: right, Y: top + radius });
+ // this._points.push({ X: right, Y: top + radius });
+ // this._points.push({ X: right, Y: bottom - radius / 2 });
+
+ // this._points.push({ X: right - radius / 2, Y: bottom });
+ // this._points.push({ X: right - radius, Y: bottom });
+ // this._points.push({ X: right - radius, Y: bottom });
+ // this._points.push({ X: left + radius / 2, Y: bottom });
+
+ // this._points.push({ X: left, Y: bottom - radius / 2 });
+ // this._points.push({ X: left, Y: bottom - radius });
+ // this._points.push({ X: left, Y: bottom - radius });
+ // this._points.push({ X: left, Y: top + radius / 2 });
+
+ // this._points.push({ X: left + radius / 2, Y: top });
+ // this._points.push({ X: left + radius, Y: top });
+
+
+
+
+
+
+
break;
case "line":
this._points.push({ X: left, Y: top });
this._points.push({ X: right, Y: bottom });
- // this._points.push({ X: right, Y: bottom - 1 });
break;
case "arrow":
const x1 = left;
@@ -781,11 +819,12 @@ export default class GestureOverlay extends Touchable {
this._points.push({ X: x2, Y: y2 });
// this._points.push({ X: x1, Y: y1 - 1 });
}
+ return true;
}
dispatchGesture = (gesture: "box" | "line" | "startbracket" | "endbracket" | "stroke" | "scribble" | "text", stroke?: InkData, data?: any) => {
const target = document.elementFromPoint((stroke ?? this._points)[0].X, (stroke ?? this._points)[0].Y);
- target?.dispatchEvent(
+ return target?.dispatchEvent(
new CustomEvent<GestureUtils.GestureEvent>("dashOnGesture",
{
bubbles: true,
@@ -797,7 +836,7 @@ export default class GestureOverlay extends Touchable {
}
}
)
- );
+ ) || false;
}
getBounds = (stroke: InkData) => {
@@ -816,10 +855,11 @@ export default class GestureOverlay extends Touchable {
@computed get elements() {
const width = Number(ActiveInkWidth());
+ const rect = this._overlayRef.current?.getBoundingClientRect();
const B = this.svgBounds;
B.left = B.left - width / 2;
B.right = B.right + width / 2;
- B.top = B.top - width / 2;
+ B.top = B.top - width / 2 - (rect?.y || 0);
B.bottom = B.bottom + width / 2;
B.width += width;
B.height += width;
@@ -836,7 +876,7 @@ export default class GestureOverlay extends Touchable {
}),
this._points.length <= 1 ? (null) : <svg key="svg" width={B.width} height={B.height}
style={{ transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}>
- {InteractionUtils.CreatePolyline(this._points, B.left, B.top, ActiveInkColor(), width, width, ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), 1, 1, this.InkShape, "none", false, false)}
+ {InteractionUtils.CreatePolyline(this._points.map(p => ({ X: p.X, Y: p.Y - (rect?.y || 0) })), B.left, B.top, ActiveInkColor(), width, width, ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), 1, 1, this.InkShape, "none", false, false)}
</svg>]
];
}
@@ -885,7 +925,8 @@ export default class GestureOverlay extends Touchable {
render() {
return (
- <div className="gestureOverlay-cont" onPointerDown={this.onPointerDown} onTouchStart={this.onReactTouchStart}>
+ <div className="gestureOverlay-cont" ref={this._overlayRef}
+ onPointerDown={this.onPointerDown} onTouchStart={this.onReactTouchStart}>
{this.showMobileInkOverlay ? <MobileInkOverlay /> : <></>}
{this.elements}
@@ -912,7 +953,7 @@ export default class GestureOverlay extends Touchable {
}
}
-// export class
+// export class
export enum ToolglassTools {
InkToText = "inktotext",
@@ -949,4 +990,4 @@ Scripting.addGlobal(function resetPen() {
}, "resets the pen tool");
Scripting.addGlobal(function createText(text: any, x: any, y: any) {
GestureOverlay.Instance.dispatchGesture("text", [{ X: x, Y: y }], text);
-}, "creates a text document with inputted text and coordinates", "(text: any, x: any, y: any)"); \ No newline at end of file
+}, "creates a text document with inputted text and coordinates", "(text: any, x: any, y: any)");
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 2e5c018ba..49ddc7374 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -7,6 +7,7 @@ import { List } from "../../fields/List";
import { ScriptField } from "../../fields/ScriptField";
import { Cast, PromiseValue } from "../../fields/Types";
import GoogleAuthenticationManager from "../apis/GoogleAuthenticationManager";
+import HypothesisAuthenticationManager from "../apis/HypothesisAuthenticationManager";
import { DocServer } from "../DocServer";
import { DocumentType } from "../documents/DocumentTypes";
import { DictationManager } from "../util/DictationManager";
@@ -21,6 +22,8 @@ import { DocumentView } from "./nodes/DocumentView";
import { DocumentLinksButton } from "./nodes/DocumentLinksButton";
import PDFMenu from "./pdf/PDFMenu";
import { ContextMenu } from "./ContextMenu";
+import GroupManager from "../util/GroupManager";
+import { CollectionFreeFormViewChrome } from "./collections/CollectionMenu";
const modifiers = ["control", "meta", "shift", "alt"];
type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise<KeyControlInfo>;
@@ -79,7 +82,15 @@ export default class KeyManager {
// MarqueeView.DragMarquee = !MarqueeView.DragMarquee; // bcz: this needs a better disclosure UI
break;
case "escape":
+ // if (DocumentLinksButton.StartLink) {
+ // if (DocumentLinksButton.StartLink.Document) {
+ // action((e: React.PointerEvent<HTMLDivElement>) => {
+ // Doc.UnBrushDoc(DocumentLinksButton.StartLink?.Document as Doc);
+ // });
+ // }
+ // }
DocumentLinksButton.StartLink = undefined;
+
const main = MainView.Instance;
Doc.SetSelectedTool(InkTool.None);
var doDeselect = true;
@@ -94,9 +105,11 @@ export default class KeyManager {
}
doDeselect && SelectionManager.DeselectAll();
DictationManager.Controls.stop();
- // RecommendationsBox.Instance.closeMenu();
GoogleAuthenticationManager.Instance.cancel();
+ HypothesisAuthenticationManager.Instance.cancel();
SharingManager.Instance.close();
+ GroupManager.Instance.close();
+ CollectionFreeFormViewChrome.Instance.clearKeep();
break;
case "delete":
case "backspace":
@@ -295,13 +308,13 @@ export default class KeyManager {
const targetDataDoc = Doc.GetProto(first.props.Document);
const fieldKey = Doc.LayoutFieldKey(first.props.Document);
const docList = DocListCast(targetDataDoc[fieldKey]);
- docids.map((did, i) => i && DocServer.GetRefField(did).then(doc => {
+ docids.map((did, i) => i && DocServer.GetRefField(did).then(async doc => {
count++;
if (doc instanceof Doc) {
list.push(doc);
}
if (count === docids.length) {
- const added = list.filter(d => !docList.includes(d)).map(d => clone ? Doc.MakeClone(d) : d);
+ const added = await Promise.all(list.filter(d => !docList.includes(d)).map(async d => clone ? (await Doc.MakeClone(d)).clone : d));
if (added.length) {
added.map(doc => doc.context = targetDataDoc);
undoBatch(() => {
@@ -337,4 +350,4 @@ export default class KeyManager {
};
});
-} \ No newline at end of file
+}
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index c32e76cec..5892e8346 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -15,6 +15,9 @@ import { FieldView, FieldViewProps } from "./nodes/FieldView";
import React = require("react");
import { Scripting } from "../util/Scripting";
import { Doc } from "../../fields/Doc";
+import FormatShapePane from "./collections/collectionFreeForm/FormatShapePane";
+import { action } from "mobx";
+import { setupMoveUpEvents } from "../../Utils";
library.add(faPaintBrush);
@@ -38,10 +41,49 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
this.props.Document.isInkMask = true;
}
+ @action
+ private formatShape = () => {
+ FormatShapePane.Instance.Pinned = true;
+ }
+
+ private _prevX = 0;
+ private _prevY = 0;
+ private _controlNum = 0;
+ @action
+ onControlDown = (e: React.PointerEvent, i: number): void => {
+ setupMoveUpEvents(this, e, this.onControlMove, this.onControlup, (e) => { });
+ this._prevX = e.clientX;
+ this._prevY = e.clientY;
+ this._controlNum = i;
+ }
+
+ @action
+ changeCurrPoint = (i: number) => {
+ FormatShapePane.Instance._currPoint = i;
+ }
+
+ @action
+ onControlMove = (e: PointerEvent, down: number[]): boolean => {
+ const xDiff = this._prevX - e.clientX;
+ const yDiff = this._prevY - e.clientY;
+ FormatShapePane.Instance.control(xDiff, yDiff, this._controlNum);
+ this._prevX = e.clientX;
+ this._prevY = e.clientY;
+ return false;
+ }
+
+ onControlup = (e: PointerEvent) => {
+ this._prevX = 0;
+ this._prevY = 0;
+ this._controlNum = 0;
+ }
+
+ public static MaskDim = 50000;
render() {
TraceMobx();
const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? [];
- const strokeWidth = Number(StrCast(this.layoutDoc.strokeWidth, ActiveInkWidth()));
+ // const strokeWidth = Number(StrCast(this.layoutDoc.strokeWidth, ActiveInkWidth()));
+ const strokeWidth = Number(this.layoutDoc.strokeWidth);
const xs = data.map(p => p.X);
const ys = data.map(p => p.Y);
const left = Math.min(...xs) - strokeWidth / 2;
@@ -52,33 +94,101 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
const height = bottom - top;
const scaleX = (this.props.PanelWidth() - strokeWidth) / (width - strokeWidth);
const scaleY = (this.props.PanelHeight() - strokeWidth) / (height - strokeWidth);
- const strokeColor = StrCast(this.layoutDoc.color, ActiveInkColor());
+ const strokeColor = StrCast(this.layoutDoc.color, "");
+
const points = InteractionUtils.CreatePolyline(data, left, top, strokeColor, strokeWidth, strokeWidth,
- StrCast(this.layoutDoc.strokeBezier, ActiveInkBezierApprox()), StrCast(this.layoutDoc.fillColor, ActiveFillColor()),
- StrCast(this.layoutDoc.arrowStart, ActiveArrowStart()), StrCast(this.layoutDoc.arrowEnd, ActiveArrowEnd()),
- StrCast(this.layoutDoc.dash, ActiveDash()), scaleX, scaleY, "", "none", this.props.isSelected() && strokeWidth <= 5, false);
+ StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "transparent"),
+ StrCast(this.layoutDoc.strokeStartMarker), StrCast(this.layoutDoc.strokeEndMarker),
+ StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", this.props.isSelected() && strokeWidth <= 5, false);
+
const hpoints = InteractionUtils.CreatePolyline(data, left, top,
this.props.isSelected() && strokeWidth > 5 ? strokeColor : "transparent", strokeWidth, (strokeWidth + 15),
- StrCast(this.layoutDoc.strokeBezier, ActiveInkBezierApprox()), StrCast(this.layoutDoc.fillColor, ActiveFillColor()),
+ StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "transparent"),
"none", "none", "0", scaleX, scaleY, "", this.props.active() ? "visiblepainted" : "none", false, true);
+
+ var controlPoints: { X: number, Y: number, I: number }[] = [];
+ var handlePoints: { X: number, Y: number, I: number, dot1: number, dot2: number }[] = [];
+ var handleLine: { X1: number, Y1: number, X2: number, Y2: number, X3: number, Y3: number, dot1: number, dot2: number }[] = [];
+ if (data.length >= 4) {
+ for (var i = 0; i <= data.length - 4; i += 4) {
+ controlPoints.push({ X: data[i].X, Y: data[i].Y, I: i });
+ controlPoints.push({ X: data[i + 3].X, Y: data[i + 3].Y, I: i + 3 });
+ handlePoints.push({ X: data[i + 1].X, Y: data[i + 1].Y, I: i + 1, dot1: i, dot2: i === 0 ? i : i - 1 });
+ handlePoints.push({ X: data[i + 2].X, Y: data[i + 2].Y, I: i + 2, dot1: i + 3, dot2: i === data.length ? i + 3 : i + 4 });
+ }
+
+ handleLine.push({ X1: data[0].X, Y1: data[0].Y, X2: data[0].X, Y2: data[0].Y, X3: data[1].X, Y3: data[1].Y, dot1: 0, dot2: 0 });
+ for (var i = 2; i < data.length - 4; i += 4) {
+
+ handleLine.push({ X1: data[i].X, Y1: data[i].Y, X2: data[i + 1].X, Y2: data[i + 1].Y, X3: data[i + 3].X, Y3: data[i + 3].Y, dot1: i + 1, dot2: i + 2 });
+
+ }
+ handleLine.push({ X1: data[data.length - 2].X, Y1: data[data.length - 2].Y, X2: data[data.length - 1].X, Y2: data[data.length - 1].Y, X3: data[data.length - 1].X, Y3: data[data.length - 1].Y, dot1: data.length - 1, dot2: data.length - 1 });
+
+
+ }
+ if (data.length <= 4) {
+ handlePoints = [];
+ handleLine = [];
+ controlPoints = [];
+ for (var i = 0; i < data.length; i++) {
+ controlPoints.push({ X: data[i].X, Y: data[i].Y, I: i });
+ }
+
+ }
+ const dotsize = String(Math.min(width * scaleX, height * scaleY) / 40);
+
+ const controls = controlPoints.map((pts, i) =>
+
+ <svg height="10" width="10">
+ <circle cx={(pts.X - left - strokeWidth / 2) * scaleX + strokeWidth / 2} cy={(pts.Y - top - strokeWidth / 2) * scaleY + strokeWidth / 2} r={dotsize} stroke="black" stroke-width={String(Number(dotsize) / 2)} fill="red"
+ onPointerDown={(e) => { this.changeCurrPoint(pts.I); this.onControlDown(e, pts.I); }} pointerEvents="all" cursor="all-scroll" />
+ </svg>);
+ const handles = handlePoints.map((pts, i) =>
+
+ <svg height="10" width="10">
+ <circle cx={(pts.X - left - strokeWidth / 2) * scaleX + strokeWidth / 2} cy={(pts.Y - top - strokeWidth / 2) * scaleY + strokeWidth / 2} r={dotsize} stroke="black" stroke-width={String(Number(dotsize) / 2)} fill="green"
+ onPointerDown={(e) => this.onControlDown(e, pts.I)} pointerEvents="all" cursor="all-scroll" display={(pts.dot1 === FormatShapePane.Instance._currPoint || pts.dot2 === FormatShapePane.Instance._currPoint) ? "inherit" : "none"} />
+ </svg>);
+ const handleLines = handleLine.map((pts, i) =>
+
+ <svg height="100" width="100">
+ <line x1={(pts.X1 - left - strokeWidth / 2) * scaleX + strokeWidth / 2} y1={(pts.Y1 - top - strokeWidth / 2) * scaleY + strokeWidth / 2}
+ x2={(pts.X2 - left - strokeWidth / 2) * scaleX + strokeWidth / 2} y2={(pts.Y2 - top - strokeWidth / 2) * scaleY + strokeWidth / 2} stroke="green" stroke-width={String(Number(dotsize) / 2)}
+ display={(pts.dot1 === FormatShapePane.Instance._currPoint || pts.dot2 === FormatShapePane.Instance._currPoint) ? "inherit" : "none"} />
+ <line x1={(pts.X2 - left - strokeWidth / 2) * scaleX + strokeWidth / 2} y1={(pts.Y2 - top - strokeWidth / 2) * scaleY + strokeWidth / 2}
+ x2={(pts.X3 - left - strokeWidth / 2) * scaleX + strokeWidth / 2} y2={(pts.Y3 - top - strokeWidth / 2) * scaleY + strokeWidth / 2} stroke="green" stroke-width={String(Number(dotsize) / 2)}
+ display={(pts.dot1 === FormatShapePane.Instance._currPoint || pts.dot2 === FormatShapePane.Instance._currPoint) ? "inherit" : "none"} />
+
+ </svg>);
+
+
return (
<svg className="inkingStroke"
width={width}
height={height}
style={{
pointerEvents: this.props.Document.isInkMask ? "all" : "none",
- transform: this.props.Document.isInkMask ? "translate(2500px, 2500px)" : undefined,
+ transform: this.props.Document.isInkMask ? `translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined,
mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? "multiply" : "unset",
overflow: "visible",
}}
onContextMenu={() => {
- ContextMenu.Instance.addItem({ description: "Analyze Stroke", event: this.analyzeStrokes, icon: "paint-brush" });
- ContextMenu.Instance.addItem({ description: "Make Mask", event: this.makeMask, icon: "paint-brush" });
+ const cm = ContextMenu.Instance;
+ if (cm) {
+ !Doc.UserDoc().noviceMode && cm.addItem({ description: "Recognize Writing", event: this.analyzeStrokes, icon: "paint-brush" });
+ cm.addItem({ description: "Make Mask", event: this.makeMask, icon: "paint-brush" });
+ cm.addItem({ description: "Format Shape...", event: this.formatShape, icon: "paint-brush" });
+ }
}}
><defs>
</defs>
{hpoints}
{points}
+ {FormatShapePane.Instance._controlBtn && this.props.isSelected() ? controls : ""}
+ {FormatShapePane.Instance._controlBtn && this.props.isSelected() ? handles : ""}
+ {FormatShapePane.Instance._controlBtn && this.props.isSelected() ? handleLines : ""}
+
</svg>
);
}
@@ -94,9 +204,9 @@ export function SetActiveArrowEnd(value: string) { ActiveInkPen() && (ActiveInkP
export function SetActiveDash(dash: string): void { !isNaN(parseInt(dash)) && ActiveInkPen() && (ActiveInkPen().activeDash = dash); }
export function ActiveInkPen(): Doc { return Cast(Doc.UserDoc().activeInkPen, Doc, null); }
export function ActiveInkColor(): string { return StrCast(ActiveInkPen()?.activeInkColor, "black"); }
-export function ActiveFillColor(): string { return StrCast(ActiveInkPen()?.activeFillColor, "none"); }
-export function ActiveArrowStart(): string { return StrCast(ActiveInkPen()?.activeArrowStart, "none"); }
-export function ActiveArrowEnd(): string { return StrCast(ActiveInkPen()?.activeArrowEnd, "none"); }
+export function ActiveFillColor(): string { return StrCast(ActiveInkPen()?.activeFillColor, ""); }
+export function ActiveArrowStart(): string { return StrCast(ActiveInkPen()?.activeArrowStart, ""); }
+export function ActiveArrowEnd(): string { return StrCast(ActiveInkPen()?.activeArrowEnd, ""); }
export function ActiveDash(): string { return StrCast(ActiveInkPen()?.activeDash, "0"); }
export function ActiveInkWidth(): string { return StrCast(ActiveInkPen()?.activeInkWidth, "1"); }
export function ActiveInkBezierApprox(): string { return StrCast(ActiveInkPen()?.activeInkBezier); }
diff --git a/src/client/views/KeyphraseQueryView.tsx b/src/client/views/KeyphraseQueryView.tsx
index 1dc156968..13d52db88 100644
--- a/src/client/views/KeyphraseQueryView.tsx
+++ b/src/client/views/KeyphraseQueryView.tsx
@@ -11,7 +11,6 @@ export interface KP_Props {
export class KeyphraseQueryView extends React.Component<KP_Props>{
constructor(props: KP_Props) {
super(props);
- console.log("FIRST KEY PHRASE: ", props.keyphrases[0]);
}
render() {
diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss
index 5b142ffda..e1ddbc533 100644
--- a/src/client/views/MainView.scss
+++ b/src/client/views/MainView.scss
@@ -1,6 +1,10 @@
@import "globalCssVariables";
@import "nodeModuleOverrides";
+.dash-tooltip {
+ font-size: 11px;
+ padding: 2px;
+}
.mainView-tabButtons {
position: relative;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 0a7964cea..8995aa78b 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,12 +1,6 @@
import { library } from '@fortawesome/fontawesome-svg-core';
-import {
- faTasks, faEdit, faTrashAlt, faPalette, faAngleRight, faBell, faTrash, faCamera, faExpand, faCaretDown, faCaretLeft, faCaretRight, faCaretSquareDown, faCaretSquareRight, faArrowsAltH, faPlus, faMinus,
- faTerminal, faToggleOn, faFile as fileSolid, faExternalLinkAlt, faLocationArrow, faSearch, faFileDownload, faStop, faCalculator, faWindowMaximize, faAddressCard,
- faQuestionCircle, faArrowLeft, faArrowRight, faArrowDown, faArrowUp, faBolt, faBullseye, faCaretUp, faCat, faCheck, faChevronRight, faClipboard, faClone, faCloudUploadAlt,
- faCommentAlt, faCompressArrowsAlt, faCut, faEllipsisV, faEraser, faExclamation, faFileAlt, faFileAudio, faFilePdf, faFilm, faFilter, faFont, faGlobeAsia, faHighlighter,
- faLongArrowAltRight, faMicrophone, faMousePointer, faMusic, faObjectGroup, faPause, faPen, faPenNib, faPhone, faPlay, faPortrait, faRedoAlt, faStamp, faStickyNote,
- faThumbtack, faTree, faTv, faUndoAlt, faVideo, faAsterisk, faBrain, faImage, faPaintBrush, faTimes, faEye, faArrowsAlt, faQuoteLeft, faSortAmountDown, faAlignLeft, faAlignCenter, faAlignRight
-} from '@fortawesome/free-solid-svg-icons';
+import { faHireAHelper, faBuffer } from '@fortawesome/free-brands-svg-icons';
+import * as fa from '@fortawesome/free-solid-svg-icons';
import { ANTIMODEMENU_HEIGHT } from './globalCssVariables.scss';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, configure, observable, reaction, runInAction } from 'mobx';
@@ -35,7 +29,6 @@ import SharingManager from '../util/SharingManager';
import { Transform } from '../util/Transform';
import { CollectionDockingView } from './collections/CollectionDockingView';
import MarqueeOptionsMenu from './collections/collectionFreeForm/MarqueeOptionsMenu';
-import InkOptionsMenu from './collections/collectionFreeForm/InkOptionsMenu';
import { CollectionLinearView } from './collections/CollectionLinearView';
import { CollectionView, CollectionViewType } from './collections/CollectionView';
import { ContextMenu } from './ContextMenu';
@@ -60,6 +53,11 @@ import { DocumentLinksButton } from './nodes/DocumentLinksButton';
import { LinkMenu } from './linking/LinkMenu';
import { LinkDocPreview } from './nodes/LinkDocPreview';
import { SearchBox } from './search/SearchBox';
+import { TaskCompletionBox } from './nodes/TaskCompletedBox';
+import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup';
+import FormatShapePane from "./collections/collectionFreeForm/FormatShapePane";
+import HypothesisAuthenticationManager from '../apis/HypothesisAuthenticationManager';
+import CollectionMenu from './collections/CollectionMenu';
@observer
export class MainView extends React.Component {
@@ -85,9 +83,8 @@ export class MainView extends React.Component {
public isPointerDown = false;
-
componentDidMount() {
- DocServer.setPlaygroundFields(["dataTransition", "_viewTransition", "_panX", "_panY", "_viewScale", "_viewType"]); // can play with these fields on someone else's
+ DocServer.setPlaygroundFields(["dataTransition", "_viewTransition", "_panX", "_panY", "_viewScale", "_viewType", "_chromeStatus"]); // can play with these fields on someone else's
const tag = document.createElement('script');
@@ -102,7 +99,7 @@ export class MainView extends React.Component {
window.removeEventListener("keydown", KeyManager.Instance.handle);
window.addEventListener("keydown", KeyManager.Instance.handle);
window.addEventListener("paste", KeyManager.Instance.paste as any);
- document.addEventListener("dash", (e: any) => { // event used by chrome plugin to tell Dash which document to focus on
+ document.addEventListener("dash", (e: any) => { // event used by chrome plugin to tell Dash which document to focus on
const id = FormattedTextBox.GetDocFromUrl(e.detail);
DocServer.GetRefField(id).then(doc => {
if (doc instanceof Doc) {
@@ -140,12 +137,21 @@ export class MainView extends React.Component {
}
}
- library.add(faTasks, faEdit, faTrashAlt, faPalette, faAngleRight, faBell, faTrash, faCamera, faExpand, faCaretDown, faCaretLeft, faCaretRight, faCaretSquareDown, faCaretSquareRight, faArrowsAltH, faPlus, faMinus,
- faTerminal, faToggleOn, faExternalLinkAlt, faLocationArrow, faSearch, faFileDownload, faStop, faCalculator, faWindowMaximize, faAddressCard, fileSolid,
- faQuestionCircle, faArrowLeft, faArrowRight, faArrowDown, faArrowUp, faBolt, faBullseye, faCaretUp, faCat, faCheck, faChevronRight, faClipboard, faClone, faCloudUploadAlt,
- faCommentAlt, faCompressArrowsAlt, faCut, faEllipsisV, faEraser, faExclamation, faFileAlt, faFileAudio, faFilePdf, faFilm, faFilter, faFont, faGlobeAsia, faHighlighter,
- faLongArrowAltRight, faMicrophone, faMousePointer, faMusic, faObjectGroup, faPause, faPen, faPenNib, faPhone, faPlay, faPortrait, faRedoAlt, faStamp, faStickyNote, faTrashAlt, faAngleRight, faBell,
- faThumbtack, faTree, faTv, faUndoAlt, faVideo, faAsterisk, faBrain, faImage, faPaintBrush, faTimes, faEye, faArrowsAlt, faQuoteLeft, faSortAmountDown, faAlignLeft, faAlignCenter, faAlignRight);
+ library.add(fa.faEdit, fa.faTrash, fa.faTrashAlt, fa.faShare, fa.faDownload, fa.faExpandArrowsAlt, fa.faLayerGroup, fa.faExternalLinkAlt,
+ fa.faSquare, fa.faConciergeBell, fa.faWindowRestore, fa.faFolder, fa.faMapPin, fa.faFingerprint, fa.faCrosshairs, fa.faDesktop, fa.faUnlock,
+ fa.faLock, fa.faLaptopCode, fa.faMale, fa.faCopy, fa.faHandPointRight, fa.faCompass, fa.faSnowflake, fa.faMicrophone, fa.faKeyboard,
+ fa.faQuestion, fa.faTasks, fa.faPalette, fa.faAngleRight, fa.faBell, fa.faCamera, fa.faExpand, fa.faCaretDown, fa.faCaretLeft, fa.faCaretRight,
+ fa.faCaretSquareDown, fa.faCaretSquareRight, fa.faArrowsAltH, fa.faPlus, fa.faMinus, fa.faTerminal, fa.faToggleOn, fa.faFile, fa.faLocationArrow,
+ fa.faSearch, fa.faFileDownload, fa.faStop, fa.faCalculator, fa.faWindowMaximize, fa.faAddressCard, fa.faQuestionCircle, fa.faArrowLeft,
+ fa.faArrowRight, fa.faArrowDown, fa.faArrowUp, fa.faBolt, fa.faBullseye, fa.faCaretUp, fa.faCat, fa.faCheck, fa.faChevronRight, fa.faClipboard,
+ fa.faClone, fa.faCloudUploadAlt, fa.faCommentAlt, fa.faCompressArrowsAlt, fa.faCut, fa.faEllipsisV, fa.faEraser, fa.faExclamation, fa.faFileAlt,
+ fa.faFileAudio, fa.faFilePdf, fa.faFilm, fa.faFilter, fa.faFont, fa.faGlobeAsia, fa.faHighlighter, fa.faLongArrowAltRight, fa.faMousePointer,
+ fa.faMusic, fa.faObjectGroup, fa.faPause, fa.faPen, fa.faPenNib, fa.faPhone, fa.faPlay, fa.faPortrait, fa.faRedoAlt, fa.faStamp, fa.faStickyNote,
+ fa.faTimesCircle, fa.faThumbtack, fa.faTree, fa.faTv, fa.faUndoAlt, fa.faVideo, fa.faAsterisk, fa.faBrain, fa.faImage, fa.faPaintBrush, fa.faTimes,
+ fa.faEye, fa.faArrowsAlt, fa.faQuoteLeft, fa.faSortAmountDown, fa.faAlignLeft, fa.faAlignCenter, fa.faAlignRight, fa.faHeading, fa.faRulerCombined,
+ fa.faFillDrip, fa.faLink, fa.faUnlink, fa.faBold, fa.faItalic, fa.faChevronLeft, fa.faUnderline, fa.faStrikethrough, fa.faSuperscript, fa.faSubscript,
+ fa.faIndent, fa.faEyeDropper, fa.faPaintRoller, fa.faBars, fa.faBrush, fa.faShapes, fa.faEllipsisH, fa.faHandPaper, fa.faMap, fa.faUser, faHireAHelper,
+ fa.faBezierCurve, fa.faCircle, fa.faLongArrowAltRight, fa.faPenFancy, fa.faAngleDoubleRight, faBuffer);
this.initEventListeners();
this.initAuthenticationRouters();
}
@@ -161,17 +167,17 @@ export class MainView extends React.Component {
if (targets && (targets.length && targets[0].className.toString() !== "timeline-menu-desc" && targets[0].className.toString() !== "timeline-menu-item" && targets[0].className.toString() !== "timeline-menu-input")) {
TimelineMenu.Instance.closeMenu();
}
- if (targets && targets.length && SearchBox.Instance._searchbarOpen){
+ if (targets && targets.length && SearchBox.Instance._searchbarOpen) {
let check = false;
- targets.forEach((thing)=>{
- if (thing.className.toString()==="collectionSchemaView-table" || thing.className.toString()==="beta"|| thing.className.toString()==="collectionSchemaView-menuOptions-wrapper") {
- check=true;
+ targets.forEach((thing) => {
+ if (thing.className.toString() === "collectionSchemaView-table" || thing.className.toString() === "beta" || thing.className.toString() === "collectionSchemaView-menuOptions-wrapper") {
+ check = true;
+ }
+ });
+ if (check === false) {
+ SearchBox.Instance.closeSearch();
}
- });
- if (check===false){
- SearchBox.Instance.closeSearch();
}
- }
});
@@ -348,7 +354,7 @@ export class MainView extends React.Component {
-
+
@computed get mainDocView() {
return <DocumentView Document={this.mainContainer!}
@@ -434,7 +440,7 @@ export class MainView extends React.Component {
doc.dockingConfig ? this.openWorkspace(doc) :
CollectionDockingView.AddRightSplit(doc, libraryPath);
}
- sidebarScreenToLocal = () => new Transform(0, RichTextMenu.Instance.Pinned ? -35 : 0, 1);
+ sidebarScreenToLocal = () => new Transform(0, (RichTextMenu.Instance.Pinned ? -35 : 0) + (CollectionMenu.Instance.Pinned ? -35 : 0), 1);
mainContainerXf = () => this.sidebarScreenToLocal().translate(0, -this._buttonBarHeight);
@computed get flyout() {
@@ -500,9 +506,6 @@ export class MainView extends React.Component {
<button className="mainView-settings" key="settings" onClick={() => SettingsManager.Instance.open()}>
<FontAwesomeIcon icon="cog" size="lg" />
</button>
- <button className="mainView-settings" key="groups" onClick={() => GroupManager.Instance.open()}>
- <FontAwesomeIcon icon="columns" size="lg" />
- </button>
</div>
</div>
{this.docButtons}
@@ -511,39 +514,43 @@ export class MainView extends React.Component {
@computed get mainContent() {
const sidebar = this.userDoc?.["tabs-panelContainer"];
+ const n = (RichTextMenu.Instance?.Pinned ? 1 : 0) + (CollectionMenu.Instance?.Pinned ? 1 : 0);
+ const height = `calc(100% - ${n * Number(ANTIMODEMENU_HEIGHT.replace("px", ""))}px)`;
return !this.userDoc || !(sidebar instanceof Doc) ? (null) : (
<div>
- <div style={{height:"32px", width:"100%", backgroundColor:"black"}}>
- {this.search}
- </div>
- <div className="mainView-mainContent" style={{
- color: this.darkScheme ? "rgb(205,205,205)" : "black",
- height: RichTextMenu.Instance?.Pinned ? `calc(100% - ${ANTIMODEMENU_HEIGHT})` : "100%"
- }} >
- <div style={{ display: "contents", flexDirection: "row", position: "relative" }}>
- <div className="mainView-flyoutContainer" onPointerLeave={this.pointerLeaveDragger} style={{ width: this.flyoutWidth }}>
- <div className="mainView-libraryHandle" onPointerDown={this.onPointerDown}
- style={{ backgroundColor: this.defaultBackgroundColors(sidebar) }}>
- <span title="library View Dragger" style={{
- width: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "100%" : "3vw",
- //height: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "100%" : "100vh",
- position: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "absolute" : "fixed",
- top: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "" : "0"
- }} />
- </div>
- <div className="mainView-libraryFlyout" style={{
- //transformOrigin: this._flyoutTranslate ? "" : "left center",
- transition: this._flyoutTranslate ? "" : "width .5s",
- //transform: `scale(${this._flyoutTranslate ? 1 : 0.8})`,
- boxShadow: this._flyoutTranslate ? "" : "rgb(156, 147, 150) 0.2vw 0.2vw 0.8vw"
- }}>
- {this.flyout}
- {this.expandButton}
+ <div style={{ height: "32px", width: "100%", backgroundColor: "black" }}>
+ {this.search}
+ </div>
+ <div className="mainView-mainContent" style={{
+ color: this.darkScheme ? "rgb(205,205,205)" : "black",
+ //change to times 2 for both pinned
+ height,
+ width: (FormatShapePane.Instance?.Pinned) ? `calc(100% - 200px)` : "100%"
+ }} >
+ <div style={{ display: "contents", flexDirection: "row", position: "relative" }}>
+ <div className="mainView-flyoutContainer" onPointerLeave={this.pointerLeaveDragger} style={{ width: this.flyoutWidth }}>
+ <div className="mainView-libraryHandle" onPointerDown={this.onPointerDown}
+ style={{ backgroundColor: this.defaultBackgroundColors(sidebar) }}>
+ <span title="library View Dragger" style={{
+ width: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "100%" : "3vw",
+ //height: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "100%" : "100vh",
+ position: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "absolute" : "fixed",
+ top: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "" : "0"
+ }} />
+ </div>
+ <div className="mainView-libraryFlyout" style={{
+ //transformOrigin: this._flyoutTranslate ? "" : "left center",
+ transition: this._flyoutTranslate ? "" : "width .5s",
+ //transform: `scale(${this._flyoutTranslate ? 1 : 0.8})`,
+ boxShadow: this._flyoutTranslate ? "" : "rgb(156, 147, 150) 0.2vw 0.2vw 0.8vw"
+ }}>
+ {this.flyout}
+ {this.expandButton}
+ </div>
</div>
+ {this.dockingContent}
</div>
- {this.dockingContent}
</div>
- </div>
</div>);
}
@@ -656,21 +663,27 @@ export class MainView extends React.Component {
<SettingsManager />
<GroupManager />
<GoogleAuthenticationManager />
+ <HypothesisAuthenticationManager />
<DocumentDecorations />
- <GestureOverlay>
- <RichTextMenu key="rich" />
- {this.mainContent}
- </GestureOverlay>
- <PreviewCursor />
+ <CollectionMenu />
+ <FormatShapePane />
+ <RichTextMenu key="rich" />
+ {LinkDescriptionPopup.descriptionPopup ? <LinkDescriptionPopup /> : null}
{DocumentLinksButton.EditLink ? <LinkMenu location={DocumentLinksButton.EditLinkLoc} docView={DocumentLinksButton.EditLink} addDocTab={DocumentLinksButton.EditLink.props.addDocTab} changeFlyout={emptyFunction} /> : (null)}
{LinkDocPreview.LinkInfo ? <LinkDocPreview location={LinkDocPreview.LinkInfo.Location} backgroundColor={this.defaultBackgroundColors}
linkDoc={LinkDocPreview.LinkInfo.linkDoc} linkSrc={LinkDocPreview.LinkInfo.linkSrc} href={LinkDocPreview.LinkInfo.href}
addDocTab={LinkDocPreview.LinkInfo.addDocTab} /> : (null)}
+ <GestureOverlay >
+ {this.mainContent}
+ </GestureOverlay>
+ <PreviewCursor />
+ <TaskCompletionBox />
<ContextMenu />
+ <FormatShapePane />
<RadialMenu />
<PDFMenu />
<MarqueeOptionsMenu />
- <InkOptionsMenu />
+
<OverlayView />
<TimelineMenu />
{this.snapLines}
diff --git a/src/client/views/MainViewModal.scss b/src/client/views/MainViewModal.scss
index f5a9ee76c..812fe540b 100644
--- a/src/client/views/MainViewModal.scss
+++ b/src/client/views/MainViewModal.scss
@@ -6,9 +6,10 @@
align-self: center;
align-content: center;
padding: 20px;
- background: gainsboro;
+ // background: gainsboro;
+ background: white;
border-radius: 10px;
- border: 3px solid black;
+ border: 0.5px solid black;
box-shadow: #00000044 5px 5px 10px;
transform: translate(-50%, -50%);
top: 50%;
diff --git a/src/client/views/MainViewModal.tsx b/src/client/views/MainViewModal.tsx
index a7bd5882d..249715511 100644
--- a/src/client/views/MainViewModal.tsx
+++ b/src/client/views/MainViewModal.tsx
@@ -1,5 +1,6 @@
import * as React from 'react';
import "./MainViewModal.scss";
+import { observer } from 'mobx-react';
export interface MainViewOverlayProps {
isDisplayed: boolean;
@@ -9,8 +10,10 @@ export interface MainViewOverlayProps {
overlayStyle?: React.CSSProperties;
dialogueBoxDisplayedOpacity?: number;
overlayDisplayedOpacity?: number;
+ closeOnExternalClick?: () => void;
}
+@observer
export default class MainViewModal extends React.Component<MainViewOverlayProps> {
render() {
@@ -18,11 +21,10 @@ export default class MainViewModal extends React.Component<MainViewOverlayProps>
const dialogueOpacity = p.dialogueBoxDisplayedOpacity || 1;
const overlayOpacity = p.overlayDisplayedOpacity || 0.4;
return !p.isDisplayed ? (null) : (
- <div style={{ pointerEvents: p.isDisplayed ? p.interactive ? "all" : "none" : "none" }}>
+ <div style={{ pointerEvents: p.isDisplayed && p.interactive ? "all" : "none" }}>
<div
className={"dialogue-box"}
style={{
- backgroundColor: "gainsboro",
borderColor: "black",
...(p.dialogueBoxStyle || {}),
opacity: p.isDisplayed ? dialogueOpacity : 0
@@ -30,6 +32,7 @@ export default class MainViewModal extends React.Component<MainViewOverlayProps>
>{p.contents}</div>
<div
className={"overlay"}
+ onClick={this.props?.closeOnExternalClick}
style={{
backgroundColor: "gainsboro",
...(p.overlayStyle || {}),
diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx
index b0752ffb2..82ec5a5b3 100644
--- a/src/client/views/MetadataEntryMenu.tsx
+++ b/src/client/views/MetadataEntryMenu.tsx
@@ -3,14 +3,14 @@ import "./MetadataEntryMenu.scss";
import { observer } from 'mobx-react';
import { observable, action, runInAction, trace, computed, IReactionDisposer, reaction } from 'mobx';
import { KeyValueBox } from './nodes/KeyValueBox';
-import { Doc, Field, DocListCastAsync } from '../../fields/Doc';
+import { Doc, Field, DocListCastAsync, DocListCast } from '../../fields/Doc';
import * as Autosuggest from 'react-autosuggest';
-import { undoBatch } from '../util/UndoManager';
+import { undoBatch, UndoManager } from '../util/UndoManager';
import { emptyFunction, emptyPath } from '../../Utils';
export type DocLike = Doc | Doc[] | Promise<Doc> | Promise<Doc[]>;
export interface MetadataEntryProps {
- docs: DocLike | (() => DocLike);
+ docs: Doc[];
onError?: () => boolean;
suggestWithFunction?: boolean;
}
@@ -38,27 +38,14 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
let field: Field | undefined | null = null;
let onProto: boolean = false;
let value: string | undefined = undefined;
- let docs = this.props.docs;
- if (typeof docs === "function") {
- if (this.props.suggestWithFunction) {
- docs = docs();
- } else {
- return;
- }
- }
- docs = await docs;
- if (docs instanceof Doc) {
- await docs[this._currentKey];
- value = Field.toKeyValueString(docs, this._currentKey);
- } else {
- for (const doc of docs) {
- const v = await doc[this._currentKey];
- onProto = onProto || !Object.keys(doc).includes(this._currentKey);
- if (field === null) {
- field = v;
- } else if (v !== field) {
- value = "multiple values";
- }
+ const docs = this.props.docs;
+ for (const doc of docs) {
+ const v = await doc[this._currentKey];
+ onProto = onProto || !Object.keys(doc).includes(this._currentKey);
+ if (field === null) {
+ field = v;
+ } else if (v !== field) {
+ value = "multiple values";
}
}
if (value === undefined) {
@@ -86,27 +73,16 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
const script = KeyValueBox.CompileKVPScript(this._currentValue);
if (!script) return;
- let doc = this.props.docs;
- if (typeof doc === "function") {
- doc = doc();
- }
- doc = await doc;
-
- let success: boolean;
- if (doc instanceof Doc) {
- success = KeyValueBox.ApplyKVPScript(doc, this._currentKey, script);
- } else {
- let childSuccess = true;
- if (this._addChildren) {
- for (const document of doc) {
- const collectionChildren = await DocListCastAsync(document.data);
- if (collectionChildren) {
- childSuccess = collectionChildren.every(c => KeyValueBox.ApplyKVPScript(c, this._currentKey, script));
- }
+ let childSuccess = true;
+ if (this._addChildren) {
+ for (const document of this.props.docs) {
+ const collectionChildren = DocListCast(document.data);
+ if (collectionChildren) {
+ childSuccess = collectionChildren.every(c => KeyValueBox.ApplyKVPScript(c, this._currentKey, script));
}
}
- success = doc.every(d => KeyValueBox.ApplyKVPScript(d, this._currentKey, script)) && childSuccess;
}
+ const success = this.props.docs.every(d => KeyValueBox.ApplyKVPScript(d, this._currentKey, script)) && childSuccess;
if (!success) {
if (this.props.onError) {
if (this.props.onError()) {
@@ -132,24 +108,12 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
}
}
- getKeySuggestions = async (value: string): Promise<string[]> => {
+ getKeySuggestions = (value: string) => {
value = value.toLowerCase();
- let docs = this.props.docs;
- if (typeof docs === "function") {
- if (this.props.suggestWithFunction) {
- docs = docs();
- } else {
- return [];
- }
- }
- docs = await docs;
- if (docs instanceof Doc) {
- return Object.keys(docs).filter(key => key.toLowerCase().startsWith(value));
- } else {
- const keys = new Set<string>();
- docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key)));
- return Array.from(keys).filter(key => key.toLowerCase().startsWith(value));
- }
+ const docs = this.props.docs;
+ const keys = new Set<string>();
+ docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key)));
+ return Array.from(keys).filter(key => key.toLowerCase().startsWith(value));
}
getSuggestionValue = (suggestion: string) => suggestion;
@@ -157,9 +121,8 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
return (null);
}
componentDidMount() {
-
this._suggestionDispser = reaction(() => this._currentKey,
- () => this.getKeySuggestions(this._currentKey).then(action((s: string[]) => this._allSuggestions = s)),
+ () => this._allSuggestions = this.getKeySuggestions(this._currentKey),
{ fireImmediately: true });
}
componentWillUnmount() {
@@ -171,19 +134,8 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
}
private get considerChildOptions() {
- let docSource = this.props.docs;
- if (typeof docSource === "function") {
- docSource = docSource();
- }
- docSource = docSource as Doc[] | Doc;
- if (docSource instanceof Doc) {
- if (docSource._viewType === undefined) {
- return (null);
- }
- } else if (Array.isArray(docSource)) {
- if (!docSource.every(doc => doc._viewType !== undefined)) {
- return null;
- }
+ if (!this.props.docs.every(doc => doc._viewType !== undefined)) {
+ return null;
}
return (
<div style={{ display: "flex" }}>
diff --git a/src/client/views/OverlayView.scss b/src/client/views/OverlayView.scss
index 26c2e0e1e..09a349012 100644
--- a/src/client/views/OverlayView.scss
+++ b/src/client/views/OverlayView.scss
@@ -3,6 +3,8 @@
overflow: hidden;
display: flex;
flex-direction: column;
+ top: 0;
+ left: 0;
}
.overlayWindow-outerDiv,
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index 37d8dd23b..5c3a8185c 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -124,7 +124,9 @@ export class OverlayView extends React.Component {
ele = <div key={Utils.GenerateGuid()} className="overlayView-wrapperDiv" style={{
transform: `translate(${options.x}px, ${options.y}px)`,
width: options.width,
- height: options.height
+ height: options.height,
+ top: 0,
+ left: 0
}}>{ele}</div>;
this._elements.push(ele);
return remove;
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index 6583589f3..b4116e980 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -69,11 +69,11 @@ export class PreviewCursor extends React.Component<{}> {
const list: Doc[] = [];
let first: Doc | undefined;
- docids.map((did, i) => i && DocServer.GetRefField(did).then(doc => {
+ docids.map((did, i) => i && DocServer.GetRefField(did).then(async doc => {
count++;
if (doc instanceof Doc) {
i === 1 && (first = doc);
- const alias = clone ? Doc.MakeClone(doc) : doc;
+ const alias = clone ? (await Doc.MakeClone(doc)).clone : doc;
const deltaX = NumCast(doc.x) - NumCast(first!.x) - ptx;
const deltaY = NumCast(doc.y) - NumCast(first!.y) - pty;
alias.x = newPoint[0] + deltaX;
diff --git a/src/client/views/RecommendationsBox.scss b/src/client/views/RecommendationsBox.scss
deleted file mode 100644
index 7d89042a4..000000000
--- a/src/client/views/RecommendationsBox.scss
+++ /dev/null
@@ -1,69 +0,0 @@
-@import "globalCssVariables";
-
-.rec-content *{
- display: inline-block;
- margin: auto;
- width: 50;
- height: 150px;
- border: 1px dashed grey;
- padding: 10px 10px;
-}
-
-.rec-content {
- float: left;
- width: inherit;
- align-content: center;
-}
-
-.rec-scroll {
- overflow-y: scroll;
- overflow-x: hidden;
- position: absolute;
- pointer-events: all;
- // display: flex;
- z-index: 10000;
- box-shadow: gray 0.2vw 0.2vw 0.4vw;
- // flex-direction: column;
- background: whitesmoke;
- padding-bottom: 10px;
- padding-top: 20px;
- // border-radius: 15px;
- border: solid #BBBBBBBB 1px;
- width: 100%;
- text-align: center;
- // max-height: 250px;
- height: 100%;
- text-transform: uppercase;
- color: grey;
- letter-spacing: 2px;
-}
-
-.content {
- padding: 10px;
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: center;
-}
-
-.image-background {
- pointer-events: none;
- background-color: transparent;
- width: 50%;
- text-align: center;
- margin-left: 5px;
-}
-
-// bcz: UGH!! Can't have global settings like this!!!
-// img{
-// width: 100%;
-// height: 100%;
-// }
-
-.score {
- // margin-left: 15px;
- width: 50%;
- height: 100%;
- text-align: center;
- margin-left: 10px;
-}
diff --git a/src/client/views/RecommendationsBox.tsx b/src/client/views/RecommendationsBox.tsx
deleted file mode 100644
index cdde32c21..000000000
--- a/src/client/views/RecommendationsBox.tsx
+++ /dev/null
@@ -1,201 +0,0 @@
-import { observer } from "mobx-react";
-import React = require("react");
-import { observable, action, computed, runInAction } from "mobx";
-import Measure from "react-measure";
-import "./RecommendationsBox.scss";
-import { Doc, DocListCast, WidthSym, HeightSym } from "../../fields/Doc";
-import { DocumentIcon } from "./nodes/DocumentIcon";
-import { StrCast, NumCast } from "../../fields/Types";
-import { returnFalse, emptyFunction, returnEmptyString, returnOne, emptyPath, returnZero, returnEmptyFilter } from "../../Utils";
-import { Transform } from "../util/Transform";
-import { ObjectField } from "../../fields/ObjectField";
-import { DocumentView } from "./nodes/DocumentView";
-import { DocumentType } from '../documents/DocumentTypes';
-import { ClientRecommender } from "../ClientRecommender";
-import { DocServer } from "../DocServer";
-import { Id } from "../../fields/FieldSymbols";
-import { FieldView, FieldViewProps } from "./nodes/FieldView";
-import { DocumentManager } from "../util/DocumentManager";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { library } from "@fortawesome/fontawesome-svg-core";
-import { faBullseye, faLink } from "@fortawesome/free-solid-svg-icons";
-import { DocUtils } from "../documents/Documents";
-
-export interface RecProps {
- documents: { preview: Doc, similarity: number }[];
- node: Doc;
-}
-
-library.add(faBullseye, faLink);
-
-@observer
-export class RecommendationsBox extends React.Component<FieldViewProps> {
-
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(RecommendationsBox, fieldKey); }
-
- // @observable private _display: boolean = false;
- @observable private _pageX: number = 0;
- @observable private _pageY: number = 0;
- @observable private _width: number = 0;
- @observable private _height: number = 0;
- @observable.shallow private _docViews: JSX.Element[] = [];
- // @observable private _documents: { preview: Doc, score: number }[] = [];
- private previewDocs: Doc[] = [];
-
- constructor(props: FieldViewProps) {
- super(props);
- }
-
- @action
- private DocumentIcon(doc: Doc) {
- const layoutresult = StrCast(doc.type);
- let renderDoc = doc;
- //let box: number[] = [];
- if (layoutresult.indexOf(DocumentType.COL) !== -1) {
- renderDoc = Doc.MakeDelegate(renderDoc);
- }
- const returnXDimension = () => 150;
- const returnYDimension = () => 150;
- const scale = () => returnXDimension() / NumCast(renderDoc._nativeWidth, returnXDimension());
- //let scale = () => 1;
- const newRenderDoc = Doc.MakeAlias(renderDoc); /// newRenderDoc -> renderDoc -> render"data"Doc -> TextProt
- newRenderDoc.height = NumCast(this.props.Document.documentIconHeight);
- newRenderDoc.autoHeight = false;
- const docview = <div>
- <DocumentView
- fitToBox={StrCast(doc.type).indexOf(DocumentType.COL) !== -1}
- Document={newRenderDoc}
- addDocument={returnFalse}
- LibraryPath={emptyPath}
- removeDocument={returnFalse}
- rootSelected={returnFalse}
- ScreenToLocalTransform={Transform.Identity}
- addDocTab={returnFalse}
- pinToPres={returnFalse}
- renderDepth={1}
- NativeHeight={returnZero}
- NativeWidth={returnZero}
- PanelWidth={returnXDimension}
- PanelHeight={returnYDimension}
- focus={emptyFunction}
- backgroundColor={returnEmptyString}
- parentActive={returnFalse}
- whenActiveChanged={returnFalse}
- bringToFront={emptyFunction}
- docFilters={returnEmptyFilter}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
- ContentScaling={scale}
- />
- </div>;
- return docview;
-
- }
-
- // @action
- // closeMenu = () => {
- // this._display = false;
- // this.previewDocs.forEach(doc => DocServer.DeleteDocument(doc[Id]));
- // this.previewDocs = [];
- // }
-
- // @action
- // resetDocuments = () => {
- // this._documents = [];
- // }
-
- // @action
- // displayRecommendations(x: number, y: number) {
- // this._pageX = x;
- // this._pageY = y;
- // this._display = true;
- // }
-
- static readonly buffer = 20;
-
- // get pageX() {
- // const x = this._pageX;
- // if (x < 0) {
- // return 0;
- // }
- // const width = this._width;
- // if (x + width > window.innerWidth - RecommendationsBox.buffer) {
- // return window.innerWidth - RecommendationsBox.buffer - width;
- // }
- // return x;
- // }
-
- // get pageY() {
- // const y = this._pageY;
- // if (y < 0) {
- // return 0;
- // }
- // const height = this._height;
- // if (y + height > window.innerHeight - RecommendationsBox.buffer) {
- // return window.innerHeight - RecommendationsBox.buffer - height;
- // }
- // return y;
- // }
-
- // get createDocViews() {
- // return DocListCast(this.props.Document.data).map(doc => {
- // return (
- // <div className="content">
- // <span style={{ height: NumCast(this.props.Document.documentIconHeight) }} className="image-background">
- // {this.DocumentIcon(doc)}
- // </span>
- // <span className="score">{NumCast(doc.score).toFixed(4)}</span>
- // <div style={{ marginRight: 50 }} onClick={() => DocumentManager.Instance.jumpToDocument(doc, false)}>
- // <FontAwesomeIcon className="documentdecorations-icon" icon={"bullseye"} size="sm" />
- // </div>
- // <div style={{ marginRight: 50 }} onClick={() => DocUtils.MakeLink({ doc: this.props.Document.sourceDoc as Doc }, { doc: doc }, "User Selected Link", "Generated from Recommender", undefined)}>
- // <FontAwesomeIcon className="documentdecorations-icon" icon={"link"} size="sm" />
- // </div>
- // </div>
- // );
- // });
- // }
-
- componentDidMount() { //TODO: invoking a computedFn from outside an reactive context won't be memoized, unless keepAlive is set
- runInAction(() => {
- if (this._docViews.length === 0) {
- this._docViews = DocListCast(this.props.Document.data).map(doc => {
- return (
- <div className="content">
- <span style={{ height: NumCast(this.props.Document.documentIconHeight) }} className="image-background">
- {this.DocumentIcon(doc)}
- </span>
- <span className="score">{NumCast(doc.score).toFixed(4)}</span>
- <div style={{ marginRight: 50 }} onClick={() => DocumentManager.Instance.jumpToDocument(doc, false)}>
- <FontAwesomeIcon className="documentdecorations-icon" icon={"bullseye"} size="sm" />
- </div>
- <div style={{ marginRight: 50 }} onClick={() => DocUtils.MakeLink({ doc: this.props.Document.sourceDoc as Doc }, { doc: doc }, "Recommender", undefined)}>
- <FontAwesomeIcon className="documentdecorations-icon" icon={"link"} size="sm" />
- </div>
- </div>
- );
- });
- }
- });
- }
-
- render() { //TODO: Invariant violation: max depth exceeded error. Occurs when images are rendered.
- // if (!this._display) {
- // return null;
- // }
- // let style = { left: this.pageX, top: this.pageY };
- //const transform = "translate(" + (NumCast(this.props.node.x) + 350) + "px, " + NumCast(this.props.node.y) + "px"
- let title = StrCast((this.props.Document.sourceDoc as Doc).title);
- if (title.length > 15) {
- title = title.substring(0, 15) + "...";
- }
- return (
- <div className="rec-scroll">
- <p>Recommendations for "{title}"</p>
- {this._docViews}
- </div>
- );
- }
- //
- //
-} \ No newline at end of file
diff --git a/src/client/views/ScriptBox.tsx b/src/client/views/ScriptBox.tsx
index 888f84dfa..2c185be86 100644
--- a/src/client/views/ScriptBox.tsx
+++ b/src/client/views/ScriptBox.tsx
@@ -39,7 +39,7 @@ export class ScriptBox extends React.Component<ScriptBoxProps> {
@action
onError = (error: string) => {
- console.log(error);
+ console.log("ScriptBox: " + error);
}
overlayDisposer?: () => void;
diff --git a/src/client/views/SearchDocBox.tsx b/src/client/views/SearchDocBox.tsx
index e038d8213..084f952a3 100644
--- a/src/client/views/SearchDocBox.tsx
+++ b/src/client/views/SearchDocBox.tsx
@@ -60,8 +60,6 @@ export class SearchDocBox extends React.Component<FieldViewProps> {
componentDidMount() {
runInAction(() => {
- console.log("didit"
- );
this.query = StrCast(this.props.Document.searchText);
this.content = (Docs.Create.TreeDocument(DocListCast(Doc.GetProto(this.props.Document).data), { _width: 200, _height: 400, _chromeStatus: "disabled", title: `Search Docs:` + this.query }));
@@ -83,12 +81,9 @@ export class SearchDocBox extends React.Component<FieldViewProps> {
if (newKey.length > 1) {
const newdocs = await this.getAllResults(this.query);
const things = newdocs.docs;
- console.log(things);
- console.log(this.content);
runInAction(() => {
this.content = Docs.Create.TreeDocument(things, { _width: 200, _height: 400, _chromeStatus: "disabled", title: `Search Docs:` + this.query });
});
- console.log(this.content);
}
@@ -150,12 +145,9 @@ export class SearchDocBox extends React.Component<FieldViewProps> {
}
enter = async (e: React.KeyboardEvent) => {
- console.log(e.key);
if (e.key === "Enter") {
const newdocs = await this.getAllResults(this.query);
- console.log(newdocs.docs);
this.content = Docs.Create.TreeDocument(newdocs.docs, { _width: 200, _height: 400, _chromeStatus: "disabled", title: `Search Docs: "Results"` });
-
}
}
@@ -256,7 +248,6 @@ export class SearchDocBox extends React.Component<FieldViewProps> {
startDragCollection = async () => {
const res = await this.getAllResults(FilterBox.Instance.getFinalQuery(this._searchString));
const filtered = FilterBox.Instance.filterDocsByType(res.docs);
- // console.log(this._results)
const docs = filtered.map(doc => {
const isProto = Doc.GetT(doc, "isPrototype", "boolean", true);
if (isProto) {
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index 916e631d0..eb20fc257 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -63,14 +63,6 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
this.props.docViews.map(dv => dv.switchViews(false, "layout"));
}
- toggleFloat = (e: React.ChangeEvent<HTMLInputElement>): void => {
- SelectionManager.DeselectAll();
- const topDocView = this.props.docViews[0];
- const ex = e.target.getBoundingClientRect().left;
- const ey = e.target.getBoundingClientRect().top;
- DocumentView.FloatDoc(topDocView, ex, ey);
- }
-
toggleAudio = (e: React.ChangeEvent<HTMLInputElement>): void => {
this.props.docViews.map(dv => dv.props.Document._showAudio = e.target.checked);
}
@@ -108,8 +100,9 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
return100 = () => 100;
@computed get scriptField() {
- return ScriptField.MakeScript("docs.map(d => switchView(d, this))", { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name, firstDoc: Doc.name },
+ const script = ScriptField.MakeScript("docs.map(d => switchView(d, this))", { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name, firstDoc: Doc.name },
{ docs: new List<Doc>(this.props.docViews.map(dv => dv.props.Document)) });
+ return script ? () => script : undefined;
}
templateIsUsed = (selDoc: Doc, templateDoc: Doc) => {
const template = StrCast(templateDoc.dragFactory ? Cast(templateDoc.dragFactory, Doc, null)?.title : templateDoc.title);
@@ -126,7 +119,6 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
this.props.templates.forEach((checked, template) =>
templateMenu.push(<TemplateToggle key={template.Name} template={template} checked={checked} toggle={this.toggleTemplate} />));
templateMenu.push(<OtherToggle key={"audio"} name={"Audio"} checked={firstDoc._showAudio ? true : false} toggle={this.toggleAudio} />);
- templateMenu.push(<OtherToggle key={"float"} name={"Float"} checked={firstDoc.z ? true : false} toggle={this.toggleFloat} />);
templateMenu.push(<OtherToggle key={"chrome"} name={"Chrome"} checked={layout._chromeStatus !== "disabled"} toggle={this.toggleChrome} />);
templateMenu.push(<OtherToggle key={"default"} name={"Default"} checked={templateName === "layout"} toggle={this.toggleDefault} />);
addedTypes.concat(noteTypes).map(template => template.treeViewChecked = this.templateIsUsed(firstDoc, template));
@@ -142,8 +134,8 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
ContainingCollectionView={undefined}
docFilters={returnEmptyFilter}
rootSelected={returnFalse}
- onCheckedClick={this.scriptField!}
- onChildClick={this.scriptField!}
+ onCheckedClick={this.scriptField}
+ onChildClick={this.scriptField}
LibraryPath={emptyPath}
dropAction={undefined}
active={returnTrue}
diff --git a/src/client/views/Touchable.tsx b/src/client/views/Touchable.tsx
index 5e48d5ffb..bb9e108cb 100644
--- a/src/client/views/Touchable.tsx
+++ b/src/client/views/Touchable.tsx
@@ -12,7 +12,7 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
private holdEndDisposer?: InteractionUtils.MultiTouchEventDisposer;
- protected abstract multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
+ protected abstract _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
protected _touchDrag: boolean = false;
protected prevPoints: Map<number, React.Touch> = new Map<number, React.Touch>();
@@ -54,7 +54,6 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
}
});
- // console.log(ptsToDelete.length);
ptsToDelete.forEach(pt => this.prevPoints.delete(pt));
if (this.prevPoints.size) {
@@ -86,7 +85,6 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
// if we're not actually moving a lot, don't consider it as dragging yet
if (!InteractionUtils.IsDragging(this.prevPoints, myTouches, 5) && !this._touchDrag) return;
this._touchDrag = true;
- // console.log(myTouches.length);
switch (myTouches.length) {
case 1:
this.handle1PointerMove(te, me);
@@ -107,7 +105,6 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
@action
protected onTouchEnd = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => {
- // console.log(InteractionUtils.GetMyTargetTouches(e, this.prevPoints).length + " up");
// remove all the touches associated with the event
const te = me.touchEvent;
for (const pt of me.changedTouches) {
diff --git a/src/client/views/animationtimeline/Keyframe.tsx b/src/client/views/animationtimeline/Keyframe.tsx
index 3a7182a94..1b81c544a 100644
--- a/src/client/views/animationtimeline/Keyframe.tsx
+++ b/src/client/views/animationtimeline/Keyframe.tsx
@@ -527,10 +527,6 @@ export class Keyframe extends React.Component<IProps> {
*/
//154, 206, 223
render() {
- trace();
- console.log(this.props.RegionData.position);
- console.log(this.regiondata.position);
- console.log(this.pixelPosition);
return (
<div className="bar" ref={this._bar} style={{
transform: `translate(${this.pixelPosition}px)`,
diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx
index f54bd3aff..df828897e 100644
--- a/src/client/views/animationtimeline/Timeline.tsx
+++ b/src/client/views/animationtimeline/Timeline.tsx
@@ -385,10 +385,10 @@ export class Timeline extends React.Component<FieldViewProps> {
const ttime = `Total: ${this.toReadTime(this._time)}`;
return (
<div style={{ flexDirection: "column" }}>
- <div className="animation-text" style={{ fontSize: "10px", width: "100%", display: !this.props.Document.isATOn ? "block" : "none" }}>
+ <div className="animation-text" style={{ fontSize: "10pt", width: "100%", display: !this.props.Document.isATOn ? "block" : "none" }}>
{ctime}
</div>
- <div className="animation-text" style={{ fontSize: "10px", width: "100%", display: !this.props.Document.isATOn ? "block" : "none" }}>
+ <div className="animation-text" style={{ fontSize: "10pt", width: "100%", display: !this.props.Document.isATOn ? "block" : "none" }}>
{ttime}
</div>
</div>
@@ -466,7 +466,6 @@ export class Timeline extends React.Component<FieldViewProps> {
//TODO: remove undefineds and duplicates
}
});
- // console.log(longestTime);
return longestTime;
}
diff --git a/src/client/views/animationtimeline/Track.tsx b/src/client/views/animationtimeline/Track.tsx
index 4987006e7..25c2e68e7 100644
--- a/src/client/views/animationtimeline/Track.tsx
+++ b/src/client/views/animationtimeline/Track.tsx
@@ -134,7 +134,6 @@ export class Track extends React.Component<IProps> {
autoCreateKeyframe = () => {
const objects = this.objectWhitelist.map(key => this.props.node[key]);
intercept(this.props.node, change => {
- console.log(change);
return change;
});
return reaction(() => {
@@ -174,7 +173,6 @@ export class Track extends React.Component<IProps> {
if (regiondata) {
this.props.node.hidden = false;
// if (!this._autoKfReaction) {
- // // console.log("creating another reaction");
// // this._autoKfReaction = this.autoCreateKeyframe();
// }
this.timeChange();
@@ -204,7 +202,6 @@ export class Track extends React.Component<IProps> {
}
});
} else {
- console.log("reverting state");
//this.revertState();
}
});
@@ -229,7 +226,6 @@ export class Track extends React.Component<IProps> {
const rightkf: (Doc | undefined) = await KeyframeFunc.calcMinRight(regiondata, this.time); //right keyframe, if it exists
const currentkf: (Doc | undefined) = await this.calcCurrent(regiondata); //if the scrubber is on top of the keyframe
if (currentkf) {
- console.log("is current");
await this.applyKeys(currentkf);
this.saveStateKf = currentkf;
this.saveStateRegion = regiondata;
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index 1344b70f4..0f3b6f212 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -38,15 +38,15 @@ export class CollectionCarousel3DView extends CollectionSubView(Carousel3DDocume
panelWidth = () => this.props.PanelWidth() / 3;
panelHeight = () => this.props.PanelHeight() * 0.6;
+ onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
@computed get content() {
const currentIndex = NumCast(this.layoutDoc._itemIndex);
const displayDoc = (childPair: { layout: Doc, data: Doc }) => {
+ const script = ScriptField.MakeScript("child._showCaption = 'caption'", { child: Doc.name }, { child: childPair.layout });
+ const onChildClick = script && (() => script);
return <ContentFittingDocumentView {...this.props}
- onDoubleClick={ScriptCast(this.layoutDoc.onChildDoubleClick)}
- onClick={ScriptField.MakeScript(
- "child._showCaption = 'caption'",
- { child: Doc.name },
- { child: childPair.layout })}
+ onDoubleClick={this.onChildDoubleClick}
+ onClick={onChildClick}
renderDepth={this.props.renderDepth + 1}
LayoutTemplate={this.props.ChildLayoutTemplate}
LayoutTemplateString={this.props.ChildLayoutString}
@@ -86,7 +86,6 @@ export class CollectionCarousel3DView extends CollectionSubView(Carousel3DDocume
interval?: number;
startAutoScroll = (direction: number) => {
this.interval = window.setInterval(() => {
- console.log(this.interval, this.scrollSpeed);
this.changeSlide(direction);
}, this.scrollSpeed);
}
@@ -108,29 +107,16 @@ export class CollectionCarousel3DView extends CollectionSubView(Carousel3DDocume
}, 1500);
}
- 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()) {
- ContextMenu.Instance.addItem({
- description: "Make Hero Image", event: () => {
- const index = NumCast(this.layoutDoc._itemIndex);
- (this.dataDoc || Doc.GetProto(this.props.Document)).hero = ObjectField.MakeCopy(this.childLayoutPairs[index].layout.data as ObjectField);
- }, icon: "plus"
- });
- }
- }
_downX = 0;
_downY = 0;
onPointerDown = (e: React.PointerEvent) => {
this._downX = e.clientX;
this._downY = e.clientY;
- console.log("CAROUSEL down");
document.addEventListener("pointerup", this.onpointerup);
}
private _lastTap: number = 0;
private _doubleTap = false;
onpointerup = (e: PointerEvent) => {
- console.log("CAROUSEL up");
this._doubleTap = (Date.now() - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2);
this._lastTap = Date.now();
}
@@ -184,7 +170,7 @@ export class CollectionCarousel3DView extends CollectionSubView(Carousel3DDocume
const index = NumCast(this.layoutDoc._itemIndex);
const translateX = (1 - index) / this.childLayoutPairs.length * 100;
- return <div className="collectionCarousel3DView-outer" onClick={this.onClick} onPointerDown={this.onPointerDown} ref={this.createDashEventsTarget} onContextMenu={this.onContextMenu}>
+ return <div className="collectionCarousel3DView-outer" onClick={this.onClick} onPointerDown={this.onPointerDown} ref={this.createDashEventsTarget}>
<div className="carousel-wrapper" style={{ transform: `translateX(calc(${translateX}%` }}>
{this.content}
</div>
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index f65a89422..27aea4b99 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -14,6 +14,7 @@ import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import { ContextMenu } from '../ContextMenu';
import { ObjectField } from '../../../fields/ObjectField';
import { returnFalse } from '../../../Utils';
+import { ScriptField } from '../../../fields/ScriptField';
type CarouselDocument = makeInterface<[typeof documentSchema, typeof collectionSchema]>;
const CarouselDocument = makeInterface(documentSchema, collectionSchema);
@@ -40,14 +41,16 @@ export class CollectionCarouselView extends CollectionSubView(CarouselDocument)
this.layoutDoc._itemIndex = (NumCast(this.layoutDoc._itemIndex) - 1 + this.childLayoutPairs.length) % this.childLayoutPairs.length;
}
panelHeight = () => this.props.PanelHeight() - 50;
+ onContentDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
+ onContentClick = () => ScriptCast(this.layoutDoc.onChildClick);
@computed get content() {
const index = NumCast(this.layoutDoc._itemIndex);
return !(this.childLayoutPairs?.[index]?.layout instanceof Doc) ? (null) :
<>
<div className="collectionCarouselView-image" key="image">
<ContentFittingDocumentView {...this.props}
- onDoubleClick={ScriptCast(this.layoutDoc.onChildDoubleClick)}
- onClick={ScriptCast(this.layoutDoc.onChildClick)}
+ onDoubleClick={this.onContentDoubleClick}
+ onClick={this.onContentClick}
renderDepth={this.props.renderDepth + 1}
LayoutTemplate={this.props.ChildLayoutTemplate}
LayoutTemplateString={this.props.ChildLayoutString}
@@ -83,30 +86,16 @@ export class CollectionCarouselView extends CollectionSubView(CarouselDocument)
</>;
}
-
- 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()) {
- ContextMenu.Instance.addItem({
- description: "Make Hero Image", event: () => {
- const index = NumCast(this.layoutDoc._itemIndex);
- (this.dataDoc || Doc.GetProto(this.props.Document)).hero = ObjectField.MakeCopy(this.childLayoutPairs[index].layout.data as ObjectField);
- }, icon: "plus"
- });
- }
- }
_downX = 0;
_downY = 0;
onPointerDown = (e: React.PointerEvent) => {
this._downX = e.clientX;
this._downY = e.clientY;
- console.log("CAROUSEL down");
document.addEventListener("pointerup", this.onpointerup);
}
private _lastTap: number = 0;
private _doubleTap = false;
onpointerup = (e: PointerEvent) => {
- console.log("CAROUSEL up");
this._doubleTap = (Date.now() - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2);
this._lastTap = Date.now();
}
@@ -119,7 +108,7 @@ export class CollectionCarouselView extends CollectionSubView(CarouselDocument)
}
render() {
- return <div className="collectionCarouselView-outer" onClick={this.onClick} onPointerDown={this.onPointerDown} ref={this.createDashEventsTarget} onContextMenu={this.onContextMenu}>
+ return <div className="collectionCarouselView-outer" onClick={this.onClick} onPointerDown={this.onPointerDown} ref={this.createDashEventsTarget}>
{this.content}
{this.props.Document._chromeStatus !== "replaced" ? this.buttons : (null)}
</div>;
diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss
index 18d642510..1895c06a1 100644
--- a/src/client/views/collections/CollectionDockingView.scss
+++ b/src/client/views/collections/CollectionDockingView.scss
@@ -1,5 +1,23 @@
@import "../../views/globalCssVariables.scss";
+.miniMap {
+ position: absolute;
+ overflow: hidden;
+ right: 10;
+ bottom: 10;
+ border: solid 1px;
+ box-shadow: black 0.4vw 0.4vw 0.8vw;
+
+ .miniOverlay {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ .miniThumb {
+ background: #25252525;
+ position: absolute;
+ }
+ }
+}
.lm_title {
margin-top: 3px;
border-radius: 5px;
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 79c577b6d..4952dedc9 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -11,7 +11,7 @@ import { Id } from '../../../fields/FieldSymbols';
import { FieldId } from "../../../fields/RefField";
import { Cast, NumCast, StrCast } from "../../../fields/Types";
import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, returnOne, returnTrue, Utils, returnZero, returnEmptyFilter, setupMoveUpEvents, returnFalse } from "../../../Utils";
+import { emptyFunction, returnOne, returnTrue, Utils, returnZero, returnEmptyFilter, setupMoveUpEvents, returnFalse, emptyPath, aggregateBounds } from "../../../Utils";
import { DocServer } from "../../DocServer";
import { Docs } from '../../documents/Documents';
import { DocumentManager } from '../../util/DocumentManager';
@@ -28,6 +28,9 @@ import { DockingViewButtonSelector } from './ParentDocumentSelector';
import React = require("react");
import { CollectionViewType } from './CollectionView';
import { SnappingManager } from '../../util/SnappingManager';
+import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
+import { listSpec } from '../../../fields/Schema';
+import { clamp } from 'lodash';
const _global = (window /* browser */ || global /* node */) as any;
@observer
@@ -390,6 +393,12 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
reactionDisposer?: Lambda;
componentDidMount: () => void = () => {
if (this._containerRef.current) {
+ const observer = new _global.ResizeObserver(action((entries: any) => {
+ for (const entry of entries) {
+ this.onResize(null as any);
+ }
+ }));
+ observer.observe(this._containerRef.current);
this.reactionDisposer = reaction(
() => this.props.Document.dockingConfig,
() => {
@@ -430,7 +439,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
const cur = this._containerRef.current;
// bcz: since GoldenLayout isn't a React component itself, we need to notify it to resize when its document container's size has changed
- this._goldenLayout && this._goldenLayout.updateSize(cur!.getBoundingClientRect().width, cur!.getBoundingClientRect().height);
+ this._goldenLayout?.updateSize(cur!.getBoundingClientRect().width, cur!.getBoundingClientRect().height);
}
@action
@@ -586,7 +595,6 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
stackCreated = (stack: any) => {
//stack.header.controlsContainer.find('.lm_popout').hide();
- stack.header.element[0].style.backgroundColor = DocServer.Control.isReadOnly() ? "#228540" : undefined;
stack.header.element.on('mousedown', (e: any) => {
if (e.target === stack.header.element[0] && e.button === 1) {
this.AddTab(stack, Docs.Create.FreeformDocument([], { _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), title: "Untitled Collection" }));
@@ -646,16 +654,8 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
if (this.props.renderDepth > 0) {
return <div style={{ width: "100%", height: "100%" }}>Nested workspaces can't be rendered</div>;
}
- return (
- <Measure offset onResize={this.onResize}>
- {({ measureRef }) =>
- <div ref={measureRef}>
- <div className="collectiondockingview-container" id="menuContainer"
- onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef} />
- </div>
- }
- </Measure>
- );
+ return <div className="collectiondockingview-container" id="menuContainer"
+ onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef} />;
}
}
@@ -664,7 +664,6 @@ interface DockedFrameProps {
documentId: FieldId;
glContainer: any;
libraryPath: (FieldId[]);
- backgroundColor?: (doc: Doc) => string | undefined;
//collectionDockingView: CollectionDockingView
}
@observer
@@ -818,35 +817,109 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
}
}
+ @computed get renderContentBounds() {
+ const bounds = this._document ? Cast(this._document._renderContentBounds, listSpec("number"), [0, 0, this.returnMiniSize(), this.returnMiniSize()]) : [0, 0, 0, 0];
+ const xbounds = bounds[2] - bounds[0];
+ const ybounds = bounds[3] - bounds[1];
+ const dim = Math.max(xbounds, ybounds);
+ return { l: bounds[0] + xbounds / 2 - dim / 2, t: bounds[1] + ybounds / 2 - dim / 2, cx: bounds[0] + xbounds / 2, cy: bounds[1] + ybounds / 2, dim };
+ }
+ @computed get miniLeft() { return 50 + (NumCast(this._document?._panX) - this.renderContentBounds.cx) / this.renderContentBounds.dim * 100 - this.miniWidth / 2; }
+ @computed get miniTop() { return 50 + (NumCast(this._document?._panY) - this.renderContentBounds.cy) / this.renderContentBounds.dim * 100 - this.miniHeight / 2; }
+ @computed get miniWidth() { return this.panelWidth() / NumCast(this._document?._viewScale, 1) / this.renderContentBounds.dim * 100; }
+ @computed get miniHeight() { return this.panelHeight() / NumCast(this._document?._viewScale, 1) / this.renderContentBounds.dim * 100; }
+ childLayoutTemplate = () => Cast(this._document?.childLayoutTemplate, Doc, null);
+ returnMiniSize = () => NumCast(this._document?._miniMapSize, 150);
+ miniDown = (e: React.PointerEvent) => {
+ setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => {
+ this._document!._panX = clamp(NumCast(this._document!._panX) + delta[0] / this.returnMiniSize() * this.renderContentBounds.dim, this.renderContentBounds.l, this.renderContentBounds.l + this.renderContentBounds.dim);
+ this._document!._panY = clamp(NumCast(this._document!._panY) + delta[1] / this.returnMiniSize() * this.renderContentBounds.dim, this.renderContentBounds.t, this.renderContentBounds.t + this.renderContentBounds.dim);
+ return false;
+ }), emptyFunction, emptyFunction);
+ }
+ renderMiniMap() {
+ return <div className="miniMap" style={{
+ width: this.returnMiniSize(), height: this.returnMiniSize(), background: StrCast(this._document!._backgroundColor,
+ StrCast(this._document!.backgroundColor, CollectionDockingView.Instance.props.backgroundColor?.(this._document!))),
+ }}>
+ <CollectionFreeFormView
+ Document={this._document!}
+ LibraryPath={emptyPath}
+ CollectionView={undefined}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
+ ChildLayoutTemplate={this.childLayoutTemplate} // bcz: Ugh .. should probably be rendering a CollectionView or the minimap should be part of the collectionFreeFormView to avoid havin to set stuff like this.
+ noOverlay={true} // don't render overlay Docs since they won't scale
+ active={returnTrue}
+ select={emptyFunction}
+ dropAction={undefined}
+ isSelected={returnFalse}
+ dontRegisterView={true}
+ annotationsKey={""}
+ fieldKey={Doc.LayoutFieldKey(this._document!)}
+ bringToFront={emptyFunction}
+ rootSelected={returnTrue}
+ addDocument={returnFalse}
+ moveDocument={returnFalse}
+ removeDocument={returnFalse}
+ ContentScaling={returnOne}
+ PanelWidth={this.returnMiniSize}
+ PanelHeight={this.returnMiniSize}
+ NativeHeight={returnZero}
+ NativeWidth={returnZero}
+ ScreenToLocalTransform={this.ScreenToLocalTransform}
+ renderDepth={0}
+ whenActiveChanged={emptyFunction}
+ focus={emptyFunction}
+ backgroundColor={CollectionDockingView.Instance.props.backgroundColor}
+ addDocTab={this.addDocTab}
+ pinToPres={DockedFrameRenderer.PinDoc}
+ docFilters={returnEmptyFilter}
+ fitToBox={true}
+ />
+ <div className="miniOverlay" onPointerDown={this.miniDown} >
+ <div className="miniThumb" style={{
+ width: `${this.miniWidth}%`,
+ height: `${this.miniHeight}%`,
+ left: `${this.miniLeft}%`,
+ top: `${this.miniTop}%`,
+ }}
+ />
+ </div>
+ </div>;
+ }
@computed get docView() {
TraceMobx();
if (!this._document) return (null);
const document = this._document;
- const resolvedDataDoc = !Doc.AreProtosEqual(this._document[DataSym], this._document) ? this._document[DataSym] : undefined;// document.layout instanceof Doc ? document : this._dataDoc;
- return <DocumentView key={document[Id]}
- LibraryPath={this._libraryPath}
- Document={document}
- DataDoc={resolvedDataDoc}
- bringToFront={emptyFunction}
- rootSelected={returnTrue}
- addDocument={undefined}
- removeDocument={undefined}
- ContentScaling={this.contentScaling}
- PanelWidth={this.panelWidth}
- PanelHeight={this.panelHeight}
- NativeHeight={this.nativeHeight}
- NativeWidth={this.nativeWidth}
- ScreenToLocalTransform={this.ScreenToLocalTransform}
- renderDepth={0}
- parentActive={returnTrue}
- whenActiveChanged={emptyFunction}
- focus={emptyFunction}
- backgroundColor={CollectionDockingView.Instance.props.backgroundColor}
- addDocTab={this.addDocTab}
- pinToPres={DockedFrameRenderer.PinDoc}
- docFilters={returnEmptyFilter}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined} />;
+ const resolvedDataDoc = !Doc.AreProtosEqual(this._document[DataSym], this._document) ? this._document[DataSym] : undefined;
+ return <>
+ <DocumentView key={document[Id]}
+ LibraryPath={this._libraryPath}
+ Document={document}
+ DataDoc={resolvedDataDoc}
+ bringToFront={emptyFunction}
+ rootSelected={returnTrue}
+ addDocument={undefined}
+ removeDocument={undefined}
+ ContentScaling={this.contentScaling}
+ PanelWidth={this.panelWidth}
+ PanelHeight={this.panelHeight}
+ NativeHeight={this.nativeHeight}
+ NativeWidth={this.nativeWidth}
+ ScreenToLocalTransform={this.ScreenToLocalTransform}
+ renderDepth={0}
+ parentActive={returnTrue}
+ whenActiveChanged={emptyFunction}
+ focus={emptyFunction}
+ backgroundColor={CollectionDockingView.Instance.props.backgroundColor}
+ addDocTab={this.addDocTab}
+ pinToPres={DockedFrameRenderer.PinDoc}
+ docFilters={returnEmptyFilter}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined} />
+ {document._viewType === CollectionViewType.Freeform && !this._document?.hideMinimap ? this.renderMiniMap() : (null)}
+ </>;
}
render() {
diff --git a/src/client/views/collections/CollectionLinearView.scss b/src/client/views/collections/CollectionLinearView.scss
index 123a27deb..b8b72e756 100644
--- a/src/client/views/collections/CollectionLinearView.scss
+++ b/src/client/views/collections/CollectionLinearView.scss
@@ -1,12 +1,63 @@
@import "../globalCssVariables";
@import "../_nodeModuleOverrides";
-.collectionLinearView-outer{
+.collectionLinearView-outer {
overflow: hidden;
- height:100%;
+ height: 100%;
+
.collectionLinearView {
- display:flex;
+ display: flex;
height: 100%;
+
+ >span {
+ background: $dark-color;
+ color: $light-color;
+ border-radius: 18px;
+ margin-right: 6px;
+ cursor: pointer;
+ }
+
+ .bottomPopup-background {
+ padding-right: 14px;
+ height: 35;
+ transform: translate3d(6px, 5px, 0px);
+ padding-top: 6.5px;
+ padding-bottom: 7px;
+ padding-left: 5px;
+ }
+
+ .bottomPopup-text {
+ display: inline;
+ white-space: nowrap;
+ padding-left: 8px;
+ padding-right: 4px;
+ vertical-align: middle;
+ font-size: 12.5px;
+ }
+
+ .bottomPopup-descriptions {
+ display: inline;
+ white-space: nowrap;
+ padding-left: 8px;
+ padding-right: 8px;
+ vertical-align: middle;
+ background-color: lightgrey;
+ border-radius: 5.5px;
+ color: black;
+ margin-right: 5px;
+ }
+
+ .bottomPopup-exit {
+ display: inline;
+ white-space: nowrap;
+ padding-left: 8px;
+ padding-right: 8px;
+ vertical-align: middle;
+ background-color: lightgrey;
+ border-radius: 5.5px;
+ color: black;
+ }
+
>label {
margin-top: "auto";
margin-bottom: "auto";
@@ -17,15 +68,15 @@
font-size: 12.5px;
width: 18px;
height: 18px;
- margin-top:auto;
- margin-bottom:auto;
+ margin-top: auto;
+ margin-bottom: auto;
margin-right: 3px;
cursor: pointer;
transition: transform 0.2s;
}
label p {
- padding-left:5px;
+ padding-left: 5px;
}
label:hover {
@@ -36,6 +87,7 @@
>input {
display: none;
}
+
>input:not(:checked)~.collectionLinearView-content {
display: none;
}
@@ -52,12 +104,14 @@
position: relative;
margin-top: auto;
- .collectionLinearView-docBtn, .collectionLinearView-docBtn-scalable {
- position:relative;
- margin:auto;
+ .collectionLinearView-docBtn,
+ .collectionLinearView-docBtn-scalable {
+ position: relative;
+ margin: auto;
margin-left: 3px;
transform-origin: center 80%;
}
+
.collectionLinearView-docBtn-scalable:hover {
transform: scale(1.15);
}
@@ -68,10 +122,10 @@
border-radius: 18px;
font-size: 15px;
}
-
+
.collectionLinearView-round-button:hover {
transform: scale(1.15);
}
}
}
-}
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx
index f38eeaf41..3b31947f7 100644
--- a/src/client/views/collections/CollectionLinearView.tsx
+++ b/src/client/views/collections/CollectionLinearView.tsx
@@ -13,6 +13,10 @@ import { CollectionSubView } from './CollectionSubView';
import { DocumentView } from '../nodes/DocumentView';
import { documentSchema } from '../../../fields/documentSchemas';
import { Id } from '../../../fields/FieldSymbols';
+import { DocumentLinksButton } from '../nodes/DocumentLinksButton';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { LinkDescriptionPopup } from '../nodes/LinkDescriptionPopup';
+import { Tooltip } from '@material-ui/core';
type LinearDocument = makeInterface<[typeof documentSchema,]>;
@@ -75,17 +79,54 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
return new Transform(-translateX, -translateY, 1);
}
+ @action
+ exitLongLinks = () => {
+ if (DocumentLinksButton.StartLink) {
+ if (DocumentLinksButton.StartLink.Document) {
+ action((e: React.PointerEvent<HTMLDivElement>) => {
+ Doc.UnBrushDoc(DocumentLinksButton.StartLink?.Document as Doc);
+ });
+ }
+ }
+ DocumentLinksButton.StartLink = undefined;
+ }
+
+ @action
+ changeDescriptionSetting = () => {
+ if (LinkDescriptionPopup.showDescriptions) {
+ if (LinkDescriptionPopup.showDescriptions === "ON") {
+ LinkDescriptionPopup.showDescriptions = "OFF";
+ LinkDescriptionPopup.descriptionPopup = false;
+ } else {
+ LinkDescriptionPopup.showDescriptions = "ON";
+ }
+ } else {
+ LinkDescriptionPopup.showDescriptions = "OFF";
+ LinkDescriptionPopup.descriptionPopup = false;
+ }
+ }
+
render() {
const guid = Utils.GenerateGuid();
const flexDir: any = StrCast(this.Document.flexDirection);
const backgroundColor = StrCast(this.props.Document.backgroundColor, "black");
const color = StrCast(this.props.Document.color, "white");
+
+ const menuOpener = <label htmlFor={`${guid}`} style={{
+ background: backgroundColor === color ? "black" : backgroundColor,
+ // width: "18px", height: "18px", fontSize: "12.5px",
+ // transition: this.props.Document.linearViewIsExpanded ? "transform 0.2s" : "transform 0.5s",
+ // transform: this.props.Document.linearViewIsExpanded ? "" : "rotate(45deg)"
+ }}
+ onPointerDown={e => e.stopPropagation()} >
+ <p>+</p>
+ </label>;
+
return <div className="collectionLinearView-outer">
<div className="collectionLinearView" ref={this.createDashEventsTarget} >
- <label htmlFor={`${guid}`} title="Close Menu" style={{ background: backgroundColor === color ? "black" : backgroundColor }}
- onPointerDown={e => e.stopPropagation()} >
- <p>+</p>
- </label>
+ <Tooltip title={<><div className="dash-tooltip">{BoolCast(this.props.Document.linearViewIsExpanded) ? "Close menu" : "Open menu"}</div></>} placement="top">
+ {menuOpener}
+ </Tooltip>
<input id={`${guid}`} type="checkbox" checked={BoolCast(this.props.Document.linearViewIsExpanded)} ref={this.addMenuToggle}
onChange={action((e: any) => this.props.Document.linearViewIsExpanded = this.addMenuToggle.current!.checked)} />
@@ -130,6 +171,31 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
</div>;
})}
</div>
+ {DocumentLinksButton.StartLink ? <span className="bottomPopup-background" style={{
+ background: backgroundColor === color ? "black" : backgroundColor
+ }}
+ onPointerDown={e => e.stopPropagation()} >
+ <span className="bottomPopup-text" >
+ Creating link from: {DocumentLinksButton.StartLink.props.Document.title}
+ </span>
+
+ <Tooltip title={<><div className="dash-tooltip">{LinkDescriptionPopup.showDescriptions ? "Turn off description pop-up" :
+ "Turn on description pop-up"} </div></>} placement="top">
+ <span className="bottomPopup-descriptions" onClick={this.changeDescriptionSetting}>
+ Labels: {LinkDescriptionPopup.showDescriptions ? LinkDescriptionPopup.showDescriptions : "ON"}
+ </span>
+ </Tooltip>
+
+ <Tooltip title={<><div className="dash-tooltip">Exit link clicking mode </div></>} placement="top">
+ <span className="bottomPopup-exit" onClick={this.exitLongLinks}>
+ Clear
+ </span>
+ </Tooltip>
+
+ {/* <FontAwesomeIcon icon="times-circle" size="lg" style={{ color: "red" }}
+ onClick={this.exitLongLinks} /> */}
+
+ </span> : null}
</div>
</div>;
}
diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
index 627b22417..9a7ea2c93 100644
--- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
+++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
@@ -84,7 +84,6 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
@undoBatch
rowDrop = action((e: Event, de: DragManager.DropEvent) => {
- console.log("masronry row drop");
this._createAliasSelected = false;
if (de.complete.docDragData) {
(this.props.parent.Document.dropConverter instanceof ScriptField) &&
diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionMenu.scss
index 822e15aed..9da204787 100644
--- a/src/client/views/collections/CollectionViewChromes.scss
+++ b/src/client/views/collections/CollectionMenu.scss
@@ -1,35 +1,47 @@
@import "../globalCssVariables";
-@import '~js-datepicker/dist/datepicker.min.css';
-.collectionViewChrome-cont {
- position: absolute;
+
+.collectionMenu-cont {
+ position:relative;
+ display:inline-flex;
width: 100%;
opacity: 0.9;
z-index: 9001;
transition: top .5s;
- background: lightgrey;
+ background: #323232;
+ color: white;
transform-origin: top left;
+ top: 0;
+ width:100%;
+
+ .antimodeMenu-button {
+ padding: 0;
+ width: 30px;
+ display: flex;
+ svg {
+ margin:auto;
+ }
+ }
- .collectionViewChrome {
+ .collectionMenu {
display: flex;
padding-bottom: 1px;
height: 32px;
border-bottom: .5px solid rgb(180, 180, 180);
overflow: visible;
z-index: 9001;
+ border: unset;
.collectionViewBaseChrome {
display: flex;
.collectionViewBaseChrome-viewPicker {
font-size: 75%;
- //text-transform: uppercase;
- //letter-spacing: 2px;
- background: rgb(238, 238, 238);
- color: grey;
+ background: #323232;
outline-color: black;
+ color: white;
border: none;
- //padding: 12px 10px 11px 10px;
+ border-right: solid gray 1px;
}
.collectionViewBaseChrome-viewPicker:active {
@@ -52,21 +64,22 @@
margin-left: 3px;
margin-right: 0px;
font-size: 75%;
- background: rgb(238, 238, 238);
+ background: #323232;
+ color: white;
border: none;
- color: grey;
+ border-right: solid gray 1px;
}
.commandEntry-outerDiv {
pointer-events: all;
- background-color: gray;
+ background-color: #323232;
display: flex;
flex-direction: row;
height: 30px;
.commandEntry-drop {
color: white;
- width: 25px;
+ width: 30px;
margin-top: auto;
margin-bottom: auto;
}
@@ -75,12 +88,18 @@
.collectionViewBaseChrome-collapse {
transition: all .5s, opacity 0.3s;
position: absolute;
- width: 40px;
+ width: 30px;
transform-origin: top left;
pointer-events: all;
// margin-top: 10px;
}
+ @media only screen and (max-device-width: 480px) {
+ .collectionViewBaseChrome-collapse {
+ display: none;
+ }
+ }
+
.collectionViewBaseChrome-template,
.collectionViewBaseChrome-viewModes {
display: grid;
@@ -88,27 +107,25 @@
color: grey;
margin-top: auto;
margin-bottom: auto;
- margin-left: 5px;
}
-
- .collectionViewBaseChrome-viewModes {
- margin-left: 25px;
- }
-
.collectionViewBaseChrome-viewSpecs {
margin-left: 5px;
display: grid;
+ border: none;
+ border-right: solid gray 1px;
.collectionViewBaseChrome-filterIcon {
position: relative;
display: flex;
margin: auto;
- background: gray;
+ background: #323232;
color: white;
width: 30px;
height: 30px;
align-items: center;
justify-content: center;
+ border: none;
+ border-right: solid gray 1px;
}
.collectionViewBaseChrome-viewSpecsInput {
@@ -184,11 +201,12 @@
font-size: 75%;
//text-transform: uppercase;
//letter-spacing: 2px;
- background: rgb(238, 238, 238);
- color: grey;
+ background: #121721;
+ color: white;
outline-color: black;
+ color: white;
border: none;
- //padding: 12px 10px 11px 10px;
+ border-right: solid gray 1px;
}
.collectionGridViewChrome-viewPicker:active {
@@ -291,29 +309,55 @@
}
}
-.collectionFreeFormViewChrome-cont {
- width: 60px;
- display: flex;
+.collectionFreeFormMenu-cont {
+ display: inline-flex;
position: relative;
align-items: center;
+ .antimodeMenu-button {
+ text-align: center;
+ display: block;
+ }
+ .color-previewI {
+ width: 80%;
+ height: 20%;
+ bottom: 0;
+ position: absolute;
+ }
+ .color-previewII {
+ width: 80%;
+ height: 80%;
+ margin-left: 10%;
+ }
+
+ .btn-group {
+ display: grid;
+ grid-template-columns: auto auto auto auto;
+ margin: auto;
+ /* Make the buttons appear below each other */
+ }
+
+ .btn-draw {
+ display: inline-flex;
+ margin: auto;
+ /* Make the buttons appear below each other */
+ }
+
.fwdKeyframe,
.numKeyframe,
.backKeyframe {
cursor: pointer;
- position: absolute;
+ position: relative;
width: 20;
height: 30;
bottom: 0;
- background: gray;
- display: flex;
+ background: #323232;
+ display: inline-flex;
align-items: center;
color: white;
}
.backKeyframe {
- left: 0;
-
svg {
display: block;
margin: auto;
@@ -321,19 +365,16 @@
}
.numKeyframe {
- left: 20;
- display: flex;
flex-direction: column;
- padding: 5px;
+ padding-top: 5px;
}
.fwdKeyframe {
- left: 40;
-
svg {
display: block;
margin: auto;
}
+ border-right: solid gray 1px;
}
}
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
new file mode 100644
index 000000000..0ca86172f
--- /dev/null
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -0,0 +1,974 @@
+import React = require("react");
+import { FontAwesomeIcon, FontAwesomeIconProps } from "@fortawesome/react-fontawesome";
+import { action, computed, observable, reaction, runInAction, Lambda } from "mobx";
+import { observer } from "mobx-react";
+import { Doc, DocListCast, Opt, Field } from "../../../fields/Doc";
+import { BoolCast, Cast, StrCast, NumCast } from "../../../fields/Types";
+import AntimodeMenu from "../AntimodeMenu";
+import "./CollectionMenu.scss";
+import { undoBatch } from "../../util/UndoManager";
+import { CollectionViewType, CollectionView, COLLECTION_BORDER_WIDTH } from "./CollectionView";
+import { emptyFunction, setupMoveUpEvents, Utils } from "../../../Utils";
+import { DragManager } from "../../util/DragManager";
+import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView";
+import { List } from "../../../fields/List";
+import { EditableView } from "../EditableView";
+import { Id } from "../../../fields/FieldSymbols";
+import { listSpec } from "../../../fields/Schema";
+import FormatShapePane from "./collectionFreeForm/FormatShapePane";
+import { ActiveFillColor, SetActiveInkWidth, ActiveInkColor, SetActiveBezierApprox, SetActiveArrowEnd, SetActiveArrowStart, SetActiveFillColor, SetActiveInkColor } from "../InkingStroke";
+import GestureOverlay from "../GestureOverlay";
+import { InkTool } from "../../../fields/InkField";
+import { DocumentType } from "../../documents/DocumentTypes";
+import { Document } from "../../../fields/documentSchemas";
+import { SelectionManager } from "../../util/SelectionManager";
+import { DocumentView } from "../nodes/DocumentView";
+import { ColorState } from "react-color";
+import { ObjectField } from "../../../fields/ObjectField";
+import { ScriptField } from "../../../fields/ScriptField";
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { DocUtils } from "../../documents/Documents";
+
+@observer
+export default class CollectionMenu extends AntimodeMenu {
+ static Instance: CollectionMenu;
+
+ @observable SelectedCollection: DocumentView | undefined;
+ @observable FieldKey: string;
+
+ constructor(props: Readonly<{}>) {
+ super(props);
+ this.FieldKey = "";
+ CollectionMenu.Instance = this;
+ this._canFade = false; // don't let the inking menu fade away
+ this.Pinned = Cast(Doc.UserDoc()["menuCollections-pinned"], "boolean", true);
+ this.jumpTo(300, 300);
+ }
+
+ componentDidMount() {
+ reaction(() => SelectionManager.SelectedDocuments().length && SelectionManager.SelectedDocuments()[0],
+ (doc) => doc && this.SetSelection(doc))
+ }
+
+ @action
+ SetSelection(view: DocumentView) {
+ this.SelectedCollection = view;
+ }
+
+ @action
+ toggleMenuPin = (e: React.MouseEvent) => {
+ Doc.UserDoc()["menuCollections-pinned"] = this.Pinned = !this.Pinned;
+ if (!this.Pinned && this._left < 0) {
+ this.jumpTo(300, 300);
+ }
+ }
+
+ render() {
+ const button = <button className="antimodeMenu-button" key="pin menu" title="Pin menu" onClick={this.toggleMenuPin} style={{ backgroundColor: "#121721" }}>
+ <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.Pinned ? 45 : 0}deg)` }} />
+ </button>;
+
+ return this.getElement(!this.SelectedCollection ? [button] :
+ [<CollectionViewBaseChrome key="chrome"
+ docView={this.SelectedCollection}
+ fieldKey={Doc.LayoutFieldKey(this.SelectedCollection?.props.Document)}
+ type={StrCast(this.SelectedCollection?.props.Document._viewType, CollectionViewType.Invalid) as CollectionViewType} />,
+ button]);
+ }
+}
+
+interface CollectionMenuProps {
+ type: CollectionViewType;
+ fieldKey: string;
+ docView: DocumentView;
+}
+
+const stopPropagation = (e: React.SyntheticEvent) => e.stopPropagation();
+
+@observer
+export class CollectionViewBaseChrome extends React.Component<CollectionMenuProps> {
+ //(!)?\(\(\(doc.(\w+) && \(doc.\w+ as \w+\).includes\(\"(\w+)\"\)
+
+ get document() { return this.props.docView?.props.Document; }
+ get target() { return this.document; }
+ _templateCommand = {
+ params: ["target", "source"], title: "item view",
+ script: "self.target.childLayoutTemplate = getDocTemplate(self.source?.[0])",
+ immediate: undoBatch((source: Doc[]) => source.length && (this.target.childLayoutTemplate = Doc.getDocTemplate(source?.[0]))),
+ initialize: emptyFunction,
+ };
+ _narrativeCommand = {
+ params: ["target", "source"], title: "child click view",
+ script: "self.target.childClickedOpenTemplateView = getDocTemplate(self.source?.[0])",
+ immediate: undoBatch((source: Doc[]) => source.length && (this.target.childClickedOpenTemplateView = Doc.getDocTemplate(source?.[0]))),
+ initialize: emptyFunction,
+ };
+ _contentCommand = {
+ params: ["target", "source"], title: "set content",
+ script: "getProto(self.target).data = copyField(self.source);",
+ immediate: undoBatch((source: Doc[]) => Doc.GetProto(this.target).data = new List<Doc>(source)), // Doc.aliasDocs(source),
+ initialize: emptyFunction,
+ };
+ _onClickCommand = {
+ params: ["target", "proxy"], title: "copy onClick",
+ script: `{ if (self.proxy?.[0]) {
+ getProto(self.proxy[0]).onClick = copyField(self.target.onClick);
+ getProto(self.proxy[0]).target = self.target.target;
+ getProto(self.proxy[0]).source = copyField(self.target.source);
+ }}`,
+ immediate: undoBatch((source: Doc[]) => { }),
+ initialize: emptyFunction,
+ };
+ _openLinkInCommand = {
+ params: ["target", "container"], title: "link follow target",
+ script: `{ if (self.container?.length) {
+ getProto(self.target).linkContainer = self.container[0];
+ getProto(self.target).isLinkButton = true;
+ getProto(self.target).onClick = makeScript("getProto(self.linkContainer).data = new List([self.links[0]?.anchor2])");
+ }}`,
+ immediate: undoBatch((container: Doc[]) => {
+ if (container.length) {
+ Doc.GetProto(this.target).linkContainer = container[0];
+ Doc.GetProto(this.target).isLinkButton = true;
+ Doc.GetProto(this.target).onClick = ScriptField.MakeScript("getProto(self.linkContainer).data = new List([self.links[0]?.anchor2])");
+ }
+ }),
+ initialize: emptyFunction,
+ };
+ _viewCommand = {
+ params: ["target"], title: "bookmark view",
+ script: "self.target._panX = self['target-panX']; self.target._panY = self['target-panY']; self.target._viewScale = self['target-viewScale'];",
+ immediate: undoBatch((source: Doc[]) => { this.target._panX = 0; this.target._panY = 0; this.target._viewScale = 1; }),
+ initialize: (button: Doc) => { button['target-panX'] = this.target._panX; button['target-panY'] = this.target._panY; button['target-viewScale'] = this.target._viewScale; },
+ };
+ _clusterCommand = {
+ params: ["target"], title: "fit content",
+ script: "self.target._fitToBox = !self.target._fitToBox;",
+ immediate: undoBatch((source: Doc[]) => this.target._fitToBox = !this.target._fitToBox),
+ initialize: emptyFunction
+ };
+ _fitContentCommand = {
+ params: ["target"], title: "toggle clusters",
+ script: "self.target.useClusters = !self.target.useClusters;",
+ immediate: undoBatch((source: Doc[]) => this.target.useClusters = !this.target.useClusters),
+ initialize: emptyFunction
+ };
+ _saveFilterCommand = {
+ params: ["target"], title: "save filter",
+ script: "self.target._docFilters = copyField(self['target-docFilters']);",
+ immediate: undoBatch((source: Doc[]) => this.target._docFilters = undefined),
+ initialize: (button: Doc) => { button['target-docFilters'] = this.target._docFilters instanceof ObjectField ? ObjectField.MakeCopy(this.target._docFilters as any as ObjectField) : ""; },
+ };
+
+ _freeform_commands = [this._viewCommand, this._saveFilterCommand, this._fitContentCommand, this._clusterCommand, this._contentCommand, this._templateCommand, this._narrativeCommand];
+ _stacking_commands = [this._contentCommand, this._templateCommand];
+ _masonry_commands = [this._contentCommand, this._templateCommand];
+ _schema_commands = [this._templateCommand, this._narrativeCommand];
+ _doc_commands = [this._openLinkInCommand, this._onClickCommand];
+ _tree_commands = [];
+ private get _buttonizableCommands() {
+ switch (this.props.type) {
+ default: return this._doc_commands;
+ case CollectionViewType.Freeform: return this._freeform_commands;
+ case CollectionViewType.Tree: return this._tree_commands;
+ case CollectionViewType.Schema: return this._schema_commands;
+ case CollectionViewType.Stacking: return this._stacking_commands;
+ case CollectionViewType.Masonry: return this._stacking_commands;
+ case CollectionViewType.Time: return this._freeform_commands;
+ case CollectionViewType.Carousel: return this._freeform_commands;
+ case CollectionViewType.Carousel3D: return this._freeform_commands;
+ }
+ }
+ private _picker: any;
+ private _commandRef = React.createRef<HTMLInputElement>();
+ private _viewRef = React.createRef<HTMLInputElement>();
+ @observable private _currentKey: string = "";
+
+ componentDidMount = action(() => {
+ this._currentKey = this._currentKey || (this._buttonizableCommands.length ? this._buttonizableCommands[0]?.title : "");
+ });
+
+ @undoBatch
+ viewChanged = (e: React.ChangeEvent) => {
+ //@ts-ignore
+ this.document._viewType = e.target.selectedOptions[0].value;
+ }
+
+ commandChanged = (e: React.ChangeEvent) => {
+ //@ts-ignore
+ runInAction(() => this._currentKey = e.target.selectedOptions[0].value);
+ }
+
+ @action
+ toggleViewSpecs = (e: React.SyntheticEvent) => {
+ this.document._facetWidth = this.document._facetWidth ? 0 : 200;
+ e.stopPropagation();
+ }
+
+ @action closeViewSpecs = () => {
+ this.document._facetWidth = 0;
+ }
+
+ @computed get subChrome() {
+ switch (this.props.type) {
+ default:
+ case CollectionViewType.Freeform: return (<CollectionFreeFormViewChrome key="collchrome" {...this.props} isOverlay={this.props.type === CollectionViewType.Invalid} />);
+ case CollectionViewType.Stacking: return (<CollectionStackingViewChrome key="collchrome" {...this.props} />);
+ case CollectionViewType.Schema: return (<CollectionSchemaViewChrome key="collchrome" {...this.props} />);
+ case CollectionViewType.Tree: return (<CollectionTreeViewChrome key="collchrome" {...this.props} />);
+ case CollectionViewType.Masonry: return (<CollectionStackingViewChrome key="collchrome" {...this.props} />);
+ case CollectionViewType.Carousel3D: return (<Collection3DCarouselViewChrome key="collchrome" {...this.props} />);
+ case CollectionViewType.Grid: return (<CollectionGridViewChrome key="collchrome" {...this.props} />);
+ case CollectionViewType.Docking: return (<CollectionDockingChrome key="collchrome" {...this.props} />);
+ }
+ }
+ private dropDisposer?: DragManager.DragDropDisposer;
+ protected createDropTarget = (ele: HTMLDivElement) => {
+ this.dropDisposer?.();
+ if (ele) {
+ this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.document);
+ }
+ }
+
+ @undoBatch
+ @action
+ protected drop(e: Event, de: DragManager.DropEvent): boolean {
+ const docDragData = de.complete.docDragData;
+ if (docDragData?.draggedDocuments.length) {
+ this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => c.immediate(docDragData.draggedDocuments || []));
+ e.stopPropagation();
+ }
+ return true;
+ }
+
+ dragViewDown = (e: React.PointerEvent) => {
+ setupMoveUpEvents(this, e, (e, down, delta) => {
+ const vtype = this.props.type;
+ const c = {
+ params: ["target"], title: vtype,
+ script: `this.target._viewType = '${StrCast(this.props.type)}'`,
+ immediate: (source: Doc[]) => this.document._viewType = Doc.getDocTemplate(source?.[0]),
+ initialize: emptyFunction,
+ };
+ DragManager.StartButtonDrag([this._viewRef.current!], c.script, StrCast(c.title),
+ { target: this.document }, c.params, c.initialize, e.clientX, e.clientY);
+ return true;
+ }, emptyFunction, emptyFunction);
+ }
+ dragCommandDown = (e: React.PointerEvent) => {
+ setupMoveUpEvents(this, e, (e, down, delta) => {
+ this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c =>
+ DragManager.StartButtonDrag([this._commandRef.current!], c.script, c.title,
+ { target: this.document }, c.params, c.initialize, e.clientX, e.clientY));
+ return true;
+ }, emptyFunction, () => {
+ this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => c.immediate([]));
+ });
+ }
+
+ @computed get templateChrome() {
+ return <div className="collectionViewBaseChrome-template" ref={this.createDropTarget} >
+ <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._commandRef} onPointerDown={this.dragCommandDown}>
+ <button className={"antimodeMenu-button"} >
+ <FontAwesomeIcon icon="bullseye" size="lg" />
+ </button>
+ <select
+ className="collectionViewBaseChrome-cmdPicker" onPointerDown={stopPropagation} onChange={this.commandChanged} value={this._currentKey}>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={"empty"} value={""} />
+ {this._buttonizableCommands.map(cmd =>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={cmd.title} value={cmd.title}>{cmd.title}</option>
+ )}
+ </select>
+ </div>
+ </div>;
+ }
+
+ @computed get viewModes() {
+ return <div className="collectionViewBaseChrome-viewModes" >
+ <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._viewRef} onPointerDown={this.dragViewDown}>
+ <button className={"antimodeMenu-button"}>
+ <FontAwesomeIcon icon="bullseye" size="lg" />
+ </button>
+ <select
+ className="collectionViewBaseChrome-viewPicker"
+ onPointerDown={stopPropagation}
+ onChange={this.viewChanged}
+ value={StrCast(this.props.type)}>
+ {Object.values(CollectionViewType).map(type => [CollectionViewType.Invalid, CollectionViewType.Docking].includes(type) ? (null) : (
+ <option
+ key={Utils.GenerateGuid()}
+ className="collectionViewBaseChrome-viewOption"
+ onPointerDown={stopPropagation}
+ value={type}>
+ {type[0].toUpperCase() + type.substring(1)}
+ </option>
+ ))}
+ </select>
+ </div>
+ </div>;
+ }
+
+ render() {
+ return (
+ <div className="collectionMenu-cont" >
+ <div className="collectionMenu">
+ <div className="collectionViewBaseChrome">
+ {this.props.type === CollectionViewType.Invalid || this.props.type === CollectionViewType.Docking ? (null) : this.viewModes}
+ {this.props.type === CollectionViewType.Docking ? (null) : this.templateChrome}
+ <div className="collectionViewBaseChrome-viewSpecs" title="filter documents to show" style={{ display: "grid" }}>
+ <button className={"antimodeMenu-button"} onClick={this.toggleViewSpecs} >
+ <FontAwesomeIcon icon="filter" size="lg" />
+ </button>
+ </div>
+
+ {this.props.docView.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform ? (null) : <button className={"antimodeMenu-button"} key="float"
+ style={{ backgroundColor: !this.props.docView.layoutDoc.isAnnotating ? "121212" : undefined, borderRight: "1px solid gray" }}
+ title="Toggle Overlay Layer"
+ onClick={() => DocumentView.FloatDoc(this.props.docView)}>
+ <FontAwesomeIcon icon={["fab", "buffer"]} size={"lg"} />
+ </button>}
+ </div>
+ {this.subChrome}
+ </div>
+ </div>
+ );
+ }
+}
+
+@observer
+export class CollectionDockingChrome extends React.Component<CollectionMenuProps> {
+ render() {
+ return (null);
+ }
+}
+
+@observer
+export class CollectionFreeFormViewChrome extends React.Component<CollectionMenuProps & { isOverlay: boolean }> {
+ public static Instance: CollectionFreeFormViewChrome;
+ constructor(props: any) {
+ super(props);
+ CollectionFreeFormViewChrome.Instance = this;
+ }
+ get document() { return this.props.docView.props.Document; }
+ @computed get dataField() {
+ return this.document[Doc.LayoutFieldKey(this.document)];
+ }
+ @computed get childDocs() {
+ return DocListCast(this.dataField);
+ }
+ @undoBatch
+ @action
+ nextKeyframe = (): void => {
+ const currentFrame = Cast(this.document.currentFrame, "number", null);
+ if (currentFrame === undefined) {
+ this.document.currentFrame = 0;
+ CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
+ }
+ CollectionFreeFormDocumentView.updateKeyframe(this.childDocs, currentFrame || 0);
+ this.document.currentFrame = Math.max(0, (currentFrame || 0) + 1);
+ this.document.lastFrame = Math.max(NumCast(this.document.currentFrame), NumCast(this.document.lastFrame));
+ }
+ @undoBatch
+ @action
+ prevKeyframe = (): void => {
+ const currentFrame = Cast(this.document.currentFrame, "number", null);
+ if (currentFrame === undefined) {
+ this.document.currentFrame = 0;
+ CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
+ }
+ CollectionFreeFormDocumentView.gotoKeyframe(this.childDocs.slice());
+ this.document.currentFrame = Math.max(0, (currentFrame || 0) - 1);
+ }
+ @undoBatch
+ @action
+ miniMap = (): void => {
+ this.document.hideMinimap = !this.document.hideMinimap;
+ }
+ private _palette = ["#D0021B", "#F5A623", "#F8E71C", "#8B572A", "#7ED321", "#417505", "#9013FE", "#4A90E2", "#50E3C2", "#B8E986", "#000000", "#4A4A4A", "#9B9B9B", "#FFFFFF", ""];
+ private _width = ["1", "5", "10", "100"];
+ // private _draw = ["⎯", "→", "↔︎", "∿", "↝", "↭", "ロ", "O", "∆"];
+ // private _head = ["", "", "arrow", "", "", "arrow", "", "", ""];
+ // private _end = ["", "arrow", "arrow", "", "arrow", "arrow", "", "", ""];
+ // private _shape = ["line", "line", "line", "", "", "", "rectangle", "circle", "triangle"];
+ private _dotsize = [10, 20, 30, 40];
+ private _draw = ["∿", "⎯", "→", "↔︎", "ロ", "O"];
+ private _head = ["", "", "", "arrow", "", ""];
+ private _end = ["", "", "arrow", "arrow", "", ""];
+ private _shape = ["", "line", "line", "line", "rectangle", "circle"];
+ private _title = ["pen", "line", "line with arrow", "line with double arrows", "square", "circle",];
+ private _faName = ["pen-fancy", "minus", "long-arrow-alt-right", "arrows-alt-h", "square", "circle"];
+ @observable _shapesNum = this._shape.length;
+ @observable _selected = this._shapesNum;
+
+ @observable _keepMode = false;
+
+ @observable _colorBtn = false;
+ @observable _widthBtn = false;
+ @observable _fillBtn = false;
+
+ @action
+ clearKeep() { this._selected = this._shapesNum; }
+
+ @action
+ changeColor = (color: string, type: string) => {
+ const col: ColorState = {
+ hex: color, hsl: { a: 0, h: 0, s: 0, l: 0, source: "" }, hsv: { a: 0, h: 0, s: 0, v: 0, source: "" },
+ rgb: { a: 0, r: 0, b: 0, g: 0, source: "" }, oldHue: 0, source: "",
+ };
+ if (type === "color") {
+ SetActiveInkColor(Utils.colorString(col));
+ } else if (type === "fill") {
+ SetActiveFillColor(Utils.colorString(col));
+ }
+ }
+
+ @action
+ editProperties = (value: any, field: string) => {
+ SelectionManager.SelectedDocuments().forEach(action((element: DocumentView) => {
+ const doc = Document(element.rootDoc);
+ if (doc.type === DocumentType.INK) {
+ switch (field) {
+ case "width": doc.strokeWidth = Number(value); break;
+ case "color": doc.color = String(value); break;
+ case "fill": doc.fillColor = String(value); break;
+ case "dash": doc.strokeDash = value;
+ }
+ }
+ }));
+ }
+
+ @computed get drawButtons() {
+ const func = action((i: number, keep: boolean) => {
+ this._keepMode = keep;
+ if (this._selected !== i) {
+ this._selected = i;
+ Doc.SetSelectedTool(InkTool.Pen);
+ SetActiveArrowStart(this._head[i]);
+ SetActiveArrowEnd(this._end[i]);
+ SetActiveBezierApprox("300");
+
+ GestureOverlay.Instance.InkShape = this._shape[i];
+ } else {
+ this._selected = this._shapesNum;
+ Doc.SetSelectedTool(InkTool.None);
+ SetActiveArrowStart("");
+ SetActiveArrowEnd("");
+ GestureOverlay.Instance.InkShape = "";
+ SetActiveBezierApprox("0");
+ }
+ });
+ return <div className="btn-draw" key="draw">
+ {this._draw.map((icon, i) =>
+ <button className="antimodeMenu-button" title={this._title[i]} key={icon} onPointerDown={() => func(i, false)} onDoubleClick={() => func(i, true)}
+ style={{ backgroundColor: i === this._selected ? "121212" : "", fontSize: "20" }}>
+ {/* {this._draw[i]} */}
+ <FontAwesomeIcon icon={this._faName[i] as IconProp} size="sm" />
+
+ </button>)}
+ </div>;
+ }
+
+ toggleButton = (key: string, value: boolean, setter: () => {}, icon: FontAwesomeIconProps["icon"], ele: JSX.Element | null) => {
+ return <button className="antimodeMenu-button" key={key} title={key}
+ onPointerDown={action(e => setter())}
+ style={{ backgroundColor: value ? "121212" : "" }}>
+ <FontAwesomeIcon icon={icon} size="lg" />
+ {ele}
+ </button>;
+ }
+
+ @computed get widthPicker() {
+ const widthPicker = this.toggleButton("stroke width", this._widthBtn, () => this._widthBtn = !this._widthBtn, "bars", null);
+ return !this._widthBtn ? widthPicker :
+ <div className="btn2-group" key="width">
+ {widthPicker}
+ {this._width.map((wid, i) =>
+ <button className="antimodeMenu-button" key={wid} title="change width"
+ onPointerDown={action(() => { SetActiveInkWidth(wid); this._widthBtn = false; this.editProperties(wid, "width"); })}
+ style={{ backgroundColor: this._widthBtn ? "121212" : "", zIndex: 1001, fontSize: this._dotsize[i], padding: 0, textAlign: "center" }}>
+ •
+ </button>)}
+ </div>;
+ }
+
+ @computed get colorPicker() {
+ const colorPicker = this.toggleButton("stroke color", this._colorBtn, () => this._colorBtn = !this._colorBtn, "pen-nib",
+ <div className="color-previewI" style={{ backgroundColor: ActiveInkColor() ?? "121212" }} />);
+ return !this._colorBtn ? colorPicker :
+ <div className="btn-group" key="color">
+ {colorPicker}
+ {this._palette.map(color =>
+ <button className="antimodeMenu-button" key={color}
+ onPointerDown={action(() => { this.changeColor(color, "color"); this._colorBtn = false; this.editProperties(color, "color"); })}
+ style={{ backgroundColor: this._colorBtn ? "121212" : "", zIndex: 1001 }}>
+ {/* <FontAwesomeIcon icon="pen-nib" size="lg" /> */}
+ <div className="color-previewII" style={{ backgroundColor: color }} />
+ </button>)}
+ </div>;
+ }
+ @computed get fillPicker() {
+ const fillPicker = this.toggleButton("shape fill color", this._fillBtn, () => this._fillBtn = !this._fillBtn, "fill-drip",
+ <div className="color-previewI" style={{ backgroundColor: ActiveFillColor() ?? "121212" }} />);
+ return !this._fillBtn ? fillPicker :
+ <div className="btn-group" key="fill" >
+ {fillPicker}
+ {this._palette.map(color =>
+ <button className="antimodeMenu-button" key={color}
+ onPointerDown={action(() => { this.changeColor(color, "fill"); this._fillBtn = false; this.editProperties(color, "fill"); })}
+ style={{ backgroundColor: this._fillBtn ? "121212" : "", zIndex: 1001 }}>
+ <div className="color-previewII" style={{ backgroundColor: color }}></div>
+ </button>)}
+
+ </div>;
+ }
+
+ @computed get formatPane() {
+ return <button className="antimodeMenu-button" key="format" title="toggle foramatting pane"
+ onPointerDown={action(e => FormatShapePane.Instance.Pinned = !FormatShapePane.Instance.Pinned)}
+ style={{ backgroundColor: this._fillBtn ? "121212" : "" }}>
+ <FontAwesomeIcon icon="angle-double-right" size="lg" />
+ </button>;
+ }
+
+ render() {
+ return !this.props.docView.layoutDoc ? (null) : <div className="collectionFreeFormMenu-cont">
+ {this.props.docView.props.renderDepth !== 0 ? (null) :
+ <div key="map" title="mini map" className="backKeyframe" onClick={this.miniMap}>
+ <FontAwesomeIcon icon={"map"} size={"lg"} />
+ </div>
+ }
+ <div key="back" title="back frame" className="backKeyframe" onClick={this.prevKeyframe}>
+ <FontAwesomeIcon icon={"caret-left"} size={"lg"} />
+ </div>
+ <div key="num" title="toggle view all" className="numKeyframe" style={{ backgroundColor: this.document.editing ? "#759c75" : "#c56565" }}
+ onClick={action(() => this.document.editing = !this.document.editing)} >
+ {NumCast(this.document.currentFrame)}
+ </div>
+ <div key="fwd" title="forward frame" className="fwdKeyframe" onClick={this.nextKeyframe}>
+ <FontAwesomeIcon icon={"caret-right"} size={"lg"} />
+ </div>
+
+ {!this.props.isOverlay || this.document.type !== DocumentType.WEB ? (null) :
+ <button className={"antimodeMenu-button"} key="hypothesis"
+ style={{ backgroundColor: !this.props.docView.layoutDoc.isAnnotating ? "121212" : undefined, borderRight: "1px solid gray" }}
+ title="Use Hypothesis"
+ onClick={() => this.props.docView.layoutDoc.isAnnotating = !this.props.docView.layoutDoc.isAnnotating}>
+ <FontAwesomeIcon icon={["fab", "hire-a-helper"]} size={"lg"} />
+ </button>
+ }
+ {!this.props.isOverlay || this.props.docView.layoutDoc.isAnnotating ?
+ <>
+ {this.drawButtons}
+ {this.widthPicker}
+ {this.colorPicker}
+ {this.fillPicker}
+ {this.formatPane}
+ </> :
+ (null)
+ }
+ </div>;
+ }
+}
+@observer
+export class CollectionStackingViewChrome extends React.Component<CollectionMenuProps> {
+ @observable private _currentKey: string = "";
+ @observable private suggestions: string[] = [];
+
+ get document() { return this.props.docView.props.Document; }
+
+ @computed private get descending() { return StrCast(this.document._columnsSort) === "descending"; }
+ @computed get pivotField() { return StrCast(this.document._pivotField); }
+
+ getKeySuggestions = async (value: string): Promise<string[]> => {
+ value = value.toLowerCase();
+ const docs = DocListCast(this.document[this.props.fieldKey]);
+ 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 = [];
+ }
+
+ @action
+ setValue = (value: string) => {
+ this.document._pivotField = value;
+ return true;
+ }
+
+ @action toggleSort = () => {
+ this.document._columnsSort =
+ this.document._columnsSort === "descending" ? "ascending" :
+ this.document._columnsSort === "ascending" ? undefined : "descending";
+ }
+ @action resetValue = () => { this._currentKey = this.pivotField; };
+
+ render() {
+ return (
+ <div className="collectionStackingViewChrome-cont">
+ <div className="collectionStackingViewChrome-pivotField-cont">
+ <div className="collectionStackingViewChrome-pivotField-label">
+ GROUP BY:
+ </div>
+ <div className="collectionStackingViewChrome-sortIcon" onClick={this.toggleSort} style={{ transform: `rotate(${this.descending ? "180" : "0"}deg)` }}>
+ <FontAwesomeIcon icon="caret-up" size="2x" color="white" />
+ </div>
+ <div className="collectionStackingViewChrome-pivotField">
+ <EditableView
+ GetValue={() => this.pivotField}
+ 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.pivotField ? this.pivotField : "N/A"}
+ />
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+
+@observer
+export class CollectionSchemaViewChrome extends React.Component<CollectionMenuProps> {
+ // private _textwrapAllRows: boolean = Cast(this.document.textwrappedSchemaRows, listSpec("string"), []).length > 0;
+ get document() { return this.props.docView.props.Document; }
+
+ @undoBatch
+ togglePreview = () => {
+ const dividerWidth = 4;
+ const borderWidth = Number(COLLECTION_BORDER_WIDTH);
+ const panelWidth = this.props.docView.props.PanelWidth();
+ const previewWidth = NumCast(this.document.schemaPreviewWidth);
+ const tableWidth = panelWidth - 2 * borderWidth - dividerWidth - previewWidth;
+ this.document.schemaPreviewWidth = previewWidth === 0 ? Math.min(tableWidth / 3, 200) : 0;
+ }
+
+ @undoBatch
+ @action
+ toggleTextwrap = async () => {
+ const textwrappedRows = Cast(this.document.textwrappedSchemaRows, listSpec("string"), []);
+ if (textwrappedRows.length) {
+ this.document.textwrappedSchemaRows = new List<string>([]);
+ } else {
+ const docs = DocListCast(this.document[this.props.fieldKey]);
+ const allRows = docs instanceof Doc ? [docs[Id]] : docs.map(doc => doc[Id]);
+ this.document.textwrappedSchemaRows = new List<string>(allRows);
+ }
+ }
+
+
+ render() {
+ const previewWidth = NumCast(this.document.schemaPreviewWidth);
+ const textWrapped = Cast(this.document.textwrappedSchemaRows, listSpec("string"), []).length > 0;
+
+ return (
+ <div className="collectionSchemaViewChrome-cont">
+ <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 >
+ );
+ }
+}
+
+@observer
+export class CollectionTreeViewChrome extends React.Component<CollectionMenuProps> {
+
+ get document() { return this.props.docView.props.Document; }
+ get sortAscending() {
+ return this.document[this.props.fieldKey + "-sortAscending"];
+ }
+ set sortAscending(value) {
+ this.document[this.props.fieldKey + "-sortAscending"] = value;
+ }
+ @computed private get ascending() {
+ return Cast(this.sortAscending, "boolean", null);
+ }
+
+ @action toggleSort = () => {
+ if (this.sortAscending) this.sortAscending = undefined;
+ else if (this.sortAscending === undefined) this.sortAscending = false;
+ else this.sortAscending = true;
+ }
+
+ render() {
+ return (
+ <div className="collectionTreeViewChrome-cont">
+ <button className="collectionTreeViewChrome-sort" onClick={this.toggleSort}>
+ <div className="collectionTreeViewChrome-sortLabel">
+ Sort
+ </div>
+ <div className="collectionTreeViewChrome-sortIcon" style={{ transform: `rotate(${this.ascending === undefined ? "90" : this.ascending ? "180" : "0"}deg)` }}>
+ <FontAwesomeIcon icon="caret-up" size="2x" color="white" />
+ </div>
+ </button>
+ </div>
+ );
+ }
+}
+
+// Enter scroll speed for 3D Carousel
+@observer
+export class Collection3DCarouselViewChrome extends React.Component<CollectionMenuProps> {
+ get document() { return this.props.docView.props.Document; }
+ @computed get scrollSpeed() {
+ return this.document._autoScrollSpeed;
+ }
+
+ @action
+ setValue = (value: string) => {
+ const numValue = Number(StrCast(value));
+ if (numValue > 0) {
+ this.document._autoScrollSpeed = numValue;
+ return true;
+ }
+ return false;
+ }
+
+ render() {
+ return (
+ <div className="collection3DCarouselViewChrome-cont">
+ <div className="collection3DCarouselViewChrome-scrollSpeed-cont">
+ <div className="collectionStackingViewChrome-scrollSpeed-label">
+ AUTOSCROLL SPEED:
+ </div>
+ <div className="collection3DCarouselViewChrome-scrollSpeed">
+ <EditableView
+ GetValue={() => StrCast(this.scrollSpeed)}
+ oneLine
+ SetValue={this.setValue}
+ contents={this.scrollSpeed ? this.scrollSpeed : 1000} />
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+/**
+ * Chrome for grid view.
+ */
+@observer
+export class CollectionGridViewChrome extends React.Component<CollectionMenuProps> {
+
+ private clicked: boolean = false;
+ private entered: boolean = false;
+ private decrementLimitReached: boolean = false;
+ @observable private resize = false;
+ private resizeListenerDisposer: Opt<Lambda>;
+ get document() { return this.props.docView.props.Document; }
+
+ componentDidMount() {
+
+ runInAction(() => this.resize = this.props.docView.props.PanelWidth() < 700);
+
+ // listener to reduce text on chrome resize (panel resize)
+ this.resizeListenerDisposer = computed(() => this.props.docView.props.PanelWidth()).observe(({ newValue }) => {
+ runInAction(() => this.resize = newValue < 700);
+ });
+ }
+
+ componentWillUnmount() {
+ this.resizeListenerDisposer?.();
+ }
+
+ get numCols() { return NumCast(this.document.gridNumCols, 10); }
+
+ /**
+ * Sets the value of `numCols` on the grid's Document to the value entered.
+ */
+ @undoBatch
+ onNumColsEnter = (e: React.KeyboardEvent<HTMLInputElement>) => {
+ if (e.key === "Enter" || e.key === "Tab") {
+ if (e.currentTarget.valueAsNumber > 0) {
+ this.document.gridNumCols = e.currentTarget.valueAsNumber;
+ }
+
+ }
+ }
+
+ /**
+ * Sets the value of `rowHeight` on the grid's Document to the value entered.
+ */
+ // @undoBatch
+ // onRowHeightEnter = (e: React.KeyboardEvent<HTMLInputElement>) => {
+ // if (e.key === "Enter" || e.key === "Tab") {
+ // if (e.currentTarget.valueAsNumber > 0 && this.document.rowHeight as number !== e.currentTarget.valueAsNumber) {
+ // this.document.rowHeight = e.currentTarget.valueAsNumber;
+ // }
+ // }
+ // }
+
+ /**
+ * Sets whether the grid is flexible or not on the grid's Document.
+ */
+ @undoBatch
+ toggleFlex = () => {
+ this.document.gridFlex = !BoolCast(this.document.gridFlex, true);
+ }
+
+ /**
+ * Increments the value of numCols on button click
+ */
+ onIncrementButtonClick = () => {
+ this.clicked = true;
+ this.entered && (this.document.gridNumCols as number)--;
+ undoBatch(() => this.document.gridNumCols = this.numCols + 1)();
+ this.entered = false;
+ }
+
+ /**
+ * Decrements the value of numCols on button click
+ */
+ onDecrementButtonClick = () => {
+ this.clicked = true;
+ if (!this.decrementLimitReached) {
+ this.entered && (this.document.gridNumCols as number)++;
+ undoBatch(() => this.document.gridNumCols = this.numCols - 1)();
+ }
+ this.entered = false;
+ }
+
+ /**
+ * Increments the value of numCols on button hover
+ */
+ incrementValue = () => {
+ this.entered = true;
+ if (!this.clicked && !this.decrementLimitReached) {
+ this.document.gridNumCols = this.numCols + 1;
+ }
+ this.decrementLimitReached = false;
+ this.clicked = false;
+ }
+
+ /**
+ * Decrements the value of numCols on button hover
+ */
+ decrementValue = () => {
+ this.entered = true;
+ if (!this.clicked) {
+ if (this.numCols !== 1) {
+ this.document.gridNumCols = this.numCols - 1;
+ }
+ else {
+ this.decrementLimitReached = true;
+ }
+ }
+
+ this.clicked = false;
+ }
+
+ /**
+ * Toggles the value of preventCollision
+ */
+ toggleCollisions = () => {
+ this.document.gridPreventCollision = !this.document.gridPreventCollision;
+ }
+
+ /**
+ * Changes the value of the compactType
+ */
+ changeCompactType = (e: React.ChangeEvent<HTMLSelectElement>) => {
+ // need to change startCompaction so that this operation will be undoable.
+ this.document.gridStartCompaction = e.target.selectedOptions[0].value;
+ }
+
+ render() {
+ return (
+ <div className="collectionGridViewChrome-cont" >
+ <span className="grid-control" style={{ width: this.resize ? "25%" : "30%" }}>
+ <span className="grid-icon">
+ <FontAwesomeIcon icon="columns" size="1x" />
+ </span>
+ <input className="collectionGridViewChrome-entryBox" type="number" placeholder={this.numCols.toString()} onKeyDown={this.onNumColsEnter} onClick={(e: React.MouseEvent<HTMLInputElement, MouseEvent>) => { e.stopPropagation(); e.preventDefault(); e.currentTarget.focus(); }} />
+ <input className="columnButton" onClick={this.onIncrementButtonClick} onMouseEnter={this.incrementValue} onMouseLeave={this.decrementValue} type="button" value="↑" />
+ <input className="columnButton" style={{ marginRight: 5 }} onClick={this.onDecrementButtonClick} onMouseEnter={this.decrementValue} onMouseLeave={this.incrementValue} type="button" value="↓" />
+ </span>
+ {/* <span className="grid-control">
+ <span className="grid-icon">
+ <FontAwesomeIcon icon="text-height" size="1x" />
+ </span>
+ <input className="collectionGridViewChrome-entryBox" type="number" placeholder={this.document.rowHeight as string} onKeyDown={this.onRowHeightEnter} onClick={(e: React.MouseEvent<HTMLInputElement, MouseEvent>) => { e.stopPropagation(); e.preventDefault(); e.currentTarget.focus(); }} />
+ </span> */}
+ <span className="grid-control" style={{ width: this.resize ? "12%" : "20%" }}>
+ <input type="checkbox" style={{ marginRight: 5 }} onChange={this.toggleCollisions} checked={!this.document.gridPreventCollision} />
+ <label className="flexLabel">{this.resize ? "Coll" : "Collisions"}</label>
+ </span>
+
+ <select className="collectionGridViewChrome-viewPicker"
+ style={{ marginRight: 5 }}
+ onPointerDown={stopPropagation}
+ onChange={this.changeCompactType}
+ value={StrCast(this.document.gridStartCompaction, StrCast(this.document.gridCompaction))}>
+ {["vertical", "horizontal", "none"].map(type =>
+ <option className="collectionGridViewChrome-viewOption"
+ onPointerDown={stopPropagation}
+ value={type}>
+ {this.resize ? type[0].toUpperCase() + type.substring(1) : "Compact: " + type}
+ </option>
+ )}
+ </select>
+
+ <span className="grid-control" style={{ width: this.resize ? "12%" : "20%" }}>
+ <input style={{ marginRight: 5 }} type="checkbox" onChange={this.toggleFlex}
+ checked={BoolCast(this.document.gridFlex, true)} />
+ <label className="flexLabel">{this.resize ? "Flex" : "Flexible"}</label>
+ </span>
+
+ <button onClick={() => this.document.gridResetLayout = true}>
+ {!this.resize ? "Reset" :
+ <FontAwesomeIcon icon="redo-alt" size="1x" />}
+ </button>
+
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx
index 22a3877ab..2e4055256 100644
--- a/src/client/views/collections/CollectionPileView.tsx
+++ b/src/client/views/collections/CollectionPileView.tsx
@@ -39,7 +39,15 @@ export class CollectionPileView extends CollectionSubView(doc => doc) {
@computed get contents() {
return <div className="collectionPileView-innards" style={{ pointerEvents: this.layoutEngine() === "starburst" ? undefined : "none" }} >
- <CollectionFreeFormView {...this.props} layoutEngine={this.layoutEngine} />
+ <CollectionFreeFormView {...this.props} layoutEngine={this.layoutEngine}
+ addDocument={(doc: Doc | Doc[]) => {
+ (doc instanceof Doc ? [doc] : doc).map((d) => DocUtils.iconify(d));
+ return this.props.addDocument(doc);
+ }}
+ moveDocument={(doc: Doc | Doc[], targetCollection: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => {
+ (doc instanceof Doc ? [doc] : doc).map((d) => Doc.deiconifyView(d));
+ return this.props.moveDocument(doc, targetCollection, addDoc);
+ }} />
</div>;
}
toggleStarburst = action(() => {
@@ -72,24 +80,13 @@ export class CollectionPileView extends CollectionSubView(doc => doc) {
}
});
- @undoBatch
- @action
- onInternalDrop = (e: Event, de: DragManager.DropEvent) => {
- if (super.onInternalDrop(e, de)) {
- if (de.complete.docDragData) {
- DocUtils.pileup(this.childDocs);
- }
- }
- return true;
- }
-
_undoBatch: UndoManager.Batch | undefined;
pointerDown = (e: React.PointerEvent) => {
let dist = 0;
SnappingManager.SetIsDragging(true);
// this._lastTap should be set to 0, and this._doubleTap should be set to false in the class header
setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => {
- if (this.layoutEngine() === "pass" && this.childDocs.length && this.props.isSelected(true)) {
+ if (this.layoutEngine() === "pass" && this.childDocs.length && e.shiftKey) {
dist += Math.sqrt(delta[0] * delta[0] + delta[1] * delta[1]);
if (dist > 100) {
if (!this._undoBatch) {
@@ -110,11 +107,11 @@ export class CollectionPileView extends CollectionSubView(doc => doc) {
if (!this.childDocs.length) {
this.props.ContainingCollectionView?.removeDocument(this.props.Document);
}
- }, emptyFunction, false, this.layoutEngine() === "pass" && this.props.isSelected(true)); // this sets _doubleTap
+ }, emptyFunction, e.shiftKey && this.layoutEngine() === "pass", this.layoutEngine() === "pass" && e.shiftKey); // this sets _doubleTap
}
onClick = (e: React.MouseEvent) => {
- if (e.button === 0 && this._doubleTap) {
+ if (e.button === 0) {//} && this._doubleTap) {
SelectionManager.DeselectAll();
this.toggleStarburst();
e.stopPropagation();
@@ -124,7 +121,6 @@ export class CollectionPileView extends CollectionSubView(doc => doc) {
render() {
return <div className={"collectionPileView"} onClick={this.onClick} onPointerDown={this.pointerDown}
- ref={this.createDashEventsTarget}
style={{ width: this.props.PanelWidth(), height: `calc(100% - ${this.props.Document._chromeStatus === "enabled" ? 51 : 0}px)` }}>
{this.contents}
</div>;
diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx
index 11f0edf23..bf826857e 100644
--- a/src/client/views/collections/CollectionSchemaCells.tsx
+++ b/src/client/views/collections/CollectionSchemaCells.tsx
@@ -152,7 +152,6 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
// 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);
@@ -339,7 +338,6 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
if (script.compiled) {
retVal = this.applyToDoc(props.Document, this.props.row, this.props.col, script.run);
- console.log("compiled");
}
}
@@ -403,17 +401,13 @@ export class CollectionSchemaDateCell extends CollectionSchemaCell {
@action
handleChange = (date: any) => {
- console.log(date);
this._date = date;
// const script = CompileScript(date.toString(), { requiredType: "Date", addReturn: true, params: { this: Doc.name } });
// if (script.compiled) {
- // console.log("scripting");
// this.applyToDoc(this._document, this.props.row, this.props.col, script.run);
// } else {
- console.log(DateCast(date));
// ^ DateCast is always undefined for some reason, but that is what the field should be set to
this._document[this.props.rowProps.column.id as string] = date as Date;
- console.log(this._document[this.props.rowProps.column.id as string]);
//}
}
@@ -478,8 +472,6 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell {
const results = script.compiled && script.run();
if (results && results.success) {
-
- console.log(results.result);
this._doc = results.result;
this._document[this.prop.fieldKey] = results.result;
this._docTitle = this._doc?.title;
@@ -510,10 +502,8 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell {
this._preview = false;
} else {
if (bool) {
- console.log("show doc");
this.props.showDoc(this._doc, this.prop.DataDoc, e.clientX, e.clientY);
} else {
- console.log("no doc");
this.props.showDoc(undefined);
}
}
@@ -728,7 +718,6 @@ export class CollectionSchemaListCell extends CollectionSchemaCell {
@action
toggleOpened(open: boolean) {
- console.log("open: " + open);
this._opened = open;
}
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index aceaa2bae..11382b722 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -352,7 +352,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
@action
openHeader = (col: any, screenx: number, screeny: number) => {
- console.log("header opening");
this._col = col;
this._headerOpen = !this._headerOpen;
this._pointerX = screenx;
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index 3d8ec2fd5..8fc74a9c6 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -33,8 +33,9 @@
.collectionStackingViewFieldColumn {
height: max-content;
}
+
.collectionStackingViewFieldColumnDragging {
- height:100%;
+ height: 100%;
}
.collectionSchemaView-previewDoc {
@@ -425,4 +426,15 @@
.rc-switch-checked .rc-switch-inner {
left: 8px;
}
+}
+
+@media only screen and (max-device-width: 480px) {
+
+ .collectionStackingView .collectionStackingView-columnDragger,
+ .collectionMasonryView .collectionStackingView-columnDragger {
+ width: 0.1;
+ height: 0.1;
+ opacity: 0;
+ font-size: 0;
+ }
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 460f1e486..1c5cf4290 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -126,7 +126,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument)
changed = true;
});
}
- changed && setTimeout(action(() => { if (this.columnHeaders) { this.columnHeaders.length = 0; this.columnHeaders.push(...columnHeaders); } }), 0);
+ changed && setTimeout(action(() => this.columnHeaders?.splice(0, this.columnHeaders.length, ...columnHeaders)), 0);
return fields;
}
@@ -166,8 +166,8 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument)
this.createDashEventsTarget(ele!); //so the whole grid is the drop target?
}
- @computed get onChildClickHandler() { return this.props.childClickScript || ScriptCast(this.Document.onChildClick); }
- @computed get onChildDoubleClickHandler() { return this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); }
+ @computed get onChildClickHandler() { return () => this.props.childClickScript || ScriptCast(this.Document.onChildClick); }
+ @computed get onChildDoubleClickHandler() { return () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); }
addDocTab = (doc: Doc, where: string) => {
if (where === "inPlace" && this.layoutDoc.isInPlaceContainer) {
@@ -211,7 +211,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument)
NativeHeight={returnZero}
NativeWidth={returnZero}
fitToBox={false}
- dontRegisterView={this.props.dontRegisterView}
+ dontRegisterView={BoolCast(this.layoutDoc.dontRegisterChildViews, this.props.dontRegisterView)}
rootSelected={this.rootSelected}
dropAction={StrCast(this.layoutDoc.childDropAction) as dropActionType}
onClick={this.onChildClickHandler}
@@ -252,7 +252,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument)
return wid * aspect;
}
return layoutDoc._fitWidth ? !nh ? this.props.PanelHeight() - 2 * this.yMargin :
- Math.min(wid * NumCast(layoutDoc.scrollHeight, nh) / (nw || 1), this.props.PanelHeight() - 2 * this.yMargin) : layoutDoc[HeightSym]();
+ Math.min(wid * NumCast(layoutDoc.scrollHeight, nh) / (nw || 1), this.props.PanelHeight() - 2 * this.yMargin) : Math.max(20, layoutDoc[HeightSym]());
}
columnDividerDown = (e: React.PointerEvent) => {
@@ -501,4 +501,4 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument)
</div> </div>
);
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 2f4a25bfe..76af70cd1 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -2,7 +2,7 @@ import React = require("react");
import { library } from '@fortawesome/fontawesome-svg-core';
import { faPalette } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, observable, runInAction } from "mobx";
+import { action, observable, runInAction, computed } from "mobx";
import { observer } from "mobx-react";
import { Doc, DocListCast } from "../../../fields/Doc";
import { RichTextField } from "../../../fields/RichTextField";
@@ -279,8 +279,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
const pt = this.props.screenToLocalTransform().inverse().transformPoint(x, y);
ContextMenu.Instance.displayMenu(x, y);
}
-
- render() {
+ @computed get innards() {
TraceMobx();
const cols = this.props.cols();
const key = StrCast(this.props.parent.props.Document._pivotField);
@@ -351,6 +350,43 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
</div> : (null);
for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth / style.numGroupColumns}px `;
const chromeStatus = this.props.parent.props.Document._chromeStatus;
+
+ return <>
+ {this.props.parent.Document._columnsHideIfEmpty ? (null) : headingView}
+ {
+ this.collapsed ? (null) :
+ <div style={{ marginTop: 5 }}>
+ <div key={`${heading}-stack`} className={`collectionStackingView-masonry${singleColumn ? "Single" : "Grid"}`}
+ style={{
+ padding: singleColumn ? `${columnYMargin}px ${0}px ${style.yMargin}px ${0}px` : `${columnYMargin}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.props.parent.children(this.props.docList, uniqueHeadings.length)}
+ {singleColumn ? (null) : this.props.parent.columnDragger}
+ </div>
+ {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ?
+ <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton"
+ style={{ width: style.columnWidth / style.numGroupColumns }}>
+ <EditableView {...newEditableViewProps} menuCallback={this.menuCallback} />
+ </div> : null}
+ </div>
+ }
+ </>;
+ }
+
+
+ render() {
+ TraceMobx();
+ const headings = this.props.headings();
+ const heading = this._heading;
+ const uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
+ const chromeStatus = this.props.parent.props.Document._chromeStatus;
return (
<div className={"collectionStackingViewFieldColumn" + (SnappingManager.GetIsDragging() ? "Dragging" : "")} key={heading}
style={{
@@ -359,31 +395,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
background: this._background
}}
ref={this.createColumnDropRef} onPointerEnter={this.pointerEntered} onPointerLeave={this.pointerLeave}>
- {this.props.parent.Document._columnsHideIfEmpty ? (null) : headingView}
- {
- this.collapsed ? (null) :
- <div style={{ marginTop: 5 }}>
- <div key={`${heading}-stack`} className={`collectionStackingView-masonry${singleColumn ? "Single" : "Grid"}`}
- style={{
- padding: singleColumn ? `${columnYMargin}px ${0}px ${style.yMargin}px ${0}px` : `${columnYMargin}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.props.parent.children(this.props.docList, uniqueHeadings.length)}
- {singleColumn ? (null) : this.props.parent.columnDragger}
- </div>
- {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ?
- <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton"
- style={{ width: style.columnWidth / style.numGroupColumns }}>
- <EditableView {...newEditableViewProps} menuCallback={this.menuCallback} />
- </div> : null}
- </div>
- }
+ {this.innards}
</div >
);
}
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 9dad9a056..dacb06e5b 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -1,4 +1,4 @@
-import { action, computed, IReactionDisposer, reaction } from "mobx";
+import { action, computed, IReactionDisposer, reaction, observable, runInAction } from "mobx";
import { basename } from 'path';
import CursorField from "../../../fields/CursorField";
import { Doc, Opt, Field, DocListCast } from "../../../fields/Doc";
@@ -19,6 +19,7 @@ import { DocComponent } from "../DocComponent";
import { FieldViewProps } from "../nodes/FieldView";
import React = require("react");
import * as rp from 'request-promise';
+import ReactLoading from 'react-loading';
export interface CollectionViewProps extends FieldViewProps {
addDocument: (document: Doc | Doc[]) => boolean;
@@ -54,17 +55,17 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
class CollectionSubView extends DocComponent<X & SubCollectionViewProps, T>(schemaCtor) {
private dropDisposer?: DragManager.DragDropDisposer;
private gestureDisposer?: GestureUtils.GestureEventDisposer;
- protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
+ protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
protected _mainCont?: HTMLDivElement;
protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view
this.dropDisposer?.();
this.gestureDisposer?.();
- this.multiTouchDisposer?.();
+ this._multiTouchDisposer?.();
if (ele) {
this._mainCont = ele;
this.dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc, this.onInternalPreDrop.bind(this));
this.gestureDisposer = GestureUtils.MakeGestureTarget(ele, this.onGesture.bind(this));
- this.multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(ele, this.onTouchStart.bind(this));
+ this._multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(ele, this.onTouchStart.bind(this));
}
}
protected CreateDropTarget(ele: HTMLDivElement) { //used in schema view
@@ -73,7 +74,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
componentWillUnmount() {
this.gestureDisposer?.();
- this.multiTouchDisposer?.();
+ this._multiTouchDisposer?.();
}
@computed get dataDoc() {
@@ -90,6 +91,11 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
// to its children which may be templates.
// If 'annotationField' is specified, then all children exist on that field of the extension document, otherwise, they exist directly on the data document under 'fieldKey'
@computed get dataField() {
+ // sets the dataDoc's data field to an empty list if the data field is undefined - prevents issues with addonly
+ // setTimeout changes it outside of the @computed section
+ setTimeout(() => {
+ if (!this.dataDoc[this.props.annotationsKey || this.props.fieldKey]) this.dataDoc[this.props.annotationsKey || this.props.fieldKey] = new List<Doc>();
+ }, 1000);
return this.dataDoc[this.props.annotationsKey || this.props.fieldKey];
}
@@ -106,16 +112,6 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
[...this.props.docFilters(), ...Cast(this.props.Document._docFilters, listSpec("string"), [])];
}
@computed get childDocs() {
- const docFilters = this.docFilters();
- const docRangeFilters = this.props.ignoreFields?.includes("_docRangeFilters") ? [] : Cast(this.props.Document._docRangeFilters, listSpec("string"), []);
- const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields
- for (let i = 0; i < docFilters.length; i += 3) {
- const [key, value, modifiers] = docFilters.slice(i, i + 3);
- if (!filterFacets[key]) {
- filterFacets[key] = {};
- }
- filterFacets[key][value] = modifiers;
- }
let rawdocs: (Doc | Promise<Doc>)[] = [];
if (this.dataField instanceof Doc) { // if collection data is just a document, then promote it to a singleton list;
@@ -128,6 +124,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
const rootDoc = Cast(this.props.Document.rootDocument, Doc, null);
rawdocs = rootDoc && !this.props.annotationsKey ? [Doc.GetProto(rootDoc)] : [];
}
+
const docs = rawdocs.filter(d => !(d instanceof Promise)).map(d => d as Doc);
const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField);
let childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;
@@ -136,35 +133,10 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
if (searchDocs !== undefined && searchDocs.length > 0) {
childDocs = searchDocs;
}
+ const docFilters = this.docFilters();
+ const docRangeFilters = this.props.ignoreFields?.includes("_docRangeFilters") ? [] : Cast(this.props.Document._docRangeFilters, listSpec("string"), []);
- const filteredDocs = docFilters.length && !this.props.dontRegisterView ? childDocs.filter(d => {
- for (const facetKey of Object.keys(filterFacets)) {
- const facet = filterFacets[facetKey];
- const satisfiesFacet = Object.keys(facet).some(value => {
- if (facet[value] === "match") {
- return d[facetKey] === undefined || Field.toString(d[facetKey] as Field).includes(value);
- }
- return (facet[value] === "x") !== Doc.matchFieldValue(d, facetKey, value);
- });
- if (!satisfiesFacet) {
- return false;
- }
- }
- return true;
- }) : childDocs;
- const rangeFilteredDocs = filteredDocs.filter(d => {
- for (let i = 0; i < docRangeFilters.length; i += 3) {
- const key = docRangeFilters[i];
- const min = Number(docRangeFilters[i + 1]);
- const max = Number(docRangeFilters[i + 2]);
- const val = Cast(d[key], "number", null);
- if (val !== undefined && (val < min || val > max)) {
- return false;
- }
- }
- return true;
- });
- return rangeFilteredDocs;
+ return this.props.Document.dontRegisterView ? docs : DocUtils.FilterDocs(docs, docFilters, docRangeFilters, viewSpecScript);
}
@action
@@ -229,7 +201,11 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
const movedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] === d);
const addedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] !== d);
const res = addedDocs.length ? this.addDocument(addedDocs) : true;
- added = movedDocs.length ? docDragData.moveDocument(movedDocs, this.props.Document, Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.props.Document) || de.embedKey || !this.props.isAnnotationOverlay ? this.addDocument : returnFalse) : res;
+ if (movedDocs.length) {
+ const canAdd = this.props.Document._viewType === CollectionViewType.Pile || de.embedKey || !this.props.isAnnotationOverlay ||
+ Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.props.Document);
+ added = docDragData.moveDocument(movedDocs, this.props.Document, canAdd ? this.addDocument : returnFalse);
+ } else added = res;
} else {
added = this.addDocument(docDragData.droppedDocuments);
}
@@ -254,6 +230,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
const { dataTransfer } = e;
const html = dataTransfer.getData("text/html");
const text = dataTransfer.getData("text/plain");
+ const uriList = dataTransfer.getData("text/uri-list");
if (text && text.startsWith("<div")) {
return;
@@ -325,7 +302,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
const reg = new RegExp(Utils.prepend(""), "g");
const modHtml = srcUrl ? html.replace(reg, srcUrl) : html;
const htmlDoc = Docs.Create.HtmlDocument(modHtml, { ...options, title: "-web page-", _width: 300, _height: 300 });
- Doc.GetProto(htmlDoc)["data-text"] = text;
+ Doc.GetProto(htmlDoc)["data-text"] = Doc.GetProto(htmlDoc)["text"] = text;
this.props.addDocument(htmlDoc);
if (srcWeb) {
const focusNode = (SelectionManager.SelectedDocuments()[0].ContentDiv?.getElementsByTagName("iframe")[0].contentDocument?.getSelection()?.focusNode as any);
@@ -333,7 +310,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
const rect = "getBoundingClientRect" in focusNode ? focusNode.getBoundingClientRect() : focusNode?.parentElement.getBoundingClientRect();
const x = (rect?.x || 0);
const y = NumCast(srcWeb._scrollTop) + (rect?.y || 0);
- const anchor = Docs.Create.FreeformDocument([], { _backgroundColor: "transparent", _width: 25, _height: 25, x, y, annotationOn: srcWeb });
+ const anchor = Docs.Create.FreeformDocument([], { _backgroundColor: "transparent", _width: 75, _height: 40, x, y, annotationOn: srcWeb });
anchor.context = srcWeb;
const key = Doc.LayoutFieldKey(srcWeb);
Doc.AddDocToList(srcWeb, key + "-annotations", anchor);
@@ -346,9 +323,9 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
}
}
- if (text) {
- if (text.includes("www.youtube.com/watch") || text.includes("www.youtube.com/embed")) {
- const url = text.replace("youtube.com/watch?v=", "youtube.com/embed/").split("&")[0];
+ if (uriList || text) {
+ if ((uriList || text).includes("www.youtube.com/watch") || text.includes("www.youtube.com/embed")) {
+ const url = (uriList || text).replace("youtube.com/watch?v=", "youtube.com/embed/").split("&")[0];
this.addDocument(Docs.Create.VideoDocument(url, {
...options,
title: url,
@@ -373,10 +350,20 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
// if ((matches = /(https:\/\/)?photos\.google\.com\/(u\/3\/)?album\/([^\\]+)/g.exec(text)) !== null) {
// const albumId = matches[3];
// const mediaItems = await GooglePhotos.Query.AlbumSearch(albumId);
- // console.log(mediaItems);
// return;
// }
}
+ if (uriList) {
+ this.addDocument(Docs.Create.WebDocument(uriList, {
+ ...options,
+ title: uriList,
+ _width: 400,
+ _height: 315,
+ _nativeWidth: 600,
+ _nativeHeight: 472.5
+ }));
+ return;
+ }
const { items } = e.dataTransfer;
const { length } = items;
@@ -403,7 +390,6 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
file?.type && files.push(file);
file?.type === "application/json" && Utils.readUploadedFileAsText(file).then(result => {
- console.log(result);
const json = JSON.parse(result as string);
this.addDocument(Docs.Create.TreeDocument(
json["rectangular-puzzle"].crossword.clues[0].clue.map((c: any) => {
@@ -417,13 +403,20 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
});
}
}
+ this.slowLoadDocuments(files, options, generatedDocuments, text, completed, e.clientX, e.clientY);
+ batch.end();
+ }
+ slowLoadDocuments = async (files: File[], options: DocumentOptions, generatedDocuments: Doc[], text: string, completed: (() => void) | undefined, clientX: number, clientY: number) => {
+ runInAction(() => CollectionSubViewLoader.Waiting = "block");
+ const disposer = OverlayView.Instance.addElement(
+ <ReactLoading type={"spinningBubbles"} color={"green"} height={250} width={250} />, { x: clientX - 125, y: clientY - 125 });
generatedDocuments.push(...await DocUtils.uploadFilesToDocs(files, options));
if (generatedDocuments.length) {
const set = generatedDocuments.length > 1 && generatedDocuments.map(d => DocUtils.iconify(d));
if (set) {
- this.addDocument(DocUtils.pileup(generatedDocuments, options.x!, options.y!)!);
+ UndoManager.RunInBatch(() => this.addDocument(DocUtils.pileup(generatedDocuments, options.x!, options.y!)!), "drop");
} else {
- generatedDocuments.forEach(this.addDocument);
+ UndoManager.RunInBatch(() => generatedDocuments.forEach(this.addDocument), "drop");
}
completed?.();
} else {
@@ -431,18 +424,24 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
this.addDocument(Docs.Create.TextDocument(text, { ...options, _width: 400, _height: 315 }));
}
}
- batch.end();
+ disposer();
}
}
return CollectionSubView;
}
+export class CollectionSubViewLoader {
+ @observable public static Waiting = "none";
+}
+
import { DragManager, dropActionType } from "../../util/DragManager";
import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents";
import { CurrentUserUtils } from "../../util/CurrentUserUtils";
import { DocumentType } from "../../documents/DocumentTypes";
import { FormattedTextBox, GoogleRef } from "../nodes/formattedText/FormattedTextBox";
-import { CollectionView } from "./CollectionView";
+import { CollectionView, CollectionViewType } from "./CollectionView";
import { SelectionManager } from "../../util/SelectionManager";
+import { OverlayView } from "../OverlayView";
+import { setTimeout } from "timers";
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 620b977fa..705871a6f 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -61,8 +61,8 @@ export interface TreeViewProps {
treeViewHideHeaderFields: () => boolean;
treeViewPreventOpen: boolean;
renderedIds: string[]; // list of document ids rendered used to avoid unending expansion of items in a cycle
- onCheckedClick?: ScriptField;
- onChildClick?: ScriptField;
+ onCheckedClick?: () => ScriptField;
+ onChildClick?: () => ScriptField;
ignoreFields?: string[];
}
@@ -76,7 +76,7 @@ export interface TreeViewProps {
* treeViewExpandedView : name of field whose contents are being displayed as the document's subtree
*/
class TreeView extends React.Component<TreeViewProps> {
- private _editTitleScript: ScriptField | undefined;
+ private _editTitleScript: (() => ScriptField) | undefined;
private _header?: React.RefObject<HTMLDivElement> = React.createRef();
private _treedropDisposer?: DragManager.DragDropDisposer;
private _dref = React.createRef<HTMLDivElement>();
@@ -100,8 +100,8 @@ class TreeView extends React.Component<TreeViewProps> {
childDocList(field: string) {
const layout = Doc.LayoutField(this.doc) instanceof Doc ? Doc.LayoutField(this.doc) as Doc : undefined;
return ((this.props.dataDoc ? DocListCast(this.props.dataDoc[field]) : undefined) || // if there's a data doc for an expanded template, use it's data field
- (layout ? Cast(layout[field], listSpec(Doc)) : undefined) || // else if there's a layout doc, display it's fields
- Cast(this.doc[field], listSpec(Doc))) as Doc[]; // otherwise use the document's data field
+ (layout ? DocListCast(layout[field]) : undefined) || // else if there's a layout doc, display it's fields
+ DocListCast(this.doc[field])) as Doc[]; // otherwise use the document's data field
}
@computed get childDocs() { return this.childDocList(this.fieldKey); }
@computed get childLinks() { return this.childDocList("links"); }
@@ -124,7 +124,8 @@ class TreeView extends React.Component<TreeViewProps> {
constructor(props: any) {
super(props);
- this._editTitleScript = ScriptField.MakeScript(`{setInPlace(self, 'editTitle', '${this._uniqueId}'); selectDoc(self);} `);
+ const script = ScriptField.MakeScript(`{setInPlace(self, 'editTitle', '${this._uniqueId}'); selectDoc(self);} `);
+ this._editTitleScript = script && (() => script);
if (Doc.GetT(this.doc, "editTitle", "string", true) === "*") Doc.SetInPlace(this.doc, "editTitle", this._uniqueId, false);
}
@@ -207,7 +208,7 @@ class TreeView extends React.Component<TreeViewProps> {
if (complete.linkDragData) {
const sourceDoc = complete.linkDragData.linkSourceDocument;
const destDoc = this.doc;
- DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc }, "tree link");
+ DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc }, "tree link", "");
e.stopPropagation();
}
const docDragData = complete.docDragData;
@@ -368,13 +369,13 @@ class TreeView extends React.Component<TreeViewProps> {
}
}
- get onCheckedClick() { return this.props.onCheckedClick || ScriptCast(this.doc.onCheckedClick); }
+ get onCheckedClick() { return this.props.onCheckedClick?.() ?? ScriptCast(this.doc.onCheckedClick); }
@action
bulletClick = (e: React.MouseEvent) => {
if (this.onCheckedClick && this.doc.type !== DocumentType.COL) {
// this.props.document.treeViewChecked = this.props.document.treeViewChecked === "check" ? "x" : this.props.document.treeViewChecked === "x" ? undefined : "check";
- this.onCheckedClick.script.run({
+ this.onCheckedClick?.script.run({
this: this.doc.isTemplateForField && this.props.dataDoc ? this.props.dataDoc : this.doc,
heading: this.props.containingCollection.title,
checked: this.doc.treeViewChecked === "check" ? "x" : this.doc.treeViewChecked === "x" ? undefined : "check",
@@ -404,6 +405,7 @@ class TreeView extends React.Component<TreeViewProps> {
contextMenuItems = () => [{ script: ScriptField.MakeFunction(`DocFocus(self)`)!, label: "Focus" }];
truncateTitleWidth = () => NumCast(this.props.treeViewDoc.treeViewTruncateTitleWidth, 0);
showTitleEdit = () => ["*", this._uniqueId].includes(Doc.GetT(this.doc, "editTitle", "string", true) || "");
+ onChildClick = () => this.props.onChildClick?.() ?? (this._editTitleScript?.() || ScriptCast(this.doc.editTitleScript));
/**
* Renders the EditableView title element for placement into the tree.
*/
@@ -437,7 +439,7 @@ class TreeView extends React.Component<TreeViewProps> {
addDocTab={this.props.addDocTab}
rootSelected={returnTrue}
pinToPres={emptyFunction}
- onClick={this.props.onChildClick || this._editTitleScript}
+ onClick={this.onChildClick}
dropAction={this.props.dropAction}
moveDocument={this.move}
removeDocument={this.removeDoc}
@@ -539,8 +541,8 @@ class TreeView extends React.Component<TreeViewProps> {
treeViewPreventOpen: boolean,
renderedIds: string[],
libraryPath: Doc[] | undefined,
- onCheckedClick: ScriptField | undefined,
- onChildClick: ScriptField | undefined,
+ onCheckedClick: undefined | (() => ScriptField),
+ onChildClick: undefined | (() => ScriptField),
ignoreFields: string[] | undefined
) {
const viewSpecScript = Cast(containingCollection.viewSpecScript, ScriptField);
@@ -658,8 +660,8 @@ class TreeView extends React.Component<TreeViewProps> {
export type collectionTreeViewProps = {
treeViewHideTitle?: boolean;
treeViewHideHeaderFields?: boolean;
- onCheckedClick?: ScriptField;
- onChildClick?: ScriptField;
+ onCheckedClick?: () => ScriptField;
+ onChildClick?: () => ScriptField;
};
@observer
@@ -780,7 +782,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
onClicks.push({
description: "Edit onChecked Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.doc, undefined, "onCheckedClick"), "edit onCheckedClick"), icon: "edit"
});
- !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
+ !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", noexpand: true, subitems: onClicks, icon: "hand-point-right" });
}
outerXf = () => Utils.GetScreenTransform(this._mainEle!);
onTreeDrop = (e: React.DragEvent) => this.onExternalDrop(e, {});
@@ -794,8 +796,8 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
</div >;
}
- onKeyPress = (e: React.KeyboardEvent) => {
- console.log(e);
+ onChildClick = () => {
+ return this.props.onChildClick?.() || ScriptCast(this.doc.onChildClick);
}
render() {
TraceMobx();
@@ -814,7 +816,6 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
paddingTop: `${NumCast(this.doc._yPadding, 20)}px`,
pointerEvents: !this.props.active() && !SnappingManager.GetIsDragging() ? "none" : undefined
}}
- onKeyPress={this.onKeyPress}
onWheel={(e) => this._mainEle && this._mainEle.scrollHeight > this._mainEle.clientHeight && e.stopPropagation()}
onDrop={this.onTreeDrop}
ref={this.createTreeDropTarget}>
@@ -839,7 +840,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform,
this.outerXf, this.props.active, this.props.PanelWidth, this.props.ChromeHeight, this.props.renderDepth, () => this.props.treeViewHideHeaderFields || BoolCast(this.doc.treeViewHideHeaderFields),
BoolCast(this.doc.treeViewPreventOpen), [], this.props.LibraryPath, this.props.onCheckedClick,
- this.props.onChildClick || ScriptCast(this.doc.onChildClick), this.props.ignoreFields)
+ this.onChildClick, this.props.ignoreFields)
}
</ul>
</div >
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index f9dc666d6..d41248a77 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -8,30 +8,31 @@ import * as React from 'react';
import Lightbox from 'react-image-lightbox-with-rotate';
import 'react-image-lightbox-with-rotate/style.css'; // This only needs to be imported once in your app
import { DateField } from '../../../fields/DateField';
-import { AclAddonly, AclReadonly, AclSym, DataSym, Doc, DocListCast, Field, Opt } from '../../../fields/Doc';
+import { AclAddonly, AclReadonly, DataSym, Doc, DocListCast, Field, Opt, AclEdit, AclSym, AclPrivate, AclAdmin } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { ObjectField } from '../../../fields/ObjectField';
+import { RichTextField } from '../../../fields/RichTextField';
import { listSpec } from '../../../fields/Schema';
import { ComputedField, ScriptField } from '../../../fields/ScriptField';
import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
-import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, emptyPath, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils, returnEmptyFilter } from '../../../Utils';
+import { TraceMobx, GetEffectiveAcl, getPlaygroundMode, distributeAcls, SharingPermissions } from '../../../fields/util';
+import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
import { CurrentUserUtils } from '../../util/CurrentUserUtils';
import { ImageUtils } from '../../util/Import & Export/ImageUtils';
import { InteractionUtils } from '../../util/InteractionUtils';
+import { UndoManager } from '../../util/UndoManager';
import { ContextMenu } from "../ContextMenu";
import { FieldView, FieldViewProps } from '../nodes/FieldView';
-import { ScriptBox } from '../ScriptBox';
import { Touchable } from '../Touchable';
-import { CollectionCarouselView } from './CollectionCarouselView';
import { CollectionCarousel3DView } from './CollectionCarousel3DView';
+import { CollectionCarouselView } from './CollectionCarouselView';
import { CollectionDockingView } from "./CollectionDockingView";
-import { AddCustomFreeFormLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines';
import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
+import { CollectionGridView } from './collectionGrid/CollectionGridView';
import { CollectionLinearView } from './CollectionLinearView';
import CollectionMapView from './CollectionMapView';
import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView';
@@ -43,12 +44,8 @@ import { CollectionStaffView } from './CollectionStaffView';
import { SubCollectionViewProps } from './CollectionSubView';
import { CollectionTimeView } from './CollectionTimeView';
import { CollectionTreeView } from "./CollectionTreeView";
-import { CollectionGridView } from './collectionGrid/CollectionGridView';
import './CollectionView.scss';
-import { CollectionViewBaseChrome } from './CollectionViewChromes';
-import { UndoManager } from '../../util/UndoManager';
-import { RichTextField } from '../../../fields/RichTextField';
-import { TextField } from '../../util/ProsemirrorCopy/prompt';
+import { ContextMenuProps } from '../ContextMenuItem';
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -90,6 +87,7 @@ export interface CollectionRenderProps {
active: () => boolean;
whenActiveChanged: (isActive: boolean) => void;
PanelWidth: () => number;
+ PanelHeight: () => number;
ChildLayoutTemplate?: () => Doc;
ChildLayoutString?: string;
}
@@ -105,7 +103,15 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
@observable private static _safeMode = false;
public static SetSafeMode(safeMode: boolean) { this._safeMode = safeMode; }
- protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
+ protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
+
+ private AclMap = new Map<symbol, string>([
+ [AclPrivate, SharingPermissions.None],
+ [AclReadonly, SharingPermissions.View],
+ [AclAddonly, SharingPermissions.Add],
+ [AclEdit, SharingPermissions.Edit],
+ [AclAdmin, SharingPermissions.Admin]
+ ]);
get collectionViewType(): CollectionViewType | undefined {
const viewField = StrCast(this.props.Document._viewType);
@@ -129,38 +135,56 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
if (this.props.filterAddDocument?.(doc) === false) {
return false;
}
+
const docs = doc instanceof Doc ? [doc] : doc;
const targetDataDoc = this.props.Document[DataSym];
const docList = DocListCast(targetDataDoc[this.props.fieldKey]);
const added = docs.filter(d => !docList.includes(d));
+ const effectiveAcl = GetEffectiveAcl(this.props.Document);
+
if (added.length) {
- if (this.dataDoc[AclSym] === AclReadonly) {
+ if (effectiveAcl === AclPrivate || (effectiveAcl === AclReadonly && !getPlaygroundMode())) {
return false;
- } else if (this.dataDoc[AclSym] === AclAddonly) {
- added.map(doc => Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc));
- } else {
- added.map(doc => {
- const context = Cast(doc.context, Doc, null);
- if (context && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG) &&
- !DocListCast(doc.links).some(d => d.isPushpin)) {
- const pushpin = Docs.Create.FontIconDocument({
- icon: "map-pin", x: Cast(doc.x, "number", null), y: Cast(doc.y, "number", null), _backgroundColor: "#0000003d", color: "#ACCEF7",
- _width: 15, _height: 15, _xPadding: 0, isLinkButton: true, displayTimecode: Cast(doc.displayTimecode, "number", null)
- });
- Doc.AddDocToList(context, Doc.LayoutFieldKey(context) + "-annotations", pushpin);
- const pushpinLink = DocUtils.MakeLink({ doc: pushpin }, { doc: doc }, "pushpin");
- const first = DocListCast(pushpin.links).find(d => d instanceof Doc);
- first && (first.hidden = true);
- pushpinLink && (Doc.GetProto(pushpinLink).isPushpin = true);
- doc.displayTimecode = undefined;
- }
- doc.context = this.props.Document;
- });
- added.map(add => Doc.AddDocToList(Cast(Doc.UserDoc().myCatalog, Doc, null), "data", add));
- targetDataDoc[this.props.fieldKey] = new List<Doc>([...docList, ...added]);
- targetDataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now()));
- targetDataDoc["lastModified"] = new DateField(new Date(Date.now()));
+ }
+ else {
+ if (this.props.Document[AclSym]) {
+ // change so it only adds if more restrictive
+ added.forEach(d => {
+ // const dataDoc = d[DataSym];
+ for (const [key, value] of Object.entries(this.props.Document[AclSym])) {
+ distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true);
+ }
+ // dataDoc[AclSym] = d[AclSym] = this.props.Document[AclSym];
+ });
+ }
+ if (effectiveAcl === AclAddonly) {
+ added.map(doc => Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc));
+ }
+ else {
+ added.map(doc => {
+ const context = Cast(doc.context, Doc, null);
+ if (context && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) {
+ const pushpin = Docs.Create.FontIconDocument({
+ title: "pushpin", label: "",
+ icon: "map-pin", x: Cast(doc.x, "number", null), y: Cast(doc.y, "number", null), _backgroundColor: "#0000003d", color: "#ACCEF7",
+ _width: 15, _height: 15, _xPadding: 0, isLinkButton: true, displayTimecode: Cast(doc.displayTimecode, "number", null)
+ });
+ pushpin.isPushpin = true;
+ Doc.GetProto(pushpin).annotationOn = doc.annotationOn;
+ Doc.SetInPlace(doc, "annotationOn", undefined, true);
+ Doc.AddDocToList(context, Doc.LayoutFieldKey(context) + "-annotations", pushpin);
+ const pushpinLink = DocUtils.MakeLink({ doc: pushpin }, { doc: doc }, "pushpin", "");
+ doc.displayTimecode = undefined;
+ }
+ doc.context = this.props.Document;
+ });
+ added.map(add => Doc.AddDocToList(Cast(Doc.UserDoc().myCatalog, Doc, null), "data", add));
+ targetDataDoc[this.props.fieldKey] = new List<Doc>([...docList, ...added]);
+ targetDataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now()));
+ targetDataDoc["lastModified"] = new DateField(new Date(Date.now()));
+
+ }
}
}
return true;
@@ -168,13 +192,16 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
@action.bound
removeDocument = (doc: any): boolean => {
- const docs = doc instanceof Doc ? [doc] : doc as Doc[];
- const targetDataDoc = this.props.Document[DataSym];
- const value = DocListCast(targetDataDoc[this.props.fieldKey]);
- const result = value.filter(v => !docs.includes(v));
- if (result.length !== value.length) {
- targetDataDoc[this.props.fieldKey] = new List<Doc>(result);
- return true;
+ const effectiveAcl = GetEffectiveAcl(this.props.Document);
+ if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin || getPlaygroundMode()) {
+ const docs = doc instanceof Doc ? [doc] : doc as Doc[];
+ const targetDataDoc = this.props.Document[DataSym];
+ const value = DocListCast(targetDataDoc[this.props.fieldKey]);
+ const result = value.filter(v => !docs.includes(v));
+ if (result.length !== value.length) {
+ targetDataDoc[this.props.fieldKey] = new List<Doc>(result);
+ return true;
+ }
}
return false;
}
@@ -182,7 +209,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
// this is called with the document that was dragged and the collection to move it into.
// if the target collection is the same as this collection, then the move will be allowed.
// otherwise, the document being moved must be able to be removed from its container before
- // moving it into the target.
+ // moving it into the target.
@action.bound
moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => {
if (Doc.AreProtosEqual(this.props.Document, targetCollection)) {
@@ -204,7 +231,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
showIsTagged = () => {
return (null);
- // this section would display an icon in the bototm right of a collection to indicate that all
+ // this section would display an icon in the bototm right of a collection to indicate that all
// photos had been processed through Google's content analysis API and Google's tags had been
// assigned to the documents googlePhotosTags field.
// const children = DocListCast(this.props.Document[this.props.fieldKey]);
@@ -237,23 +264,14 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
}
}
- @action
- private collapse = (value: boolean) => {
- this.props.Document._chromeStatus = value ? "collapsed" : "enabled";
- }
-
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
- const chrome = this.props.Document._chromeStatus === "disabled" || this.props.Document._chromeStatus === "replaced" || type === CollectionViewType.Docking ? (null) :
- <CollectionViewBaseChrome key="chrome" CollectionView={this} PanelWidth={this.bodyPanelWidth} type={type} collapse={this.collapse} />;
- return <>{chrome} {this.SubViewHelper(type, renderProps)}</>;
+ return this.SubViewHelper(type, renderProps);
}
setupViewTypes(category: string, func: (viewType: CollectionViewType) => Doc, addExtras: boolean) {
- const existingVm = ContextMenu.Instance.findByDescription(category);
- const subItems = existingVm && "subitems" in existingVm ? existingVm.subitems : [];
+ const subItems: ContextMenuProps[] = [];
subItems.push({ description: "Freeform", event: () => func(CollectionViewType.Freeform), icon: "signature" });
if (addExtras && CollectionView._safeMode) {
ContextMenu.Instance.addItem({ description: "Test Freeform", event: () => func(CollectionViewType.Invalid), icon: "project-diagram" });
@@ -271,58 +289,62 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
subItems.push({ description: "Pivot/Time", event: () => func(CollectionViewType.Time), icon: "columns" });
subItems.push({ description: "Map", event: () => func(CollectionViewType.Map), icon: "globe-americas" });
subItems.push({ description: "Grid", event: () => func(CollectionViewType.Grid), icon: "th-list" });
- if (addExtras && this.props.Document._viewType === CollectionViewType.Freeform) {
- subItems.push({ description: "Custom", icon: "fingerprint", event: AddCustomFreeFormLayout(this.props.Document, this.props.fieldKey) });
- }
addExtras && subItems.push({ description: "lightbox", event: action(() => this._isLightboxOpen = true), icon: "eye" });
- !existingVm && ContextMenu.Instance.addItem({ description: category, subitems: subItems, icon: "eye" });
+
+ const existingVm = ContextMenu.Instance.findByDescription(category);
+ const catItems = existingVm && "subitems" in existingVm ? existingVm.subitems : [];
+ catItems.push({ description: "Add a Perspective...", addDivider: true, noexpand: true, subitems: subItems, icon: "eye" });
+ !existingVm && ContextMenu.Instance.addItem({ description: category, subitems: catItems, icon: "eye" });
}
onContextMenu = (e: React.MouseEvent): void => {
- if (!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
- this.setupViewTypes("Add a Perspective...", vtype => {
+ const cm = ContextMenu.Instance;
+ if (cm && !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
+ this.setupViewTypes("UI Controls...", vtype => {
const newRendition = Doc.MakeAlias(this.props.Document);
newRendition._viewType = vtype;
this.props.addDocTab(newRendition, "onRight");
return newRendition;
}, false);
- const existing = ContextMenu.Instance.findByDescription("Options...");
- const layoutItems = existing && "subitems" in existing ? existing.subitems : [];
- layoutItems.push({ description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" });
+ const options = cm.findByDescription("Options...");
+ const optionItems = options && "subitems" in options ? options.subitems : [];
+ optionItems.splice(0, 0, { description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" });
if (this.props.Document.childLayout instanceof Doc) {
- layoutItems.push({ description: "View Child Layout", event: () => this.props.addDocTab(this.props.Document.childLayout as Doc, "onRight"), icon: "project-diagram" });
+ optionItems.push({ description: "View Child Layout", event: () => this.props.addDocTab(this.props.Document.childLayout as Doc, "onRight"), icon: "project-diagram" });
}
if (this.props.Document.childClickedOpenTemplateView instanceof Doc) {
- layoutItems.push({ description: "View Child Detailed Layout", event: () => this.props.addDocTab(this.props.Document.childClickedOpenTemplateView as Doc, "onRight"), icon: "project-diagram" });
+ optionItems.push({ description: "View Child Detailed Layout", event: () => this.props.addDocTab(this.props.Document.childClickedOpenTemplateView as Doc, "onRight"), icon: "project-diagram" });
}
- layoutItems.push({ description: `${this.props.Document.isInPlaceContainer ? "Unset" : "Set"} inPlace Container`, event: () => this.props.Document.isInPlaceContainer = !this.props.Document.isInPlaceContainer, icon: "project-diagram" });
+ !Doc.UserDoc().noviceMode && optionItems.push({ description: `${this.props.Document.isInPlaceContainer ? "Unset" : "Set"} inPlace Container`, event: () => this.props.Document.isInPlaceContainer = !this.props.Document.isInPlaceContainer, icon: "project-diagram" });
- !existing && ContextMenu.Instance.addItem({ description: "Options...", subitems: layoutItems, icon: "hand-point-right" });
+ !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "hand-point-right" });
- const existingOnClick = ContextMenu.Instance.findByDescription("OnClick...");
+ const existingOnClick = cm.findByDescription("OnClick...");
const onClicks = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : [];
const funcs = [
{ key: "onChildClick", name: "On Child Clicked" },
{ key: "onChildDoubleClick", name: "On Child Double Clicked" }];
funcs.map(func => onClicks.push({
description: `Edit ${func.name} script`, icon: "edit", event: (obj: any) => {
- ScriptBox.EditButtonScript(func.name + "...", this.props.Document, func.key, obj.x, obj.y, { thisContainer: Doc.name });
+ const alias = Doc.MakeAlias(this.props.Document);
+ DocUtils.makeCustomViewClicked(alias, undefined, func.key);
+ this.props.addDocTab(alias, "onRight");
}
}));
DocListCast(Cast(Doc.UserDoc()["clickFuncs-child"], Doc, null).data).forEach(childClick =>
onClicks.push({
description: `Set child ${childClick.title}`,
icon: "edit",
- event: () => this.props.Document[StrCast(childClick.targetScriptKey)] = ObjectField.MakeCopy(ScriptCast(childClick.data)),
+ event: () => Doc.GetProto(this.props.Document)[StrCast(childClick.targetScriptKey)] = ObjectField.MakeCopy(ScriptCast(childClick.data)),
}));
- !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
+ !existingOnClick && cm.addItem({ description: "OnClick...", noexpand: true, subitems: onClicks, icon: "hand-point-right" });
if (!Doc.UserDoc().noviceMode) {
- const more = ContextMenu.Instance.findByDescription("More...");
+ const more = cm.findByDescription("More...");
const moreItems = more && "subitems" in more ? more.subitems : [];
moreItems.push({ description: "Export Image Hierarchy", icon: "columns", event: () => ImageUtils.ExportHierarchyToFileSystem(this.props.Document) });
- !more && ContextMenu.Instance.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" });
+ !more && cm.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" });
}
}
}
@@ -472,7 +494,8 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
get ignoreFields() { return ["_docFilters", "_docRangeFilters"]; } // this makes the tree view collection ignore these filters (otherwise, the filters would filter themselves)
@computed get scriptField() {
const scriptText = "setDocFilter(containingTreeView, heading, this.title, checked)";
- return ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name });
+ const script = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name });
+ return script ? () => script : undefined;
}
@computed get filterView() {
TraceMobx();
@@ -498,6 +521,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
</div>
<div className="collectionTimeView-tree" key="tree">
<CollectionTreeView
+ PanelPosition={""}
Document={facetCollection}
DataDoc={facetCollection}
fieldKey={`${this.props.fieldKey}-filter`}
@@ -525,7 +549,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
ContentScaling={returnOne}
focus={returnFalse}
treeViewHideHeaderFields={true}
- onCheckedClick={this.scriptField!}
+ onCheckedClick={this.scriptField}
ignoreFields={this.ignoreFields}
annotationsKey={""}
dontRegisterView={true}
@@ -548,6 +572,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
active: this.active,
whenActiveChanged: this.whenActiveChanged,
PanelWidth: this.bodyPanelWidth,
+ PanelHeight: this.props.PanelHeight,
ChildLayoutTemplate: this.childLayoutTemplate,
ChildLayoutString: this.childLayoutString,
};
@@ -565,7 +590,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
Utils.CorsProxy(Cast(d.data, ImageField)!.url.href) : Cast(d.data, ImageField)!.url.href
:
""))}
- {(!this.props.isSelected() || this.props.Document.hideFilterView) && !this.props.Document.forceActive ? (null) :
+ {(!this.props.isSelected() && !this.props.Document.forceActive) || this.props.Document.hideFilterView ? (null) :
<div className="collectionView-filterDragger" title="library View Dragger" onPointerDown={this.onPointerDown}
style={{ right: this.facetWidth() - 1, top: this.props.Document._viewType === CollectionViewType.Docking ? "25%" : "55%" }} />
}
@@ -573,5 +598,3 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
</div>);
}
}
-
-
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
deleted file mode 100644
index 4e91a2928..000000000
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ /dev/null
@@ -1,800 +0,0 @@
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, observable, runInAction, Lambda } from "mobx";
-import { observer } from "mobx-react";
-import * as React from "react";
-import { Doc, DocListCast, Opt } from "../../../fields/Doc";
-import { Id } from "../../../fields/FieldSymbols";
-import { List } from "../../../fields/List";
-import { listSpec } from "../../../fields/Schema";
-import { BoolCast, Cast, NumCast, StrCast } from "../../../fields/Types";
-import { Utils, emptyFunction, setupMoveUpEvents } from "../../../Utils";
-import { DragManager } from "../../util/DragManager";
-import { undoBatch } from "../../util/UndoManager";
-import { EditableView } from "../EditableView";
-import { COLLECTION_BORDER_WIDTH } from "../globalCssVariables.scss";
-import { CollectionViewType } from "./CollectionView";
-import { CollectionView } from "./CollectionView";
-import "./CollectionViewChromes.scss";
-import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView";
-
-interface CollectionViewChromeProps {
- CollectionView: CollectionView;
- type: CollectionViewType;
- collapse?: (value: boolean) => any;
- PanelWidth: () => number;
-}
-
-interface Filter {
- key: string;
- value: string;
- contains: boolean;
-}
-
-const stopPropagation = (e: React.SyntheticEvent) => e.stopPropagation();
-
-@observer
-export class CollectionViewBaseChrome extends React.Component<CollectionViewChromeProps> {
- //(!)?\(\(\(doc.(\w+) && \(doc.\w+ as \w+\).includes\(\"(\w+)\"\)
-
- get target() { return this.props.CollectionView.props.Document; }
- _templateCommand = {
- params: ["target", "source"], title: "=> item view",
- script: "this.target.childLayout = getDocTemplate(this.source?.[0])",
- immediate: undoBatch((source: Doc[]) => source.length && (this.target.childLayout = Doc.getDocTemplate(source?.[0]))),
- initialize: emptyFunction,
- };
- _narrativeCommand = {
- params: ["target", "source"], title: "=> child click view",
- script: "this.target.childClickedOpenTemplateView = getDocTemplate(this.source?.[0])",
- immediate: undoBatch((source: Doc[]) => source.length && (this.target.childClickedOpenTemplateView = Doc.getDocTemplate(source?.[0]))),
- initialize: emptyFunction,
- };
- _contentCommand = {
- params: ["target", "source"], title: "=> clear content",
- script: "getProto(this.target).data = copyField(this.source);",
- immediate: undoBatch((source: Doc[]) => Doc.GetProto(this.target).data = new List<Doc>(source)), // Doc.aliasDocs(source),
- initialize: emptyFunction,
- };
- _viewCommand = {
- params: ["target"], title: "=> reset view",
- script: "this.target._panX = this.restoredPanX; this.target._panY = this.restoredPanY; this.target.scale = this.restoredScale;",
- immediate: undoBatch((source: Doc[]) => { this.target._panX = 0; this.target._panY = 0; this.target.scale = 1; }),
- initialize: (button: Doc) => { button.restoredPanX = this.target._panX; button.restoredPanY = this.target._panY; button.restoredScale = this.target.scale; },
- };
- _clusterCommand = {
- params: ["target"], title: "=> fit content",
- script: "this.target._fitToBox = !this.target._fitToBox;",
- immediate: undoBatch((source: Doc[]) => this.target._fitToBox = !this.target._fitToBox),
- initialize: emptyFunction
- };
- _fitContentCommand = {
- params: ["target"], title: "=> toggle clusters",
- script: "this.target.useClusters = !this.target.useClusters;",
- immediate: undoBatch((source: Doc[]) => this.target.useClusters = !this.target.useClusters),
- initialize: emptyFunction
- };
-
- _freeform_commands = [this._viewCommand, this._fitContentCommand, this._clusterCommand, this._contentCommand, this._templateCommand, this._narrativeCommand];
- _stacking_commands = [this._contentCommand, this._templateCommand];
- _masonry_commands = [this._contentCommand, this._templateCommand];
- _schema_commands = [this._templateCommand, this._narrativeCommand];
- _tree_commands = [];
- private get _buttonizableCommands() {
- switch (this.props.type) {
- case CollectionViewType.Tree: return this._tree_commands;
- case CollectionViewType.Schema: return this._schema_commands;
- case CollectionViewType.Stacking: return this._stacking_commands;
- case CollectionViewType.Masonry: return this._stacking_commands;
- case CollectionViewType.Freeform: return this._freeform_commands;
- case CollectionViewType.Time: return this._freeform_commands;
- case CollectionViewType.Carousel: return this._freeform_commands;
- case CollectionViewType.Carousel3D: return this._freeform_commands;
- }
- return [];
- }
- private _picker: any;
- private _commandRef = React.createRef<HTMLInputElement>();
- private _viewRef = React.createRef<HTMLInputElement>();
- @observable private _currentKey: string = "";
-
- componentDidMount = action(() => {
- this._currentKey = this._currentKey || (this._buttonizableCommands.length ? this._buttonizableCommands[0]?.title : "");
- // chrome status is one of disabled, collapsed, or visible. this determines initial state from document
- switch (this.props.CollectionView.props.Document._chromeStatus) {
- case "disabled":
- throw new Error("how did you get here, if chrome status is 'disabled' on a collection, a chrome shouldn't even be instantiated!");
- case "collapsed":
- this.props.collapse?.(true);
- break;
- }
- });
-
- @undoBatch
- viewChanged = (e: React.ChangeEvent) => {
- //@ts-ignore
- this.document._viewType = e.target.selectedOptions[0].value;
- }
-
- commandChanged = (e: React.ChangeEvent) => {
- //@ts-ignore
- runInAction(() => this._currentKey = e.target.selectedOptions[0].value);
- }
-
- @action
- toggleViewSpecs = (e: React.SyntheticEvent) => {
- this.document._facetWidth = this.document._facetWidth ? 0 : 200;
- e.stopPropagation();
- }
-
- @action closeViewSpecs = () => {
- this.document._facetWidth = 0;
- }
-
- // @action
- // openDatePicker = (e: React.PointerEvent) => {
- // 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();
- // }
- // }
-
- // <input className="collectionViewBaseChrome-viewSpecsMenu-rowRight"
- // id={Utils.GenerateGuid()}
- // ref={this.datePickerRef}
- // value={this._dateValue instanceof Date ? this._dateValue.toLocaleDateString() : this._dateValue}
- // onChange={(e) => runInAction(() => this._dateValue = e.target.value)}
- // onPointerDown={this.openDatePicker}
- // placeholder="Value" />
- // @action.bound
- // applyFilter = (e: React.MouseEvent) => {
- // const keyRestrictionScript = "(" + this._keyRestrictions.map(i => i[1]).filter(i => i.length > 0).join(" && ") + ")";
- // const yearOffset = this._dateWithinValue[1] === 'y' ? 1 : 0;
- // const monthOffset = this._dateWithinValue[1] === 'm' ? parseInt(this._dateWithinValue[0]) : 0;
- // const weekOffset = this._dateWithinValue[1] === 'w' ? parseInt(this._dateWithinValue[0]) : 0;
- // const dayOffset = (this._dateWithinValue[1] === 'd' ? parseInt(this._dateWithinValue[0]) : 0) + weekOffset * 7;
- // let dateRestrictionScript = "";
- // if (this._dateValue instanceof Date) {
- // const lowerBound = new Date(this._dateValue.getFullYear() - yearOffset, this._dateValue.getMonth() - monthOffset, this._dateValue.getDate() - dayOffset);
- // const 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 {
- // const createdDate = new Date(this._dateValue);
- // if (!isNaN(createdDate.getTime())) {
- // const lowerBound = new Date(createdDate.getFullYear() - yearOffset, createdDate.getMonth() - monthOffset, createdDate.getDate() - dayOffset);
- // const 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()})`;
- // }
- // }
- // const fullScript = dateRestrictionScript.length || keyRestrictionScript.length ? dateRestrictionScript.length ?
- // `${dateRestrictionScript} ${keyRestrictionScript.length ? "&&" : ""} (${keyRestrictionScript})` :
- // `(${keyRestrictionScript}) ${dateRestrictionScript.length ? "&&" : ""} ${dateRestrictionScript}` :
- // "true";
-
- // this.props.CollectionView.props.Document.viewSpecScript = ScriptField.MakeFunction(fullScript, { doc: Doc.name });
- // }
-
- // datePickerRef = (node: HTMLInputElement) => {
- // if (node) {
- // try {
- // this._picker = datepicker("#" + node.id, {
- // disabler: (date: Date) => date > new Date(),
- // onSelect: (instance: any, date: Date) => runInAction(() => {}), // this._dateValue = date),
- // dateSelected: new Date()
- // });
- // } catch (e) {
- // console.log("date picker exception:" + e);
- // }
- // }
- // }
-
-
- @action
- toggleCollapse = () => {
- this.document._chromeStatus = this.document._chromeStatus === "enabled" ? "collapsed" : "enabled";
- if (this.props.collapse) {
- this.props.collapse(this.props.CollectionView.props.Document._chromeStatus !== "enabled");
- }
- }
-
- @computed get subChrome() {
- const collapsed = this.document._chromeStatus !== "enabled";
- if (collapsed) return null;
- switch (this.props.type) {
- case CollectionViewType.Freeform: return (<CollectionFreeFormViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />);
- case CollectionViewType.Stacking: return (<CollectionStackingViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />);
- case CollectionViewType.Schema: return (<CollectionSchemaViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />);
- case CollectionViewType.Tree: return (<CollectionTreeViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />);
- case CollectionViewType.Masonry: return (<CollectionStackingViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />);
- case CollectionViewType.Carousel3D: return (<Collection3DCarouselViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />);
- case CollectionViewType.Grid: return (<CollectionGridViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />);
- default: return null;
- }
- }
-
- private get document() {
- return this.props.CollectionView.props.Document;
- }
-
- private dropDisposer?: DragManager.DragDropDisposer;
- protected createDropTarget = (ele: HTMLDivElement) => {
- this.dropDisposer?.();
- if (ele) {
- this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.document);
- }
- }
-
- @undoBatch
- @action
- protected drop(e: Event, de: DragManager.DropEvent): boolean {
- const docDragData = de.complete.docDragData;
- if (docDragData?.draggedDocuments.length) {
- this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => c.immediate(docDragData.draggedDocuments || []));
- e.stopPropagation();
- }
- return true;
- }
-
- dragViewDown = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, (e, down, delta) => {
- const vtype = this.props.CollectionView.collectionViewType;
- const c = {
- params: ["target"], title: vtype,
- script: `this.target._viewType = '${StrCast(this.props.CollectionView.props.Document._viewType)}'`,
- immediate: (source: Doc[]) => this.props.CollectionView.props.Document._viewType = Doc.getDocTemplate(source?.[0]),
- initialize: emptyFunction,
- };
- DragManager.StartButtonDrag([this._viewRef.current!], c.script, StrCast(c.title),
- { target: this.props.CollectionView.props.Document }, c.params, c.initialize, e.clientX, e.clientY);
- return true;
- }, emptyFunction, emptyFunction);
- }
- dragCommandDown = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, (e, down, delta) => {
- this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c =>
- DragManager.StartButtonDrag([this._commandRef.current!], c.script, c.title,
- { target: this.props.CollectionView.props.Document }, c.params, c.initialize, e.clientX, e.clientY));
- return true;
- }, emptyFunction, () => {
- this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => c.immediate([]));
- });
- }
-
- @computed get templateChrome() {
- const collapsed = this.props.CollectionView.props.Document._chromeStatus !== "enabled";
- return <div className="collectionViewBaseChrome-template" ref={this.createDropTarget} style={{ display: collapsed ? "none" : undefined }}>
- <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._commandRef} onPointerDown={this.dragCommandDown}>
- <div className="commandEntry-drop">
- <FontAwesomeIcon icon="bullseye" size="2x" />
- </div>
- <select
- className="collectionViewBaseChrome-cmdPicker" onPointerDown={stopPropagation} onChange={this.commandChanged} value={this._currentKey}
- style={{ width: this.props.PanelWidth() < 300 ? 15 : undefined }}>
- <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={"empty"} value={""} />
- {this._buttonizableCommands.map(cmd =>
- <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={cmd.title} value={cmd.title}>{cmd.title}</option>
- )}
- </select>
- </div>
- </div>;
- }
-
- @computed get viewModes() {
- const collapsed = this.props.CollectionView.props.Document._chromeStatus !== "enabled";
- return <div className="collectionViewBaseChrome-viewModes" style={{ display: collapsed ? "none" : undefined }}>
- <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._viewRef} onPointerDown={this.dragViewDown}>
- <div className="commandEntry-drop">
- <FontAwesomeIcon icon="bullseye" size="2x" />
- </div>
- <select
- className="collectionViewBaseChrome-viewPicker" style={{ width: this.props.PanelWidth() < 300 ? 15 : undefined }}
- onPointerDown={stopPropagation}
- onChange={this.viewChanged}
- value={StrCast(this.props.CollectionView.props.Document._viewType)}>
- {Object.values(CollectionViewType).map(type => ["invalid", "docking"].includes(type) ? (null) : (
- <option
- key={Utils.GenerateGuid()}
- className="collectionViewBaseChrome-viewOption"
- onPointerDown={stopPropagation}
- value={type}>
- {type[0].toUpperCase() + type.substring(1)}
- </option>
- ))}
- </select>
- </div>
- </div>;
- }
-
- render() {
- const collapsed = this.props.CollectionView.props.Document._chromeStatus !== "enabled";
- const scale = Math.min(1, this.props.CollectionView.props.ScreenToLocalTransform().Scale);
- return (
- <div className="collectionViewChrome-cont" style={{
- top: collapsed ? -70 : 0, height: collapsed ? 0 : undefined,
- transform: collapsed ? "" : `scale(${scale})`,
- width: `${this.props.PanelWidth() / scale}px`
- }}>
- <div className="collectionViewChrome" style={{ border: "unset", pointerEvents: collapsed ? "none" : undefined }}>
- <div className="collectionViewBaseChrome">
- <button className="collectionViewBaseChrome-collapse"
- style={{
- top: collapsed ? 70 : 10,
- transform: `rotate(${collapsed ? 180 : 0}deg) scale(0.5) translate(${collapsed ? "-100%, -100%" : "0, 0"})`,
- opacity: 0.9,
- display: (collapsed && !this.props.CollectionView.props.isSelected()) ? "none" : undefined,
- left: (collapsed ? 0 : "unset"),
- }}
- title="Collapse collection chrome" onClick={this.toggleCollapse}>
- <FontAwesomeIcon icon="caret-up" size="2x" />
- </button>
- {this.viewModes}
- <div className="collectionViewBaseChrome-viewSpecs" title="filter documents to show" style={{ display: collapsed ? "none" : "grid" }}>
- <div className="collectionViewBaseChrome-filterIcon" onPointerDown={this.toggleViewSpecs} >
- <FontAwesomeIcon icon="filter" size="2x" />
- </div>
- </div>
- {this.templateChrome}
- </div>
- {this.subChrome}
- </div>
- </div>
- );
- }
-}
-
-@observer
-export class CollectionFreeFormViewChrome extends React.Component<CollectionViewChromeProps> {
-
- get Document() { return this.props.CollectionView.props.Document; }
- @computed get dataField() {
- return this.props.CollectionView.props.Document[Doc.LayoutFieldKey(this.props.CollectionView.props.Document)];
- }
- @computed get childDocs() {
- return DocListCast(this.dataField);
- }
- @undoBatch
- @action
- nextKeyframe = (): void => {
- const currentFrame = NumCast(this.Document.currentFrame);
- if (currentFrame === undefined) {
- this.Document.currentFrame = 0;
- CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
- }
- CollectionFreeFormDocumentView.updateKeyframe(this.childDocs, currentFrame || 0);
- this.Document.currentFrame = Math.max(0, (currentFrame || 0) + 1);
- this.Document.lastFrame = Math.max(NumCast(this.Document.currentFrame), NumCast(this.Document.lastFrame));
- }
- @undoBatch
- @action
- prevKeyframe = (): void => {
- const currentFrame = NumCast(this.Document.currentFrame);
- if (currentFrame === undefined) {
- this.Document.currentFrame = 0;
- CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
- }
- CollectionFreeFormDocumentView.gotoKeyframe(this.childDocs.slice());
- this.Document.currentFrame = Math.max(0, (currentFrame || 0) - 1);
- }
- render() {
- return this.Document.isAnnotationOverlay ? (null) :
- <div className="collectionFreeFormViewChrome-cont">
- <div key="back" title="back frame" className="backKeyframe" onClick={this.prevKeyframe}>
- <FontAwesomeIcon icon={"caret-left"} size={"lg"} />
- </div>
- <div key="num" title="toggle view all" className="numKeyframe" style={{ backgroundColor: this.Document.editing ? "#759c75" : "#c56565" }}
- onClick={action(() => this.Document.editing = !this.Document.editing)} >
- {NumCast(this.Document.currentFrame)}
- </div>
- <div key="fwd" title="forward frame" className="fwdKeyframe" onClick={this.nextKeyframe}>
- <FontAwesomeIcon icon={"caret-right"} size={"lg"} />
- </div>
- </div>;
- }
-}
-
-@observer
-export class CollectionStackingViewChrome extends React.Component<CollectionViewChromeProps> {
- @observable private _currentKey: string = "";
- @observable private suggestions: string[] = [];
-
- @computed private get descending() { return StrCast(this.props.CollectionView.props.Document._columnsSort) === "descending"; }
- @computed get pivotField() { return StrCast(this.props.CollectionView.props.Document._pivotField); }
-
- getKeySuggestions = async (value: string): Promise<string[]> => {
- value = value.toLowerCase();
- const docs = DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey]);
- 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 = [];
- }
-
- @action
- setValue = (value: string) => {
- this.props.CollectionView.props.Document._pivotField = value;
- return true;
- }
-
- @action toggleSort = () => {
- this.props.CollectionView.props.Document._columnsSort =
- this.props.CollectionView.props.Document._columnsSort === "descending" ? "ascending" :
- this.props.CollectionView.props.Document._columnsSort === "ascending" ? undefined : "descending";
- }
- @action resetValue = () => { this._currentKey = this.pivotField; };
-
- render() {
- return (
- <div className="collectionStackingViewChrome-cont">
- <div className="collectionStackingViewChrome-pivotField-cont">
- <div className="collectionStackingViewChrome-pivotField-label">
- GROUP BY:
- </div>
- <div className="collectionStackingViewChrome-sortIcon" onClick={this.toggleSort} style={{ transform: `rotate(${this.descending ? "180" : "0"}deg)` }}>
- <FontAwesomeIcon icon="caret-up" size="2x" color="white" />
- </div>
- <div className="collectionStackingViewChrome-pivotField">
- <EditableView
- GetValue={() => this.pivotField}
- 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.pivotField ? this.pivotField : "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 = () => {
- const dividerWidth = 4;
- const borderWidth = Number(COLLECTION_BORDER_WIDTH);
- const panelWidth = this.props.CollectionView.props.PanelWidth();
- const previewWidth = NumCast(this.props.CollectionView.props.Document.schemaPreviewWidth);
- const tableWidth = panelWidth - 2 * borderWidth - dividerWidth - previewWidth;
- this.props.CollectionView.props.Document.schemaPreviewWidth = previewWidth === 0 ? Math.min(tableWidth / 3, 200) : 0;
- }
-
- @undoBatch
- @action
- toggleTextwrap = async () => {
- const textwrappedRows = Cast(this.props.CollectionView.props.Document.textwrappedSchemaRows, listSpec("string"), []);
- if (textwrappedRows.length) {
- this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>([]);
- } else {
- const docs = DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey]);
- const allRows = docs instanceof Doc ? [docs[Id]] : docs.map(doc => doc[Id]);
- this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>(allRows);
- }
- }
-
-
- render() {
- const previewWidth = NumCast(this.props.CollectionView.props.Document.schemaPreviewWidth);
- const 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">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 >
- );
- }
-}
-
-@observer
-export class CollectionTreeViewChrome extends React.Component<CollectionViewChromeProps> {
-
- get sortAscending() {
- return this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey + "-sortAscending"];
- }
- set sortAscending(value) {
- this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey + "-sortAscending"] = value;
- }
- @computed private get ascending() {
- return Cast(this.sortAscending, "boolean", null);
- }
-
- @action toggleSort = () => {
- if (this.sortAscending) this.sortAscending = undefined;
- else if (this.sortAscending === undefined) this.sortAscending = false;
- else this.sortAscending = true;
- }
-
- render() {
- return (
- <div className="collectionTreeViewChrome-cont">
- <button className="collectionTreeViewChrome-sort" onClick={this.toggleSort}>
- <div className="collectionTreeViewChrome-sortLabel">
- Sort
- </div>
- <div className="collectionTreeViewChrome-sortIcon" style={{ transform: `rotate(${this.ascending === undefined ? "90" : this.ascending ? "180" : "0"}deg)` }}>
- <FontAwesomeIcon icon="caret-up" size="2x" color="white" />
- </div>
- </button>
- </div>
- );
- }
-}
-
-// Enter scroll speed for 3D Carousel
-@observer
-export class Collection3DCarouselViewChrome extends React.Component<CollectionViewChromeProps> {
- @computed get scrollSpeed() {
- return this.props.CollectionView.props.Document._autoScrollSpeed;
- }
-
- @action
- setValue = (value: string) => {
- const numValue = Number(StrCast(value));
- if (numValue > 0) {
- this.props.CollectionView.props.Document._autoScrollSpeed = numValue;
- return true;
- }
- return false;
- }
-
- render() {
- return (
- <div className="collection3DCarouselViewChrome-cont">
- <div className="collection3DCarouselViewChrome-scrollSpeed-cont">
- <div className="collectionStackingViewChrome-scrollSpeed-label">
- AUTOSCROLL SPEED:
- </div>
- <div className="collection3DCarouselViewChrome-scrollSpeed">
- <EditableView
- GetValue={() => StrCast(this.scrollSpeed)}
- oneLine
- SetValue={this.setValue}
- contents={this.scrollSpeed ? this.scrollSpeed : 1000} />
- </div>
- </div>
- </div>
- );
- }
-}
-
-/**
- * Chrome for grid view.
- */
-@observer
-export class CollectionGridViewChrome extends React.Component<CollectionViewChromeProps> {
-
- private clicked: boolean = false;
- private entered: boolean = false;
- private decrementLimitReached: boolean = false;
- @observable private resize = false;
- private resizeListenerDisposer: Opt<Lambda>;
-
- componentDidMount() {
-
- runInAction(() => this.resize = this.props.CollectionView.props.PanelWidth() < 700);
-
- // listener to reduce text on chrome resize (panel resize)
- this.resizeListenerDisposer = computed(() => this.props.CollectionView.props.PanelWidth()).observe(({ newValue }) => {
- runInAction(() => this.resize = newValue < 700);
- });
- }
-
- componentWillUnmount() {
- this.resizeListenerDisposer?.();
- }
-
- get numCols() { return NumCast(this.props.CollectionView.props.Document.gridNumCols, 10); }
-
- /**
- * Sets the value of `numCols` on the grid's Document to the value entered.
- */
- @undoBatch
- onNumColsEnter = (e: React.KeyboardEvent<HTMLInputElement>) => {
- if (e.key === "Enter" || e.key === "Tab") {
- if (e.currentTarget.valueAsNumber > 0) {
- this.props.CollectionView.props.Document.gridNumCols = e.currentTarget.valueAsNumber;
- }
-
- }
- }
-
- /**
- * Sets the value of `rowHeight` on the grid's Document to the value entered.
- */
- // @undoBatch
- // onRowHeightEnter = (e: React.KeyboardEvent<HTMLInputElement>) => {
- // if (e.key === "Enter" || e.key === "Tab") {
- // if (e.currentTarget.valueAsNumber > 0 && this.props.CollectionView.props.Document.rowHeight as number !== e.currentTarget.valueAsNumber) {
- // this.props.CollectionView.props.Document.rowHeight = e.currentTarget.valueAsNumber;
- // }
- // }
- // }
-
- /**
- * Sets whether the grid is flexible or not on the grid's Document.
- */
- @undoBatch
- toggleFlex = () => {
- this.props.CollectionView.props.Document.gridFlex = !BoolCast(this.props.CollectionView.props.Document.gridFlex, true);
- }
-
- /**
- * Increments the value of numCols on button click
- */
- onIncrementButtonClick = () => {
- this.clicked = true;
- this.entered && (this.props.CollectionView.props.Document.gridNumCols as number)--;
- undoBatch(() => this.props.CollectionView.props.Document.gridNumCols = this.numCols + 1)();
- this.entered = false;
- }
-
- /**
- * Decrements the value of numCols on button click
- */
- onDecrementButtonClick = () => {
- this.clicked = true;
- if (!this.decrementLimitReached) {
- this.entered && (this.props.CollectionView.props.Document.gridNumCols as number)++;
- undoBatch(() => this.props.CollectionView.props.Document.gridNumCols = this.numCols - 1)();
- }
- this.entered = false;
- }
-
- /**
- * Increments the value of numCols on button hover
- */
- incrementValue = () => {
- this.entered = true;
- if (!this.clicked && !this.decrementLimitReached) {
- this.props.CollectionView.props.Document.gridNumCols = this.numCols + 1;
- }
- this.decrementLimitReached = false;
- this.clicked = false;
- }
-
- /**
- * Decrements the value of numCols on button hover
- */
- decrementValue = () => {
- this.entered = true;
- if (!this.clicked) {
- if (this.numCols !== 1) {
- this.props.CollectionView.props.Document.gridNumCols = this.numCols - 1;
- }
- else {
- this.decrementLimitReached = true;
- }
- }
-
- this.clicked = false;
- }
-
- /**
- * Toggles the value of preventCollision
- */
- toggleCollisions = () => {
- this.props.CollectionView.props.Document.gridPreventCollision = !this.props.CollectionView.props.Document.gridPreventCollision;
- }
-
- /**
- * Changes the value of the compactType
- */
- changeCompactType = (e: React.ChangeEvent<HTMLSelectElement>) => {
- // need to change startCompaction so that this operation will be undoable.
- this.props.CollectionView.props.Document.gridStartCompaction = e.target.selectedOptions[0].value;
- }
-
- render() {
- return (
- <div className="collectionGridViewChrome-cont" >
- <span className="grid-control" style={{ width: this.resize ? "25%" : "30%" }}>
- <span className="grid-icon">
- <FontAwesomeIcon icon="columns" size="1x" />
- </span>
- <input className="collectionGridViewChrome-entryBox" type="number" placeholder={this.numCols.toString()} onKeyDown={this.onNumColsEnter} onClick={(e: React.MouseEvent<HTMLInputElement, MouseEvent>) => { e.stopPropagation(); e.preventDefault(); e.currentTarget.focus(); }} />
- <input className="columnButton" onClick={this.onIncrementButtonClick} onMouseEnter={this.incrementValue} onMouseLeave={this.decrementValue} type="button" value="↑" />
- <input className="columnButton" style={{ marginRight: 5 }} onClick={this.onDecrementButtonClick} onMouseEnter={this.decrementValue} onMouseLeave={this.incrementValue} type="button" value="↓" />
- </span>
- {/* <span className="grid-control">
- <span className="grid-icon">
- <FontAwesomeIcon icon="text-height" size="1x" />
- </span>
- <input className="collectionGridViewChrome-entryBox" type="number" placeholder={this.props.CollectionView.props.Document.rowHeight as string} onKeyDown={this.onRowHeightEnter} onClick={(e: React.MouseEvent<HTMLInputElement, MouseEvent>) => { e.stopPropagation(); e.preventDefault(); e.currentTarget.focus(); }} />
- </span> */}
- <span className="grid-control" style={{ width: this.resize ? "12%" : "20%" }}>
- <input type="checkbox" style={{ marginRight: 5 }} onChange={this.toggleCollisions} checked={!this.props.CollectionView.props.Document.gridPreventCollision} />
- <label className="flexLabel">{this.resize ? "Coll" : "Collisions"}</label>
- </span>
-
- <select className="collectionGridViewChrome-viewPicker"
- style={{ marginRight: 5, width: this.props.PanelWidth() < 300 ? 25 : undefined }}
- onPointerDown={stopPropagation}
- onChange={this.changeCompactType}
- value={StrCast(this.props.CollectionView.props.Document.gridStartCompaction, StrCast(this.props.CollectionView.props.Document.gridCompaction))}>
- {["vertical", "horizontal", "none"].map(type =>
- <option className="collectionGridViewChrome-viewOption"
- onPointerDown={stopPropagation}
- value={type}>
- {this.resize ? type[0].toUpperCase() + type.substring(1) : "Compact: " + type}
- </option>
- )}
- </select>
-
- <span className="grid-control" style={{ width: this.resize ? "12%" : "20%" }}>
- <input style={{ marginRight: 5 }} type="checkbox" onChange={this.toggleFlex}
- checked={BoolCast(this.props.CollectionView.props.Document.gridFlex, true)} />
- <label className="flexLabel">{this.resize ? "Flex" : "Flexible"}</label>
- </span>
-
- <button onClick={() => this.props.CollectionView.props.Document.gridResetLayout = true}>
- {!this.resize ? "Reset" :
- <FontAwesomeIcon icon="redo-alt" size="1x" />}
- </button>
-
- </div>
- );
- }
-}
diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx
index 649406e6c..8c0b8de9d 100644
--- a/src/client/views/collections/ParentDocumentSelector.tsx
+++ b/src/client/views/collections/ParentDocumentSelector.tsx
@@ -40,15 +40,21 @@ export class SelectorContextMenu extends React.Component<SelectorProps> {
this._reaction?.();
}
async fetchDocuments() {
- const aliases = (await SearchUtil.GetAliasesOfDocument(this.props.Document)).filter(doc => doc !== this.props.Document);
- const { docs } = await SearchUtil.Search("", true, { fq: `data_l:"${this.props.Document[Id]}"` });
- const map: Map<Doc, Doc> = new Map;
- const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search("", true, { fq: `data_l:"${doc[Id]}"` }).then(result => result.docs)));
- allDocs.forEach((docs, index) => docs.forEach(doc => map.set(doc, aliases[index])));
- docs.forEach(doc => map.delete(doc));
+ const aliases = (await SearchUtil.GetAliasesOfDocument(this.props.Document));
+ const containerProtoSets = await Promise.all(aliases.map(async alias =>
+ await Promise.all((await SearchUtil.Search("", true, { fq: `data_l:"${alias[Id]}"` })).docs)));
+ const containerProtos = containerProtoSets.reduce((p, set) => { set.map(s => p.add(s)); return p; }, new Set<Doc>());
+ const containerSets = await Promise.all(Array.from(containerProtos.keys()).map(async container => {
+ return (await SearchUtil.GetAliasesOfDocument(container));
+ }));
+ const containers = containerSets.reduce((p, set) => { set.map(s => p.add(s)); return p; }, new Set<Doc>());
+ const doclayoutSets = await Promise.all(Array.from(containers.keys()).map(async (dp) => {
+ return (await SearchUtil.GetAliasesOfDocument(dp));
+ }));
+ const doclayouts = Array.from(doclayoutSets.reduce((p, set) => { set.map(s => p.add(s)); return p; }, new Set<Doc>()).keys());
runInAction(() => {
- this._docs = docs.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance.props.Document)).map(doc => ({ col: doc, target: this.props.Document }));
- this._otherDocs = Array.from(map.entries()).filter(entry => !Doc.AreProtosEqual(entry[0], CollectionDockingView.Instance.props.Document)).map(([col, target]) => ({ col, target }));
+ this._docs = doclayouts.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance.props.Document)).map(doc => ({ col: doc, target: this.props.Document }));
+ this._otherDocs = [];
});
}
diff --git a/src/client/views/collections/SchemaTable.tsx b/src/client/views/collections/SchemaTable.tsx
index c820cb661..829db9c6a 100644
--- a/src/client/views/collections/SchemaTable.tsx
+++ b/src/client/views/collections/SchemaTable.tsx
@@ -135,7 +135,6 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
@action
changeSorting = (col: any) => {
- console.log(col.heading);
if (col.desc === undefined) {
// no sorting
this.props.changeColumnSort(col, true);
@@ -149,7 +148,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
}
@action
- changeTitleMode = () => { console.log("header clicked"); this._showTitleDropdown = !this._showTitleDropdown; }
+ changeTitleMode = () => this._showTitleDropdown = !this._showTitleDropdown;
@computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); }
@computed get tableColumns(): Column<Doc>[] {
@@ -219,7 +218,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
background: col.color, padding: "2px",
display: "flex", cursor: "default", height: "100%",
}}>
- <FontAwesomeIcon icon={icon} size="lg" style={{ display: "inline", paddingBottom: "1px", paddingTop:"4px" }} />
+ <FontAwesomeIcon icon={icon} size="lg" style={{ display: "inline", paddingBottom: "1px", paddingTop: "4px" }} />
{/* <div className="keys-dropdown"
style={{ display: "inline", zIndex: 1000 }}> */}
{keysDropdown}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index a4fd5384f..b00074cc6 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -142,9 +142,16 @@ export function computePivotLayout(
const fieldKey = "data";
const pivotColumnGroups = new Map<FieldResult<Field>, PivotColumn>();
+ let nonNumbers = 0;
const pivotFieldKey = toLabel(pivotDoc._pivotField);
childPairs.map(pair => {
- const lval = Cast(pair.layout[pivotFieldKey], listSpec("string"), null);
+ const lval = pivotFieldKey === "#" ? Array.from(Object.keys(Doc.GetProto(pair.layout))).filter(k => k.startsWith("#")).map(k => k.substring(1)) :
+ Cast(pair.layout[pivotFieldKey], listSpec("string"), null);
+
+ const num = toNumber(pair.layout[pivotFieldKey]);
+ if (num === undefined || Number.isNaN(num)) {
+ nonNumbers++;
+ }
const val = Field.toString(pair.layout[pivotFieldKey] as Field);
if (lval) {
lval.forEach((val, i) => {
@@ -168,13 +175,6 @@ export function computePivotLayout(
});
}
});
- let nonNumbers = 0;
- childPairs.map(pair => {
- const num = toNumber(pair.layout[pivotFieldKey]);
- if (num === undefined || Number.isNaN(num)) {
- nonNumbers++;
- }
- });
const pivotNumbers = nonNumbers / childPairs.length < .1;
if (pivotColumnGroups.size > 10) {
const arrayofKeys = Array.from(pivotColumnGroups.keys());
@@ -434,27 +434,3 @@ function normalizeResults(
payload: gname.payload
})));
}
-
-export function AddCustomFreeFormLayout(doc: Doc, dataKey: string): () => void {
- return () => {
- const addOverlay = (key: "arrangeScript" | "arrangeInit", options: OverlayElementOptions, params?: Record<string, string>, requiredType?: string) => {
- let overlayDisposer: () => void = emptyFunction; // filled in below after we have a reference to the scriptingBox
- const scriptField = Cast(doc[key], ScriptField);
- const scriptingBox = <ScriptBox initialText={scriptField && scriptField.script.originalScript}
- // tslint:disable-next-line: no-unnecessary-callback-wrapper
- onCancel={() => overlayDisposer()} // don't get rid of the function wrapper-- we don't want to use the current value of overlayDiposer, but the one set below
- onSave={(text, onError) => {
- const script = CompileScript(text, { params, requiredType, typecheck: false });
- if (!script.compiled) {
- onError(script.errors.map(error => error.messageText).join("\n"));
- } else {
- 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}");
- };
-}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
index 05111adb4..8cbda310a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
@@ -16,4 +16,5 @@
stroke: rgb(0,0,0);
opacity: 0.5;
pointer-events: all;
+ cursor: move;
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index a24693c30..bfe569853 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -1,13 +1,12 @@
import { observer } from "mobx-react";
import { Doc } from "../../../../fields/Doc";
-import { Utils } from '../../../../Utils';
+import { Utils, setupMoveUpEvents, emptyFunction, returnFalse } from '../../../../Utils';
import { DocumentView } from "../../nodes/DocumentView";
import "./CollectionFreeFormLinkView.scss";
import React = require("react");
-import v5 = require("uuid/v5");
import { DocumentType } from "../../../documents/DocumentTypes";
-import { observable, action, reaction, IReactionDisposer } from "mobx";
-import { StrCast, Cast } from "../../../../fields/Types";
+import { observable, action, reaction, IReactionDisposer, trace, computed } from "mobx";
+import { StrCast, Cast, NumCast } from "../../../../fields/Types";
import { Id } from "../../../../fields/FieldSymbols";
import { SnappingManager } from "../../../util/SnappingManager";
@@ -20,18 +19,27 @@ export interface CollectionFreeFormLinkViewProps {
@observer
export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFormLinkViewProps> {
@observable _opacity: number = 0;
+ @observable _start = 0;
_anchorDisposer: IReactionDisposer | undefined;
+ _timeout: NodeJS.Timeout | undefined;
+ componentWillUnmount() {
+ this._anchorDisposer?.();
+ }
@action
+ timeout = () => (Date.now() < this._start++ + 1000) && setTimeout(this.timeout, 25)
componentDidMount() {
this._anchorDisposer = reaction(() => [this.props.A.props.ScreenToLocalTransform(), this.props.B.props.ScreenToLocalTransform(), this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document), this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document)],
action(() => {
- if (SnappingManager.GetIsDragging()) return;
+ this._start = Date.now();
+ this._timeout && clearTimeout(this._timeout);
+ this._timeout = setTimeout(this.timeout, 25);
+ if (SnappingManager.GetIsDragging() || !this.props.A.ContentDiv || !this.props.B.ContentDiv) return;
setTimeout(action(() => this._opacity = 1), 0); // since the render code depends on querying the Dom through getBoudndingClientRect, we need to delay triggering render()
setTimeout(action(() => (!this.props.LinkDocs.length || !this.props.LinkDocs[0].linkDisplay) && (this._opacity = 0.05)), 750); // this will unhighlight the link line.
- const acont = this.props.A.props.Document.type === DocumentType.LINK ? this.props.A.ContentDiv!.getElementsByClassName("linkAnchorBox-cont") : [];
- const bcont = this.props.B.props.Document.type === DocumentType.LINK ? this.props.B.ContentDiv!.getElementsByClassName("linkAnchorBox-cont") : [];
- const adiv = (acont.length ? acont[0] : this.props.A.ContentDiv!);
- const bdiv = (bcont.length ? bcont[0] : this.props.B.ContentDiv!);
+ const acont = this.props.A.props.Document.type === DocumentType.LINK ? this.props.A.ContentDiv.getElementsByClassName("linkAnchorBox-cont") : [];
+ const bcont = this.props.B.props.Document.type === DocumentType.LINK ? this.props.B.ContentDiv.getElementsByClassName("linkAnchorBox-cont") : [];
+ const adiv = (acont.length ? acont[0] : this.props.A.ContentDiv);
+ const bdiv = (bcont.length ? bcont[0] : this.props.B.ContentDiv);
const a = adiv.getBoundingClientRect();
const b = bdiv.getBoundingClientRect();
const abounds = adiv.parentElement!.getBoundingClientRect();
@@ -82,17 +90,31 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
})
, { fireImmediately: true });
}
- @action
- componentWillUnmount() {
- this._anchorDisposer?.();
+
+
+ pointerDown = (e: React.PointerEvent) => {
+ setupMoveUpEvents(this, e, (e, down, delta) => {
+ this.props.LinkDocs[0].linkOffsetX = NumCast(this.props.LinkDocs[0].linkOffsetX) + delta[0];
+ this.props.LinkDocs[0].linkOffsetY = NumCast(this.props.LinkDocs[0].linkOffsetY) + delta[1];
+ return false;
+ }, emptyFunction, () => {
+ // OverlayView.Instance.addElement(
+ // <LinkEditor sourceDoc={this.props.A.props.Document} linkDoc={this.props.LinkDocs[0]}
+ // showLinks={action(() => { })}
+ // />, { x: 300, y: 300 });
+ });
}
- render() {
- if (SnappingManager.GetIsDragging()) return null;
+
+ @computed get renderData() {
+ this._start;
+ if (SnappingManager.GetIsDragging() || !this.props.A.ContentDiv || !this.props.B.ContentDiv || !this.props.LinkDocs.length) {
+ return undefined;
+ }
this.props.A.props.ScreenToLocalTransform().transform(this.props.B.props.ScreenToLocalTransform());
- const acont = this.props.A.ContentDiv!.getElementsByClassName("linkAnchorBox-cont");
- const bcont = this.props.B.ContentDiv!.getElementsByClassName("linkAnchorBox-cont");
- const a = (acont.length ? acont[0] : this.props.A.ContentDiv!).getBoundingClientRect();
- const b = (bcont.length ? bcont[0] : this.props.B.ContentDiv!).getBoundingClientRect();
+ const acont = this.props.A.ContentDiv.getElementsByClassName("linkAnchorBox-cont");
+ const bcont = this.props.B.ContentDiv.getElementsByClassName("linkAnchorBox-cont");
+ const a = (acont.length ? acont[0] : this.props.A.ContentDiv).getBoundingClientRect();
+ const b = (bcont.length ? bcont[0] : this.props.B.ContentDiv).getBoundingClientRect();
const apt = Utils.closestPtBetweenRectangles(a.left, a.top, a.width, a.height,
b.left, b.top, b.width, b.height,
a.left + a.width / 2, a.top + a.height / 2);
@@ -105,18 +127,26 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
const pt2vec = [pt2[0] - (b.left + b.width / 2), pt2[1] - (b.top + b.height / 2)];
const pt1len = Math.sqrt((pt1vec[0] * pt1vec[0]) + (pt1vec[1] * pt1vec[1]));
const pt2len = Math.sqrt((pt2vec[0] * pt2vec[0]) + (pt2vec[1] * pt2vec[1]));
- const ptlen = Math.sqrt((pt1[0] - pt2[0]) * (pt1[0] - pt2[0]) + (pt1[1] - pt2[1]) * (pt1[1] - pt2[1])) / 3;
+ const ptlen = Math.sqrt((pt1[0] - pt2[0]) * (pt1[0] - pt2[0]) + (pt1[1] - pt2[1]) * (pt1[1] - pt2[1])) / 2;
const pt1norm = [pt1vec[0] / pt1len * ptlen, pt1vec[1] / pt1len * ptlen];
const pt2norm = [pt2vec[0] / pt2len * ptlen, pt2vec[1] / pt2len * ptlen];
const aActive = this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document);
- const bActive = this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document);
- const text = StrCast(this.props.A.props.Document.linkRelationship);
- return !a.width || !b.width || ((!this.props.LinkDocs.length || !this.props.LinkDocs[0].linkDisplay) && !aActive && !bActive) ? (null) : (<>
- <text x={(Math.min(pt1[0], pt2[0]) * 2 + Math.max(pt1[0], pt2[0])) / 3} y={(pt1[1] + pt2[1]) / 2}>
- {text !== "-ungrouped-" ? text : ""}
- </text>
+ const bActive = this.props.B.isSelected() || Doc.IsBrushed(this.props.B.props.Document);
+
+ const textX = (Math.min(pt1[0], pt2[0]) + Math.max(pt1[0], pt2[0])) / 2 + NumCast(this.props.LinkDocs[0].linkOffsetX);
+ const textY = (pt1[1] + pt2[1]) / 2 + NumCast(this.props.LinkDocs[0].linkOffsetY);
+ return { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1, pt2 };
+ }
+
+ render() {
+ if (!this.renderData) return (null);
+ const { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1, pt2 } = this.renderData;
+ return !a.width || !b.width || ((!this.props.LinkDocs[0].linkDisplay) && !aActive && !bActive) ? (null) : (<>
<path className="collectionfreeformlinkview-linkLine" style={{ opacity: this._opacity, strokeDasharray: "2 2" }}
d={`M ${pt1[0]} ${pt1[1]} C ${pt1[0] + pt1norm[0]} ${pt1[1] + pt1norm[1]}, ${pt2[0] + pt2norm[0]} ${pt2[1] + pt2norm[1]}, ${pt2[0]} ${pt2[1]}`} />
+ <text className="collectionfreeformlinkview-linkText" x={textX} y={textY} onPointerDown={this.pointerDown} >
+ {StrCast(this.props.LinkDocs[0].description)}
+ </text>
</>);
}
} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 546a4307c..57336131a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,53 +1,53 @@
import { library } from "@fortawesome/fontawesome-svg-core";
-import { faEye, faEyeSlash } from "@fortawesome/free-regular-svg-icons";
+import { faEye } from "@fortawesome/free-regular-svg-icons";
import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faFileUpload, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons";
-import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, _allowStateChangesInsideComputed, trace } from "mobx";
+import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
import { computedFn } from "mobx-utils";
-import { Doc, HeightSym, Opt, WidthSym, DocListCast } from "../../../../fields/Doc";
-import { documentSchema, collectionSchema } from "../../../../fields/documentSchemas";
+import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../../fields/Doc";
+import { collectionSchema, documentSchema } from "../../../../fields/documentSchemas";
import { Id } from "../../../../fields/FieldSymbols";
-import { InkData, InkField, InkTool, PointData } from "../../../../fields/InkField";
+import { InkData, InkField, InkTool } from "../../../../fields/InkField";
import { List } from "../../../../fields/List";
import { RichTextField } from "../../../../fields/RichTextField";
-import { createSchema, listSpec, makeInterface } from "../../../../fields/Schema";
-import { ScriptField, ComputedField } from "../../../../fields/ScriptField";
+import { createSchema, makeInterface } from "../../../../fields/Schema";
+import { ScriptField } from "../../../../fields/ScriptField";
import { BoolCast, Cast, FieldValue, NumCast, ScriptCast, StrCast } from "../../../../fields/Types";
import { TraceMobx } from "../../../../fields/util";
import { GestureUtils } from "../../../../pen-gestures/GestureUtils";
-import { aggregateBounds, intersectRect, returnOne, Utils, returnZero, returnFalse, numberRange } from "../../../../Utils";
+import { aggregateBounds, intersectRect, returnFalse, returnOne, returnZero, Utils } from "../../../../Utils";
import { CognitiveServices } from "../../../cognitive_services/CognitiveServices";
import { DocServer } from "../../../DocServer";
import { Docs, DocUtils } from "../../../documents/Documents";
+import { DocumentType } from "../../../documents/DocumentTypes";
import { DocumentManager } from "../../../util/DocumentManager";
import { DragManager, dropActionType } from "../../../util/DragManager";
import { HistoryUtil } from "../../../util/History";
import { InteractionUtils } from "../../../util/InteractionUtils";
import { SelectionManager } from "../../../util/SelectionManager";
+import { SnappingManager } from "../../../util/SnappingManager";
import { Transform } from "../../../util/Transform";
import { undoBatch, UndoManager } from "../../../util/UndoManager";
import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss";
+import { Timeline } from "../../animationtimeline/Timeline";
import { ContextMenu } from "../../ContextMenu";
-import { ContextMenuProps } from "../../ContextMenuItem";
+import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth } from "../../InkingStroke";
import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
-import { DocumentViewProps, DocumentView } from "../../nodes/DocumentView";
+import { DocumentLinksButton } from "../../nodes/DocumentLinksButton";
+import { DocumentViewProps } from "../../nodes/DocumentView";
import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox";
import { pageSchema } from "../../nodes/ImageBox";
-import PDFMenu from "../../pdf/PDFMenu";
import { CollectionDockingView } from "../CollectionDockingView";
import { CollectionSubView } from "../CollectionSubView";
-import { computePivotLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult, computerStarburstLayout, computerPassLayout } from "./CollectionFreeFormLayoutEngines";
+import { CollectionViewType } from "../CollectionView";
+import { computePivotLayout, computerPassLayout, computerStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from "./CollectionFreeFormLayoutEngines";
import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors";
import "./CollectionFreeFormView.scss";
import MarqueeOptionsMenu from "./MarqueeOptionsMenu";
import { MarqueeView } from "./MarqueeView";
import React = require("react");
-import { CollectionViewType } from "../CollectionView";
-import { Timeline } from "../../animationtimeline/Timeline";
-import { SnappingManager } from "../../../util/SnappingManager";
-import { InkingStroke, ActiveArrowStart, ActiveArrowEnd, ActiveInkColor, ActiveFillColor, ActiveInkWidth, ActiveInkBezierApprox, ActiveDash } from "../../InkingStroke";
-import { DocumentType } from "../../../documents/DocumentTypes";
-import { DocumentLinksButton } from "../../nodes/DocumentLinksButton";
+import { SearchUtil } from "../../../util/SearchUtil";
+import { LinkManager } from "../../../util/LinkManager";
library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
@@ -57,7 +57,6 @@ export const panZoomSchema = createSchema({
currentTimecode: "number",
displayTimecode: "number",
currentFrame: "number",
- arrangeScript: ScriptField,
arrangeInit: ScriptField,
useClusters: "boolean",
fitToBox: "boolean",
@@ -77,7 +76,8 @@ export type collectionFreeformViewProps = {
forceScaling?: boolean; // whether to force scaling of content (needed by ImageBox)
viewDefDivClick?: ScriptField;
childPointerEvents?: boolean;
- scaleField?: string;
+ scaleField?: string; // used by formattedTextBox when displaying a sidebar freeform view which needs its own scale field
+ noOverlay?: boolean; // used to suppress docs in the overlay (z) layer (ie, for minimap since overlay doesn't scale)
};
@observer
@@ -103,6 +103,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
@observable _clusterSets: (Doc[])[] = [];
@observable _timelineRef = React.createRef<Timeline>();
+ @observable _marqueeRef = React.createRef<HTMLDivElement>();
+ @observable canPanX: boolean = true;
+ @observable canPanY: boolean = true;
+
@computed get fitToContentScaling() { return this.fitToContent ? NumCast(this.layoutDoc.fitToContentScaling, 1) : 1; }
@computed get fitToContent() { return (this.props.fitToBox || this.Document._fitToBox) && !this.isAnnotationOverlay; }
@computed get parentScaling() { return this.props.ContentScaling && this.fitToContent && !this.isAnnotationOverlay ? this.props.ContentScaling() : 1; }
@@ -182,7 +186,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
private selectDocuments = (docs: Doc[]) => {
SelectionManager.DeselectAll();
- docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).map(dv => dv && SelectionManager.SelectDoc(dv, true));
+ docs.map(doc => DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView)).map(dv => dv && SelectionManager.SelectDoc(dv, true));
}
public isCurrent(doc: Doc) { return (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document.currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); }
@@ -207,7 +211,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
for (let i = 0; i < docDragData.droppedDocuments.length; i++) {
const d = docDragData.droppedDocuments[i];
const layoutDoc = Doc.Layout(d);
- if (this.Document.currentFrame !== undefined && !this.props.isAnnotationOverlay) {
+ if (this.Document.currentFrame !== undefined) {
const vals = CollectionFreeFormDocumentView.getValues(d, NumCast(d.activeFrame, 1000));
CollectionFreeFormDocumentView.setValues(this.Document.currentFrame, d, x + vals.x - dropPos[0], y + vals.y - dropPos[1], vals.opacity);
} else {
@@ -248,7 +252,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
} else {
const source = Docs.Create.TextDocument("", { _width: 200, _height: 75, x: xp, y: yp, title: "dropped annotation" });
this.props.addDocument(source);
- linkDragData.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: linkDragData.linkSourceDocument }, "doc annotation"); // TODODO this is where in text links get passed
+ linkDragData.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: linkDragData.linkSourceDocument }, "doc annotation", ""); // TODODO this is where in text links get passed
e.stopPropagation();
return true;
}
@@ -492,10 +496,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
const start = this.getTransform().transformPoint(Math.min(...ge.points.map(p => p.X)), Math.min(...ge.points.map(p => p.Y)));
this._inkToTextStartX = start[0];
this._inkToTextStartY = start[1];
- console.log("start");
break;
case GestureUtils.Gestures.EndBracket:
- console.log("end");
if (this._inkToTextStartX && this._inkToTextStartY) {
const end = this.getTransform().transformPoint(Math.max(...ge.points.map(p => p.X)), Math.max(...ge.points.map(p => p.Y)));
const setDocs = this.getActiveDocuments().filter(s => s.proto?.type === "rtf" && s.color);
@@ -602,7 +604,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
pan = (e: PointerEvent | React.Touch | { clientX: number, clientY: number }): void => {
// bcz: theres should be a better way of doing these than referencing these static instances directly
MarqueeOptionsMenu.Instance?.fadeOut(true);// I think it makes sense for the marquee menu to go away when panned. -syip2
- PDFMenu.Instance.fadeOut(true);
const [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
this.setPan((this.Document._panX || 0) - dx, (this.Document._panY || 0) - dy, undefined, true);
@@ -870,7 +871,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
focusDocument = (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: () => boolean) => {
const state = HistoryUtil.getState();
- // TODO This technically isn't correct if type !== "doc", as
+ // TODO This technically isn't correct if type !== "doc", as
// currently nothing is done, but we should probably push a new state
if (state.type === "doc" && this.Document._panX !== undefined && this.Document._panY !== undefined) {
const init = state.initializers![this.Document[Id]];
@@ -890,7 +891,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
this.props.focus(doc);
} else {
const contextHgt = Doc.AreProtosEqual(annotOn, this.props.Document) && this.props.VisibleHeight ? this.props.VisibleHeight() : NumCast(annotOn._height);
- const offset = annotOn && (contextHgt / 2 * 96 / 72);
+ const offset = annotOn && (contextHgt / 2);
this.props.Document._scrollY = NumCast(doc.y) - offset;
}
@@ -935,22 +936,26 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
}
@computed get libraryPath() { return this.props.LibraryPath ? [...this.props.LibraryPath, this.props.Document] : []; }
- @computed get onChildClickHandler() { return this.props.childClickScript || ScriptCast(this.Document.onChildClick); }
- @computed get onChildDoubleClickHandler() { return this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); }
@computed get backgroundActive() { return this.layoutDoc.isBackground && (this.props.ContainingCollectionView?.active() || this.props.active()); }
+ onChildClickHandler = () => this.props.childClickScript || ScriptCast(this.Document.onChildClick);
+ onChildDoubleClickHandler = () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick);
backgroundHalo = () => BoolCast(this.Document.useClusters);
parentActive = (outsideReaction: boolean) => this.props.active(outsideReaction) || this.backgroundActive ? true : false;
getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps {
return {
- ...this.props,
+ addDocument: this.props.addDocument,
+ removeDocument: this.props.removeDocument,
+ moveDocument: this.props.moveDocument,
+ pinToPres: this.props.pinToPres,
+ whenActiveChanged: this.props.whenActiveChanged,
NativeHeight: returnZero,
NativeWidth: returnZero,
fitToBox: false,
DataDoc: childData,
Document: childLayout,
LibraryPath: this.libraryPath,
- LayoutTemplate: this.props.ChildLayoutTemplate,
- LayoutTemplateString: this.props.ChildLayoutString,
+ LayoutTemplate: childLayout.z ? undefined : this.props.ChildLayoutTemplate,
+ LayoutTemplateString: childLayout.z ? undefined : this.props.ChildLayoutString,
FreezeDimensions: this.props.freezeChildDimensions,
layoutKey: undefined,
setupDragLines: this.setupDragLines,
@@ -999,10 +1004,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
return this.props.addDocTab(doc, where);
});
getCalculatedPositions(params: { pair: { layout: Doc, data?: Doc }, index: number, collection: Doc, docs: Doc[], state: any }): PoolData {
- const result = this.Document.arrangeScript?.script.run(params, console.log);
- if (result?.success) {
- return { x: 0, y: 0, transition: "transform 1s", ...result, pair: params.pair, replica: "" };
- }
const layoutDoc = Doc.Layout(params.pair.layout);
const { x, y, opacity } = this.Document.currentFrame === undefined ? params.pair.layout :
CollectionFreeFormDocumentView.getValues(params.pair.layout, this.Document.currentFrame || 0);
@@ -1030,7 +1031,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
const transform = `translate(${x}px, ${y}px)`;
if (viewDef.type === "text") {
const text = Cast(viewDef.text, "string"); // don't use NumCast, StrCast, etc since we want to test for undefined below
- const fontSize = Cast(viewDef.fontSize, "number");
+ const fontSize = Cast(viewDef.fontSize, "string");
return [text, x, y].some(val => val === undefined) ? undefined :
{
ele: <div className="collectionFreeform-customText" key={(text || "") + x + y + z + color} style={{ width, height, color, fontSize, transform }}>
@@ -1068,7 +1069,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
doFreeformLayout(poolData: Map<string, PoolData>) {
const layoutDocs = this.childLayoutPairs.map(pair => pair.layout);
- const initResult = this.Document.arrangeInit && this.Document.arrangeInit.script.run({ docs: layoutDocs, collection: this.Document }, console.log);
+ const initResult = this.Document.arrangeInit?.script.run({ docs: layoutDocs, collection: this.Document }, console.log);
const state = initResult?.success ? initResult.result.scriptState : undefined;
const elements = initResult?.success ? this.viewDefsToJSX(initResult.result.views) : [];
@@ -1143,13 +1144,18 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
@action
componentDidMount() {
super.componentDidMount?.();
- this._layoutComputeReaction = reaction(() => this.doLayoutComputation,
+ this._layoutComputeReaction = reaction(() => { TraceMobx(); return this.doLayoutComputation },
(elements) => this._layoutElements = elements || [],
{ fireImmediately: true, name: "doLayout" });
+
+ this._marqueeRef.current?.addEventListener("dashDragAutoScroll", this.onDragAutoScroll as any);
}
+
componentWillUnmount() {
this._layoutComputeReaction?.();
+ this._marqueeRef.current?.removeEventListener("dashDragAutoScroll", this.onDragAutoScroll as any);
}
+
@computed get views() { return this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); }
elementFunc = () => this._layoutElements;
@@ -1158,6 +1164,29 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY));
}
+
+ // <div ref={this._marqueeRef}>
+
+ @action
+ onDragAutoScroll = (e: CustomEvent<React.DragEvent>) => {
+ if ((e as any).handlePan || this.props.isAnnotationOverlay) return;
+ (e as any).handlePan = true;
+
+ if (this._marqueeRef?.current) {
+ const dragX = e.detail.clientX;
+ const dragY = e.detail.clientY;
+ const bounds = this._marqueeRef.current?.getBoundingClientRect();
+
+ const deltaX = dragX - bounds.left < 25 ? -(25 + (bounds.left - dragX)) : bounds.right - dragX < 25 ? 25 - (bounds.right - dragX) : 0;
+ const deltaY = dragY - bounds.top < 25 ? -(25 + (bounds.top - dragY)) : bounds.bottom - dragY < 25 ? 25 - (bounds.bottom - dragY) : 0;
+ if (deltaX !== 0 || deltaY !== 0) {
+ this.Document._panY = NumCast(this.Document._panY) + deltaY / 2;
+ this.Document._panX = NumCast(this.Document._panX) + deltaX / 2;
+ }
+ }
+ e.stopPropagation();
+ }
+
promoteCollection = undoBatch(action(() => {
const childDocs = this.childDocs.slice();
childDocs.forEach(doc => {
@@ -1204,59 +1233,73 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
private thumbIdentifier?: number;
onContextMenu = (e: React.MouseEvent) => {
- if (this.props.annotationsKey) return;
+ if (this.props.annotationsKey || !ContextMenu.Instance) return;
const appearance = ContextMenu.Instance.findByDescription("Appearance...");
const appearanceItems = appearance && "subitems" in appearance ? appearance.subitems : [];
- appearanceItems.push({ description: "reset view", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document[this.scaleFieldKey] = 1; }, icon: "compress-arrows-alt" });
- appearanceItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" });
- appearanceItems.push({ description: `${this.Document.useClusters ? "Uncluster" : "Use Clusters"}`, event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" });
- appearanceItems.push({ description: "Use Background Color as Default", event: () => Cast(Doc.UserDoc().emptyCollection, Doc, null)._backgroundColor = StrCast(this.layoutDoc._backgroundColor), icon: "palette" });
+ appearanceItems.push({ description: "Reset View", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document[this.scaleFieldKey] = 1; }, icon: "compress-arrows-alt" });
+ appearanceItems.push({ description: `${this.fitToContent ? "Make Zoomable" : "Scale to Window"}`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" });
+ appearanceItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" });
!appearance && ContextMenu.Instance.addItem({ description: "Appearance...", subitems: appearanceItems, icon: "eye" });
+ const viewctrls = ContextMenu.Instance.findByDescription("UI Controls...");
+ const viewCtrlItems = viewctrls && "subitems" in viewctrls ? viewctrls.subitems : [];
+ viewCtrlItems.push({ description: (Doc.UserDoc().showSnapLines ? "Hide" : "Show") + " Snap Lines", event: () => Doc.UserDoc().showSnapLines = !Doc.UserDoc().showSnapLines, icon: "compress-arrows-alt" });
+ viewCtrlItems.push({ description: (this.Document.useClusters ? "Hide" : "Show") + " Clusters", event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" });
+ !viewctrls && ContextMenu.Instance.addItem({ description: "UI Controls...", subitems: viewCtrlItems, icon: "eye" });
+
const options = ContextMenu.Instance.findByDescription("Options...");
const optionItems = options && "subitems" in options ? options.subitems : [];
- !this.props.isAnnotationOverlay &&
+ !this.props.isAnnotationOverlay && !Doc.UserDoc().noviceMode &&
optionItems.push({ description: (this.showTimeline ? "Close" : "Open") + " Animation Timeline", event: action(() => this.showTimeline = !this.showTimeline), icon: faEye });
this.props.ContainingCollectionView &&
optionItems.push({ description: "Promote Collection", event: this.promoteCollection, icon: "table" });
- optionItems.push({ description: (Doc.UserDoc().showSnapLines ? "Hide" : "Show") + " snap lines", event: () => Doc.UserDoc().showSnapLines = !Doc.UserDoc().showSnapLines, icon: "compress-arrows-alt" });
optionItems.push({ description: this.layoutDoc._lockedTransform ? "Unlock Transform" : "Lock Transform", event: this.toggleLockTransform, icon: this.layoutDoc._lockedTransform ? "unlock" : "lock" });
- optionItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" });
+ appearanceItems.push({ description: "Use Background Color as Default", event: () => Cast(Doc.UserDoc().emptyCollection, Doc, null)._backgroundColor = StrCast(this.layoutDoc._backgroundColor), icon: "palette" });
if (!Doc.UserDoc().noviceMode) {
optionItems.push({ description: (!this.layoutDoc._nativeWidth || !this.layoutDoc._nativeHeight ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" });
optionItems.push({ description: `${this.Document._freeformLOD ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document._freeformLOD = !this.Document._freeformLOD, icon: "table" });
- optionItems.push({
- description: "Import document", icon: "upload", event: ({ x, y }) => {
- const input = document.createElement("input");
- input.type = "file";
- input.accept = ".zip";
- input.onchange = async _e => {
- const upload = Utils.prepend("/uploadDoc");
- const formData = new FormData();
- const file = input.files && input.files[0];
- if (file) {
- formData.append('file', file);
- formData.append('remap', "true");
- const response = await fetch(upload, { method: "POST", body: formData });
- const json = await response.json();
- if (json !== "error") {
- const doc = await DocServer.GetRefField(json);
- if (doc instanceof Doc) {
- const [xx, yy] = this.props.ScreenToLocalTransform().transformPoint(x, y);
- doc.x = xx, doc.y = yy;
- this.props.addDocument?.(doc);
- }
- }
- }
- };
- input.click();
- }
- });
+
}
!options && ContextMenu.Instance.addItem({ description: "Options...", subitems: optionItems, icon: "eye" });
+ const mores = ContextMenu.Instance.findByDescription("More...");
+ const moreItems = mores && "subitems" in mores ? mores.subitems : [];
+ moreItems.push({ description: "Import document", icon: "upload", event: ({ x, y }) => this.importDocument(x, y) });
+ !mores && ContextMenu.Instance.addItem({ description: "More...", subitems: moreItems, icon: "eye" });
+ }
+ importDocument = (x: number, y: number) => {
+ const input = document.createElement("input");
+ input.type = "file";
+ input.accept = ".zip";
+ input.onchange = async _e => {
+ const upload = Utils.prepend("/uploadDoc");
+ const formData = new FormData();
+ const file = input.files && input.files[0];
+ if (file) {
+ formData.append('file', file);
+ formData.append('remap', "true");
+ const response = await fetch(upload, { method: "POST", body: formData });
+ const json = await response.json();
+ if (json !== "error") {
+ const doc = await DocServer.GetRefField(json);
+ if (doc instanceof Doc) {
+ const [xx, yy] = this.props.ScreenToLocalTransform().transformPoint(x, y);
+ doc.x = xx, doc.y = yy;
+ this.props.addDocument?.(doc);
+ setTimeout(() => {
+ SearchUtil.Search(`{!join from=id to=proto_i}id:link*`, true, {}).then(docs => {
+ docs.docs.forEach(d => LinkManager.Instance.addLink(d));
+ })
+ }, 2000); // need to give solr some time to update so that this query will find any link docs we've added.
+ }
+ }
+ }
+ };
+ input.click();
}
+
+
@observable showTimeline = false;
intersectRect(r1: { left: number, top: number, width: number, height: number },
@@ -1339,7 +1382,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
return false;
});
@computed get marqueeView() {
- return <MarqueeView {...this.props}
+ return <MarqueeView
+ {...this.props}
nudge={this.nudge}
addDocTab={this.addDocTab}
activeDocuments={this.getActiveDocuments}
@@ -1349,14 +1393,15 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
getContainerTransform={this.getContainerTransform}
getTransform={this.getTransform}
isAnnotationOverlay={this.isAnnotationOverlay}>
- <CollectionFreeFormViewPannableContents
- centeringShiftX={this.centeringShiftX}
- centeringShiftY={this.centeringShiftY}
- transition={Cast(this.layoutDoc._viewTransition, "string", null)}
- viewDefDivClick={this.props.viewDefDivClick}
- zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
- {this.children}
- </CollectionFreeFormViewPannableContents>
+ <div ref={this._marqueeRef}>
+ <CollectionFreeFormViewPannableContents
+ centeringShiftX={this.centeringShiftX}
+ centeringShiftY={this.centeringShiftY}
+ transition={Cast(this.layoutDoc._viewTransition, "string", null)}
+ viewDefDivClick={this.props.viewDefDivClick}
+ zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
+ {this.children}
+ </CollectionFreeFormViewPannableContents></div>
{this.showTimeline ? <Timeline ref={this._timelineRef} {...this.props} /> : (null)}
</MarqueeView>;
}
@@ -1373,6 +1418,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
render() {
TraceMobx();
const clientRect = this._mainCont?.getBoundingClientRect();
+ !this.fitToContent && this._layoutElements?.length && setTimeout(() => this.Document._renderContentBounds = new List<number>([this.contentBounds.x, this.contentBounds.y, this.contentBounds.r, this.contentBounds.b]), 0);
return <div className={"collectionfreeformview-container"} ref={this.createDashEventsTarget}
onPointerOver={this.onPointerOver}
onWheel={this.onPointerWheel}
@@ -1391,7 +1437,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
}}>
{this.Document._freeformLOD && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ?
this.placeholder : this.marqueeView}
- <CollectionFreeFormOverlayView elements={this.elementFunc} />
+ {!this.props.noOverlay ? <CollectionFreeFormOverlayView elements={this.elementFunc} /> : (null)}
<div className={"pullpane-indicator"}
style={{
@@ -1453,4 +1499,4 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF
{this.props.children()}
</div>;
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.scss b/src/client/views/collections/collectionFreeForm/FormatShapePane.scss
index a9fab4c1e..010beb836 100644
--- a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.scss
+++ b/src/client/views/collections/collectionFreeForm/FormatShapePane.scss
@@ -1,4 +1,8 @@
.antimodeMenu-button {
+ width: 200px;
+ position: relative;
+ text-align: left;
+
.color-previewI {
width: 100%;
height: 40%;
@@ -8,12 +12,25 @@
width: 100%;
height: 100%;
}
+}
+.antimenu-Buttonup {
+ position: absolute;
+ width: 20;
+ height: 10;
+ right: 0;
+ padding: 0;
+}
+.formatShapePane-inputBtn {
+ width: inherit;
+ position: absolute;
}
.sketch-picker {
background: #323232;
+ width: 160px !important;
+ height: 80% !important;
.flexbox-fit {
background: #323232;
@@ -26,6 +43,16 @@
/* Make the buttons appear below each other */
}
+.btn-group-palette {
+ display: block;
+ /* Make the buttons appear below each other */
+}
+
+.btn-draw {
+ display: inline;
+ /* Make the buttons appear below each other */
+}
+
.btn2-group {
display: block;
background: #323232;
@@ -35,7 +62,5 @@
.antimodeMenu-button {
background: #323232;
display: block;
-
-
}
} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/FormatShapePane.tsx b/src/client/views/collections/collectionFreeForm/FormatShapePane.tsx
new file mode 100644
index 000000000..ddc282e57
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/FormatShapePane.tsx
@@ -0,0 +1,487 @@
+import React = require("react");
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { action, computed, observable } from "mobx";
+import { observer } from "mobx-react";
+import { Doc, Field, Opt } from "../../../../fields/Doc";
+import { Document } from "../../../../fields/documentSchemas";
+import { InkField } from "../../../../fields/InkField";
+import { BoolCast, Cast, NumCast } from "../../../../fields/Types";
+import { DocumentType } from "../../../documents/DocumentTypes";
+import { SelectionManager } from "../../../util/SelectionManager";
+import AntimodeMenu from "../../AntimodeMenu";
+import "./FormatShapePane.scss";
+import { undoBatch } from "../../../util/UndoManager";
+import { ColorState, SketchPicker } from 'react-color';
+import { DocumentView } from "../../../views/nodes/DocumentView"
+
+@observer
+export default class FormatShapePane extends AntimodeMenu {
+ static Instance: FormatShapePane;
+
+ private _lastFill = "#D0021B";
+ private _lastLine = "#D0021B";
+ private _lastDash = "2";
+ private _mode = ["fill-drip", "ruler-combined"];
+
+ @observable private _subOpen = [false, false];
+ @observable private _currMode = "fill-drip";
+ @observable _lock = false;
+ @observable private _fillBtn = false;
+ @observable private _lineBtn = false;
+ @observable _controlBtn = false;
+ @observable private _controlPoints: { X: number, Y: number }[] = [];
+ @observable _currPoint = -1;
+
+ getField(key: string) {
+ return this.selectedInk?.reduce((p, i) =>
+ (p === undefined || (p && p === i.rootDoc[key])) && i.rootDoc[key] !== "0" ? Field.toString(i.rootDoc[key] as Field) : "", undefined as Opt<string>);
+ }
+
+ @computed get selectedInk() {
+ const inks = SelectionManager.SelectedDocuments().filter(i => Document(i.rootDoc).type === DocumentType.INK);
+ return inks.length ? inks : undefined;
+ }
+ @computed get unFilled() { return this.selectedInk?.reduce((p, i) => p && !i.rootDoc.fillColor ? true : false, true) || false; }
+ @computed get unStrokd() { return this.selectedInk?.reduce((p, i) => p && !i.rootDoc.color ? true : false, true) || false; }
+ @computed get solidFil() { return this.selectedInk?.reduce((p, i) => p && i.rootDoc.fillColor ? true : false, true) || false; }
+ @computed get solidStk() { return this.selectedInk?.reduce((p, i) => p && i.rootDoc.color && (!i.rootDoc.strokeDash || i.rootDoc.strokeDash === "0") ? true : false, true) || false; }
+ @computed get dashdStk() { return !this.unStrokd && this.getField("strokeDash") || ""; }
+ @computed get colorFil() { const ccol = this.getField("fillColor") || ""; ccol && (this._lastFill = ccol); return ccol; }
+ @computed get colorStk() { const ccol = this.getField("color") || ""; ccol && (this._lastLine = ccol); return ccol; }
+ @computed get widthStk() { return this.getField("strokeWidth") || "1"; }
+ @computed get markHead() { return this.getField("strokeStartMarker") || ""; }
+ @computed get markTail() { return this.getField("strokeEndMarker") || ""; }
+ @computed get shapeHgt() { return this.getField("_height"); }
+ @computed get shapeWid() { return this.getField("_width"); }
+ @computed get shapeXps() { return this.getField("x"); }
+ @computed get shapeYps() { return this.getField("y"); }
+ @computed get shapeRot() { return this.getField("rotation"); }
+ set unFilled(value) { this.colorFil = value ? "" : this._lastFill; }
+ set solidFil(value) { this.unFilled = !value; }
+ set colorFil(value) { value && (this._lastFill = value); this.selectedInk?.forEach(i => i.rootDoc.fillColor = value ? value : undefined); }
+ set colorStk(value) { value && (this._lastLine = value); this.selectedInk?.forEach(i => i.rootDoc.color = value ? value : undefined); }
+ set markHead(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeStartMarker = value); }
+ set markTail(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeEndMarker = value); }
+ set unStrokd(value) { this.colorStk = value ? "" : this._lastLine; }
+ set solidStk(value) { this.dashdStk = ""; this.unStrokd = !value; }
+ set dashdStk(value) {
+ value && (this._lastDash = value) && (this.unStrokd = false);
+ this.selectedInk?.forEach(i => i.rootDoc.strokeDash = value ? this._lastDash : undefined);
+ }
+ set shapeXps(value) { this.selectedInk?.forEach(i => i.rootDoc.x = Number(value)); }
+ set shapeYps(value) { this.selectedInk?.forEach(i => i.rootDoc.y = Number(value)); }
+ set shapeRot(value) { this.selectedInk?.forEach(i => i.rootDoc.rotation = Number(value)); }
+ set widthStk(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeWidth = Number(value)); }
+ set shapeWid(value) {
+ this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => {
+ const oldWidth = NumCast(i.rootDoc._width);
+ i.rootDoc._width = Number(value);
+ this._lock && (i.rootDoc._height = (i.rootDoc._width * NumCast(i.rootDoc._height)) / oldWidth);
+ });
+ }
+ set shapeHgt(value) {
+ this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => {
+ const oldHeight = NumCast(i.rootDoc._height);
+ i.rootDoc._height = Number(value);
+ this._lock && (i.rootDoc._width = (i.rootDoc._height * NumCast(i.rootDoc._width)) / oldHeight);
+ });
+ }
+
+ constructor(props: Readonly<{}>) {
+ super(props);
+ FormatShapePane.Instance = this;
+ this._canFade = false;
+ this.Pinned = BoolCast(Doc.UserDoc()["menuFormatShape-pinned"]);
+ }
+
+ @action
+ closePane = () => {
+ this.fadeOut(false);
+ this.Pinned = false;
+ }
+
+ @action
+ upDownButtons = (dirs: string, field: string) => {
+ switch (field) {
+ case "rot": this.rotate((dirs === "up" ? .1 : -.1)); break;
+ // case "rot": this.selectedInk?.forEach(i => i.rootDoc.rotation = NumCast(i.rootDoc.rotation) + (dirs === "up" ? 0.1 : -0.1)); break;
+ case "Xps": this.selectedInk?.forEach(i => i.rootDoc.x = NumCast(i.rootDoc.x) + (dirs === "up" ? 10 : -10)); break;
+ case "Yps": this.selectedInk?.forEach(i => i.rootDoc.y = NumCast(i.rootDoc.y) + (dirs === "up" ? 10 : -10)); break;
+ case "stk": this.selectedInk?.forEach(i => i.rootDoc.strokeWidth = NumCast(i.rootDoc.strokeWidth) + (dirs === "up" ? .1 : -.1)); break;
+ case "wid": this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => {
+ //redraw points
+ const oldWidth = NumCast(i.rootDoc._width);
+ const oldHeight = NumCast(i.rootDoc._height);
+ const oldX = NumCast(i.rootDoc.x);
+ const oldY = NumCast(i.rootDoc.y);
+ i.rootDoc._width = oldWidth + (dirs === "up" ? 10 : - 10);
+ this._lock && (i.rootDoc._height = (i.rootDoc._width / oldWidth * NumCast(i.rootDoc._height)));
+ const doc = Document(i.rootDoc);
+ if (doc.type === DocumentType.INK && doc.x && doc.y && doc._height && doc._width) {
+ console.log(doc.x, doc.y, doc._height, doc._width);
+ const ink = Cast(doc.data, InkField)?.inkData;
+ console.log(ink);
+ if (ink) {
+ const newPoints: { X: number, Y: number }[] = [];
+ for (var j = 0; j < ink.length; j++) {
+ // (new x — oldx) + (oldxpoint * newWidt)/oldWidth
+ const newX = (doc.x - oldX) + (ink[j].X * doc._width) / oldWidth;
+ const newY = (doc.y - oldY) + (ink[j].Y * doc._height) / oldHeight;
+ newPoints.push({ X: newX, Y: newY });
+ }
+ doc.data = new InkField(newPoints);
+ }
+ }
+ });
+ break;
+ case "hgt": this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => {
+ const oldWidth = NumCast(i.rootDoc._width);
+ const oldHeight = NumCast(i.rootDoc._height);
+ const oldX = NumCast(i.rootDoc.x);
+ const oldY = NumCast(i.rootDoc.y); i.rootDoc._height = oldHeight + (dirs === "up" ? 10 : - 10);
+ this._lock && (i.rootDoc._width = (i.rootDoc._height / oldHeight * NumCast(i.rootDoc._width)));
+ const doc = Document(i.rootDoc);
+ if (doc.type === DocumentType.INK && doc.x && doc.y && doc._height && doc._width) {
+ console.log(doc.x, doc.y, doc._height, doc._width);
+ const ink = Cast(doc.data, InkField)?.inkData;
+ console.log(ink);
+ if (ink) {
+ const newPoints: { X: number, Y: number }[] = [];
+ for (var j = 0; j < ink.length; j++) {
+ // (new x — oldx) + (oldxpoint * newWidt)/oldWidth
+ const newX = (doc.x - oldX) + (ink[j].X * doc._width) / oldWidth;
+ const newY = (doc.y - oldY) + (ink[j].Y * doc._height) / oldHeight;
+ newPoints.push({ X: newX, Y: newY });
+ }
+ doc.data = new InkField(newPoints);
+ }
+ }
+ });
+ break;
+ }
+ }
+
+ @undoBatch
+ @action
+ rotate = (angle: number) => {
+ const _centerPoints: { X: number, Y: number }[] = [];
+ SelectionManager.SelectedDocuments().forEach(action(inkView => {
+ const doc = Document(inkView.rootDoc);
+ if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) {
+ const ink = Cast(doc.data, InkField)?.inkData;
+ if (ink) {
+ const xs = ink.map(p => p.X);
+ const ys = ink.map(p => p.Y);
+ const left = Math.min(...xs);
+ const top = Math.min(...ys);
+ const right = Math.max(...xs);
+ const bottom = Math.max(...ys);
+ _centerPoints.push({ X: left, Y: top });
+ }
+ }
+ }));
+
+ var index = 0;
+ SelectionManager.SelectedDocuments().forEach(action(inkView => {
+ const doc = Document(inkView.rootDoc);
+ if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) {
+ doc.rotation = Number(doc.rotation) + Number(angle);
+ const ink = Cast(doc.data, InkField)?.inkData;
+ if (ink) {
+
+ const newPoints: { X: number, Y: number }[] = [];
+ for (var i = 0; i < ink.length; i++) {
+ const newX = Math.cos(angle) * (ink[i].X - _centerPoints[index].X) - Math.sin(angle) * (ink[i].Y - _centerPoints[index].Y) + _centerPoints[index].X;
+ const newY = Math.sin(angle) * (ink[i].X - _centerPoints[index].X) + Math.cos(angle) * (ink[i].Y - _centerPoints[index].Y) + _centerPoints[index].Y;
+ newPoints.push({ X: newX, Y: newY });
+ }
+ doc.data = new InkField(newPoints);
+ const xs = newPoints.map(p => p.X);
+ const ys = newPoints.map(p => p.Y);
+ const left = Math.min(...xs);
+ const top = Math.min(...ys);
+ const right = Math.max(...xs);
+ const bottom = Math.max(...ys);
+
+ doc._height = (bottom - top);
+ doc._width = (right - left);
+ }
+ index++;
+ }
+ }));
+ }
+
+ @undoBatch
+ @action
+ control = (xDiff: number, yDiff: number, controlNum: number) => {
+ this.selectedInk?.forEach(action(inkView => {
+ if (this.selectedInk?.length === 1) {
+ const doc = Document(inkView.rootDoc);
+ if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) {
+ const ink = Cast(doc.data, InkField)?.inkData;
+ if (ink) {
+
+ const newPoints: { X: number, Y: number }[] = [];
+ const order = controlNum % 4;
+ for (var i = 0; i < ink.length; i++) {
+ if (controlNum === i ||
+ (order === 0 && i === controlNum + 1) ||
+ (order === 0 && controlNum !== 0 && i === controlNum - 2) ||
+ (order === 0 && controlNum !== 0 && i === controlNum - 1) ||
+ (order === 3 && i === controlNum - 1) ||
+ (order === 3 && controlNum !== ink.length - 1 && i === controlNum + 1) ||
+ (order === 3 && controlNum !== ink.length - 1 && i === controlNum + 2)
+ || ((ink[0].X === ink[ink.length - 1].X) && (ink[0].Y === ink[ink.length - 1].Y) && (i === 0 || i === ink.length - 1) && (controlNum === 0 || controlNum === ink.length - 1))
+ ) {
+ newPoints.push({ X: ink[i].X - (xDiff * inkView.props.ScreenToLocalTransform().Scale), Y: ink[i].Y - (yDiff * inkView.props.ScreenToLocalTransform().Scale) });
+ }
+ else {
+ newPoints.push({ X: ink[i].X, Y: ink[i].Y });
+ }
+ }
+ const oldx = doc.x;
+ const oldy = doc.y;
+ const xs = ink.map(p => p.X);
+ const ys = ink.map(p => p.Y);
+ const left = Math.min(...xs);
+ const top = Math.min(...ys);
+ doc.data = new InkField(newPoints);
+ const xs2 = newPoints.map(p => p.X);
+ const ys2 = newPoints.map(p => p.Y);
+ const left2 = Math.min(...xs2);
+ const top2 = Math.min(...ys2);
+ const right2 = Math.max(...xs2);
+ const bottom2 = Math.max(...ys2);
+ doc._height = (bottom2 - top2);
+ doc._width = (right2 - left2);
+ //if points move out of bounds
+
+ doc.x = oldx - (left - left2);
+ doc.y = oldy - (top - top2);
+
+ }
+ }
+ }
+ }));
+ }
+
+ @undoBatch
+ @action
+ switchStk = (color: ColorState) => {
+ const val = String(color.hex);
+ this.colorStk = val;
+ return true;
+ }
+
+ @undoBatch
+ @action
+ switchFil = (color: ColorState) => {
+ const val = String(color.hex);
+ this.colorFil = val;
+ return true;
+ }
+
+
+ colorPicker(setter: (color: string) => {}, type: string) {
+ return <div className="btn-group-palette" key="colorpicker" style={{ width: 160, margin: 10 }}>
+ <SketchPicker onChange={type === "stk" ? this.switchStk : this.switchFil} presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']}
+ color={type === "stk" ? this.colorStk : this.colorFil} />
+ </div>;
+ }
+ inputBox = (key: string, value: any, setter: (val: string) => {}) => {
+ return <>
+ <input style={{ color: "black", width: 40, position: "absolute", right: 20 }}
+ type="text" value={value}
+ onChange={undoBatch(action((e) => setter(e.target.value)))}
+ autoFocus />
+ <button className="antiMenu-Buttonup" key="up1" onPointerDown={undoBatch(action(() => this.upDownButtons("up", key)))}>
+ ˄
+ </button>
+ <br />
+ <button className="antiMenu-Buttonup" key="down1" onPointerDown={undoBatch(action(() => this.upDownButtons("down", key)))} style={{ marginTop: -8 }}>
+ ˅
+ </button>
+ </>;
+ }
+
+ inputBoxDuo = (key: string, value: any, setter: (val: string) => {}, title1: string, key2: string, value2: any, setter2: (val: string) => {}, title2: string) => {
+ return <>
+ {title1}
+ <p style={{ marginTop: -20, right: 70, position: "absolute" }}>{title2}</p>
+
+ <input style={{ color: "black", width: 40, position: "absolute", right: 130 }}
+ type="text" value={value}
+ onChange={e => setter(e.target.value)}
+ autoFocus />
+ <button className="antiMenu-Buttonup" key="up2" onPointerDown={undoBatch(action(() => this.upDownButtons("up", key)))} style={{ right: 110 }}>
+ ˄
+ </button>
+ <button className="antiMenu-Buttonup" key="down2" onPointerDown={undoBatch(action(() => this.upDownButtons("down", key)))} style={{ marginTop: 12, right: 110 }}>
+ ˅
+ </button>
+ {title2 === "" ? "" : <>
+ <input style={{ color: "black", width: 40, position: "absolute", right: 20 }}
+ type="text" value={value2}
+ onChange={e => setter2(e.target.value)}
+ autoFocus />
+ <button className="antiMenu-Buttonup" key="up3" onPointerDown={undoBatch(action(() => this.upDownButtons("up", key2)))}>
+ ˄
+ </button>
+ <br />
+ <button className="antiMenu-Buttonup" key="down3" onPointerDown={undoBatch(action(() => this.upDownButtons("down", key2)))} style={{ marginTop: -8 }}>
+ ˅
+ </button></>}
+ </>;
+ }
+
+
+ colorButton(value: string, setter: () => {}) {
+ return <>
+ <button className="antimodeMenu-button" key="color" onPointerDown={undoBatch(action(e => setter()))} style={{ position: "relative", marginTop: -5 }}>
+ <div className="color-previewII" style={{ backgroundColor: value ?? "121212" }} />
+ {value === "" || value === "transparent" ? <p style={{ fontSize: 25, color: "red", marginTop: -23, position: "fixed" }}>☒</p> : ""}
+ </button>
+ </>;
+ }
+
+ controlPointsButton() {
+ return <>
+ <button className="antimodeMenu-button" title="Edit points" key="bezier" onPointerDown={action(() => this._controlBtn = this._controlBtn ? false : true)} style={{ position: "relative", marginTop: 10, backgroundColor: this._controlBtn ? "black" : "" }}>
+ <FontAwesomeIcon icon="bezier-curve" size="lg" />
+ </button>
+ <button className="antimodeMenu-button" title="Lock ratio" key="ratio" onPointerDown={action(() => this._lock = this._lock ? false : true)} style={{ position: "relative", marginTop: 10, backgroundColor: this._lock ? "black" : "" }}>
+ <FontAwesomeIcon icon="lock" size="lg" />
+
+ </button>
+ <button className="antimodeMenu-button" key="rotate" title="Rotate 90˚" onPointerDown={action(() => this.rotate(Math.PI / 2))} style={{ position: "relative", marginTop: 10, fontSize: 15 }}>
+ ⟲
+ </button>
+ <br /> <br />
+ </>;
+ }
+
+ lockRatioButton() {
+ return <>
+ <button className="antimodeMenu-button" key="lock" onPointerDown={action(() => this._lock = this._lock ? false : true)} style={{ position: "absolute", right: 80, backgroundColor: this._lock ? "black" : "" }}>
+ {/* <FontAwesomeIcon icon="bezier-curve" size="lg" /> */}
+ <FontAwesomeIcon icon="lock" size="lg" />
+
+ </button>
+ <br /> <br />
+ </>;
+ }
+
+ rotate90Button() {
+ return <>
+ <button className="antimodeMenu-button" key="rot" onPointerDown={action(() => this.rotate(Math.PI / 2))} style={{ position: "absolute", right: 80, }}>
+ {/* <FontAwesomeIcon icon="bezier-curve" size="lg" /> */}
+ ⟲
+
+ </button>
+ <br /> <br />
+ </>;
+ }
+ @computed get fillButton() { return this.colorButton(this.colorFil, () => { this._fillBtn = !this._fillBtn; this._lineBtn = false; return true; }); }
+ @computed get lineButton() { return this.colorButton(this.colorStk, () => { this._lineBtn = !this._lineBtn; this._fillBtn = false; return true; }); }
+
+ @computed get fillPicker() { return this.colorPicker((color: string) => this.colorFil = color, "fil"); }
+ @computed get linePicker() { return this.colorPicker((color: string) => this.colorStk = color, "stk"); }
+
+ @computed get stkInput() { return this.inputBox("stk", this.widthStk, (val: string) => this.widthStk = val); }
+ @computed get dashInput() { return this.inputBox("dsh", this.widthStk, (val: string) => this.widthStk = val); }
+
+ @computed get hgtInput() { return this.inputBoxDuo("hgt", this.shapeHgt, (val: string) => this.shapeHgt = val, "H:", "wid", this.shapeWid, (val: string) => this.shapeWid = val, "W:"); }
+ @computed get widInput() { return this.inputBox("wid", this.shapeWid, (val: string) => this.shapeWid = val); }
+ @computed get rotInput() { return this.inputBoxDuo("rot", this.shapeRot, (val: string) => { this.rotate(Number(val) - Number(this.shapeRot)); this.shapeRot = val; return true; }, "∠:", "rot", this.shapeRot, (val: string) => this.shapeRot = val, ""); }
+
+ @computed get XpsInput() { return this.inputBoxDuo("Xps", this.shapeXps, (val: string) => this.shapeXps = val, "X:", "Yps", this.shapeYps, (val: string) => this.shapeYps = val, "Y:"); }
+ @computed get YpsInput() { return this.inputBox("Yps", this.shapeYps, (val: string) => this.shapeYps = val); }
+
+ @computed get controlPoints() { return this.controlPointsButton(); }
+ @computed get lockRatio() { return this.lockRatioButton(); }
+ @computed get rotate90() { return this.rotate90Button(); }
+
+
+ @computed get propertyGroupItems() {
+ const fillCheck = <div key="fill" style={{ display: (this._subOpen[0] && this.selectedInk && this.selectedInk.length >= 1) ? "" : "none", width: "inherit", backgroundColor: "#323232", color: "white", }}>
+ Fill:
+ {this.fillButton}
+ <div style={{ float: "left", width: 100 }} >
+ Stroke:
+ {this.lineButton}
+ </div>
+
+ {this._fillBtn ? this.fillPicker : ""}
+ {this._lineBtn ? this.linePicker : ""}
+ {this._fillBtn || this._lineBtn ? "" : <br />}
+ {(this.solidStk || this.dashdStk) ? "Width" : ""}
+ {(this.solidStk || this.dashdStk) ? this.stkInput : ""}
+
+
+ {(this.solidStk || this.dashdStk) ? <input type="range" defaultValue={Number(this.widthStk)} min={1} max={100} onChange={undoBatch(action((e) => this.widthStk = e.target.value))} /> : (null)}
+ <br />
+ {(this.solidStk || this.dashdStk) ? <>
+ <p style={{ position: "absolute", fontSize: 12 }}>Arrow Head</p>
+ <input key="markHead" className="formatShapePane-inputBtn" type="checkbox" checked={this.markHead !== ""} onChange={undoBatch(action(() => this.markHead = this.markHead ? "" : "arrow"))} style={{ position: "absolute", right: 110, width: 20 }} />
+ <p style={{ position: "absolute", fontSize: 12, right: 30 }}>Arrow End</p>
+ <input key="markTail" className="formatShapePane-inputBtn" type="checkbox" checked={this.markTail !== ""} onChange={undoBatch(action(() => this.markTail = this.markTail ? "" : "arrow"))} style={{ position: "absolute", right: 0, width: 20 }} />
+ <br />
+ </> : ""}
+ Dash: <input key="markHead" className="formatShapePane-inputBtn" type="checkbox" checked={this.dashdStk === "2"} onChange={undoBatch(action(() => this.dashdStk = this.dashdStk === "2" ? "0" : "2"))} style={{ position: "absolute", right: 110, width: 20 }} />
+
+
+
+ </div>;
+
+
+
+ const sizeCheck =
+
+ <div key="sizeCheck" style={{ display: (this._subOpen[1] && this.selectedInk && this.selectedInk.length >= 1) ? "" : "none", width: "inherit", backgroundColor: "#323232", color: "white", }}>
+ {this.controlPoints}
+ {this.hgtInput}
+ {this.XpsInput}
+ {this.rotInput}
+
+ </div>;
+
+
+ const subMenus = this._currMode === "fill-drip" ? [`Appearance`, 'Transform'] : [];
+ const menuItems = this._currMode === "fill-drip" ? [fillCheck, sizeCheck] : [];
+ const indexOffset = 0;
+
+ return <div className="antimodeMenu-sub" key="submenu" style={{ position: "absolute", width: "inherit", top: 60 }}>
+ {subMenus.map((subMenu, i) =>
+ <div key={subMenu} style={{ width: "inherit" }}>
+ <button className="antimodeMenu-button" onPointerDown={action(() => this._subOpen[i + indexOffset] = !this._subOpen[i + indexOffset])}
+ style={{ backgroundColor: "121212", position: "relative", width: "inherit" }}>
+ {this._subOpen[i + indexOffset] ? "▼" : "▶︎"}
+ {subMenu}
+ </button>
+ {menuItems[i]}
+ </div>)}
+ </div>;
+ }
+
+ @computed get closeBtn() {
+ return <button className="antimodeMenu-button" key="close" onPointerDown={action(() => this.closePane())} style={{ position: "absolute", right: 0 }}>
+ X
+ </button>;
+ }
+
+ @computed get propertyGroupBtn() {
+ return <div className="antimodeMenu-button-tab" key="modes">
+ {this._mode.map(mode =>
+ <button className="antimodeMenu-button" key={mode} onPointerDown={action(() => this._currMode = mode)}
+ style={{ backgroundColor: this._currMode === mode ? "121212" : "", position: "relative", top: 30 }}>
+ <FontAwesomeIcon icon={mode as IconProp} size="lg" />
+ </button>)}
+ </div>;
+ }
+
+ render() {
+ return this.getElementVert([this.closeBtn,
+ this.propertyGroupItems]);
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx
deleted file mode 100644
index f1032adaa..000000000
--- a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx
+++ /dev/null
@@ -1,328 +0,0 @@
-import React = require("react");
-import AntimodeMenu from "../../AntimodeMenu";
-import { observer } from "mobx-react";
-import { observable, action, computed } from "mobx";
-import "./InkOptionsMenu.scss";
-import { ActiveInkColor, ActiveInkBezierApprox, ActiveFillColor, ActiveArrowStart, ActiveArrowEnd, SetActiveInkWidth, SetActiveInkColor, SetActiveBezierApprox, SetActiveFillColor, SetActiveArrowStart, SetActiveArrowEnd, ActiveDash, SetActiveDash } from "../../InkingStroke";
-import { Scripting } from "../../../util/Scripting";
-import { InkTool } from "../../../../fields/InkField";
-import { ColorState } from "react-color";
-import { Utils } from "../../../../Utils";
-import GestureOverlay from "../../GestureOverlay";
-import { Doc } from "../../../../fields/Doc";
-import { SelectionManager } from "../../../util/SelectionManager";
-import { DocumentView } from "../../../views/nodes/DocumentView";
-import { Document } from "../../../../fields/documentSchemas";
-import { DocumentType } from "../../../documents/DocumentTypes";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
-import { faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSubscript, faSuperscript, faIndent, faEyeDropper, faCaretDown, faPalette, faArrowsAlt, faHighlighter, faLink, faPaintRoller, faSleigh, faBars, faFillDrip, faBrush, faPenNib, faShapes, faArrowLeft, faEllipsisH, faBezierCurve, } from "@fortawesome/free-solid-svg-icons";
-
-library.add(faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent, faEyeDropper, faCaretDown, faPalette, faArrowsAlt, faHighlighter, faLink, faPaintRoller, faBars, faFillDrip, faBrush, faPenNib, faShapes, faArrowLeft, faEllipsisH, faBezierCurve);
-
-@observer
-export default class InkOptionsMenu extends AntimodeMenu {
- static Instance: InkOptionsMenu;
-
- private _palette = ["#D0021B", "#F5A623", "#F8E71C", "#8B572A", "#7ED321", "#417505", "#9013FE", "#4A90E2", "#50E3C2", "#B8E986", "#000000", "#4A4A4A", "#9B9B9B", "#FFFFFF", "none"];
- private _width = ["1", "5", "10", "100"];
- // private _buttons = ["circle", "triangle", "rectangle", "arrow", "line"];
- // private _icons = ["O", "∆", "ロ", "➜", "-"];
- private _buttons = ["circle", "triangle", "rectangle", "line", "noRec", "",];
- private _icons = ["O", "∆", "ロ", "⎯", "✖︎", " "];
- //arrowStart and arrowEnd must match and defs must exist in Inking Stroke
- private _arrowStart = ["arrowHead", "arrowHead", "dot", "dot", "none"];
- private _arrowEnd = ["none", "arrowEnd", "none", "dot", "none"];
- private _arrowIcons = ["→", "↔︎", "•", "••", " "];
-
- @observable _colorBtn = false;
- @observable _widthBtn = false;
- @observable _fillBtn = false;
- @observable _arrowBtn = false;
- @observable _dashBtn = false;
- @observable _shapeBtn = false;
-
- constructor(props: Readonly<{}>) {
- super(props);
- InkOptionsMenu.Instance = this;
- this._canFade = false; // don't let the inking menu fade away
- }
-
- getColors = () => {
- return this._palette;
- }
-
- @action
- changeArrow = (arrowStart: string, arrowEnd: string) => {
- SetActiveArrowStart(arrowStart);
- SetActiveArrowEnd(arrowEnd);
- }
-
- @action
- changeColor = (color: string, type: string) => {
- const col: ColorState = {
- hex: color, hsl: { a: 0, h: 0, s: 0, l: 0, source: "" }, hsv: { a: 0, h: 0, s: 0, v: 0, source: "" },
- rgb: { a: 0, r: 0, b: 0, g: 0, source: "" }, oldHue: 0, source: "",
- };
- if (type === "color") {
- SetActiveInkColor(Utils.colorString(col));
- } else if (type === "fill") {
- SetActiveFillColor(Utils.colorString(col));
- }
- }
-
- @action
- editProperties = (value: any, field: string) => {
- SelectionManager.SelectedDocuments().forEach(action((element: DocumentView) => {
- const doc = Document(element.rootDoc);
- if (doc.type === DocumentType.INK) {
- switch (field) {
- case "width":
- doc.strokeWidth = Number(value);
- break;
- case "color":
- doc.color = String(value);
- break;
- case "fill":
- doc.fillColor = String(value);
- break;
- case "bezier":
- // doc.strokeBezier === 300 ? doc.strokeBezier = 0 : doc.strokeBezier = 300;
- break;
- case "arrowStart":
- doc.arrowStart = String(value);
- break;
- case "arrowEnd":
- doc.arrowEnd = String(value);
- break;
- case "dash":
- doc.dash = Number(value);
- default:
- break;
- }
- }
- }));
- }
-
-
- @action
- changeBezier = (e: React.PointerEvent): void => {
- SetActiveBezierApprox(!ActiveInkBezierApprox() ? "300" : "");
- this.editProperties(0, "bezier");
- }
- @action
- changeDash = (e: React.PointerEvent): void => {
- SetActiveDash(ActiveDash() === "0" ? "2" : "0");
- this.editProperties(ActiveDash(), "dash");
- }
-
- @computed get arrowPicker() {
- var currIcon;
- for (var i = 0; i < this._arrowStart.length; i++) {
- if (this._arrowStart[i] === ActiveArrowStart() && this._arrowEnd[i] === ActiveArrowEnd()) {
- currIcon = this._arrowIcons[i];
- if (this._arrowIcons[i] === " ") {
- currIcon = "➤";
- }
- }
- }
- var arrowPicker = <button
- className="antimodeMenu-button"
- key="arrow"
- onPointerDown={action(e => this._arrowBtn = !this._arrowBtn)}
- style={{ backgroundColor: this._arrowBtn ? "121212" : "" }}>
- {currIcon}
- </button>;
- if (this._arrowBtn) {
- arrowPicker = <div className="btn2-group" key="arrows">
- {arrowPicker}
- {this._arrowStart.map((arrowStart, i) => {
- return <button
- className="antimodeMenu-button"
- key={arrowStart}
- onPointerDown={action(() => { SetActiveArrowStart(arrowStart); SetActiveArrowEnd(this._arrowEnd[i]); this.editProperties(arrowStart, "arrowStart"), this.editProperties(this._arrowEnd[i], "arrowEnd"); this._arrowBtn = false; })}
- style={{ backgroundColor: this._arrowBtn ? "121212" : "" }}>
- {this._arrowIcons[i]}
- </button>;
- })}
- </div>;
- }
- return arrowPicker;
- }
-
- @computed get widthPicker() {
- var widthPicker = <button
- className="antimodeMenu-button"
- key="width"
- onPointerDown={action(e => this._widthBtn = !this._widthBtn)}
- style={{ backgroundColor: this._widthBtn ? "121212" : "" }}>
- <FontAwesomeIcon icon="bars" size="lg" />
- </button>;
- if (this._widthBtn) {
- widthPicker = <div className="btn2-group" key="width">
- {widthPicker}
- {this._width.map(wid => {
- return <button
- className="antimodeMenu-button"
- key={wid}
- onPointerDown={action(() => { SetActiveInkWidth(wid); this._widthBtn = false; this.editProperties(wid, "width"); })}
- style={{ backgroundColor: this._widthBtn ? "121212" : "" }}>
- {wid}
- </button>;
- })}
- </div>;
- }
- return widthPicker;
- }
-
-
-
- @computed get colorPicker() {
- var colorPicker = <button
- className="antimodeMenu-button"
- key="color"
- title="colorChanger"
- onPointerDown={action(e => this._colorBtn = !this._colorBtn)}
- style={{ backgroundColor: this._colorBtn ? "121212" : "" }}>
- <FontAwesomeIcon icon="pen-nib" size="lg" />
- <div className="color-previewI" style={{ backgroundColor: ActiveInkColor() ?? "121212" }}></div>
-
- </button>;
- if (this._colorBtn) {
- colorPicker = <div className="btn-group" key="color">
- {colorPicker}
- {this._palette.map(color => {
- return <button
- className="antimodeMenu-button"
- key={color}
- onPointerDown={action(() => { this.changeColor(color, "color"); this._colorBtn = false; this.editProperties(color, "color"); })}
- style={{ backgroundColor: this._colorBtn ? "121212" : "" }}>
- {/* <FontAwesomeIcon icon="pen-nib" size="lg" /> */}
- <div className="color-previewII" style={{ backgroundColor: color }}></div>
- </button>;
- })}
- </div>;
- }
- return colorPicker;
- }
-
- @computed get fillPicker() {
- var fillPicker = <button
- className="antimodeMenu-button"
- key="fill"
- title="fillChanger"
- onPointerDown={action(e => this._fillBtn = !this._fillBtn)}
- style={{ backgroundColor: this._fillBtn ? "121212" : "" }}>
- <FontAwesomeIcon icon="fill-drip" size="lg" />
- <div className="color-previewI" style={{ backgroundColor: ActiveFillColor() ?? "121212" }}></div>
- </button>;
- if (this._fillBtn) {
- fillPicker = <div className="btn-group" key="fill">
- {fillPicker}
- {this._palette.map(color => {
- return <button
- className="antimodeMenu-button"
- key={color}
- onPointerDown={action(() => { this.changeColor(color, "fill"); this._fillBtn = false; this.editProperties(color, "fill"); })}
- style={{ backgroundColor: this._fillBtn ? "121212" : "" }}>
- <div className="color-previewII" style={{ backgroundColor: color }}></div>
- </button>;
- })}
-
- </div>;
- }
- return fillPicker;
- }
-
- @computed get shapePicker() {
- var currIcon;
- if (GestureOverlay.Instance.InkShape === "") {
- currIcon = <FontAwesomeIcon icon="shapes" size="lg" />;
- } else {
- for (var i = 0; i < this._icons.length; i++) {
- if (GestureOverlay.Instance.InkShape === this._buttons[i]) {
- currIcon = this._icons[i];
- }
- }
- }
- var shapePicker = <button
- className="antimodeMenu-button"
- key="shape"
- onPointerDown={action(e => this._shapeBtn = !this._shapeBtn)}
- style={{ backgroundColor: this._shapeBtn ? "121212" : "" }}>
- {currIcon}
- </button>;
- if (this._shapeBtn) {
- shapePicker = <div className="btn2-group" key="shape">
- {shapePicker}
- {this._buttons.map((btn, i) => {
- var ttl = btn;
- if (btn === "") {
- ttl = "no shape";
- }
- if (btn === "noRec") {
- ttl = "disable shape recognition";
- }
- return <button
- className="antimodeMenu-button"
- title={`Draw ${btn}`}
- key={ttl}
- onPointerDown={action((e) => { GestureOverlay.Instance.InkShape = btn; this._shapeBtn = false; })}
- style={{ backgroundColor: this._shapeBtn ? "121212" : "" }}>
- {this._icons[i]}
- </button>;
- })}
- </div>;
- }
- return shapePicker;
- }
-
- @computed get bezierButton() {
- return <button
- className="antimodeMenu-button"
- title="Bezier changer"
- key="bezier"
- onPointerDown={e => this.changeBezier(e)}
- style={{ backgroundColor: ActiveInkBezierApprox() ? "121212" : "" }}>
- <FontAwesomeIcon icon="bezier-curve" size="lg" />
-
- </button>;
- }
-
- @computed get dashButton() {
- return <button
- className="antimodeMenu-button"
- title="dash changer"
- key="dash"
- onPointerDown={e => this.changeDash(e)}
- style={{ backgroundColor: ActiveDash() !== "0" ? "121212" : "" }}>
- <FontAwesomeIcon icon="ellipsis-h" size="lg" />
-
- </button>;
- }
-
- render() {
- const buttons = [
- // <button className="antimodeMenu-button" title="Drag" key="drag" onPointerDown={e => this.dragStart(e)}>
- // <FontAwesomeIcon icon="arrows-alt" size="lg" />
- // </button>,
- this.shapePicker,
- this.bezierButton,
- this.widthPicker,
- this.colorPicker,
- this.fillPicker,
- this.arrowPicker,
- this.dashButton,
- ];
- return this.getElement(buttons);
- }
-}
-Scripting.addGlobal(function activatePen(penBtn: any) {
- if (penBtn) {
- Doc.SetSelectedTool(InkTool.Pen);
- InkOptionsMenu.Instance.jumpTo(300, 300);
- } else {
- Doc.SetSelectedTool(InkTool.None);
- InkOptionsMenu.Instance.fadeOut(true);
- }
-}); \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 97ed74c10..764758eee 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -1,6 +1,7 @@
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
-import { Doc, Opt, DocListCast, DataSym } from "../../../../fields/Doc";
+import { Doc, Opt, DocListCast, DataSym, AclEdit, AclAddonly, AclAdmin } from "../../../../fields/Doc";
+import { GetEffectiveAcl, getPlaygroundMode } from "../../../../fields/util";
import { InkData, InkField, InkTool } from "../../../../fields/InkField";
import { List } from "../../../../fields/List";
import { RichTextField } from "../../../../fields/RichTextField";
@@ -129,7 +130,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
} else if (!e.ctrlKey && !e.metaKey) {
FormattedTextBox.SelectOnLoadChar = FormattedTextBox.DefaultLayout ? e.key : "";
const tbox = Docs.Create.TextDocument("", {
- _width: 200, _height: 100, x: x, y: y, _autoHeight: true, _fontSize: NumCast(Doc.UserDoc().fontSize),
+ _width: 200, _height: 100, x: x, y: y, _autoHeight: true, _fontSize: StrCast(Doc.UserDoc().fontSize),
_fontFamily: StrCast(Doc.UserDoc().fontFamily),
title: "-typed text-"
});
@@ -188,15 +189,18 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
onPointerDown = (e: React.PointerEvent): void => {
this._downX = this._lastX = e.clientX;
this._downY = this._lastY = e.clientY;
- // allow marquee if right click OR alt+left click OR space bar + left click
- if (e.button === 2 || (e.button === 0 && (e.altKey || (MarqueeView.DragMarquee && this.props.active(true))))) {
- // if (e.altKey || (MarqueeView.DragMarquee && this.props.active(true))) {
- this.setPreviewCursor(e.clientX, e.clientY, true);
- // (!e.altKey) && e.stopPropagation(); // bcz: removed so that you can alt-click on button in a collection to switch link following behaviors.
- e.preventDefault();
- // }
- // bcz: do we need this? it kills the context menu on the main collection if !altKey
- // e.stopPropagation();
+ if (!(e.nativeEvent as any).marqueeHit) {
+ (e.nativeEvent as any).marqueeHit = true;
+ // allow marquee if right click OR alt+left click OR space bar + left click
+ if (e.button === 2 || (e.button === 0 && (e.altKey || (MarqueeView.DragMarquee && this.props.active(true))))) {
+ // if (e.altKey || (MarqueeView.DragMarquee && this.props.active(true))) {
+ this.setPreviewCursor(e.clientX, e.clientY, true);
+ // (!e.altKey) && e.stopPropagation(); // bcz: removed so that you can alt-click on button in a collection to switch link following behaviors.
+ e.preventDefault();
+ // }
+ // bcz: do we need this? it kills the context menu on the main collection if !altKey
+ // e.stopPropagation();
+ }
}
}
@@ -276,7 +280,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
} else {
this._downX = x;
this._downY = y;
- PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge);
+ const effectiveAcl = GetEffectiveAcl(this.props.Document);
+ if ([AclAdmin, AclEdit, AclAddonly].includes(effectiveAcl) || getPlaygroundMode()) PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge);
this.clearSelection();
}
});
@@ -286,7 +291,10 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD &&
Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
if (Doc.GetSelectedTool() === InkTool.None) {
- !(e.nativeEvent as any).formattedHandled && this.setPreviewCursor(e.clientX, e.clientY, false);
+ if (!(e.nativeEvent as any).marqueeHit) {
+ (e.nativeEvent as any).marqueeHit = true;
+ !(e.nativeEvent as any).formattedHandled && this.setPreviewCursor(e.clientX, e.clientY, false);
+ }
}
// let the DocumentView stopPropagation of this event when it selects this document
} else { // why do we get a click event when the cursor have moved a big distance?
@@ -426,8 +434,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
});
CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then((results) => {
// const wordResults = results.filter((r: any) => r.category === "inkWord");
- // console.log(wordResults);
- // console.log(results);
// for (const word of wordResults) {
// const indices: number[] = word.strokeIds;
// indices.forEach(i => {
@@ -468,7 +474,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
// });
// }
const lines = results.filter((r: any) => r.category === "line");
- console.log(lines);
const text = lines.map((l: any) => l.recognizedText).join("\r\n");
this.props.addDocument(Docs.Create.TextDocument(text, { _width: this.Bounds.width, _height: this.Bounds.height, x: this.Bounds.left + this.Bounds.width, y: this.Bounds.top, title: text }));
});
@@ -493,7 +498,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
summary._backgroundColor = "#e2ad32";
portal.layoutKey = "layout_portal";
portal.title = "document collection";
- DocUtils.MakeLink({ doc: summary }, { doc: portal }, "summarizing");
+ DocUtils.MakeLink({ doc: summary }, { doc: portal }, "summarizing", "");
this.props.addLiveTextDocument(summary);
MarqueeOptionsMenu.Instance.fadeOut(true);
diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.scss b/src/client/views/collections/collectionGrid/CollectionGridView.scss
index 9c2d5cbff..4d8473be9 100644
--- a/src/client/views/collections/collectionGrid/CollectionGridView.scss
+++ b/src/client/views/collections/collectionGrid/CollectionGridView.scss
@@ -8,7 +8,6 @@
.collectionGridView-gridContainer {
height: 100%;
overflow-y: auto;
- background-color: white;
overflow-x: hidden;
display: flex;
@@ -22,7 +21,7 @@
}
.react-grid-layout {
- width : 100%;
+ width: 100%;
}
.react-grid-item>.react-resizable-handle {
diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
index 2015ca930..21f77e47b 100644
--- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx
+++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
@@ -1,7 +1,7 @@
import { action, computed, Lambda, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from "react";
-import { Doc, Opt } from '../../../../fields/Doc';
+import { Doc, Opt, WidthSym } from '../../../../fields/Doc';
import { documentSchema } from '../../../../fields/documentSchemas';
import { Id } from '../../../../fields/FieldSymbols';
import { makeInterface } from '../../../../fields/Schema';
@@ -30,8 +30,9 @@ export class CollectionGridView extends CollectionSubView(GridSchema) {
private _resetListenerDisposer: Opt<Lambda>; // listens for when the reset button is clicked
@observable private _rowHeight: Opt<number>; // temporary store of row height to make change undoable
@observable private _scroll: number = 0; // required to make sure the decorations box container updates on scroll
+ private dropLocation: object = {}; // sets the drop location for external drops
- @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); }
+ onChildClickHandler = () => ScriptCast(this.Document.onChildClick);
@computed get numCols() { return NumCast(this.props.Document.gridNumCols, 10); }
@computed get rowHeight() { return this._rowHeight === undefined ? NumCast(this.props.Document.gridRowHeight, 100) : this._rowHeight; }
@@ -47,6 +48,9 @@ export class CollectionGridView extends CollectionSubView(GridSchema) {
@computed get flexGrid() { return BoolCast(this.props.Document.gridFlex, true); } // is grid static/flexible i.e. whether nodes be moved around and resized
@computed get compaction() { return StrCast(this.props.Document.gridStartCompaction, StrCast(this.props.Document.gridCompaction, "vertical")); } // is grid static/flexible i.e. whether nodes be moved around and resized
+ /**
+ * Sets up the listeners for the list of documents and the reset button.
+ */
componentDidMount() {
this._changeListenerDisposer = reaction(() => this.childLayoutPairs, (pairs) => {
const newLayouts: Layout[] = [];
@@ -54,7 +58,15 @@ export class CollectionGridView extends CollectionSubView(GridSchema) {
pairs.forEach((pair, i) => {
const existing = oldLayouts.find(l => l.i === pair.layout[Id]);
if (existing) newLayouts.push(existing);
- else this.addLayoutItem(newLayouts, this.makeLayoutItem(pair.layout, this.unflexedPosition(i), !this.flexGrid));
+ else {
+ if (Object.keys(this.dropLocation).length) { // external drop
+ this.addLayoutItem(newLayouts, this.makeLayoutItem(pair.layout, this.dropLocation as { x: number, y: number }, !this.flexGrid));
+ this.dropLocation = {};
+ }
+ else { // internal drop
+ this.addLayoutItem(newLayouts, this.makeLayoutItem(pair.layout, this.unflexedPosition(i), !this.flexGrid));
+ }
+ }
});
pairs?.length && this.setLayoutList(newLayouts);
}, { fireImmediately: true });
@@ -68,11 +80,18 @@ export class CollectionGridView extends CollectionSubView(GridSchema) {
});
}
+ /**
+ * Disposes the listeners.
+ */
componentWillUnmount() {
this._changeListenerDisposer?.();
this._resetListenerDisposer?.();
}
+ /**
+ * @returns the default location of the grid node (i.e. when the grid is static)
+ * @param index
+ */
unflexedPosition(index: number): Omit<Layout, "i"> {
return {
x: (index % Math.floor(this.numCols / this.defaultW)) * this.defaultW,
@@ -83,6 +102,9 @@ export class CollectionGridView extends CollectionSubView(GridSchema) {
};
}
+ /**
+ * Maps the x- and y- coordinates of the event to a grid cell.
+ */
screenToCell(sx: number, sy: number) {
const pt = this.props.ScreenToLocalTransform().transformPoint(sx, sy);
const x = Math.floor(pt[0] / this.colWidthPlusGap);
@@ -90,10 +112,16 @@ export class CollectionGridView extends CollectionSubView(GridSchema) {
return { x, y };
}
+ /**
+ * Creates a layout object for a grid item
+ */
makeLayoutItem = (doc: Doc, pos: { x: number, y: number }, Static: boolean = false, w: number = this.defaultW, h: number = this.defaultH) => {
return ({ i: doc[Id], w, h, x: pos.x, y: pos.y, static: Static });
}
+ /**
+ * Adds a layout to the list of layouts.
+ */
addLayoutItem = (layouts: Layout[], layout: Layout) => {
const f = layouts.findIndex(l => l.i === layout.i);
f !== -1 && layouts.splice(f, 1);
@@ -215,6 +243,9 @@ export class CollectionGridView extends CollectionSubView(GridSchema) {
this.savedLayoutList.map((layout, index) => Object.assign(layout, this.unflexedPosition(index)));
}
+ /**
+ * Handles internal drop of Dash documents.
+ */
@action
onInternalDrop = (e: Event, de: DragManager.DropEvent) => {
const savedLayouts = this.savedLayoutList;
@@ -228,12 +259,24 @@ export class CollectionGridView extends CollectionSubView(GridSchema) {
}
/**
+ * Handles external drop of images/PDFs etc from outside Dash.
+ */
+ @action
+ onExternalDrop = async (e: React.DragEvent): Promise<void> => {
+ this.dropLocation = this.screenToCell(e.clientX, e.clientY);
+ super.onExternalDrop(e, {});
+ }
+
+ /**
* Handles the change in the value of the rowHeight slider.
*/
@action
onSliderChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this._rowHeight = event.currentTarget.valueAsNumber;
}
+ /**
+ * Handles the user clicking on the slider.
+ */
@action
onSliderDown = (e: React.PointerEvent) => {
this._rowHeight = this.rowHeight; // uses _rowHeight during dragging and sets doc's rowHeight when finished so that operation is undoable
@@ -248,11 +291,13 @@ export class CollectionGridView extends CollectionSubView(GridSchema) {
*/
onContextMenu = () => {
const displayOptionsMenu: ContextMenuProps[] = [];
- displayOptionsMenu.push({ description: "Contents", event: () => this.props.Document.display = "contents", icon: "copy" });
- displayOptionsMenu.push({ description: "Undefined", event: () => this.props.Document.display = undefined, icon: "exclamation" });
+ displayOptionsMenu.push({ description: "Toggle Content Display Style", event: () => this.props.Document.display = this.props.Document.display ? undefined : "contents", icon: "copy" });
ContextMenu.Instance.addItem({ description: "Display", subitems: displayOptionsMenu, icon: "tv" });
}
+ /**
+ * Handles text document creation on double click.
+ */
onPointerDown = (e: React.PointerEvent) => {
if (this.props.active(true)) {
setupMoveUpEvents(this, e, returnFalse, returnFalse,
@@ -276,8 +321,11 @@ export class CollectionGridView extends CollectionSubView(GridSchema) {
<div className="collectionGridView-contents" ref={this.createDashEventsTarget}
style={{ pointerEvents: !this.props.active() && !SnappingManager.GetIsDragging() ? "none" : undefined }}
onContextMenu={this.onContextMenu}
- onPointerDown={e => this.onPointerDown(e)} >
+ onPointerDown={this.onPointerDown}
+ onDrop={this.onExternalDrop}
+ >
<div className="collectionGridView-gridContainer" ref={this._containerRef}
+ style={{ backgroundColor: StrCast(this.layoutDoc._backgroundColor, "white") }}
onWheel={e => e.stopPropagation()}
onScroll={action(e => {
if (!this.props.isSelected()) e.currentTarget.scrollTop = this._scroll;
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
index 776266ce6..21d283547 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -10,7 +10,7 @@ import { Transform } from '../../../util/Transform';
import { undoBatch } from '../../../util/UndoManager';
import { ContentFittingDocumentView } from '../../nodes/ContentFittingDocumentView';
import { CollectionSubView } from '../CollectionSubView';
-import "./collectionMulticolumnView.scss";
+import "./CollectionMulticolumnView.scss";
import ResizeBar from './MulticolumnResizer';
import WidthLabel from './MulticolumnWidthLabel';
import { List } from '../../../../fields/List';
@@ -202,9 +202,8 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
}
- @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); }
- @computed get onChildDoubleClickHandler() { return ScriptCast(this.Document.onChildDoubleClick); }
-
+ onChildClickHandler = () => ScriptCast(this.Document.onChildClick);
+ onChildDoubleClickHandler = () => ScriptCast(this.Document.onChildDoubleClick);
addDocTab = (doc: Doc, where: string) => {
if (where === "inPlace" && this.layoutDoc.isInPlaceContainer) {
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
index 1703ff4dc..d02088a6c 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
@@ -7,7 +7,7 @@ import { Doc } from '../../../../fields/Doc';
import { NumCast, StrCast, BoolCast, ScriptCast } from '../../../../fields/Types';
import { ContentFittingDocumentView } from '../../nodes/ContentFittingDocumentView';
import { Utils, returnZero, returnFalse, returnOne } from '../../../../Utils';
-import "./collectionMultirowView.scss";
+import "./CollectionMultirowView.scss";
import { computed, trace, observable, action } from 'mobx';
import { Transform } from '../../../util/Transform';
import HeightLabel from './MultirowHeightLabel';
@@ -202,8 +202,8 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
}
- @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); }
- @computed get onChildDoubleClickHandler() { return ScriptCast(this.Document.onChildDoubleClick); }
+ onChildClickHandler = () => ScriptCast(this.Document.onChildClick);
+ onChildDoubleClickHandler = () => ScriptCast(this.Document.onChildDoubleClick);
addDocTab = (doc: Doc, where: string) => {
if (where === "inPlace" && this.layoutDoc.isInPlaceContainer) {
diff --git a/src/client/views/linking/LinkEditor.scss b/src/client/views/linking/LinkEditor.scss
index b47c8976e..d26b7920a 100644
--- a/src/client/views/linking/LinkEditor.scss
+++ b/src/client/views/linking/LinkEditor.scss
@@ -3,38 +3,188 @@
.linkEditor {
width: 100%;
height: auto;
- font-size: 12px; // TODO
+ font-size: 13px; // TODO
user-select: none;
}
-.linkEditor-back {
- margin-bottom: 6px;
+.linkEditor-button-back {
+ //margin-bottom: 6px;
+ border-radius: 10px;
+ width: 18px;
+ height: 18px;
+ padding: 0;
+
+ &:hover {
+ cursor: pointer;
+ }
}
.linkEditor-info {
- border-bottom: 0.5px solid $light-color-secondary;
- padding-bottom: 6px;
- margin-bottom: 6px;
+ //border-bottom: 0.5px solid $light-color-secondary;
+ //padding-bottom: 1px;
+ padding-top: 5px;
+ padding-left: 5px;
+ //margin-bottom: 6px;
display: flex;
justify-content: space-between;
+ color: black;
.linkEditor-linkedTo {
width: calc(100% - 26px);
+ padding-left: 5px;
+ padding-right: 5px;
+
+ .linkEditor-downArrow {
+ &:hover {
+ cursor: pointer;
+ }
+ }
+ }
+}
+
+.linkEditor-moreInfo {
+ margin-left: 12px;
+ padding-left: 13px;
+ padding-right: 6.5px;
+ padding-bottom: 4px;
+ font-size: 9px;
+ //font-style: italic;
+ text-decoration-color: grey;
+
+ .button {
+ color: black;
+
+ &:hover {
+ cursor: pointer;
+ }
+ }
+}
+
+.linkEditor-description {
+ padding-left: 6.5px;
+ padding-right: 6.5px;
+ padding-bottom: 3.5px;
+
+ .linkEditor-description-label {
+ text-decoration-color: black;
+ color: black;
+ }
+
+ .linkEditor-description-input {
+ display: flex;
+
+ .linkEditor-description-editing {
+ min-width: 85%;
+ //border: 1px solid grey;
+ //border-radius: 4px;
+ padding-left: 2px;
+ padding-right: 2px;
+ //margin-right: 4px;
+ color: black;
+ text-decoration-color: grey;
+ }
+
+ .linkEditor-description-add-button {
+ display: inline;
+ /* float: right; */
+ border-radius: 7px;
+ font-size: 9px;
+ background-color: black;
+ /* padding: 3px; */
+ padding-top: 4px;
+ padding-left: 7px;
+ padding-bottom: 4px;
+ padding-right: 8px;
+ height: 80%;
+ color: white;
+
+ &:hover {
+ cursor: pointer;
+ }
+ }
}
}
-.linkEditor-button, .linkEditor-addbutton {
+.linkEditor-followingDropdown {
+ padding-left: 6.5px;
+ padding-right: 6.5px;
+ padding-bottom: 6px;
+
+ &:hover {
+ cursor: pointer;
+ }
+
+ .linkEditor-followingDropdown-label {
+ color: black;
+ }
+
+ .linkEditor-followingDropdown-dropdown {
+
+ .linkEditor-followingDropdown-header {
+
+ border: 1px solid grey;
+ border-radius: 4px;
+ //background-color: rgb(236, 236, 236);
+ padding-left: 2px;
+ padding-right: 2px;
+ text-decoration-color: black;
+ color: rgb(94, 94, 94);
+
+ .linkEditor-followingDropdown-icon {
+ float: right;
+ color: black;
+ }
+ }
+
+ .linkEditor-followingDropdown-optionsList {
+ padding-left: 3px;
+ padding-right: 3px;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ .linkEditor-followingDropdown-option {
+ border: 0.25px solid grey;
+ //background-color: rgb(236, 236, 236);
+ padding-left: 2px;
+ padding-right: 2px;
+ color: grey;
+ text-decoration-color: grey;
+ font-size: 9px;
+ border-top: none;
+
+ &:hover {
+ background-color: rgb(187, 220, 231);
+ }
+ }
+
+ }
+ }
+
+
+}
+
+
+
+
+
+
+.linkEditor-button,
+.linkEditor-addbutton {
width: 18px;
height: 18px;
padding: 0;
// font-size: 12px;
border-radius: 10px;
+
&:disabled {
background-color: gray;
}
}
-.linkEditor-addbutton{
+
+.linkEditor-addbutton {
margin-left: 0px;
}
@@ -44,7 +194,7 @@
}
.linkEditor-group {
- background-color: $light-color-secondary;
+ background-color: $light-color-secondary;
padding: 6px;
margin: 3px 0;
border-radius: 3px;
@@ -56,7 +206,7 @@
.linkEditor-group-row-label {
margin-right: 6px;
- display:inline-block;
+ display: inline-block;
}
.linkEditor-metadata-row {
diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx
index 13b9a2459..04329182e 100644
--- a/src/client/views/linking/LinkEditor.tsx
+++ b/src/client/views/linking/LinkEditor.tsx
@@ -1,14 +1,20 @@
import { library } from "@fortawesome/fontawesome-svg-core";
import { faArrowLeft, faCog, faEllipsisV, faExchangeAlt, faPlus, faTable, faTimes, faTrash } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, observable } from "mobx";
+import { action, observable, computed, toJS } from "mobx";
import { observer } from "mobx-react";
-import { Doc } from "../../../fields/Doc";
-import { StrCast } from "../../../fields/Types";
+import { Doc, Opt } from "../../../fields/Doc";
+import { StrCast, DateCast } from "../../../fields/Types";
import { Utils } from "../../../Utils";
import { LinkManager } from "../../util/LinkManager";
import './LinkEditor.scss';
import React = require("react");
+import { DocumentView } from "../nodes/DocumentView";
+import { DocumentLinksButton } from "../nodes/DocumentLinksButton";
+import { EditableView } from "../EditableView";
+import { RefObject } from "react";
+import { Tooltip } from "@material-ui/core";
+import { undoBatch } from "../../util/UndoManager";
library.add(faArrowLeft, faEllipsisV, faTable, faTrash, faCog, faExchangeAlt, faTimes, faPlus);
@@ -281,27 +287,157 @@ interface LinkEditorProps {
@observer
export class LinkEditor extends React.Component<LinkEditorProps> {
+ @observable description = StrCast(LinkManager.currentLink?.description);
+ @observable openDropdown: boolean = false;
+ @observable showInfo: boolean = false;
+ @computed get infoIcon() { if (this.showInfo) { return "chevron-up"; } return "chevron-down"; }
+ @observable private buttonColor: string = "black";
+
+
+ //@observable description = this.props.linkDoc.description ? StrCast(this.props.linkDoc.description) : "DESCRIPTION";
+
+ @undoBatch
@action
deleteLink = (): void => {
LinkManager.Instance.deleteLink(this.props.linkDoc);
this.props.showLinks();
}
+ @action
+ setDescripValue = (value: string) => {
+ if (LinkManager.currentLink) {
+ LinkManager.currentLink.description = value;
+ this.buttonColor = "rgb(62, 133, 55)";
+ setTimeout(action(() => {
+ this.buttonColor = "black";
+ }), 750);
+ return true;
+ }
+ }
+
+ @action
+ onKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
+ if (e.key === "Enter") {
+ this.setDescripValue(this.description);
+ document.getElementById('input')?.blur();
+ }
+ }
+
+ @action
+ onDown = () => {
+ this.setDescripValue(this.description);
+ }
+
+ @action
+ handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this.description = e.target.value;
+ }
+
+
+ @computed
+ get editDescription() {
+ return <div className="linkEditor-description">
+ <div className="linkEditor-description-label">
+ Link Label:</div>
+ <div className="linkEditor-description-input">
+ <div className="linkEditor-description-editing">
+ <input
+ style={{ width: "100%" }}
+ id="input"
+ value={this.description}
+ placeholder={"enter link label"}
+ // color={"rgb(88, 88, 88)"}
+ onKeyDown={this.onKey}
+ onChange={this.handleChange}
+ ></input>
+ </div>
+ <div className="linkEditor-description-add-button"
+ style={{ backgroundColor: this.buttonColor }}
+ onPointerDown={this.onDown}>Set</div>
+ </div></div>;
+ }
+
+ @action
+ changeDropdown = () => {
+ this.openDropdown = !this.openDropdown;
+ }
+
+ @action
+ changeFollowBehavior = (follow: string) => {
+ this.openDropdown = false;
+ Doc.GetProto(this.props.linkDoc).followLinkLocation = follow;
+ }
+
+ @computed
+ get followingDropdown() {
+ return <div className="linkEditor-followingDropdown">
+ <div className="linkEditor-followingDropdown-label">
+ Follow Behavior:</div>
+ <div className="linkEditor-followingDropdown-dropdown">
+ <div className="linkEditor-followingDropdown-header"
+ onPointerDown={this.changeDropdown}>
+ {StrCast(this.props.linkDoc.followLinkLocation, "Default")}
+ <FontAwesomeIcon className="linkEditor-followingDropdown-icon"
+ icon={this.openDropdown ? "chevron-up" : "chevron-down"}
+ size={"lg"} />
+ </div>
+ <div className="linkEditor-followingDropdown-optionsList"
+ style={{ display: this.openDropdown ? "" : "none" }}>
+ <div className="linkEditor-followingDropdown-option"
+ onPointerDown={() => this.changeFollowBehavior("Default")}>
+ Default
+ </div>
+ <div className="linkEditor-followingDropdown-option"
+ onPointerDown={() => this.changeFollowBehavior("onRight")}>
+ Always open in right tab
+ </div>
+ <div className="linkEditor-followingDropdown-option"
+ onPointerDown={() => this.changeFollowBehavior("inTab")}>
+ Always open in new tab
+ </div>
+ </div>
+ </div>
+ </div>;
+ }
+
+ @action
+ changeInfo = () => {
+ this.showInfo = !this.showInfo;
+ }
+
render() {
const destination = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
const groups = [this.props.linkDoc].map(groupDoc => {
- return <LinkGroupEditor key={"gred-" + StrCast(groupDoc.linkRelationship)} linkDoc={this.props.linkDoc} sourceDoc={this.props.sourceDoc} groupDoc={groupDoc} />;
+ return <LinkGroupEditor key={"gred-" + StrCast(groupDoc.linkRelationship)} linkDoc={this.props.linkDoc}
+ sourceDoc={this.props.sourceDoc} groupDoc={groupDoc} />;
});
return !destination ? (null) : (
<div className="linkEditor">
- {this.props.hideback ? (null) : <button className="linkEditor-back" onPointerDown={() => this.props.showLinks()}><FontAwesomeIcon icon="arrow-left" size="sm" /></button>}
<div className="linkEditor-info">
- <p className="linkEditor-linkedTo">editing link to: <b>{destination.proto?.title ?? destination.title ?? "untitled"}</b></p>
- <button className="linkEditor-button" onPointerDown={() => this.deleteLink()} title="Delete link"><FontAwesomeIcon icon="trash" size="sm" /></button>
+ <Tooltip title={<><div className="dash-tooltip">Return to link menu</div></>} placement="top">
+ <button className="linkEditor-button-back"
+ style={{ display: this.props.hideback ? "none" : "" }}
+ onClick={this.props.showLinks}>
+ <FontAwesomeIcon icon="arrow-left" size="sm" /> </button>
+ </Tooltip>
+ <p className="linkEditor-linkedTo">Editing Link to: <b>{
+ destination.proto?.title ?? destination.title ?? "untitled"}</b></p>
+ <Tooltip title={<><div className="dash-tooltip">Show more link information</div></>} placement="top">
+ <div className="linkEditor-downArrow"><FontAwesomeIcon className="button" icon={this.infoIcon} size="lg" onPointerDown={this.changeInfo} /></div>
+ </Tooltip>
</div>
- {groups.length > 0 ? groups : <div className="linkEditor-group">There are currently no relationships associated with this link.</div>}
+ {this.showInfo ? <div className="linkEditor-moreInfo">
+ <div>{this.props.linkDoc.author ? <div> <b>Author:</b> {this.props.linkDoc.author}</div> : null}</div>
+ <div>{this.props.linkDoc.creationDate ? <div> <b>Creation Date:</b>
+ {DateCast(this.props.linkDoc.creationDate).toString()}</div> : null}</div>
+ </div> : null}
+
+ <div>{this.editDescription}</div>
+ <div>{this.followingDropdown}</div>
+
+ {/* {groups.length > 0 ? groups : <div className="linkEditor-group">There are currently no relationships associated with this link.</div>} */}
</div>
);
diff --git a/src/client/views/linking/LinkMenu.scss b/src/client/views/linking/LinkMenu.scss
index c372e7098..98e4171f0 100644
--- a/src/client/views/linking/LinkMenu.scss
+++ b/src/client/views/linking/LinkMenu.scss
@@ -1,22 +1,67 @@
@import "../globalCssVariables";
.linkMenu {
- width: 100%;
+ width: auto;
height: auto;
-}
-
-.linkMenu-list {
- max-height: 200px;
- overflow-y: scroll;
position: absolute;
- z-index: 10;
- background: $link-color;
- min-width: 150px
+ top: 0;
+ left: 0;
+
+ .linkMenu-list {
+
+ display: inline-block;
+
+ border: 1px solid black;
+
+ box-shadow: 3px 3px 1.5px grey;
+
+ max-height: 170px;
+ overflow-y: scroll;
+ position: relative;
+ z-index: 10;
+ background: white;
+ min-width: 170px;
+ //border-radius: 5px;
+ //padding-top: 6.5px;
+ //padding-bottom: 6.5px;
+ //padding-left: 6.5px;
+ //padding-right: 2px;
+ //width: calc(auto + 50px);
+
+ white-space: nowrap;
+ overflow-x: hidden;
+ width: 240px;
+ scrollbar-color: white;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ &:hover {
+ scrollbar-color: rgb(201, 239, 252);
+ }
+ }
+
+ .linkMenu-listEditor {
+
+ display: inline-block;
+
+ border: 1px solid black;
+
+ box-shadow: 3px 3px 1.5px grey;
+
+ max-height: 170px;
+ overflow-y: scroll;
+ position: relative;
+ z-index: 10;
+ background: white;
+ min-width: 170px;
+ }
}
.linkMenu-group {
- border-bottom: 0.5px solid lightgray;
- padding: 5px 0;
+ border-bottom: 0.5px solid lightgray;
+ //@extend: 5px 0;
&:last-child {
@@ -24,15 +69,18 @@
}
.linkMenu-group-name {
- display: flex;
+
&:hover {
p {
background-color: lightgray;
+
}
+
p.expand-one {
- width: calc(100% - 26px);
+ width: calc(100% + 20px);
}
+
.linkEditor-tableButton {
display: block;
}
@@ -40,7 +88,7 @@
p {
width: 100%;
- padding: 4px 6px;
+ //padding: 4px 6px;
line-height: 12px;
border-radius: 5px;
font-weight: bold;
@@ -50,7 +98,4 @@
display: none;
}
}
-}
-
-
-
+} \ No newline at end of file
diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx
index de1d60a09..7b5fb0127 100644
--- a/src/client/views/linking/LinkMenu.tsx
+++ b/src/client/views/linking/LinkMenu.tsx
@@ -1,4 +1,4 @@
-import { action, observable } from "mobx";
+import { action, observable, computed } from "mobx";
import { observer } from "mobx-react";
import { DocumentView } from "../nodes/DocumentView";
import { LinkEditor } from "./LinkEditor";
@@ -10,6 +10,8 @@ import { LinkMenuGroup } from "./LinkMenuGroup";
import { faTrash } from '@fortawesome/free-solid-svg-icons';
import { library } from "@fortawesome/fontawesome-svg-core";
import { DocumentLinksButton } from "../nodes/DocumentLinksButton";
+import { LinkDocPreview } from "../nodes/LinkDocPreview";
+import { isUndefined } from "util";
library.add(faTrash);
@@ -24,12 +26,28 @@ interface Props {
export class LinkMenu extends React.Component<Props> {
@observable private _editingLink?: Doc;
- @observable private _linkMenuRef: Opt<HTMLDivElement | null>;
+ @observable private _linkMenuRef = React.createRef<HTMLDivElement>();
+ private _editorRef = React.createRef<HTMLDivElement>();
+
+ //@observable private _numLinks: number = 0;
+
+ // @computed get overflow() {
+ // if (this._numLinks) {
+ // return "scroll";
+ // }
+ // return "auto";
+ // }
@action
onClick = (e: PointerEvent) => {
- if (this._linkMenuRef?.contains(e.target as any)) {
- DocumentLinksButton.EditLink = undefined;
+
+ LinkDocPreview.LinkInfo = undefined;
+
+
+ if (this._linkMenuRef && !this._linkMenuRef.current?.contains(e.target as any)) {
+ if (this._editorRef && !this._editorRef.current?.contains(e.target as any)) {
+ DocumentLinksButton.EditLink = undefined;
+ }
}
}
@action
@@ -70,11 +88,19 @@ export class LinkMenu extends React.Component<Props> {
render() {
const sourceDoc = this.props.docView.props.Document;
const groups: Map<string, Doc[]> = LinkManager.Instance.getRelatedGroupedLinks(sourceDoc);
- return <div className="linkMenu-list" ref={(r) => this._linkMenuRef = r} style={{ left: this.props.location[0], top: this.props.location[1] }}>
- {!this._editingLink ?
- this.renderAllGroups(groups) :
- <LinkEditor sourceDoc={this.props.docView.props.Document} linkDoc={this._editingLink} showLinks={action(() => this._editingLink = undefined)} />
+ return <div className="linkMenu" ref={this._linkMenuRef} >
+ {!this._editingLink ? <div className="linkMenu-list" style={{
+ left: this.props.location[0], top: this.props.location[1]
+ }}>
+ {this.renderAllGroups(groups)}
+ </div> : <div className="linkMenu-listEditor" style={{
+ left: this.props.location[0], top: this.props.location[1]
+ }}>
+ <LinkEditor sourceDoc={this.props.docView.props.Document} linkDoc={this._editingLink}
+ showLinks={action(() => this._editingLink = undefined)} />
+ </div>
}
+
</div>;
}
} \ No newline at end of file
diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx
index 89deb3a55..2ae87ac13 100644
--- a/src/client/views/linking/LinkMenuGroup.tsx
+++ b/src/client/views/linking/LinkMenuGroup.tsx
@@ -11,6 +11,7 @@ import { DocumentView } from "../nodes/DocumentView";
import './LinkMenu.scss';
import { LinkMenuItem, StartLinkTargetsDrag } from "./LinkMenuItem";
import React = require("react");
+import { Cast } from "../../../fields/Types";
interface LinkMenuGroupProps {
sourceDoc: Doc;
@@ -26,6 +27,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
private _drag = React.createRef<HTMLDivElement>();
private _table = React.createRef<HTMLDivElement>();
+ private _menuRef = React.createRef<HTMLDivElement>();
onLinkButtonDown = (e: React.PointerEvent): void => {
e.stopPropagation();
@@ -65,7 +67,8 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
render() {
const groupItems = this.props.group.map(linkDoc => {
- const destination = LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc);
+ const destination = LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc) ||
+ LinkManager.Instance.getOppositeAnchor(linkDoc, Cast(linkDoc.anchor2, Doc, null).annotationOn === this.props.sourceDoc ? Cast(linkDoc.anchor2, Doc, null) : Cast(linkDoc.anchor1, Doc, null));
if (destination && this.props.sourceDoc) {
return <LinkMenuItem key={destination[Id] + this.props.sourceDoc[Id]}
groupType={this.props.groupType}
@@ -74,17 +77,20 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
linkDoc={linkDoc}
sourceDoc={this.props.sourceDoc}
destinationDoc={destination}
- showEditor={this.props.showEditor} />;
+ showEditor={this.props.showEditor}
+ menuRef={this._menuRef} />;
}
});
return (
- <div className="linkMenu-group">
- <div className="linkMenu-group-name">
+ <div className="linkMenu-group" ref={this._menuRef}>
+
+ {/* <div className="linkMenu-group-name">
<p ref={this._drag} onPointerDown={this.onLinkButtonDown}
className={this.props.groupType === "*" || this.props.groupType === "" ? "" : "expand-one"} > {this.props.groupType}:</p>
{this.props.groupType === "*" || this.props.groupType === "" ? <></> : this.viewGroupAsTable(this.props.groupType)}
- </div>
+ </div> */}
+
<div className="linkMenu-group-wrapper">
{groupItems}
</div>
diff --git a/src/client/views/linking/LinkMenuItem.scss b/src/client/views/linking/LinkMenuItem.scss
index fd0954f65..4e13ef8c8 100644
--- a/src/client/views/linking/LinkMenuItem.scss
+++ b/src/client/views/linking/LinkMenuItem.scss
@@ -4,25 +4,101 @@
// border-top: 0.5px solid $main-accent;
position: relative;
display: flex;
- font-size: 12px;
-
+ border-bottom: 0.5px solid black;
+
+ padding-left: 6.5px;
+ padding-right: 2px;
+
+ padding-top: 6.5px;
+ padding-bottom: 6.5px;
+
+ background-color: white;
+
.linkMenu-name {
position: relative;
+ width: auto;
+
+ .linkMenu-text {
+
+ padding: 4px 2px;
+ //display: inline;
+
+ .linkMenu-source-title {
+ text-decoration: none;
+ color: rgb(43, 43, 43);
+ font-size: 7px;
+ padding-bottom: 0.75px;
+
+ margin-left: 20px;
+ }
+
+
+ .linkMenu-title-wrapper {
+
+ display: flex;
+
+ .destination-icon-wrapper {
+ //border: 0.5px solid rgb(161, 161, 161);
+ margin-right: 2px;
+ padding-right: 2px;
+
+ .destination-icon {
+ float: left;
+ width: 12px;
+ height: 12px;
+ padding: 1px;
+ margin-right: 3px;
+ color: rgb(161, 161, 161);
+ }
+ }
+
+ .linkMenu-destination-title {
+ text-decoration: none;
+ color: rgb(85, 120, 196);
+ font-size: 14px;
+ padding-bottom: 2px;
+ padding-right: 4px;
+ margin-right: 4px;
+ float: right;
+
+ &:hover {
+ text-decoration: underline;
+ color: rgb(60, 90, 156);
+ //display: inline;
+ text-overflow: break;
+ cursor: pointer;
+ }
+ }
+ }
+
+ .linkMenu-description {
+ text-decoration: none;
+ font-style: italic;
+ color: rgb(95, 97, 102);
+ font-size: 10px;
+ margin-left: 20px;
+ max-width: 125px;
+ height: auto;
+ white-space: break-spaces;
+ }
+
+ p {
+ padding-right: 4px;
+ line-height: 12px;
+ border-radius: 5px;
+ //overflow-wrap: break-word;
+ user-select: none;
+ }
- p {
- padding: 4px 6px;
- line-height: 12px;
- border-radius: 5px;
- overflow-wrap: break-word;
- user-select: none;
}
+
}
.linkMenu-item-content {
width: 100%;
}
-
+
.link-metadata {
padding: 0 10px 0 16px;
margin-bottom: 4px;
@@ -32,36 +108,40 @@
}
&:hover {
+
+ background-color: rgb(201, 239, 252);
+
.linkMenu-item-buttons {
display: flex;
}
+
.linkMenu-item-content {
- &.expand-two p {
- width: calc(100% - 52px);
- background-color: lightgray;
- }
- &.expand-three p {
- width: calc(100% - 84px);
- background-color: lightgray;
+
+ .linkMenu-destination-title {
+ text-decoration: underline;
+ color: rgb(60, 90, 156);
+ //display: inline;
+ text-overflow: break;
}
}
}
}
.linkMenu-item-buttons {
- display: none;
+ //@extend: right;
position: absolute;
top: 50%;
right: 0;
transform: translateY(-50%);
+ display: none;
.button {
width: 20px;
height: 20px;
margin: 0;
- margin-right: 6px;
+ margin-right: 4px;
+ padding-right: 6px;
border-radius: 50%;
- cursor: pointer;
pointer-events: auto;
background-color: $dark-color;
color: $light-color;
@@ -80,8 +160,10 @@
&:last-child {
margin-right: 0;
}
+
&:hover {
background: $main-accent;
+ cursor: pointer;
}
}
} \ No newline at end of file
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index edc18b6a9..d1c839c3b 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -1,6 +1,6 @@
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faArrowRight, faChevronDown, faChevronUp, faEdit, faEye, faTimes } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faArrowRight, faChevronDown, faChevronUp, faEdit, faEye, faTimes, faPencilAlt, faEyeSlash } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon, FontAwesomeIconProps } from '@fortawesome/react-fontawesome';
import { action, observable } from 'mobx';
import { observer } from "mobx-react";
import { Doc, DocListCast } from '../../../fields/Doc';
@@ -13,7 +13,12 @@ import React = require("react");
import { DocumentManager } from '../../util/DocumentManager';
import { setupMoveUpEvents, emptyFunction } from '../../../Utils';
import { DocumentView } from '../nodes/DocumentView';
-library.add(faEye, faEdit, faTimes, faArrowRight, faChevronDown, faChevronUp);
+import { DocumentLinksButton } from '../nodes/DocumentLinksButton';
+import { LinkDocPreview } from '../nodes/LinkDocPreview';
+import { Tooltip } from '@material-ui/core';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { undoBatch } from '../../util/UndoManager';
+library.add(faEye, faEdit, faTimes, faArrowRight, faChevronDown, faChevronUp, faPencilAlt, faEyeSlash);
interface LinkMenuItemProps {
@@ -24,6 +29,7 @@ interface LinkMenuItemProps {
destinationDoc: Doc;
showEditor: (linkDoc: Doc) => void;
addDocTab: (document: Doc, where: string) => boolean;
+ menuRef: React.Ref<HTMLDivElement>;
}
// drag links and drop link targets (aliasing them if needed)
@@ -69,10 +75,13 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
private _eleClone: any;
_editRef = React.createRef<HTMLDivElement>();
+ _buttonRef = React.createRef<HTMLDivElement>();
+
@observable private _showMore: boolean = false;
@action toggleShowMore(e: React.PointerEvent) { e.stopPropagation(); this._showMore = !this._showMore; }
onEdit = (e: React.PointerEvent): void => {
+ LinkManager.currentLink = this.props.linkDoc;
setupMoveUpEvents(this, e, this.editMoved, emptyFunction, () => this.props.showEditor(this.props.linkDoc));
}
@@ -95,6 +104,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
return (<div className="link-metadata">{mdRows}</div>);
}
+ @action
onLinkButtonDown = (e: React.PointerEvent): void => {
this._downX = e.clientX;
this._downY = e.clientY;
@@ -104,6 +114,10 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
document.addEventListener("pointermove", this.onLinkButtonMoved);
document.removeEventListener("pointerup", this.onLinkButtonUp);
document.addEventListener("pointerup", this.onLinkButtonUp);
+
+ if (this._buttonRef && !!!this._buttonRef.current?.contains(e.target as any)) {
+ LinkDocPreview.LinkInfo = undefined;
+ }
}
onLinkButtonUp = (e: PointerEvent): void => {
@@ -124,7 +138,10 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
e.stopPropagation();
}
+ @action
onContextMenu = (e: React.MouseEvent) => {
+ DocumentLinksButton.EditLink = undefined;
+ LinkDocPreview.LinkInfo = undefined;
e.preventDefault();
ContextMenu.Instance.addItem({ description: "Follow Default Link", event: () => this.followDefault(), icon: "arrow-right" });
ContextMenu.Instance.displayMenu(e.clientX, e.clientY);
@@ -132,25 +149,114 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
@action.bound
async followDefault() {
- DocumentManager.Instance.FollowLink(this.props.linkDoc, this.props.sourceDoc, doc => this.props.addDocTab(doc, "onRight"), false);
+ DocumentLinksButton.EditLink = undefined;
+ LinkDocPreview.LinkInfo = undefined;
+
+ if (this.props.linkDoc.followLinkLocation && this.props.linkDoc.followLinkLocation !== "Default") {
+ this.props.addDocTab(this.props.destinationDoc, StrCast(this.props.linkDoc.followLinkLocation));
+ } else {
+ DocumentManager.Instance.FollowLink(this.props.linkDoc, this.props.sourceDoc, doc => this.props.addDocTab(doc, "onRight"), false);
+ }
+ }
+
+ @undoBatch
+ @action
+ deleteLink = (): void => {
+ LinkManager.Instance.deleteLink(this.props.linkDoc);
+ LinkDocPreview.LinkInfo = undefined;
+ DocumentLinksButton.EditLink = undefined;
+ }
+
+ @action
+ showLink = () => {
+ this.props.linkDoc.hidden = !this.props.linkDoc.hidden;
}
render() {
const keys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);//groupMetadataKeys.get(this.props.groupType);
const canExpand = keys ? keys.length > 0 : false;
+ const eyeIcon = this.props.linkDoc.hidden ? "eye-slash" : "eye";
+
+ let destinationIcon: FontAwesomeIconProps["icon"] = "question";
+ switch (this.props.destinationDoc.type) {
+ case DocumentType.IMG: destinationIcon = "image"; break;
+ case DocumentType.COMPARISON: destinationIcon = "columns"; break;
+ case DocumentType.RTF: destinationIcon = "font"; break;
+ case DocumentType.COL: destinationIcon = "folder"; break;
+ case DocumentType.WEB: destinationIcon = "globe-asia"; break;
+ case DocumentType.SCREENSHOT: destinationIcon = "photo-video"; break;
+ case DocumentType.WEBCAM: destinationIcon = "video"; break;
+ case DocumentType.AUDIO: destinationIcon = "microphone"; break;
+ case DocumentType.BUTTON: destinationIcon = "bolt"; break;
+ case DocumentType.PRES: destinationIcon = "tv"; break;
+ case DocumentType.QUERY: destinationIcon = "search"; break;
+ case DocumentType.SCRIPTING: destinationIcon = "terminal"; break;
+ case DocumentType.IMPORT: destinationIcon = "cloud-upload-alt"; break;
+ case DocumentType.DOCHOLDER: destinationIcon = "expand"; break;
+ case DocumentType.VID: destinationIcon = "video"; break;
+ case DocumentType.INK: destinationIcon = "pen-nib"; break;
+ default: destinationIcon = "question"; break;
+ }
+
+ const title = StrCast(this.props.destinationDoc.title).length > 18 ?
+ StrCast(this.props.destinationDoc.title).substr(0, 14) + "..." : this.props.destinationDoc.title;
+
+ // ...
+ // from anika to bob: here's where the text that is specifically linked would show up (linkDoc.storedText)
+ // ...
+ const source = this.props.sourceDoc.type === DocumentType.RTF ? this.props.linkDoc.storedText ?
+ StrCast(this.props.linkDoc.storedText).length > 17 ?
+ StrCast(this.props.linkDoc.storedText).substr(0, 18)
+ : this.props.linkDoc.storedText : undefined : undefined;
+
return (
<div className="linkMenu-item">
<div className={canExpand ? "linkMenu-item-content expand-three" : "linkMenu-item-content expand-two"}>
- <div ref={this._drag} className="linkMenu-name" title="drag to view target. click to customize." onPointerDown={this.onLinkButtonDown}>
- <p >{StrCast(this.props.destinationDoc.title)}</p>
- <div className="linkMenu-item-buttons">
+
+ <div ref={this._drag} className="linkMenu-name" //title="drag to view target. click to customize."
+ onPointerLeave={action(() => LinkDocPreview.LinkInfo = undefined)}
+ onPointerEnter={action(e => this.props.linkDoc && (LinkDocPreview.LinkInfo = {
+ addDocTab: this.props.addDocTab,
+ linkSrc: this.props.sourceDoc,
+ linkDoc: this.props.linkDoc,
+ Location: [e.clientX, e.clientY + 20]
+ }))}
+ onPointerDown={this.onLinkButtonDown}>
+
+ <div className="linkMenu-text">
+ {source ? <p className="linkMenu-source-title">
+ <b>Source: {source}</b></p> : null}
+ <div className="linkMenu-title-wrapper">
+ <div className="destination-icon-wrapper" >
+ <FontAwesomeIcon className="destination-icon" icon={destinationIcon} size="sm" /></div>
+ <p className="linkMenu-destination-title"
+ onPointerDown={this.followDefault}>
+ {title}
+ </p>
+ </div>
+ {this.props.linkDoc.description !== "" ? <p className="linkMenu-description">
+ {StrCast(this.props.linkDoc.description)}</p> : null} </div>
+
+ <div className="linkMenu-item-buttons" ref={this._buttonRef} >
{canExpand ? <div title="Show more" className="button" onPointerDown={e => this.toggleShowMore(e)}>
<FontAwesomeIcon className="fa-icon" icon={this._showMore ? "chevron-up" : "chevron-down"} size="sm" /></div> : <></>}
- <div title="Edit link" className="button" ref={this._editRef} onPointerDown={this.onEdit}><FontAwesomeIcon className="fa-icon" icon="edit" size="sm" /></div>
- <div title="Follow link" className="button" onClick={this.followDefault} onContextMenu={this.onContextMenu}>
- <FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" />
- </div>
+
+ <Tooltip title={<><div className="dash-tooltip">{this.props.linkDoc.hidden ? "Show link" : "Hide link"}</div></>}>
+ <div className="button" ref={this._editRef} onPointerDown={this.showLink}>
+ <FontAwesomeIcon className="fa-icon" icon={eyeIcon} size="sm" /></div>
+ </Tooltip>
+
+ <Tooltip title={<><div className="dash-tooltip">Edit Link</div></>}>
+ <div className="button" ref={this._editRef} onPointerDown={this.onEdit}>
+ <FontAwesomeIcon className="fa-icon" icon="edit" size="sm" /></div>
+ </Tooltip>
+ <Tooltip title={<><div className="dash-tooltip">Delete Link</div></>}>
+ <div className="button" onPointerDown={this.deleteLink}>
+ <FontAwesomeIcon className="fa-icon" icon="trash" size="sm" /></div>
+ </Tooltip>
+ {/* <div title="Follow link" className="button" onPointerDown={this.followDefault} onContextMenu={this.onContextMenu}>
+ <FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div> */}
</div>
</div>
{this._showMore ? this.renderMetadata() : <></>}
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss
index 53b54d7e4..e9420a072 100644
--- a/src/client/views/nodes/AudioBox.scss
+++ b/src/client/views/nodes/AudioBox.scss
@@ -1,141 +1,170 @@
-.audiobox-container, .audiobox-container-interactive {
+.audiobox-container,
+.audiobox-container-interactive {
width: 100%;
height: 100%;
position: inherit;
- display:flex;
+ display: flex;
pointer-events: all;
- cursor:default;
+ position: relative;
+ cursor: default;
+
.audiobox-buttons {
display: flex;
width: 100%;
align-items: center;
}
+
.audiobox-handle {
- width:20px;
- height:100%;
- display:inline-block;
+ width: 20px;
+ height: 100%;
+ display: inline-block;
}
- .audiobox-control, .audiobox-control-interactive {
- top:0;
+
+ .audiobox-control,
+ .audiobox-control-interactive {
+ top: 0;
max-height: 32px;
width: 100%;
- display:inline-block;
+ display: inline-block;
pointer-events: none;
}
+
.audiobox-control-interactive {
pointer-events: all;
}
+
.audiobox-record {
pointer-events: all;
- width:100%;
- height:100%;
+ width: 100%;
+ height: 100%;
position: relative;
pointer-events: none;
}
+
.audiobox-record-interactive {
pointer-events: all;
- width:100%;
- height:100%;
+ width: 100%;
+ height: 100%;
position: relative;
}
+
.audiobox-controls {
- width:100%;
- height:100%;
+ width: 100%;
+ height: 100%;
position: relative;
display: flex;
padding-left: 2px;
+
.audiobox-player {
- margin-top:auto;
- margin-bottom:auto;
- width:100%;
+ margin-top: auto;
+ margin-bottom: auto;
+ width: 100%;
height: 80%;
position: relative;
padding-right: 5px;
display: flex;
- .audiobox-playhead, .audiobox-dictation {
+
+ .audiobox-playhead,
+ .audiobox-dictation {
position: relative;
margin-top: auto;
margin-bottom: auto;
width: 25px;
padding: 2px;
}
+
.audiobox-dictation {
align-items: center;
display: inherit;
background: dimgray;
}
+
.audiobox-timeline {
- position:relative;
- height:100%;
- width:100%;
+ position: relative;
+ height: 100%;
+ width: 100%;
background: white;
border: gray solid 1px;
border-radius: 3px;
+
.audiobox-current {
width: 1px;
- height:100%;
+ height: 100%;
background-color: red;
position: absolute;
}
- .audiobox-linker, .audiobox-linker-mini {
- position:absolute;
- width:15px;
- min-height:10px;
- height:15px;
- margin-left:-2.55px;
- background:gray;
+
+ .audiobox-linker,
+ .audiobox-linker-mini {
+ position: absolute;
+ width: 15px;
+ min-height: 10px;
+ height: 15px;
+ margin-left: -2.55px;
+ background: gray;
border-radius: 100%;
- opacity:0.9;
+ opacity: 0.9;
background-color: transparent;
box-shadow: black 2px 2px 1px;
+
.linkAnchorBox-cont {
position: relative !important;
- height: 100% !important;
+ height: 100% !important;
width: 100% !important;
- left:unset !important;
- top:unset !important;
+ left: unset !important;
+ top: unset !important;
}
}
+
.audiobox-linker-mini {
- width:8px;
- min-height:8px;
- height:8px;
+ width: 8px;
+ min-height: 8px;
+ height: 8px;
box-shadow: black 1px 1px 1px;
margin-left: -1;
margin-top: -2;
+
.linkAnchorBox-cont {
position: relative !important;
- height: 100% !important;
+ height: 100% !important;
width: 100% !important;
- left:unset !important;
- top:unset !important;
+ left: unset !important;
+ top: unset !important;
}
}
- .audiobox-linker:hover, .audiobox-linker-mini:hover {
- opacity:1;
+
+ .audiobox-linker:hover,
+ .audiobox-linker-mini:hover {
+ opacity: 1;
}
- .audiobox-marker-container, .audiobox-marker-minicontainer {
- position:absolute;
- width:10px;
- height:90%;
- top:2.5%;
- background:gray;
+
+ .audiobox-marker-container,
+ .audiobox-marker-minicontainer {
+ position: absolute;
+ width: 10px;
+ height: 90%;
+ top: 2.5%;
+ background: gray;
border-radius: 5px;
box-shadow: black 2px 2px 1px;
+
.audiobox-marker {
- position:relative;
+ position: relative;
height: calc(100% - 15px);
margin-top: 15px;
}
+
.audio-marker:hover {
border: orange 2px solid;
}
}
+
.audiobox-marker-minicontainer {
- width:5px;
+ width: 5px;
border-radius: 1px;
+
.audiobox-marker {
- position:relative;
+ position: relative;
height: calc(100% - 8px);
margin-top: 8px;
}
@@ -143,4 +172,27 @@
}
}
}
+}
+
+
+@media only screen and (max-device-width: 480px) {
+ .audiobox-dictation {
+ font-size: 5em;
+ display: flex;
+ width: 100;
+ justify-content: center;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .audiobox-container .audiobox-record,
+ .audiobox-container-interactive .audiobox-record {
+ font-size: 3em;
+ }
+
+ .audiobox-container .audiobox-controls .audiobox-player .audiobox-playhead,
+ .audiobox-container .audiobox-controls .audiobox-player .audiobox-dictation,
+ .audiobox-container-interactive .audiobox-controls .audiobox-player .audiobox-playhead {
+ width: 70px;
+ }
} \ No newline at end of file
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 1a935d9b0..5c921cea4 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -161,7 +161,7 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument
const funcs: ContextMenuProps[] = [];
funcs.push({ description: (this.layoutDoc.playOnSelect ? "Don't play" : "Play") + " when document selected", event: () => this.layoutDoc.playOnSelect = !this.layoutDoc.playOnSelect, icon: "expand-arrows-alt" });
- ContextMenu.Instance.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" });
+ ContextMenu.Instance?.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" });
}
stopRecording = action(() => {
@@ -193,7 +193,7 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument
const newDoc = Docs.Create.TextDocument("", {
title: "", _chromeStatus: "disabled",
x: NumCast(this.props.Document.x), y: NumCast(this.props.Document.y) + NumCast(this.props.Document._height) + 10,
- _width: NumCast(this.props.Document._width), _height: 3 * NumCast(this.props.Document._height)
+ _width: NumCast(this.props.Document._width), _height: 2 * NumCast(this.props.Document._height)
});
Doc.GetProto(newDoc).recordingSource = this.dataDoc;
Doc.GetProto(newDoc).recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.props.fieldKey}-recordingStart"]`);
@@ -228,7 +228,7 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument
{!this.path ?
<div className="audiobox-buttons">
<div className="audiobox-dictation" onClick={this.onFile}>
- <FontAwesomeIcon style={{ width: "30px", background: this.layoutDoc.playOnSelect ? "yellow" : "dimGray" }} icon="file-alt" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} />
+ <FontAwesomeIcon style={{ width: "30px", background: this.layoutDoc.playOnSelect ? "yellow" : "rgba(0,0,0,0)" }} icon="file-alt" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} />
</div>
<button className={`audiobox-record${interactive}`} style={{ backgroundColor: this.audioState === "recording" ? "red" : "black" }}>
{this.audioState === "recording" ? "STOP" : "RECORD"}
@@ -268,6 +268,7 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument
LayoutTemplate={undefined}
LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(l, la2)}`)}
ContainingCollectionDoc={this.props.Document}
+ dontRegisterView={true}
parentActive={returnTrue}
bringToFront={emptyFunction}
backgroundColor={returnTransparent} />
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index a3020f912..ce39c3735 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -15,6 +15,7 @@ import { numberRange } from "../../../Utils";
import { ComputedField } from "../../../fields/ScriptField";
import { listSpec } from "../../../fields/Schema";
import { DocumentType } from "../../documents/DocumentTypes";
+import { InkingStroke } from "../InkingStroke";
export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
dataProvider?: (doc: Doc, replica: string) => { x: number, y: number, zIndex?: number, opacity?: number, highlight?: boolean, z: number, transition?: string } | undefined;
@@ -37,7 +38,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
return min + rnd * (max - min);
}
get displayName() { return "CollectionFreeFormDocumentView(" + this.rootDoc.title + ")"; } // this makes mobx trace() statements more descriptive
- get maskCentering() { return this.props.Document.isInkMask ? 2500 : 0; }
+ get maskCentering() { return this.props.Document.isInkMask ? InkingStroke.MaskDim / 2 : 0; }
get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X - this.maskCentering}px, ${this.Y - this.maskCentering}px) rotate(${this.random(-1, 1) * this.props.jitterRotation}deg)`; }
get X() { return this.dataProvider ? this.dataProvider.x : (this.Document.x || 0); }
get Y() { return this.dataProvider ? this.dataProvider.y : (this.Document.y || 0); }
@@ -73,9 +74,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
public static getValues(doc: Doc, time: number) {
const timecode = Math.round(time);
return ({
- x: Cast(doc["x-indexed"], listSpec("number"), []).reduce((p, x, i) => (i <= timecode && x !== undefined) || p === undefined ? x : p, undefined as any as number),
- y: Cast(doc["y-indexed"], listSpec("number"), []).reduce((p, y, i) => (i <= timecode && y !== undefined) || p === undefined ? y : p, undefined as any as number),
- opacity: Cast(doc["opacity-indexed"], listSpec("number"), []).reduce((p, o, i) => i <= timecode || p === undefined ? o : p, undefined as any as number),
+ x: Cast(doc["x-indexed"], listSpec("number"), [NumCast(doc.x)]).reduce((p, x, i) => (i <= timecode && x !== undefined) || p === undefined ? x : p, undefined as any as number),
+ y: Cast(doc["y-indexed"], listSpec("number"), [NumCast(doc.y)]).reduce((p, y, i) => (i <= timecode && y !== undefined) || p === undefined ? y : p, undefined as any as number),
+ opacity: Cast(doc["opacity-indexed"], listSpec("number"), [NumCast(doc.opacity, 1)]).reduce((p, o, i) => i <= timecode || p === undefined ? o : p, undefined as any as number),
});
}
@@ -157,8 +158,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
outline: this.Highlight ? "orange solid 2px" : "",
transform: this.transform,
transition: this.props.dataTransition ? this.props.dataTransition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.dataTransition),
- width: this.props.Document.isInkMask ? 5000 : this.width,
- height: this.props.Document.isInkMask ? 5000 : this.height,
+ width: this.props.Document.isInkMask ? InkingStroke.MaskDim : this.width,
+ height: this.props.Document.isInkMask ? InkingStroke.MaskDim : this.height,
zIndex: this.ZInd,
mixBlendMode: StrCast(this.layoutDoc.mixBlendMode) as any,
display: this.ZInd === -99 ? "none" : undefined,
diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx
index 137b387c0..090cf015a 100644
--- a/src/client/views/nodes/ColorBox.tsx
+++ b/src/client/views/nodes/ColorBox.tsx
@@ -14,7 +14,7 @@ import { ViewBoxBaseComponent } from "../DocComponent";
import { ActiveInkPen, ActiveInkWidth, ActiveInkBezierApprox, SetActiveInkColor, SetActiveInkWidth, SetActiveBezierApprox } from "../InkingStroke";
import "./ColorBox.scss";
import { FieldView, FieldViewProps } from './FieldView';
-import { FormattedTextBox } from "./formattedText/FormattedTextBox";
+import { DocumentType } from "../../documents/DocumentTypes";
type ColorDocument = makeInterface<[typeof documentSchema]>;
const ColorDocument = makeInterface(documentSchema);
@@ -61,14 +61,21 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps, ColorDocument
<SketchPicker onChange={ColorBox.switchColor} presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']}
color={StrCast(ActiveInkPen()?.backgroundColor,
StrCast(selDoc?._backgroundColor, StrCast(selDoc?.backgroundColor, "black")))} />
+
<div style={{ display: "grid", gridTemplateColumns: "20% 80%", paddingTop: "10px" }}>
<div> {ActiveInkWidth() ?? 2}</div>
- <input type="range" defaultValue={ActiveInkWidth() ?? 2} min={1} max={100} onChange={(e: React.ChangeEvent<HTMLInputElement>) => SetActiveInkWidth(e.target.value)} />
+ <input type="range" defaultValue={ActiveInkWidth() ?? 2} min={1} max={100} onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
+ SetActiveInkWidth(e.target.value);
+ SelectionManager.SelectedDocuments().filter(i => StrCast(i.rootDoc.type) === DocumentType.INK).map(i => i.rootDoc.strokeWidth = Number(e.target.value));
+ }} />
<div> {ActiveInkBezierApprox() ?? 2}</div>
- <input type="range" defaultValue={ActiveInkBezierApprox() ?? 2} min={0} max={300} onChange={(e: React.ChangeEvent<HTMLInputElement>) => SetActiveBezierApprox(e.target.value)} />
+ <input type="range" defaultValue={ActiveInkBezierApprox() ?? 2} min={0} max={300} onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
+ SetActiveBezierApprox(e.target.value);
+ SelectionManager.SelectedDocuments().filter(i => StrCast(i.rootDoc.type) === DocumentType.INK).map(i => i.rootDoc.strokeBezier = e.target.value);
+ }} />
<br />
<br />
</div>
</div>;
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss
index 810a824cf..acf6b1636 100644
--- a/src/client/views/nodes/ComparisonBox.scss
+++ b/src/client/views/nodes/ComparisonBox.scss
@@ -3,7 +3,7 @@
border-radius: inherit;
width: 100%;
height: 100%;
- position: absolute;
+ position: relative;
z-index: 0;
pointer-events: none;
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index f140cc6e5..616cddfcf 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -24,7 +24,7 @@ const ComparisonDocument = makeInterface(comparisonSchema, documentSchema);
@observer
export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps, ComparisonDocument>(ComparisonDocument) {
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ComparisonBox, fieldKey); }
- protected multiTouchDisposer?: import("../../util/InteractionUtils").InteractionUtils.MultiTouchEventDisposer | undefined;
+ protected _multiTouchDisposer?: import("../../util/InteractionUtils").InteractionUtils.MultiTouchEventDisposer | undefined;
private _disposers: (DragManager.DragDropDisposer | undefined)[] = [undefined, undefined];
@observable _animating = "";
diff --git a/src/client/views/nodes/ContentFittingDocumentView.tsx b/src/client/views/nodes/ContentFittingDocumentView.tsx
index ba075886b..6081def5d 100644
--- a/src/client/views/nodes/ContentFittingDocumentView.tsx
+++ b/src/client/views/nodes/ContentFittingDocumentView.tsx
@@ -2,7 +2,6 @@ import React = require("react");
import { computed } from "mobx";
import { observer } from "mobx-react";
import { Transform } from "nodemailer/lib/xoauth2";
-import "react-table/react-table.css";
import { Doc, HeightSym, Opt, WidthSym } from "../../../fields/Doc";
import { ScriptField } from "../../../fields/ScriptField";
import { Cast, NumCast, StrCast } from "../../../fields/Types";
@@ -10,7 +9,6 @@ import { TraceMobx } from "../../../fields/util";
import { emptyFunction } from "../../../Utils";
import { dropActionType } from "../../util/DragManager";
import { CollectionView } from "../collections/CollectionView";
-import '../DocumentDecorations.scss';
import { DocumentView, DocumentViewProps } from "../nodes/DocumentView";
import "./ContentFittingDocumentView.scss";
diff --git a/src/client/views/nodes/DocHolderBox.tsx b/src/client/views/nodes/DocHolderBox.tsx
index 0cf5505cc..0c4242172 100644
--- a/src/client/views/nodes/DocHolderBox.tsx
+++ b/src/client/views/nodes/DocHolderBox.tsx
@@ -180,7 +180,7 @@ export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, Do
render() {
const containedDoc = Cast(this.dataDoc[this.fieldKey], Doc, null);
TraceMobx();
- return <div className="documentBox-container" ref={this._contRef}
+ return !containedDoc ? (null) : <div className="documentBox-container" ref={this._contRef}
onContextMenu={this.specificContextMenu}
onPointerDown={this.onPointerDown} onClick={this.onClick}
style={{
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 37718e028..c724ba50b 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -1,6 +1,6 @@
import { computed } from "mobx";
import { observer } from "mobx-react";
-import { Doc, Opt, Field, AclSym, AclPrivate } from "../../../fields/Doc";
+import { Doc, Opt, Field, AclPrivate } from "../../../fields/Doc";
import { Cast, StrCast, NumCast } from "../../../fields/Types";
import { OmitKeys, Without, emptyPath } from "../../../Utils";
import { DirectoryImportBox } from "../../util/Import & Export/DirectoryImportBox";
@@ -36,8 +36,7 @@ import { VideoBox } from "./VideoBox";
import { WebBox } from "./WebBox";
import { InkingStroke } from "../InkingStroke";
import React = require("react");
-import { RecommendationsBox } from "../RecommendationsBox";
-import { TraceMobx } from "../../../fields/util";
+import { TraceMobx, GetEffectiveAcl } from "../../../fields/util";
import { ScriptField } from "../../../fields/ScriptField";
import XRegExp = require("xregexp");
@@ -185,8 +184,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
const bindings = this.CreateBindings(onClick, onInput);
// layoutFrame = splits.length > 1 ? splits[0] + splits[1].replace(/{([^{}]|(?R))*}/, replacer4) : ""; // might have been more elegant if javascript supported recursive patterns
-
- return (this.props.renderDepth > 12 || !layoutFrame || !this.layoutDoc || this.layoutDoc[AclSym] === AclPrivate) ? (null) :
+ return (this.props.renderDepth > 12 || !layoutFrame || !this.layoutDoc || GetEffectiveAcl(this.layoutDoc) === AclPrivate) ? (null) :
<ObserverJsxParser
key={42}
blacklistedAttrs={[]}
@@ -196,13 +194,13 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox,
PDFBox, VideoBox, AudioBox, PresBox, YoutubeBox, PresElementBox, SearchBox, SearchItem,
ColorBox, DashWebRTCVideo, LinkAnchorBox, InkingStroke, DocHolderBox, LinkBox, ScriptingBox,
- RecommendationsBox, ScreenshotBox, HTMLtag, ComparisonBox
+ ScreenshotBox, HTMLtag, ComparisonBox
}}
bindings={bindings}
jsx={layoutFrame}
showWarnings={true}
- onError={(test: any) => { console.log(test); }}
+ onError={(test: any) => { console.log("DocumentContentsView:" + test); }}
/>;
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/DocumentLinksButton.scss b/src/client/views/nodes/DocumentLinksButton.scss
index 484f8c469..97e714cd5 100644
--- a/src/client/views/nodes/DocumentLinksButton.scss
+++ b/src/client/views/nodes/DocumentLinksButton.scss
@@ -13,7 +13,7 @@
color: black;
text-transform: uppercase;
letter-spacing: 2px;
- font-size: 75%;
+ font-size: 10px;
transition: transform 0.2s;
text-align: center;
display: flex;
@@ -21,17 +21,26 @@
align-items: center;
&:hover {
- background: deepskyblue;
- transform: scale(1.05);
- cursor: default;
+ // background: deepskyblue;
+ // transform: scale(1.05);
+ cursor: pointer;
}
}
+
.documentLinksButton {
background-color: $link-color;
}
+
.documentLinksButton-endLink {
border: red solid 2px;
+
+ &:hover {
+ background: deepskyblue;
+ transform: scale(1.05);
+ cursor: pointer;
+ }
}
+
.documentLinksButton-startLink {
border: red solid 2px;
background-color: rgba(255, 192, 203, 0.5);
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index 4f4f12521..3b91ac6b5 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -1,15 +1,19 @@
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { Tooltip } from "@material-ui/core";
import { action, computed, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
-import { Doc, DocListCast } from "../../../fields/Doc";
-import { emptyFunction, setupMoveUpEvents, returnFalse } from "../../../Utils";
+import { Doc } from "../../../fields/Doc";
+import { TraceMobx } from "../../../fields/util";
+import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../../Utils";
+import { DocUtils } from "../../documents/Documents";
import { DragManager } from "../../util/DragManager";
-import { UndoManager } from "../../util/UndoManager";
+import { LinkManager } from "../../util/LinkManager";
+import { undoBatch, UndoManager } from "../../util/UndoManager";
import './DocumentLinksButton.scss';
import { DocumentView } from "./DocumentView";
+import { LinkDescriptionPopup } from "./LinkDescriptionPopup";
+import { TaskCompletionBox } from "./TaskCompletedBox";
import React = require("react");
-import { DocUtils } from "../../documents/Documents";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { LinkDocPreview } from "./LinkDocPreview";
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -18,87 +22,204 @@ interface DocumentLinksButtonProps {
View: DocumentView;
Offset?: number[];
AlwaysOn?: boolean;
+ InMenu?: boolean;
+ StartLink?: boolean;
+ links: Doc[];
}
@observer
export class DocumentLinksButton extends React.Component<DocumentLinksButtonProps, {}> {
private _linkButton = React.createRef<HTMLDivElement>();
- @action
+ @observable public static StartLink: DocumentView | undefined;
+
+ @action @undoBatch
onLinkButtonMoved = (e: PointerEvent) => {
- if (this._linkButton.current !== null) {
- const linkDrag = UndoManager.StartBatch("Drag Link");
- this.props.View && DragManager.StartLinkDrag(this._linkButton.current, this.props.View.props.Document, e.pageX, e.pageY, {
- dragComplete: dropEv => {
- const linkDoc = dropEv.linkDragData?.linkDocument as Doc; // equivalent to !dropEve.aborted since linkDocument is only assigned on a completed drop
- if (this.props.View && linkDoc) {
- !linkDoc.linkRelationship && (Doc.GetProto(linkDoc).linkRelationship = "hyperlink");
-
- // we want to allow specific views to handle the link creation in their own way (e.g., rich text makes text hyperlinks)
- // the dragged view can regiser a linkDropCallback to be notified that the link was made and to update their data structures
- // however, the dropped document isn't so accessible. What we do is set the newly created link document on the documentView
- // The documentView passes a function prop returning this link doc to its descendants who can react to changes to it.
- dropEv.linkDragData?.linkDropCallback?.(dropEv.linkDragData);
- runInAction(() => this.props.View._link = linkDoc);
- setTimeout(action(() => this.props.View._link = undefined), 0);
- }
- linkDrag?.end();
- },
- hideSource: false
- });
- return true;
+ if (this.props.InMenu && this.props.StartLink) {
+ if (this._linkButton.current !== null) {
+ const linkDrag = UndoManager.StartBatch("Drag Link");
+ this.props.View && DragManager.StartLinkDrag(this._linkButton.current, this.props.View.props.Document, e.pageX, e.pageY, {
+ dragComplete: dropEv => {
+ const linkDoc = dropEv.linkDragData?.linkDocument as Doc; // equivalent to !dropEve.aborted since linkDocument is only assigned on a completed drop
+ if (this.props.View && linkDoc) {
+ !linkDoc.linkRelationship && (Doc.GetProto(linkDoc).linkRelationship = "hyperlink");
+
+ // we want to allow specific views to handle the link creation in their own way (e.g., rich text makes text hyperlinks)
+ // the dragged view can regiser a linkDropCallback to be notified that the link was made and to update their data structures
+ // however, the dropped document isn't so accessible. What we do is set the newly created link document on the documentView
+ // The documentView passes a function prop returning this link doc to its descendants who can react to changes to it.
+ dropEv.linkDragData?.linkDropCallback?.(dropEv.linkDragData);
+ runInAction(() => this.props.View._link = linkDoc);
+ setTimeout(action(() => this.props.View._link = undefined), 0);
+ }
+ linkDrag?.end();
+ },
+ hideSource: false
+ });
+ return true;
+ }
+ return false;
}
return false;
}
- @observable static StartLink: DocumentView | undefined;
+ @undoBatch
onLinkButtonDown = (e: React.PointerEvent): void => {
setupMoveUpEvents(this, e, this.onLinkButtonMoved, emptyFunction, action((e, doubleTap) => {
- if (doubleTap) {
+ if (doubleTap && this.props.InMenu && this.props.StartLink) {
+ //action(() => Doc.BrushDoc(this.props.View.Document));
DocumentLinksButton.StartLink = this.props.View;
- } else {
+ } else if (!this.props.InMenu) {
DocumentLinksButton.EditLink = this.props.View;
DocumentLinksButton.EditLinkLoc = [e.clientX + 10, e.clientY];
}
}));
}
+
+ @action @undoBatch
+ onLinkClick = (e: React.MouseEvent): void => {
+ if (this.props.InMenu && this.props.StartLink) {
+ DocumentLinksButton.StartLink = this.props.View;
+ //action(() => Doc.BrushDoc(this.props.View.Document));
+ } else if (!this.props.InMenu) {
+ DocumentLinksButton.EditLink = this.props.View;
+ DocumentLinksButton.EditLinkLoc = [e.clientX + 10, e.clientY];
+ }
+ }
+
+ @action @undoBatch
completeLink = (e: React.PointerEvent): void => {
setupMoveUpEvents(this, e, returnFalse, emptyFunction, action((e, doubleTap) => {
- if (doubleTap) {
+ if (doubleTap && this.props.InMenu && !!!this.props.StartLink) {
if (DocumentLinksButton.StartLink === this.props.View) {
DocumentLinksButton.StartLink = undefined;
} else {
- DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View &&
- DocUtils.MakeLink({ doc: DocumentLinksButton.StartLink.props.Document }, { doc: this.props.View.props.Document }, "long drag");
+
+ if (DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View) {
+ const linkDoc = DocUtils.MakeLink({ doc: DocumentLinksButton.StartLink.props.Document }, { doc: this.props.View.props.Document }, "long drag");
+ LinkManager.currentLink = linkDoc;
+
+ runInAction(() => {
+ if (linkDoc) {
+ TaskCompletionBox.textDisplayed = "Link Created";
+ TaskCompletionBox.popupX = e.screenX;
+ TaskCompletionBox.popupY = e.screenY - 133;
+ TaskCompletionBox.taskCompleted = true;
+
+ LinkDescriptionPopup.popupX = e.screenX;
+ LinkDescriptionPopup.popupY = e.screenY - 100;
+ LinkDescriptionPopup.descriptionPopup = true;
+
+ setTimeout(action(() => { TaskCompletionBox.taskCompleted = false; }), 2500);
+ }
+
+ });
+ }
}
}
}));
}
+ @action @undoBatch
+ finishLinkClick = (screenX: number, screenY: number) => {
+ if (DocumentLinksButton.StartLink === this.props.View) {
+ DocumentLinksButton.StartLink = undefined;
+ } else {
+ if (this.props.InMenu && !!!this.props.StartLink) {
+ if (DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View) {
+ const linkDoc = DocUtils.MakeLink({ doc: DocumentLinksButton.StartLink.props.Document }, { doc: this.props.View.props.Document }, "long drag");
+ // this notifies any of the subviews that a document is made so that they can make finer-grained hyperlinks (). see note above in onLInkButtonMoved
+ runInAction(() => DocumentLinksButton.StartLink!._link = this.props.View._link = linkDoc);
+ setTimeout(action(() => DocumentLinksButton.StartLink!._link = this.props.View._link = undefined), 0);
+ LinkManager.currentLink = linkDoc;
+
+ runInAction(() => {
+ if (linkDoc) {
+ TaskCompletionBox.textDisplayed = "Link Created";
+ TaskCompletionBox.popupX = screenX;
+ TaskCompletionBox.popupY = screenY - 133;
+ TaskCompletionBox.taskCompleted = true;
+
+ if (LinkDescriptionPopup.showDescriptions === "ON" || !LinkDescriptionPopup.showDescriptions) {
+ LinkDescriptionPopup.popupX = screenX;
+ LinkDescriptionPopup.popupY = screenY - 100;
+ LinkDescriptionPopup.descriptionPopup = true;
+ }
+
+ setTimeout(action(() => { TaskCompletionBox.taskCompleted = false; }), 2500);
+ }
+ });
+ }
+ }
+ }
+ }
+
@observable
public static EditLink: DocumentView | undefined;
public static EditLinkLoc: number[] = [0, 0];
@computed
get linkButton() {
- const links = DocListCast(this.props.View.props.Document.links);
- return (!links.length || links[0].hidden) && !this.props.AlwaysOn ? (null) :
- <div title="Drag(create link) Tap(view links)" ref={this._linkButton} style={{ minWidth: 20, minHeight: 20, position: "absolute", left: this.props.Offset?.[0] }}>
- <div className={"documentLinksButton"} style={{ backgroundColor: DocumentLinksButton.StartLink ? "transparent" : "" }}
- onPointerDown={this.onLinkButtonDown}
- onPointerLeave={action(() => LinkDocPreview.LinkInfo = undefined)}
- onPointerEnter={action(e => links.length && (LinkDocPreview.LinkInfo = {
- addDocTab: this.props.View.props.addDocTab,
- linkSrc: this.props.View.props.Document,
- linkDoc: links[0],
- Location: [e.clientX, e.clientY + 20]
- }))} >
- {links.length ? links.length : <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" />}
- </div>
- {DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View ? <div className={"documentLinksButton-endLink"} onPointerDown={this.completeLink} /> : (null)}
- {DocumentLinksButton.StartLink === this.props.View ? <div className={"documentLinksButton-startLink"} /> : (null)}
- </div>;
+ TraceMobx();
+ const links = this.props.links;
+
+ const menuTitle = this.props.StartLink ? "Drag or tap to start link" : "Tap to complete link";
+ const buttonTitle = "Tap to view links";
+ const title = this.props.InMenu ? menuTitle : buttonTitle;
+
+
+ const startLink = <img
+ style={{ width: "11px", height: "11px" }}
+ id={"startLink-icon"}
+ src={`/assets/${"startLink.png"}`} />;
+
+ const endLink = <img
+ style={{ width: "14px", height: "9px" }}
+ id={"endLink-icon"}
+ src={`/assets/${"endLink.png"}`} />;
+
+ const link = <img
+ style={{ width: "22px", height: "16px" }}
+ id={"link-icon"}
+ src={`/assets/${"link.png"}`} />;
+
+ const linkButton = <div ref={this._linkButton} style={{ minWidth: 20, minHeight: 20, position: "absolute", left: this.props.Offset?.[0] }}>
+ <div className={"documentLinksButton"} style={{
+ backgroundColor: this.props.InMenu ? "black" : "",
+ color: this.props.InMenu ? "white" : "black",
+ width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px", fontWeight: "bold"
+ }}
+ onPointerDown={this.onLinkButtonDown} onClick={this.onLinkClick}
+ // onPointerLeave={action(() => LinkDocPreview.LinkInfo = undefined)}
+ // onPointerEnter={action(e => links.length && (LinkDocPreview.LinkInfo = {
+ // addDocTab: this.props.View.props.addDocTab,
+ // linkSrc: this.props.View.props.Document,
+ // linkDoc: links[0],
+ // Location: [e.clientX, e.clientY + 20]
+ // }))}
+ >
+
+ {this.props.InMenu ? this.props.StartLink ? <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" /> :
+ <FontAwesomeIcon className="documentdecorations-icon" icon="hand-paper" size="sm" /> : links.length}
+
+ </div>
+ {DocumentLinksButton.StartLink && this.props.InMenu && !!!this.props.StartLink && DocumentLinksButton.StartLink !== this.props.View ? <div className={"documentLinksButton-endLink"}
+ style={{ width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px" }}
+ onPointerDown={this.completeLink} onClick={e => this.finishLinkClick(e.screenX, e.screenY)} /> : (null)}
+ {DocumentLinksButton.StartLink === this.props.View && this.props.InMenu && this.props.StartLink ? <div className={"documentLinksButton-startLink"}
+ style={{ width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px" }} /> : (null)}
+ </div>;
+
+ return (!links.length) && !this.props.AlwaysOn ? (null) :
+ this.props.InMenu ?
+ <Tooltip title={<><div className="dash-tooltip">{title}</div></>}>
+ {linkButton}
+ </Tooltip> : !!!DocumentLinksButton.EditLink ?
+ <Tooltip title={<><div className="dash-tooltip">{title}</div></>}>
+ {linkButton}
+ </Tooltip> :
+ linkButton;
}
render() {
return this.linkButton;
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 09eeaee36..15cf9556b 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,35 +1,33 @@
-import { library } from '@fortawesome/fontawesome-svg-core';
-import * as fa 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 * as rp from "request-promise";
-import { Doc, DocListCast, HeightSym, Opt, WidthSym, DataSym, AclSym, AclReadonly, AclPrivate } from "../../../fields/Doc";
+import { AclAdmin, AclEdit, AclPrivate, DataSym, Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../fields/Doc";
import { Document } from '../../../fields/documentSchemas';
import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
import { listSpec } from "../../../fields/Schema";
import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, NumCast, StrCast } from "../../../fields/Types";
-import { TraceMobx } from '../../../fields/util';
+import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from "../../../fields/Types";
+import { GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util';
+import { MobileInterface } from '../../../mobile/MobileInterface';
import { GestureUtils } from '../../../pen-gestures/GestureUtils';
-import { emptyFunction, OmitKeys, returnOne, returnTransparent, Utils, emptyPath } from "../../../Utils";
+import { emptyFunction, emptyPath, OmitKeys, returnOne, returnTransparent, Utils } from "../../../Utils";
import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
import { ClientRecommender } from '../../ClientRecommender';
import { Docs, DocUtils } from "../../documents/Documents";
import { DocumentType } from '../../documents/DocumentTypes';
import { DocumentManager } from "../../util/DocumentManager";
-import { SnappingManager } from '../../util/SnappingManager';
import { DragManager, dropActionType } from "../../util/DragManager";
import { InteractionUtils } from '../../util/InteractionUtils';
+import { LinkManager } from '../../util/LinkManager';
import { Scripting } from '../../util/Scripting';
import { SearchUtil } from '../../util/SearchUtil';
import { SelectionManager } from "../../util/SelectionManager";
import SharingManager from '../../util/SharingManager';
+import { SnappingManager } from '../../util/SnappingManager';
import { Transform } from "../../util/Transform";
import { undoBatch, UndoManager } from "../../util/UndoManager";
-import { CollectionDockingView } from "../collections/CollectionDockingView";
import { CollectionView, CollectionViewType } from '../collections/CollectionView';
import { ContextMenu } from "../ContextMenu";
import { ContextMenuProps } from '../ContextMenuItem';
@@ -37,15 +35,13 @@ import { DocComponent } from "../DocComponent";
import { EditableView } from '../EditableView';
import { KeyphraseQueryView } from '../KeyphraseQueryView';
import { DocumentContentsView } from "./DocumentContentsView";
+import { DocumentLinksButton } from './DocumentLinksButton';
import "./DocumentView.scss";
import { LinkAnchorBox } from './LinkAnchorBox';
+import { LinkDescriptionPopup } from './LinkDescriptionPopup';
import { RadialMenu } from './RadialMenu';
+import { TaskCompletionBox } from './TaskCompletedBox';
import React = require("react");
-import { DocumentLinksButton } from './DocumentLinksButton';
-
-library.add(fa.faEdit, fa.faTrash, fa.faShare, fa.faDownload, fa.faExpandArrowsAlt, fa.faCompressArrowsAlt, fa.faLayerGroup, fa.faExternalLinkAlt, fa.faAlignCenter, fa.faCaretSquareRight,
- fa.faSquare, fa.faConciergeBell, fa.faWindowRestore, fa.faFolder, fa.faMapPin, fa.faLink, fa.faFingerprint, fa.faCrosshairs, fa.faDesktop, fa.faUnlock, fa.faLock, fa.faLaptopCode, fa.faMale,
- fa.faCopy, fa.faHandPointRight, fa.faCompass, fa.faSnowflake, fa.faMicrophone, fa.faKeyboard, fa.faQuestion);
export type DocFocusFunc = () => boolean;
@@ -65,10 +61,10 @@ export interface DocumentViewProps {
ignoreAutoHeight?: boolean;
contextMenuItems?: () => { script: ScriptField, label: string }[];
rootSelected: (outsideReaction?: boolean) => boolean; // whether the root of a template has been selected
- onClick?: ScriptField;
- onDoubleClick?: ScriptField;
- onPointerDown?: ScriptField;
- onPointerUp?: ScriptField;
+ onClick?: () => ScriptField;
+ onDoubleClick?: () => ScriptField;
+ onPointerDown?: () => ScriptField;
+ onPointerUp?: () => ScriptField;
treeViewDoc?: Doc;
dropAction?: dropActionType;
dragDivName?: string;
@@ -102,36 +98,37 @@ export interface DocumentViewProps {
@observer
export class DocumentView extends DocComponent<DocumentViewProps, Document>(Document) {
+ @observable _animateScalingTo = 0;
private _downX: number = 0;
private _downY: number = 0;
+ private _firstX: number = -1;
+ private _firstY: number = -1;
private _lastTap: number = 0;
private _doubleTap = false;
private _mainCont = React.createRef<HTMLDivElement>();
private _dropDisposer?: DragManager.DragDropDisposer;
private _showKPQuery: boolean = false;
private _queries: string = "";
- private _gestureEventDisposer?: GestureUtils.GestureEventDisposer;
private _titleRef = React.createRef<EditableView>();
-
- protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
- private holdDisposer?: InteractionUtils.MultiTouchEventDisposer;
+ private _gestureEventDisposer?: GestureUtils.GestureEventDisposer;
+ private _holdDisposer?: InteractionUtils.MultiTouchEventDisposer;
+ protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
public get displayName() { return "DocumentView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive
public get ContentDiv() { return this._mainCont.current; }
- get active() { return SelectionManager.IsSelected(this, true) || this.props.parentActive(true); }
+ private get active() { return SelectionManager.IsSelected(this, true) || this.props.parentActive(true); }
@computed get topMost() { return this.props.renderDepth === 0; }
@computed get freezeDimensions() { return this.props.FreezeDimensions; }
@computed get nativeWidth() { return NumCast(this.layoutDoc._nativeWidth, this.props.NativeWidth() || (this.freezeDimensions ? this.layoutDoc[WidthSym]() : 0)); }
@computed get nativeHeight() { return NumCast(this.layoutDoc._nativeHeight, this.props.NativeHeight() || (this.freezeDimensions ? this.layoutDoc[HeightSym]() : 0)); }
- @computed get onClickHandler() { return this.props.onClick || Cast(this.Document.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null)); }
- @computed get onDoubleClickHandler() { return this.props.onDoubleClick || Cast(this.layoutDoc.onDoubleClick, ScriptField, null) || this.Document.onDoubleClick; }
- @computed get onPointerDownHandler() { return this.props.onPointerDown ? this.props.onPointerDown : this.Document.onPointerDown; }
- @computed get onPointerUpHandler() { return this.props.onPointerUp ? this.props.onPointerUp : this.Document.onPointerUp; }
+ @computed get onClickHandler() { return this.props.onClick?.() ?? Cast(this.Document.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null)); }
+ @computed get onDoubleClickHandler() { return this.props.onDoubleClick?.() ?? (Cast(this.layoutDoc.onDoubleClick, ScriptField, null) ?? this.Document.onDoubleClick); }
+ @computed get onPointerDownHandler() { return this.props.onPointerDown?.() ?? ScriptCast(this.Document.onPointerDown); }
+ @computed get onPointerUpHandler() { return this.props.onPointerUp?.() ?? ScriptCast(this.Document.onPointerUp); }
NativeWidth = () => this.nativeWidth;
NativeHeight = () => this.nativeHeight;
-
- private _firstX: number = -1;
- private _firstY: number = -1;
+ onClickFunc = () => this.onClickHandler;
+ onDoubleClickFunc = () => this.onDoubleClickHandler;
handle1PointerHoldStart = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => {
this.removeMoveListeners();
@@ -146,11 +143,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
this._firstX = pt.pageX;
this._firstY = pt.pageY;
}
-
}
handle1PointerHoldMove = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => {
-
const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1];
if (this._firstX === -1 || this._firstY === -1) {
@@ -181,10 +176,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1];
RadialMenu.Instance.openMenu(pt.pageX - 15, pt.pageY - 15);
- RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "onRight"), icon: "map-pin", selected: -1 });
- RadialMenu.Instance.addItem({ description: "Delete this document", event: () => { this.props.ContainingCollectionView?.removeDocument(this.props.Document), RadialMenu.Instance.closeMenu(); }, icon: "layer-group", selected: -1 });
- RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, "onRight"), icon: "trash", selected: -1 });
- RadialMenu.Instance.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.Document), icon: "folder", selected: -1 });
+ // RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "onRight"), icon: "map-pin", selected: -1 });
+ RadialMenu.Instance.addItem({ description: "Delete", event: () => { this.props.ContainingCollectionView?.removeDocument(this.props.Document), RadialMenu.Instance.closeMenu(); }, icon: "external-link-square-alt", selected: -1 });
+ // RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, "onRight"), icon: "trash", selected: -1 });
+ RadialMenu.Instance.addItem({ description: "Pin", event: () => this.props.pinToPres(this.props.Document), icon: "map-pin", selected: -1 });
+ RadialMenu.Instance.addItem({ description: "Open", event: () => MobileInterface.Instance.handleClick(this.props.Document), icon: "trash", selected: -1 });
SelectionManager.DeselectAll();
}
@@ -193,7 +189,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
componentDidMount() {
this._mainCont.current && (this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, this.drop.bind(this), this.props.Document));
this._mainCont.current && (this._gestureEventDisposer = GestureUtils.MakeGestureTarget(this._mainCont.current, this.onGesture.bind(this)));
- this._mainCont.current && (this.multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(this._mainCont.current, this.onTouchStart.bind(this)));
+ this._mainCont.current && (this._multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(this._mainCont.current, this.onTouchStart.bind(this)));
// this._mainCont.current && (this.holdDisposer = InteractionUtils.MakeHoldTouchTarget(this._mainCont.current, this.handle1PointerHoldStart.bind(this)));
if (!this.props.dontRegisterView) {
@@ -205,13 +201,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
componentDidUpdate() {
this._dropDisposer?.();
this._gestureEventDisposer?.();
- this.multiTouchDisposer?.();
- this.holdDisposer?.();
+ this._multiTouchDisposer?.();
+ this._holdDisposer?.();
if (this._mainCont.current) {
this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, this.drop.bind(this), this.props.Document);
this._gestureEventDisposer = GestureUtils.MakeGestureTarget(this._mainCont.current, this.onGesture.bind(this));
- this.multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(this._mainCont.current, this.onTouchStart.bind(this));
- this.holdDisposer = InteractionUtils.MakeHoldTouchTarget(this._mainCont.current, this.handle1PointerHoldStart.bind(this));
+ this._multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(this._mainCont.current, this.onTouchStart.bind(this));
+ this._holdDisposer = InteractionUtils.MakeHoldTouchTarget(this._mainCont.current, this.handle1PointerHoldStart.bind(this));
}
}
@@ -219,8 +215,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
componentWillUnmount() {
this._dropDisposer?.();
this._gestureEventDisposer?.();
- this.multiTouchDisposer?.();
- this.holdDisposer?.();
+ this._multiTouchDisposer?.();
+ this._holdDisposer?.();
Doc.UnBrushDoc(this.props.Document);
if (!this.props.dontRegisterView) {
const index = DocumentManager.Instance.DocumentViews.indexOf(this);
@@ -235,27 +231,35 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
dragData.offset = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top);
dragData.dropAction = dropAction;
dragData.removeDocument = this.props.removeDocument;
- dragData.moveDocument = this.props.moveDocument;// this.layoutDoc.onDragStart ? undefined : this.props.moveDocument;
+ dragData.moveDocument = this.props.moveDocument;
dragData.dragDivName = this.props.dragDivName;
dragData.treeViewDoc = this.props.treeViewDoc;
DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: !dropAction && !this.layoutDoc.onDragStart });
}
}
- public static FloatDoc(topDocView: DocumentView, x: number, y: number) {
+ @undoBatch @action
+ public static FloatDoc(topDocView: DocumentView, x?: number, y?: number) {
const topDoc = topDocView.props.Document;
- const de = new DragManager.DocumentDragData([topDoc]);
- de.dragDivName = topDocView.props.dragDivName;
- de.moveDocument = topDocView.props.moveDocument;
- undoBatch(action(() => topDoc.z = topDoc.z ? 0 : 1))();
- setTimeout(() => {
- const newDocView = DocumentManager.Instance.getDocumentView(topDoc);
- if (newDocView) {
- const contentDiv = newDocView.ContentDiv!;
- const xf = contentDiv.getBoundingClientRect();
- DragManager.StartDocumentDrag([contentDiv], de, x, y, { offsetX: x - xf.left, offsetY: y - xf.top, hideSource: true });
+ const container = topDocView.props.ContainingCollectionView;
+ if (container) {
+ SelectionManager.DeselectAll();
+ if (topDoc.z && (x === undefined && y === undefined)) {
+ const spt = container.screenToLocalTransform().inverse().transformPoint(NumCast(topDoc.x), NumCast(topDoc.y));
+ topDoc.z = 0;
+ topDoc.x = spt[0];
+ topDoc.y = spt[1];
+ topDocView.props.removeDocument?.(topDoc);
+ topDocView.props.addDocTab(topDoc, "inParent");
+ } else {
+ const spt = topDocView.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
+ const fpt = container.screenToLocalTransform().transformPoint(x !== undefined ? x : spt[0], y !== undefined ? y : spt[1]);
+ topDoc.z = 1;
+ topDoc.x = fpt[0];
+ topDoc.y = fpt[1];
}
- }, 0);
+ setTimeout(() => SelectionManager.SelectDoc(DocumentManager.Instance.getDocumentView(topDoc, container)!, false), 0);
+ }
}
onKeyDown = (e: React.KeyboardEvent) => {
@@ -288,47 +292,45 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
let stopPropagate = true;
let preventDefault = true;
!this.props.Document.isBackground && this.props.bringToFront(this.props.Document);
- if (this._doubleTap && this.props.renderDepth && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click
+ if (this._doubleTap && this.props.renderDepth) {// && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click
if (!(e.nativeEvent as any).formattedHandled) {
if (this.onDoubleClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) { // bcz: hack? don't execute script if you're clicking on a scripting box itself
const func = () => this.onDoubleClickHandler.script.run({
this: this.layoutDoc,
self: this.rootDoc,
- thisContainer: this.props.ContainingCollectionDoc, shiftKey: e.shiftKey
+ thisContainer: this.props.ContainingCollectionDoc,
+ shiftKey: e.shiftKey
}, console.log);
func();
} else {
UndoManager.RunInBatch(() => {
+ let fullScreenDoc = this.props.Document;
if (StrCast(this.props.Document.layoutKey) !== "layout_fullScreen" && this.props.Document.layout_fullScreen) {
- const fullScreenAlias = Doc.MakeAlias(this.props.Document);
- fullScreenAlias.layoutKey = "layout_fullScreen";
- this.props.addDocTab(fullScreenAlias, "inTab");
- } else {
- this.props.addDocTab(this.props.Document, "inTab");
+ fullScreenDoc = Doc.MakeAlias(this.props.Document);
+ fullScreenDoc.layoutKey = "layout_fullScreen";
}
+ this.props.addDocTab(fullScreenDoc, "inTab");
}, "double tap");
SelectionManager.DeselectAll();
Doc.UnBrushDoc(this.props.Document);
}
}
} else if (this.onClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) { // bcz: hack? don't execute script if you're clicking on a scripting box itself
- //SelectionManager.DeselectAll();
const func = () => this.onClickHandler.script.run({
this: this.layoutDoc,
self: this.rootDoc,
- thisContainer: this.props.ContainingCollectionDoc, shiftKey: e.shiftKey
+ thisContainer: this.props.ContainingCollectionDoc,
+ shiftKey: e.shiftKey
}, console.log);
if (this.props.Document !== Doc.UserDoc()["dockedBtn-undo"] && this.props.Document !== Doc.UserDoc()["dockedBtn-redo"]) {
UndoManager.RunInBatch(func, "on click");
} else func();
} else if (this.Document["onClick-rawScript"] && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) {// bcz: hack? don't edit a script if you're clicking on a scripting box itself
- const alias = Doc.MakeAlias(this.props.Document);
- DocUtils.makeCustomViewClicked(alias, undefined, "onClick");
- this.props.addDocTab(alias, "onRight");
- } else if (this.props.Document.links && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) {
- DocListCast(this.props.Document.links).length && this.followLinkClick(e.altKey, e.ctrlKey, e.shiftKey);
+ this.props.addDocTab(DocUtils.makeCustomViewClicked(Doc.MakeAlias(this.props.Document), undefined, "onClick"), "onRight");
+ } else if (this.allLinks && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) {
+ this.allLinks.length && this.followLinkClick(e.altKey, e.ctrlKey, e.shiftKey);
} else {
- if ((this.layoutDoc.onDragStart || (this.props.Document.rootDocument)) && !(e.ctrlKey || e.button > 0)) { // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTEmplaetForField implies we're clicking on part of a template instance and we want to select the whole template, not the part
+ if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) { // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplaetForField implies we're clicking on part of a template instance and we want to select the whole template, not the part
stopPropagate = false; // don't stop propagation for field templates -- want the selection to propagate up to the root document of the template
} else {
SelectionManager.SelectDoc(this, e.ctrlKey || e.shiftKey);
@@ -345,12 +347,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
// depending on the followLinkLocation property of the source (or the link itself as a fallback);
followLinkClick = async (altKey: boolean, ctrlKey: boolean, shiftKey: boolean) => {
const batch = UndoManager.StartBatch("follow link click");
- // open up target if it's not already in view ...
+ // open up target if it's not already in view ...
const createViewFunc = (doc: Doc, followLoc: string, finished: Opt<() => void>) => {
const targetFocusAfterDocFocus = () => {
const where = StrCast(this.Document.followLinkLocation) || followLoc;
const hackToCallFinishAfterFocus = () => {
- finished && setTimeout(finished, 0); // finished() needs to be called right after hackToCallFinishAfterFocus(), but there's no callback for that so we use the hacky timeout.
+ finished && setTimeout(finished, 0); // finished() needs to be called right after hackToCallFinishAfterFocus(), but there's no callback for that so we use the hacky timeout.
return false; // we must return false here so that the zoom to the document is not reversed. If it weren't for needing to call finished(), we wouldn't need this function at all since not having it is equivalent to returning false
};
this.props.addDocTab(doc, where) && this.props.focus(doc, BoolCast(this.Document.followLinkZoom, true), undefined, hackToCallFinishAfterFocus); // add the target and focus on it.
@@ -400,7 +402,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
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();
-
}
}
@@ -438,12 +439,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
const oldPoint2 = this.prevPoints.get(pt2.identifier);
const pinching = InteractionUtils.Pinning(pt1, pt2, oldPoint1!, oldPoint2!);
if (pinching !== 0 && oldPoint1 && oldPoint2) {
- // let dX = (Math.min(pt1.clientX, pt2.clientX) - Math.min(oldPoint1.clientX, oldPoint2.clientX));
- // let dY = (Math.min(pt1.clientY, pt2.clientY) - Math.min(oldPoint1.clientY, oldPoint2.clientY));
- // let dX = Math.sign(Math.abs(pt1.clientX - oldPoint1.clientX) - Math.abs(pt2.clientX - oldPoint2.clientX));
- // let dY = Math.sign(Math.abs(pt1.clientY - oldPoint1.clientY) - Math.abs(pt2.clientY - oldPoint2.clientY));
- // let dW = -dX;
- // let dH = -dY;
const dW = (Math.abs(pt1.clientX - pt2.clientX) - Math.abs(oldPoint1.clientX - oldPoint2.clientX));
const dH = (Math.abs(pt1.clientY - pt2.clientY) - Math.abs(oldPoint1.clientY - oldPoint2.clientY));
const dX = -1 * Math.sign(dW);
@@ -526,7 +521,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
onPointerMove = (e: PointerEvent): void => {
-
if ((e as any).formattedHandled) { e.stopPropagation(); return; }
if ((InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || Doc.GetSelectedTool() === InkTool.Highlighter || Doc.GetSelectedTool() === InkTool.Pen)) return;
if (e.cancelBubble && this.active) {
@@ -547,17 +541,15 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
onPointerUp = (e: PointerEvent): void => {
this.cleanUpInteractions();
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
if (this.onPointerUpHandler?.script && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
this.onPointerUpHandler.script.run({ self: this.rootDoc, this: this.layoutDoc }, console.log);
- document.removeEventListener("pointerup", this.onPointerUp);
- return;
+ } else {
+ this._doubleTap = (Date.now() - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2);
+ this._lastTap = Date.now();
}
-
- document.removeEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- this._doubleTap = (Date.now() - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2);
- this._lastTap = Date.now();
}
onGesture = (e: Event, ge: GestureUtils.GestureEvent) => {
@@ -579,75 +571,56 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
}
-
- @undoBatch
- toggleLinkButtonBehavior = (): void => {
- if (this.Document.isLinkButton || this.onClickHandler || this.Document.ignoreClick) {
- this.Document.isLinkButton = false;
- const first = DocListCast(this.Document.links).find(d => d instanceof Doc);
- first && (first.hidden = false);
- this.Document.ignoreClick = false;
- this.Document.onClick = this.layoutDoc.onClick = undefined;
- } else {
- this.Document.isLinkButton = true;
- const first = DocListCast(this.Document.links).find(d => d instanceof Doc);
- first && (first.hidden = true);
- this.Document.followLinkZoom = false;
- this.Document.followLinkLocation = undefined;
- }
- }
-
- @undoBatch
- toggleFollowInPlace = (): void => {
- if (this.Document.isLinkButton) {
- this.Document.isLinkButton = false;
- const first = DocListCast(this.Document.links).find(d => d instanceof Doc);
- first && (first.hidden = false);
+ @undoBatch @action
+ toggleFollowLink = (location: Opt<string>, zoom: boolean, setPushpin: boolean): void => {
+ this.Document.ignoreClick = false;
+ this.Document.isLinkButton = !this.Document.isLinkButton;
+ setPushpin && (this.Document.isPushpin = this.Document.isLinkButton);
+ if (this.Document.isLinkButton && !this.onClickHandler) {
+ this.Document.followLinkZoom = zoom;
+ this.Document.followLinkLocation = location;
} else {
- this.Document.isLinkButton = true;
- const first = DocListCast(this.Document.links).find(d => d instanceof Doc);
- first && (first.hidden = true);
- this.Document.followLinkZoom = true;
- this.Document.followLinkLocation = "inPlace";
- }
- }
-
- @undoBatch
- toggleFollowOnRight = (): void => {
- if (this.Document.isLinkButton) {
- this.Document.isLinkButton = false;
- const first = DocListCast(this.Document.links).find(d => d instanceof Doc);
- first && (first.hidden = false);
- } else {
- this.Document.isLinkButton = true;
- this.Document.followLinkZoom = false;
- const first = DocListCast(this.Document.links).find(d => d instanceof Doc);
- first && (first.hidden = true);
- this.Document.followLinkLocation = "onRight";
+ this.Document.onClick = this.layoutDoc.onClick = undefined;
}
}
- @undoBatch
- @action
+ @undoBatch @action
drop = async (e: Event, de: DragManager.DropEvent) => {
if (this.props.Document === Doc.UserDoc().activeWorkspace) {
alert("linking to document tabs not yet supported. Drop link on document content.");
return;
}
+ const makeLink = action((linkDoc: Doc) => {
+ LinkManager.currentLink = linkDoc;
+
+ TaskCompletionBox.textDisplayed = "Link Created";
+ TaskCompletionBox.popupX = de.x;
+ TaskCompletionBox.popupY = de.y - 33;
+ TaskCompletionBox.taskCompleted = true;
+
+ LinkDescriptionPopup.popupX = de.x;
+ LinkDescriptionPopup.popupY = de.y;
+ LinkDescriptionPopup.descriptionPopup = true;
+
+ setTimeout(action(() => TaskCompletionBox.taskCompleted = false), 2500);
+ });
if (de.complete.annoDragData) {
/// this whole section for handling PDF annotations looks weird. Need to rethink this to make it cleaner
e.stopPropagation();
de.complete.annoDragData.linkedToDoc = true;
- DocUtils.MakeLink({ doc: de.complete.annoDragData.annotationDocument }, { doc: this.props.Document }, "link");
+ const linkDoc = DocUtils.MakeLink({ doc: de.complete.annoDragData.annotationDocument }, { doc: this.props.Document }, "link");
+ linkDoc && makeLink(linkDoc);
}
if (de.complete.linkDragData) {
e.stopPropagation();
- // const docs = await SearchUtil.Search(`data_l:"${destDoc[Id]}"`, true);
- // const views = docs.map(d => DocumentManager.Instance.getDocumentView(d)).filter(d => d).map(d => d as DocumentView);
- de.complete.linkDragData.linkSourceDocument !== this.props.Document &&
- (de.complete.linkDragData.linkDocument = DocUtils.MakeLink({ doc: de.complete.linkDragData.linkSourceDocument },
- { doc: this.props.Document }, `link`)); // TODODO this is where in text links get passed
+ const linkSource = de.complete.linkDragData.linkSourceDocument;
+ if (linkSource !== this.props.Document) {
+ const linkDoc = DocUtils.MakeLink({ doc: linkSource }, { doc: this.props.Document }, `link`);
+ linkSource !== this.props.Document && (de.complete.linkDragData.linkDocument = linkDoc); // TODODO this is where in text links get passed
+ linkDoc && makeLink(linkDoc);
+ }
+
}
}
@@ -659,8 +632,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@undoBatch
@action
+ toggleLockPosition = (): void => {
+ this.Document.lockedPosition = this.Document.lockedPosition ? undefined : true;
+ }
+
+ @undoBatch
+ @action
makeIntoPortal = async () => {
- const portalLink = DocListCast(this.Document.links).find(d => d.anchor1 === this.props.Document);
+ const portalLink = this.allLinks.find(d => d.anchor1 === this.props.Document);
if (!portalLink) {
const portal = Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: NumCast(this.layoutDoc._height), title: StrCast(this.props.Document.title) + ".portal" });
DocUtils.MakeLink({ doc: this.props.Document }, { doc: portal }, "portal to");
@@ -671,9 +650,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@undoBatch
@action
- toggleBackground = (temporary: boolean): void => {
- this.Document._overflow = temporary ? "visible" : "hidden";
- this.Document.isBackground = !temporary ? !this.Document.isBackground : (this.Document.isBackground ? undefined : true);
+ toggleBackground = () => {
+ this.Document.isBackground = (this.Document.isBackground ? undefined : true);
+ this.Document._overflow = this.Document.isBackground ? "visible" : undefined;
if (this.Document.isBackground) {
this.props.bringToFront(this.props.Document, true);
this.props.Document[DataSym][Doc.LayoutFieldKey(this.Document) + "-nativeWidth"] = this.Document[WidthSym]();
@@ -681,40 +660,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
}
- @undoBatch
- @action
- toggleLockPosition = (): void => {
- this.Document.lockedPosition = this.Document.lockedPosition ? undefined : true;
- }
-
- @undoBatch
- @action
- setAcl = (acl: "readOnly" | "addOnly" | "ownerOnly" | "write") => {
- this.dataDoc.ACL = this.props.Document.ACL = acl;
- DocListCast(this.dataDoc[Doc.LayoutFieldKey(this.dataDoc)]).map(d => {
- if (d.author === Doc.CurrentUserEmail) d.ACL = acl;
- const data = d[DataSym];
- if (data && data.author === Doc.CurrentUserEmail) data.ACL = acl;
- });
- }
- @undoBatch
- @action
- testAcl = (acl: "readOnly" | "addOnly" | "ownerOnly" | "write") => {
- this.dataDoc.author = this.props.Document.author = "ADMIN";
- this.dataDoc.ACL = this.props.Document.ACL = acl;
- DocListCast(this.dataDoc[Doc.LayoutFieldKey(this.dataDoc)]).map(d => {
- if (d.author === Doc.CurrentUserEmail) {
- d.author = "ADMIN";
- d.ACL = acl;
- }
- const data = d[DataSym];
- if (data && data.author === Doc.CurrentUserEmail) {
- data.author = "ADMIN";
- data.ACL = acl;
- }
- });
- }
-
@action
onContextMenu = async (e: React.MouseEvent | Touch): Promise<void> => {
// the touch onContextMenu is button 0, the pointer onContextMenu is button 2
@@ -724,17 +669,18 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
return;
}
e.persist();
- e?.stopPropagation();
+ e.stopPropagation();
- if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3 ||
- e.isDefaultPrevented()) {
- e.preventDefault();
+ if (Math.abs(this._downX - e?.clientX) > 3 || Math.abs(this._downY - e?.clientY) > 3 ||
+ e?.isDefaultPrevented()) {
+ e?.preventDefault();
return;
}
e.preventDefault();
}
const cm = ContextMenu.Instance;
+ if (!cm) return;
const customScripts = Cast(this.props.Document.contextMenuScripts, listSpec(ScriptField), []);
Cast(this.props.Document.contextMenuLabels, listSpec("string"), []).forEach((label, i) =>
@@ -742,34 +688,42 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
this.props.contextMenuItems?.().forEach(item =>
cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.layoutDoc, self: this.rootDoc }), icon: "sticky-note" }));
+ const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layoutKey)], Doc, null);
+ const appearance = cm.findByDescription("UI Controls...");
+ const appearanceItems: ContextMenuProps[] = appearance && "subitems" in appearance ? appearance.subitems : [];
+ templateDoc && appearanceItems.push({ description: "Open Template ", event: () => this.props.addDocTab(templateDoc, "onRight"), icon: "eye" });
+ //DocListCast(this.Document.links).length && appearanceItems.splice(0, 0, { description: `${this.layoutDoc.hideLinkButton ? "Show" : "Hide"} Link Button`, event: action(() => this.layoutDoc.hideLinkButton = !this.layoutDoc.hideLinkButton), icon: "eye" });
+ !appearance && cm.addItem({ description: "UI Controls...", subitems: appearanceItems, icon: "compass" });
+
const options = cm.findByDescription("Options...");
const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : [];
- const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layoutKey)], Doc, null);
- templateDoc && optionItems.push({ description: "Open Template ", event: () => this.props.addDocTab(templateDoc, "onRight"), icon: "eye" });
- optionItems.push({ description: "Toggle Show Each Link Dot", event: () => this.layoutDoc.showLinks = !this.layoutDoc.showLinks, icon: "eye" });
+ optionItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" });
!options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "compass" });
+
const existingOnClick = cm.findByDescription("OnClick...");
const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : [];
onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" });
onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(self, "${this.Document.layoutKey}")`), icon: "window-restore" });
onClicks.push({ description: this.Document.ignoreClick ? "Select" : "Do Nothing", event: () => this.Document.ignoreClick = !this.Document.ignoreClick, icon: this.Document.ignoreClick ? "unlock" : "lock" });
- onClicks.push({ description: this.Document.isLinkButton ? "Remove Follow Behavior" : "Follow Link in Place", event: this.toggleFollowInPlace, icon: "concierge-bell" });
- onClicks.push({ description: this.Document.isLinkButton ? "Remove Follow Behavior" : "Follow Link on Right", event: this.toggleFollowOnRight, icon: "concierge-bell" });
- onClicks.push({ description: this.Document.isLinkButton || this.onClickHandler ? "Remove Click Behavior" : "Follow Link", event: this.toggleLinkButtonBehavior, icon: "concierge-bell" });
+ onClicks.push({ description: this.Document.isLinkButton ? "Remove Follow Behavior" : "Follow Link in Place", event: () => this.toggleFollowLink("inPlace", true, false), icon: "concierge-bell" });
+ !this.Document.isLinkButton && onClicks.push({ description: "Follow Link on Right", event: () => this.toggleFollowLink("onRight", false, false), icon: "concierge-bell" });
+ onClicks.push({ description: this.Document.isLinkButton || this.onClickHandler ? "Remove Click Behavior" : "Follow Link", event: () => this.toggleFollowLink(undefined, false, false), icon: "concierge-bell" });
+ onClicks.push({ description: (this.Document.isPushpin ? "Remove" : "Make") + " Pushpin", event: () => this.toggleFollowLink(undefined, false, true), icon: "snowflake" });
onClicks.push({ description: "Edit onClick Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"), icon: "edit" });
- !existingOnClick && cm.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
+ !existingOnClick && cm.addItem({ description: "OnClick...", noexpand: true, addDivider: true, subitems: onClicks, icon: "hand-point-right" });
const funcs: ContextMenuProps[] = [];
if (this.layoutDoc.onDragStart) {
funcs.push({ description: "Drag an Alias", icon: "edit", event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getAlias(this.dragFactory)')) });
funcs.push({ description: "Drag a Copy", icon: "edit", event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')) });
funcs.push({ description: "Drag Document", icon: "edit", event: () => this.layoutDoc.onDragStart = undefined });
- cm.addItem({ description: "OnDrag...", subitems: funcs, icon: "asterisk" });
+ cm.addItem({ description: "OnDrag...", noexpand: true, subitems: funcs, icon: "asterisk" });
}
const more = cm.findByDescription("More...");
const moreItems = more && "subitems" in more ? more.subitems : [];
+ moreItems.push({ description: "Download document", icon: "download", event: async () => Doc.Zip(this.props.Document) });
if (!Doc.UserDoc().noviceMode) {
moreItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: "concierge-bell" });
moreItems.push({ description: `${this.Document._chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document._chromeStatus = (this.Document._chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" });
@@ -779,209 +733,32 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
moreItems.push({ description: "Tag Child Images via Google Photos", event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: "caret-square-right" });
moreItems.push({ description: "Write Back Link to Album", event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: "caret-square-right" });
}
- moreItems.push({
- description: "Download document", icon: "download", event: async () => {
- const response = await rp.get(Utils.CorsProxy("http://localhost:8983/solr/dash/select"), {
- qs: { q: 'world', fq: 'NOT baseProto_b:true AND NOT deleted:true', start: '0', rows: '100', hl: true, 'hl.fl': '*' }
- });
- console.log(response ? JSON.parse(response) : undefined);
- }
- // 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();
- });
+ moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "fingerprint" });
}
- moreItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" });
- moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "fingerprint" });
- moreItems.push({ description: "Delete", event: this.deleteClicked, icon: "trash" });
+ const effectiveAcl = GetEffectiveAcl(this.props.Document);
+ (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) && moreItems.push({ description: "Delete", event: this.deleteClicked, icon: "trash" });
moreItems.push({ description: "Share", event: () => SharingManager.Instance.open(this), icon: "external-link-alt" });
!more && cm.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" });
cm.moveAfter(cm.findByDescription("More...")!, cm.findByDescription("OnClick...")!);
const help = cm.findByDescription("Help...");
const helpItems: ContextMenuProps[] = help && "subitems" in help ? help.subitems : [];
- helpItems.push({ description: "Text Shortcuts Ctrl+/", event: () => this.props.addDocTab(Docs.Create.PdfDocument("http://localhost:1050/assets/cheat-sheet.pdf", { _width: 300, _height: 300 }), "onRight"), icon: "keyboard" });
+ helpItems.push({ description: "Text Shortcuts Ctrl+/", event: () => this.props.addDocTab(Docs.Create.PdfDocument(Utils.prepend("/assets/cheat-sheet.pdf"), { _width: 300, _height: 300 }), "onRight"), icon: "keyboard" });
helpItems.push({ description: "Show Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "onRight"), icon: "layer-group" });
- cm.addItem({ description: "Help...", subitems: helpItems, icon: "question" });
-
- const existingAcls = cm.findByDescription("Privacy...");
- const aclItems: ContextMenuProps[] = existingAcls && "subitems" in existingAcls ? existingAcls.subitems : [];
- aclItems.push({ description: "Make Add Only", event: () => this.setAcl("addOnly"), icon: "concierge-bell" });
- aclItems.push({ description: "Make Read Only", event: () => this.setAcl("readOnly"), icon: "concierge-bell" });
- aclItems.push({ description: "Make Private", event: () => this.setAcl("ownerOnly"), icon: "concierge-bell" });
- aclItems.push({ description: "Make Editable", event: () => this.setAcl("write"), icon: "concierge-bell" });
- aclItems.push({ description: "Test Private", event: () => this.testAcl("ownerOnly"), icon: "concierge-bell" });
- aclItems.push({ description: "Test Readonly", event: () => this.testAcl("readOnly"), icon: "concierge-bell" });
- !existingAcls && cm.addItem({ description: "Privacy...", subitems: aclItems, icon: "question" });
-
- // const recommender_subitems: ContextMenuProps[] = [];
-
- // recommender_subitems.push({
- // description: "Internal recommendations",
- // event: () => this.recommender(),
- // icon: "brain"
- // });
-
- // const ext_recommender_subitems: ContextMenuProps[] = [];
-
- // ext_recommender_subitems.push({
- // description: "arXiv",
- // event: () => this.externalRecommendation("arxiv"),
- // icon: "brain"
- // });
- // ext_recommender_subitems.push({
- // description: "Bing",
- // event: () => this.externalRecommendation("bing"),
- // icon: "brain"
- // });
-
- // recommender_subitems.push({
- // description: "External recommendations",
- // subitems: ext_recommender_subitems,
- // icon: "brain"
- // });
-
-
- //moreItems.push({ description: "Recommender System", subitems: recommender_subitems, icon: "brain" });
- //moreItems.push({ description: "Publish", event: () => DocUtils.Publish(this.props.Document, this.Document.title || "", this.props.addDocument, this.props.removeDocument), icon: "file" });
- //moreItems.push({ description: "Undo Debug Test", event: () => UndoManager.TraceOpenBatches(), icon: "exclamation" });
-
- // runInAction(() => {
- // const setWriteMode = (mode: DocServer.WriteMode) => {
- // DocServer.AclsMode = mode;
- // const mode1 = mode;
- // const mode2 = mode === DocServer.WriteMode.Default ? mode : DocServer.WriteMode.Playground;
- // DocServer.setFieldWriteMode("x", mode1);
- // DocServer.setFieldWriteMode("y", mode1);
- // DocServer.setFieldWriteMode("_width", mode1);
- // DocServer.setFieldWriteMode("_height", mode1);
-
- // DocServer.setFieldWriteMode("_panX", mode2);
- // DocServer.setFieldWriteMode("_panY", mode2);
- // DocServer.setFieldWriteMode("scale", mode2);
- // DocServer.setFieldWriteMode("_viewType", mode2);
- // };
- // const aclsMenu: ContextMenuProps[] = [];
- // aclsMenu.push({ description: "Default (write/read all)", event: () => setWriteMode(DocServer.WriteMode.Default), icon: DocServer.AclsMode === DocServer.WriteMode.Default ? "check" : "exclamation" });
- // aclsMenu.push({ description: "Playground (write own/no read)", event: () => setWriteMode(DocServer.WriteMode.Playground), icon: DocServer.AclsMode === DocServer.WriteMode.Playground ? "check" : "exclamation" });
- // aclsMenu.push({ description: "Live Playground (write own/read others)", event: () => setWriteMode(DocServer.WriteMode.LivePlayground), icon: DocServer.AclsMode === DocServer.WriteMode.LivePlayground ? "check" : "exclamation" });
- // aclsMenu.push({ description: "Live Readonly (no write/read others)", event: () => setWriteMode(DocServer.WriteMode.LiveReadonly), icon: DocServer.AclsMode === DocServer.WriteMode.LiveReadonly ? "check" : "exclamation" });
- // cm.addItem({ description: "Collaboration ...", subitems: aclsMenu, icon: "share" });
- // });
+ helpItems.push({ description: "Print Document in Console", event: () => console.log(this.props.Document), icon: "hand-point-right" });
+ cm.addItem({ description: "Help...", noexpand: true, subitems: helpItems, icon: "question" });
+
runInAction(() => {
if (!this.topMost && !(e instanceof Touch)) {
- // DocumentViews should stop propagation of this event
- e.stopPropagation();
+ e.stopPropagation(); // DocumentViews should stop propagation of this event
}
- ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
+ cm.displayMenu(e.pageX - 15, e.pageY - 15);
!SelectionManager.IsSelected(this, true) && SelectionManager.SelectDoc(this, false);
});
- const path = this.props.LibraryPath.reduce((p: string, d: Doc) => p + "/" + (Doc.AreProtosEqual(d, (Doc.UserDoc()["tabs-button-library"] as Doc).sourcePanel as Doc) ? "" : d.title), "");
- const item = ({
- description: `path: ${path}`, event: () => {
- if (this.props.LibraryPath !== emptyPath) {
- this.props.LibraryPath.map(lp => Doc.GetProto(lp).treeViewOpen = lp.treeViewOpen = true);
- Doc.linkFollowHighlight(this.props.Document);
- } else {
- Doc.AddDocToList(Doc.GetProto(Doc.UserDoc().myCatalog as Doc), "data", this.props.Document[DataSym]);
- }
- }, icon: "check"
- });
- //cm.addItem(item);
- }
-
- recommender = async () => {
- if (!ClientRecommender.Instance) new ClientRecommender({ title: "Client Recommender" });
- const documents: Doc[] = [];
- const allDocs = await SearchUtil.GetAllDocs();
- // allDocs.forEach(doc => console.log(doc.title));
- // clears internal representation of documents as vectors
- ClientRecommender.Instance.reset_docs();
- //ClientRecommender.Instance.arxivrequest("electrons");
- await Promise.all(allDocs.map((doc: Doc) => {
- let isMainDoc: boolean = false;
- const dataDoc = Doc.GetProto(doc);
- if (doc.type === DocumentType.RTF) {
- if (dataDoc === Doc.GetProto(this.props.Document)) {
- isMainDoc = true;
- }
- if (!documents.includes(dataDoc)) {
- documents.push(dataDoc);
- const extdoc = doc.data_ext as Doc;
- return ClientRecommender.Instance.extractText(doc, extdoc ? extdoc : doc, true, "", isMainDoc);
- }
- }
- if (doc.type === DocumentType.IMG) {
- if (dataDoc === Doc.GetProto(this.props.Document)) {
- isMainDoc = true;
- }
- if (!documents.includes(dataDoc)) {
- documents.push(dataDoc);
- const extdoc = doc.data_ext as Doc;
- return ClientRecommender.Instance.extractText(doc, extdoc ? extdoc : doc, true, "", isMainDoc, true);
- }
- }
- }));
- const doclist = ClientRecommender.Instance.computeSimilarities("cosine");
- const recDocs: { preview: Doc, score: number }[] = [];
- // tslint:disable-next-line: prefer-for-of
- for (let i = 0; i < doclist.length; i++) {
- recDocs.push({ preview: doclist[i].actualDoc, score: doclist[i].score });
- }
-
- const data = recDocs.map(unit => {
- unit.preview.score = unit.score;
- return unit.preview;
- });
-
- console.log(recDocs.map(doc => doc.score));
-
- const title = `Showing ${data.length} recommendations for "${StrCast(this.props.Document.title)}"`;
- const recommendations = Docs.Create.RecommendationsDocument(data, { title });
- recommendations.documentIconHeight = 150;
- recommendations.sourceDoc = this.props.Document;
- recommendations.sourceDocContext = this.props.ContainingCollectionView!.props.Document;
- CollectionDockingView.AddRightSplit(recommendations, undefined);
-
- // RecommendationsBox.Instance.displayRecommendations(e.pageX + 100, e.pageY);
- }
-
- @action
- externalRecommendation = async (api: string) => {
- if (!ClientRecommender.Instance) new ClientRecommender({ title: "Client Recommender" });
- ClientRecommender.Instance.reset_docs();
- const doc = Doc.GetDataDoc(this.props.Document);
- const extdoc = doc.data_ext as Doc;
- const recs_and_kps = await ClientRecommender.Instance.extractText(doc, extdoc ? extdoc : doc, false, api);
- let recs: any;
- let kps: any;
- if (recs_and_kps) {
- recs = recs_and_kps.recs;
- kps = recs_and_kps.keyterms;
- }
- else {
- console.log("recommender system failed :(");
- return;
- }
- console.log("ibm keyterms: ", kps.toString());
- const headers = [new SchemaHeaderField("title"), new SchemaHeaderField("href")];
- const bodies: Doc[] = [];
- const titles = recs.title_vals;
- const urls = recs.url_vals;
- for (let i = 0; i < 5; i++) {
- const body = Docs.Create.FreeformDocument([], { title: titles[i] });
- body.href = urls[i];
- bodies.push(body);
- }
- CollectionDockingView.AddRightSplit(Docs.Create.SchemaDocument(headers, bodies, { title: `Showing External Recommendations for "${StrCast(doc.title)}"` }), undefined);
- this._showKPQuery = true;
- this._queries = kps.toString();
}
// does Document set a layout prop
- // does Document set a layout prop
+ // does Document set a layout prop
setsLayoutProp = (prop: string) => this.props.Document[prop] !== this.props.Document["default" + prop[0].toUpperCase() + prop.slice(1)] && this.props.Document["default" + prop[0].toUpperCase() + prop.slice(1)];
// get the a layout prop by first choosing the prop from Document, then falling back to the layout doc otherwise.
getLayoutPropStr = (prop: string) => StrCast(this.setsLayoutProp(prop) ? this.props.Document[prop] : this.layoutDoc[prop]);
@@ -1007,6 +784,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
return this.isSelected(outsideReaction) || (this.props.Document.rootDocument && this.props.rootSelected?.(outsideReaction)) || false;
}
childScaling = () => (this.layoutDoc._fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling());
+ @computed.struct get linkOffset() { return [-15, 0]; }
@computed get contents() {
TraceMobx();
return (<div style={{ position: "absolute", width: "100%", height: "100%" }}>
@@ -1045,16 +823,18 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
ChromeHeight={this.chromeHeight}
isSelected={this.isSelected}
select={this.select}
- onClick={this.onClickHandler}
+ onClick={this.onClickFunc}
layoutKey={this.finalLayoutKey} />
- {this.layoutDoc.showLinks ? this.anchors : (null)}
- {this.props.forcedBackgroundColor?.(this.Document) === "transparent" || this.props.dontRegisterView ? (null) : <DocumentLinksButton View={this} Offset={[-15, 0]} />}
+ {this.layoutDoc.hideAllLinks ? (null) : this.allAnchors}
+ {/* {this.allAnchors} */}
+ {this.props.forcedBackgroundColor?.(this.Document) === "transparent" || this.layoutDoc.isLinkButton || this.layoutDoc.hideLinkButton || this.props.dontRegisterView ? (null) :
+ <DocumentLinksButton View={this} links={this.allLinks} Offset={this.linkOffset} />}
</div>
);
}
// used to decide whether a link anchor view should be created or not.
- // if it's a tempoarl link (currently just for Audio), then the audioBox will display the anchor and we don't want to display it here.
+ // if it's a temporal link (currently just for Audio), then the audioBox will display the anchor and we don't want to display it here.
// would be good to generalize this some way.
isNonTemporalLink = (linkDoc: Doc) => {
const anchor = Cast(Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? linkDoc.anchor1 : linkDoc.anchor2, Doc) as Doc;
@@ -1062,7 +842,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
return anchor.type === DocumentType.AUDIO && NumCast(ept) ? false : true;
}
-
@observable _link: Opt<Doc>; // see DocumentButtonBar for explanation of how this works
makeLink = () => this._link; // pass the link placeholde to child views so they can react to make a specialized anchor. This is essentially a function call to the descendants since the value of the _link variable will immediately get set back to undefined.
@@ -1070,12 +849,17 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
hideLinkAnchor = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg: boolean, doc) => flg && (doc.hidden = true), true)
anchorPanelWidth = () => this.props.PanelWidth() || 1;
anchorPanelHeight = () => this.props.PanelHeight() || 1;
- @computed get anchors() {
+
+ @computed.struct get directLinks() { return LinkManager.Instance.getAllDirectLinks(this.Document); }
+ @computed.struct get allLinks() { return DocListCast(this.Document.links); }
+ @computed.struct get allAnchors() {
TraceMobx();
+ if (this.props.LayoutTemplateString?.includes("LinkAnchorBox")) return null;
return (this.props.treeViewDoc && this.props.LayoutTemplateString) || // render nothing for: tree view anchor dots
this.layoutDoc.presBox || // presentationbox nodes
+ this.rootDoc.type === DocumentType.LINK ||
this.props.dontRegisterView ? (null) : // view that are not registered
- DocListCast(this.Document.links).filter(d => !d.hidden && this.isNonTemporalLink).map((d, i) =>
+ DocUtils.FilterDocs(this.directLinks, this.props.docFilters(), []).filter(d => !d.hidden && this.isNonTemporalLink).map((d, i) =>
<DocumentView {...this.props} key={i + 1}
Document={d}
ContainingCollectionView={this.props.ContainingCollectionView}
@@ -1093,10 +877,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
@computed get innards() {
TraceMobx();
- if (this.props.treeViewDoc && !this.props.LayoutTemplateString) { // this happens when the document is a tree view label (but not an anchor dot)
+ if (this.props.treeViewDoc && !this.props.LayoutTemplateString?.includes("LinkAnchorBox")) { // this happens when the document is a tree view label (but not an anchor dot)
return <div className="documentView-treeView" style={{ maxWidth: this.props.PanelWidth() || undefined }}>
{StrCast(this.props.Document.title)}
- {this.anchors}
+ {this.allAnchors}
</div>;
}
@@ -1113,7 +897,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
ChromeHeight={this.chromeHeight}
isSelected={this.isSelected}
select={this.select}
- onClick={this.onClickHandler}
+ onClick={this.onClickFunc}
layoutKey={this.finalLayoutKey} />
</div>);
const titleView = (!showTitle ? (null) :
@@ -1148,7 +932,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
DocUtils.makeCustomViewClicked(this.props.Document, Docs.Create.StackingDocument, layout, undefined);
}
}
- @observable _animateScalingTo = 0;
+
switchViews = action((custom: boolean, view: string) => {
this._animateScalingTo = 0.1; // shrink doc
setTimeout(action(() => {
@@ -1162,7 +946,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
return (this.Document.isBackground !== undefined || this.isSelected(false)) &&
((this.Document.type === DocumentType.COL && this.Document._viewType !== CollectionViewType.Pile) || this.Document.type === DocumentType.IMG) &&
this.props.renderDepth > 0 && !this.props.treeViewDoc ?
- <div className="documentView-lock" onClick={() => this.toggleBackground(true)}>
+ <div className="documentView-lock" onClick={this.toggleBackground}>
<FontAwesomeIcon icon={this.Document.isBackground ? "unlock" : "lock"} style={{ color: this.Document.isBackground ? "red" : undefined }} size="lg" />
</div>
: (null);
@@ -1170,7 +954,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
render() {
if (!(this.props.Document instanceof Doc)) return (null);
- if (this.props.Document[AclSym] && this.props.Document[AclSym] === AclPrivate) return (null);
+ if (GetEffectiveAcl(this.props.Document) === AclPrivate) return (null);
if (this.props.Document.hidden) return (null);
const backgroundColor = Doc.UserDoc().renderStyle === "comic" ? undefined : this.props.forcedBackgroundColor?.(this.Document) || StrCast(this.layoutDoc._backgroundColor) || StrCast(this.layoutDoc.backgroundColor) || StrCast(this.Document.backgroundColor) || this.props.backgroundColor?.(this.Document);
const opacity = Cast(this.layoutDoc._opacity, "number", Cast(this.layoutDoc.opacity, "number", Cast(this.Document.opacity, "number", null)));
@@ -1186,11 +970,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid"];
let highlighting = fullDegree && this.layoutDoc.type !== DocumentType.FONTICON && this.layoutDoc._viewType !== CollectionViewType.Linear && this.props.Document.type !== DocumentType.INK;
highlighting = highlighting && this.props.focus !== emptyFunction; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way
- return <div className={`documentView-node${this.topMost ? "-topmost" : ""}`}
+ const topmost = this.topMost ? "-topmost" : "";
+ return <div className={`documentView-node${topmost}`}
id={this.props.Document[Id]}
ref={this._mainCont} onKeyDown={this.onKeyDown}
onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
- onPointerEnter={action(() => Doc.BrushDoc(this.props.Document))}
+ onPointerEnter={action(() => { Doc.BrushDoc(this.props.Document); })}
onPointerLeave={action((e: React.PointerEvent<HTMLDivElement>) => {
let entered = false;
const target = document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y);
@@ -1199,7 +984,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
entered = true;
}
}
+ // if (this.props.Document !== DocumentLinksButton.StartLink?.Document) {
!entered && Doc.UnBrushDoc(this.props.Document);
+ //}
+
})}
style={{
transformOrigin: this._animateScalingTo ? "center center" : undefined,
@@ -1216,7 +1004,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
background: finalColor,
opacity: finalOpacity,
fontFamily: StrCast(this.Document._fontFamily, "inherit"),
- fontSize: Cast(this.Document._fontSize, "number", null)
+ fontSize: Cast(this.Document._fontSize, "string", null)
}}>
{this.onClickHandler && this.props.ContainingCollectionView?.props.Document._viewType === CollectionViewType.Time ? <>
{this.innards}
@@ -1225,7 +1013,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
this.innards}
{this.renderLock()}
</div>;
- { this._showKPQuery ? <KeyphraseQueryView keyphrases={this._queries}></KeyphraseQueryView> : undefined; }
}
}
@@ -1233,4 +1020,4 @@ Scripting.addGlobal(function toggleDetail(doc: any, layoutKey: string, otherKey:
const dv = DocumentManager.Instance.getDocumentView(doc);
if (dv?.props.Document.layoutKey === layoutKey) dv?.switchViews(otherKey !== "layout", otherKey.replace("layout_", ""));
else dv?.switchViews(true, layoutKey.replace("layout_", ""));
-}); \ No newline at end of file
+});
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index c9fc99a31..c10ff22fe 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -25,7 +25,7 @@ export interface FieldViewProps {
Document: Doc;
DataDoc?: Doc;
LibraryPath: Doc[];
- onClick?: ScriptField;
+ onClick?: () => ScriptField;
dropAction: dropActionType;
backgroundHalo?: () => boolean;
docFilters: () => string[];
@@ -46,8 +46,8 @@ export interface FieldViewProps {
dontRegisterView?: boolean;
focus: (doc: Doc) => void;
ignoreAutoHeight?: boolean;
- PanelWidth: () => number | string;
- PanelHeight: () => number | string;
+ PanelWidth: () => number;
+ PanelHeight: () => number;
PanelPosition: string;
overflow?: boolean;
NativeHeight: () => number;
diff --git a/src/client/views/nodes/FontIconBox.scss b/src/client/views/nodes/FontIconBox.scss
index 68b00a5be..5b85d8b0b 100644
--- a/src/client/views/nodes/FontIconBox.scss
+++ b/src/client/views/nodes/FontIconBox.scss
@@ -11,13 +11,15 @@
.fontIconBox-label {
background: gray;
color:white;
- margin-left: -10px;
border-radius: 8px;
width:100%;
position: absolute;
text-align: center;
font-size: 8px;
margin-top:4px;
+ letter-spacing: normal;
+ left: 0;
+ overflow: hidden;
}
svg {
diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx
index 5e8dd2497..2611d2ca7 100644
--- a/src/client/views/nodes/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox.tsx
@@ -11,6 +11,7 @@ import { runInAction, observable, reaction, IReactionDisposer } from 'mobx';
import { Doc } from '../../../fields/Doc';
import { ContextMenu } from '../ContextMenu';
import { ScriptField } from '../../../fields/ScriptField';
+import { Tooltip } from '@material-ui/core';
const FontIconSchema = createSchema({
icon: "string"
});
@@ -60,14 +61,18 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>(
render() {
const referenceDoc = (this.layoutDoc.dragFactory instanceof Doc ? this.layoutDoc.dragFactory : this.layoutDoc);
const refLayout = Doc.Layout(referenceDoc);
- return <button className="fontIconBox-outerDiv" title={StrCast(this.layoutDoc.title)} ref={this._ref} onContextMenu={this.specificContextMenu}
+ const button = <button className="fontIconBox-outerDiv" ref={this._ref} onContextMenu={this.specificContextMenu}
style={{
padding: Cast(this.layoutDoc._xPadding, "number", null),
background: StrCast(refLayout._backgroundColor, StrCast(refLayout.backgroundColor)),
boxShadow: this.layoutDoc.ischecked ? `4px 4px 12px black` : undefined
}}>
- <FontAwesomeIcon className="fontIconBox-icon" icon={this.dataDoc.icon as any} color={StrCast(this.layoutDoc.color, this._foregroundColor)} size="sm" />
- {!this.rootDoc.label ? (null) : <div className="fontIconBox-label"> {StrCast(this.rootDoc.label).substring(0, 5)} </div>}
+ <FontAwesomeIcon className="fontIconBox-icon" icon={StrCast(this.dataDoc.icon, "user") as any} color={StrCast(this.layoutDoc.color, this._foregroundColor)} size="sm" />
+ {!this.rootDoc.title ? (null) : <div className="fontIconBox-label" style={{ width: this.rootDoc.label ? "max-content" : undefined }}> {StrCast(this.rootDoc.label, StrCast(this.rootDoc.title).substring(0, 6))} </div>}
</button>;
+ return !this.layoutDoc.toolTip ? button :
+ <Tooltip title={<div className="dash-tooltip">{StrCast(this.layoutDoc.toolTip)}</div>}>
+ {button}
+ </Tooltip>;
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 15148d01d..c1b95b308 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -2,7 +2,7 @@
border-radius: inherit;
width: 100%;
height: 100%;
- position: absolute;
+ position: relative;
transform-origin: top left;
.imageBox-fader {
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index c1c6f6baf..d668d332b 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -66,7 +66,7 @@ const uploadIcons = {
@observer
export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageDocument>(ImageDocument) {
- protected multiTouchDisposer?: import("../../util/InteractionUtils").InteractionUtils.MultiTouchEventDisposer | undefined;
+ protected _multiTouchDisposer?: import("../../util/InteractionUtils").InteractionUtils.MultiTouchEventDisposer | undefined;
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ImageBox, fieldKey); }
private _imgRef: React.RefObject<HTMLImageElement> = React.createRef();
private _dropDisposer?: DragManager.DragDropDisposer;
@@ -120,7 +120,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD
});
const files = await res.json();
const url = Utils.prepend(files[0].path);
- // upload to server with known URL
+ // upload to server with known URL
const audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", _width: 200, _height: 32 });
audioDoc.treeViewExpandedView = "layout";
const audioAnnos = Cast(this.dataDoc[this.fieldKey + "-audioAnnotations"], listSpec(Doc));
@@ -157,31 +157,34 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD
const field = Cast(this.dataDoc[this.fieldKey], ImageField);
if (field) {
const funcs: ContextMenuProps[] = [];
- funcs.push({ description: "Rotate", event: this.rotate, icon: "expand-arrows-alt" });
- funcs.push({ description: "Export to Google Photos", event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: "caret-square-right" });
- funcs.push({ description: "Copy path", event: () => Utils.CopyText(field.url.href), icon: "expand-arrows-alt" });
- // funcs.push({
- // description: "Reset Native Dimensions", event: action(async () => {
- // const curNW = NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"]);
- // const curNH = NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"]);
- // if (this.props.PanelWidth() / this.props.PanelHeight() > curNW / curNH) {
- // this.dataDoc[this.fieldKey + "-nativeWidth"] = this.props.PanelHeight() * curNW / curNH;
- // this.dataDoc[this.fieldKey + "-nativeHeight"] = this.props.PanelHeight();
- // } else {
- // this.dataDoc[this.fieldKey + "-nativeWidth"] = this.props.PanelWidth();
- // this.dataDoc[this.fieldKey + "-nativeHeight"] = this.props.PanelWidth() * curNH / curNW;
- // }
- // }), icon: "expand-arrows-alt"
- // });
-
- const existingAnalyze = ContextMenu.Instance.findByDescription("Analyzers...");
- const modes: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : [];
- modes.push({ description: "Generate Tags", event: this.generateMetadata, icon: "tag" });
- modes.push({ description: "Find Faces", event: this.extractFaces, icon: "camera" });
- //modes.push({ description: "Recommend", event: this.extractText, icon: "brain" });
- !existingAnalyze && ContextMenu.Instance.addItem({ description: "Analyzers...", subitems: modes, icon: "hand-point-right" });
-
- ContextMenu.Instance.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" });
+ funcs.push({ description: "Rotate Clockwise 90", event: this.rotate, icon: "expand-arrows-alt" });
+ funcs.push({ description: "Make Background", event: () => this.layoutDoc.isBackground = true, icon: "expand-arrows-alt" });
+ if (!Doc.UserDoc().noviceMode) {
+ funcs.push({ description: "Export to Google Photos", event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: "caret-square-right" });
+ funcs.push({ description: "Copy path", event: () => Utils.CopyText(field.url.href), icon: "expand-arrows-alt" });
+ // funcs.push({
+ // description: "Reset Native Dimensions", event: action(async () => {
+ // const curNW = NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"]);
+ // const curNH = NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"]);
+ // if (this.props.PanelWidth() / this.props.PanelHeight() > curNW / curNH) {
+ // this.dataDoc[this.fieldKey + "-nativeWidth"] = this.props.PanelHeight() * curNW / curNH;
+ // this.dataDoc[this.fieldKey + "-nativeHeight"] = this.props.PanelHeight();
+ // } else {
+ // this.dataDoc[this.fieldKey + "-nativeWidth"] = this.props.PanelWidth();
+ // this.dataDoc[this.fieldKey + "-nativeHeight"] = this.props.PanelWidth() * curNH / curNW;
+ // }
+ // }), icon: "expand-arrows-alt"
+ // });
+
+ const existingAnalyze = ContextMenu.Instance?.findByDescription("Analyzers...");
+ const modes: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : [];
+ modes.push({ description: "Generate Tags", event: this.generateMetadata, icon: "tag" });
+ modes.push({ description: "Find Faces", event: this.extractFaces, icon: "camera" });
+ //modes.push({ description: "Recommend", event: this.extractText, icon: "brain" });
+ !existingAnalyze && ContextMenu.Instance?.addItem({ description: "Analyzers...", subitems: modes, icon: "hand-point-right" });
+ }
+
+ ContextMenu.Instance?.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" });
}
}
@@ -236,6 +239,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD
if (this._curSuffix === "_m") this._mediumRetryCount++;
if (this._curSuffix === "_l") this._largeRetryCount++;
}
+
@action onError = (error: any) => {
const timeout = this._curSuffix === "_s" ? this._smallRetryCount : this._curSuffix === "_m" ? this._mediumRetryCount : this._largeRetryCount;
if (timeout < 5) {
@@ -312,7 +316,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD
considerGooglePhotosLink = () => {
const remoteUrl = this.dataDoc.googlePhotosUrl;
- return !remoteUrl ? (null) : (<img
+ return !remoteUrl ? (null) : (<img draggable={false}
style={{ transform: `scale(${this.props.ContentScaling()})`, transformOrigin: "bottom right" }}
id={"google-photos"}
src={"/assets/google_photos.png"}
@@ -337,7 +341,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD
}
return (
<img
- id={"upload-icon"}
+ id={"upload-icon"} draggable={false}
style={{ transform: `scale(${1 / this.props.ContentScaling()})`, transformOrigin: "bottom right" }}
src={`/assets/${this.uploadIcon}`}
onClick={async () => {
@@ -412,7 +416,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD
<div className="imageBox-fader" >
<img key={this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys
src={srcpath}
- style={{ transform, transformOrigin }}
+ style={{ transform, transformOrigin }} draggable={false}
width={nativeWidth}
ref={this._imgRef}
onError={this.onError} />
@@ -420,7 +424,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD
<img className="imageBox-fadeaway"
key={"fadeaway" + this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys
src={fadepath}
- style={{ transform, transformOrigin }}
+ style={{ transform, transformOrigin }} draggable={false}
width={nativeWidth}
ref={this._imgRef}
onError={this.onError} /></div>}
@@ -490,4 +494,4 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD
</CollectionFreeFormView>
</div >);
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index 100e2d61e..209a3cc6b 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -43,7 +43,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps, LabelDocument
}, icon: "trash"
});
- ContextMenu.Instance.addItem({ description: "OnClick...", subitems: funcs, icon: "asterisk" });
+ ContextMenu.Instance.addItem({ description: "OnClick...", noexpand: true, subitems: funcs, icon: "asterisk" });
}
@undoBatch
@@ -61,7 +61,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps, LabelDocument
- @observable backColor= "unset";
+ @observable backColor = "unset";
@observable clicked = false;
// (!missingParams || !missingParams.length ? "" : "(" + missingParams.map(m => m + ":").join(" ") + ")")
@@ -71,15 +71,14 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps, LabelDocument
params?.map(p => DocListCast(this.paramsDoc[p])); // bcz: really hacky form of prefetching ...
console.log(this.backColor);
return (
- <div className="labelBox-outerDiv" onClick={()=>runInAction(()=>{this.clicked=!this.clicked; this.clicked? this.backColor=StrCast(this.layoutDoc.hovercolor) : this.backColor ="unset"})} onMouseLeave={()=>runInAction(()=>{ !this.clicked ?this.backColor="unset" : null})}
- onMouseOver={()=>runInAction(()=>{this.backColor=StrCast(this.layoutDoc.hovercolor);})}ref={this.createDropTarget} onContextMenu={this.specificContextMenu}
+ <div className="labelBox-outerDiv" onClick={() => runInAction(() => { this.clicked = !this.clicked; this.clicked ? this.backColor = StrCast(this.layoutDoc.hovercolor) : this.backColor = "unset" })} onMouseLeave={() => runInAction(() => { !this.clicked ? this.backColor = "unset" : null })}
+ onMouseOver={() => runInAction(() => { this.backColor = StrCast(this.layoutDoc.hovercolor); })} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}
style={{ boxShadow: this.layoutDoc.opacity ? StrCast(this.layoutDoc.boxShadow) : "" }}>
<div className="labelBox-mainButton" style={{
background: StrCast(this.layoutDoc.backgroundColor),
color: StrCast(this.layoutDoc.color),
- backgroundColor:this.backColor,
+ backgroundColor: this.backColor,
fontSize: NumCast(this.layoutDoc.fontSize) || "inherit",
- fontSize: NumCast(this.layoutDoc._fontSize) || "inherit",
fontFamily: StrCast(this.layoutDoc._fontFamily) || "inherit",
letterSpacing: StrCast(this.layoutDoc.letterSpacing),
textTransform: StrCast(this.layoutDoc.textTransform) as any,
diff --git a/src/client/views/nodes/LinkAnchorBox.scss b/src/client/views/nodes/LinkAnchorBox.scss
index 710f2178b..42ef2958e 100644
--- a/src/client/views/nodes/LinkAnchorBox.scss
+++ b/src/client/views/nodes/LinkAnchorBox.scss
@@ -1,4 +1,5 @@
-.linkAnchorBox-cont, .linkAnchorBox-cont-small {
+.linkAnchorBox-cont,
+.linkAnchorBox-cont-small {
cursor: default;
position: absolute;
width: 15;
@@ -24,6 +25,6 @@
}
.linkAnchorBox-cont-small {
- width:5px;
- height:5px;
+ width: 5px;
+ height: 5px;
} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx
index d4ab70200..be6292bb6 100644
--- a/src/client/views/nodes/LinkAnchorBox.tsx
+++ b/src/client/views/nodes/LinkAnchorBox.tsx
@@ -49,14 +49,13 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch
const bounds = cdiv.getBoundingClientRect();
const pt = Utils.getNearestPointInPerimeter(bounds.left, bounds.top, bounds.width, bounds.height, e.clientX, e.clientY);
const separation = Math.sqrt((pt[0] - e.clientX) * (pt[0] - e.clientX) + (pt[1] - e.clientY) * (pt[1] - e.clientY));
- const dragdist = Math.sqrt((pt[0] - down[0]) * (pt[0] - down[0]) + (pt[1] - down[1]) * (pt[1] - down[1]));
if (separation > 100) {
const dragData = new DragManager.DocumentDragData([this.rootDoc]);
dragData.dropAction = "alias";
dragData.removeDropProperties = ["anchor1_x", "anchor1_y", "anchor2_x", "anchor2_y", "isLinkButton"];
- DragManager.StartDocumentDrag([this._ref.current!], dragData, down[0], down[1]);
+ DragManager.StartDocumentDrag([this._ref.current!], dragData, pt[0], pt[1]);
return true;
- } else if (dragdist > separation) {
+ } else {
this.rootDoc[this.fieldKey + "_x"] = (pt[0] - bounds.left) / bounds.width * 100;
this.rootDoc[this.fieldKey + "_y"] = (pt[1] - bounds.top) / bounds.height * 100;
}
diff --git a/src/client/views/nodes/LinkDescriptionPopup.scss b/src/client/views/nodes/LinkDescriptionPopup.scss
new file mode 100644
index 000000000..d92823ccc
--- /dev/null
+++ b/src/client/views/nodes/LinkDescriptionPopup.scss
@@ -0,0 +1,77 @@
+.linkDescriptionPopup {
+
+ display: flex;
+
+ border: 1px solid rgb(170, 26, 26);
+
+ width: auto;
+ position: absolute;
+
+ height: auto;
+ z-index: 10000;
+ border-radius: 10px;
+ font-size: 12px;
+ //white-space: nowrap;
+
+ background-color: rgba(250, 250, 250, 0.95);
+ padding-top: 9px;
+ padding-bottom: 9px;
+ padding-left: 9px;
+ padding-right: 9px;
+
+ .linkDescriptionPopup-input {
+ float: left;
+ background-color: rgba(250, 250, 250, 0.95);
+ color: rgb(100, 100, 100);
+ border: none;
+ min-width: 160px;
+ }
+
+ .linkDescriptionPopup-btn {
+
+ float: right;
+
+ justify-content: center;
+ vertical-align: middle;
+
+
+ .linkDescriptionPopup-btn-dismiss {
+ background-color: white;
+ color: black;
+ display: inline;
+ right: 0;
+ border-radius: 10px;
+ border: 1px solid black;
+ padding: 3px;
+ font-size: 9px;
+ text-align: center;
+ position: relative;
+ margin-right: 4px;
+ justify-content: center;
+
+ &:hover{
+ cursor: pointer;
+ }
+ }
+
+ .linkDescriptionPopup-btn-add {
+ background-color: black;
+ color: white;
+ display: inline;
+ right: 0;
+ border-radius: 10px;
+ border: 1px solid black;
+ padding: 3px;
+ font-size: 9px;
+ text-align: center;
+ position: relative;
+ justify-content: center;
+
+ &:hover{
+ cursor: pointer;
+ }
+ }
+ }
+
+
+} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkDescriptionPopup.tsx b/src/client/views/nodes/LinkDescriptionPopup.tsx
new file mode 100644
index 000000000..d8fe47f4e
--- /dev/null
+++ b/src/client/views/nodes/LinkDescriptionPopup.tsx
@@ -0,0 +1,65 @@
+import React = require("react");
+import { observer } from "mobx-react";
+import "./LinkDescriptionPopup.scss";
+import { observable, action } from "mobx";
+import { EditableView } from "../EditableView";
+import { LinkManager } from "../../util/LinkManager";
+import { TaskCompletionBox } from "./TaskCompletedBox";
+
+
+@observer
+export class LinkDescriptionPopup extends React.Component<{}> {
+
+ @observable public static descriptionPopup: boolean = false;
+ @observable public static showDescriptions: string = "ON";
+ @observable public static popupX: number = 700;
+ @observable public static popupY: number = 350;
+ @observable description: string = "";
+ @observable popupRef = React.createRef<HTMLDivElement>();
+
+ @action
+ descriptionChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
+ LinkManager.currentLink && (LinkManager.currentLink.description = e.currentTarget.value);
+ }
+
+ @action
+ onDismiss = () => {
+ LinkDescriptionPopup.descriptionPopup = false;
+ }
+
+ @action
+ onClick = (e: PointerEvent) => {
+ if (this.popupRef && !!!this.popupRef.current?.contains(e.target as any)) {
+ LinkDescriptionPopup.descriptionPopup = false;
+ TaskCompletionBox.taskCompleted = false;
+ }
+ }
+
+ @action
+ componentDidMount() {
+ document.addEventListener("pointerdown", this.onClick);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener("pointerdown", this.onClick);
+ }
+
+ render() {
+ return <div className="linkDescriptionPopup" ref={this.popupRef}
+ style={{
+ left: LinkDescriptionPopup.popupX ? LinkDescriptionPopup.popupX : 700,
+ top: LinkDescriptionPopup.popupY ? LinkDescriptionPopup.popupY : 350,
+ }}>
+ <input className="linkDescriptionPopup-input" onKeyPress={e => e.key === "Enter" && this.onDismiss()}
+ placeholder={"(optional) enter link label..."}
+ onChange={(e) => this.descriptionChanged(e)}>
+ </input>
+ <div className="linkDescriptionPopup-btn">
+ <div className="linkDescriptionPopup-btn-dismiss"
+ onPointerDown={this.onDismiss}> Dismiss </div>
+ <div className="linkDescriptionPopup-btn-add"
+ onPointerDown={this.onDismiss}> Add </div>
+ </div>
+ </div>;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 92b443d3b..ebb916307 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -10,6 +10,12 @@ import { Transform } from "../../util/Transform";
import { ContentFittingDocumentView } from "./ContentFittingDocumentView";
import React = require("react");
import { DocumentView } from './DocumentView';
+import { sortAndDeduplicateDiagnostics } from 'typescript';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { LinkManager } from '../../util/LinkManager';
+import { DocumentLinksButton } from './DocumentLinksButton';
+import { ContextMenu } from '../ContextMenu';
+import { undoBatch } from '../../util/UndoManager';
interface Props {
linkDoc?: Doc;
@@ -24,6 +30,23 @@ export class LinkDocPreview extends React.Component<Props> {
@observable public static LinkInfo: Opt<{ linkDoc?: Doc; addDocTab: (document: Doc, where: string) => boolean, linkSrc: Doc; href?: string; Location: number[] }>;
@observable _targetDoc: Opt<Doc>;
@observable _toolTipText = "";
+ _editRef = React.createRef<HTMLDivElement>();
+
+ @action
+ onContextMenu = (e: React.MouseEvent) => {
+ DocumentLinksButton.EditLink = undefined;
+ LinkDocPreview.LinkInfo = undefined;
+ e.preventDefault();
+ ContextMenu.Instance.addItem({ description: "Follow Default Link", event: () => this.followDefault(), icon: "arrow-right" });
+ ContextMenu.Instance.displayMenu(e.clientX, e.clientY);
+ }
+
+ @action.bound
+ async followDefault() {
+ DocumentLinksButton.EditLink = undefined;
+ LinkDocPreview.LinkInfo = undefined;
+ this._targetDoc ? DocumentManager.Instance.FollowLink(this.props.linkDoc, this._targetDoc, doc => this.props.addDocTab(doc, "onRight"), false) : null;
+ }
componentDidUpdate() { this.updatePreview(); }
componentDidMount() { this.updatePreview(); }
@@ -56,20 +79,23 @@ export class LinkDocPreview extends React.Component<Props> {
this.props.addDocTab(Docs.Create.WebDocument(this.props.href, { title: this.props.href, _width: 200, _height: 400, UseCors: true }), "onRight");
}
}
- width = () => Math.min(350, NumCast(this._targetDoc?.[WidthSym](), 350));
- height = () => Math.min(350, NumCast(this._targetDoc?.[HeightSym](), 350));
+ width = () => Math.min(225, NumCast(this._targetDoc?.[WidthSym](), 225));
+ height = () => Math.min(225, NumCast(this._targetDoc?.[HeightSym](), 225));
@computed get targetDocView() {
return !this._targetDoc ?
- <div style={{ pointerEvents: "all", maxWidth: 350, maxHeight: 250, width: "100%", height: "100%", overflow: "hidden" }}>
+ <div style={{
+ pointerEvents: "all", maxWidth: 225, maxHeight: 225, width: "100%", height: "100%",
+ overflow: "hidden"
+ }}>
<div style={{ width: "100%", height: "100%", textOverflow: "ellipsis", }} onPointerDown={this.pointerDown}>
{this._toolTipText}
</div>
</div> :
+
<ContentFittingDocumentView
Document={this._targetDoc}
LibraryPath={emptyPath}
fitToBox={true}
- backgroundColor={this.props.backgroundColor}
moveDocument={returnFalse}
rootSelected={returnFalse}
ScreenToLocalTransform={Transform.Identity}
@@ -83,15 +109,15 @@ export class LinkDocPreview extends React.Component<Props> {
ContainingCollectionDoc={undefined}
ContainingCollectionView={undefined}
renderDepth={0}
- PanelWidth={this.width}
- PanelHeight={this.height}
+ PanelWidth={() => this.width() - 16} //Math.min(350, NumCast(target._width, 350))}
+ PanelHeight={() => this.height() - 16} //Math.min(250, NumCast(target._height, 250))}
focus={emptyFunction}
whenActiveChanged={returnFalse}
bringToFront={returnFalse}
ContentScaling={returnOne}
NativeWidth={returnZero}
NativeHeight={returnZero}
- />;
+ backgroundColor={this.props.backgroundColor} />;
}
render() {
@@ -99,9 +125,12 @@ export class LinkDocPreview extends React.Component<Props> {
style={{
position: "absolute", left: this.props.location[0],
top: this.props.location[1], width: this.width(), height: this.height(),
- boxShadow: "black 2px 2px 1em"
+ zIndex: 1000,
+ border: "8px solid white", borderRadius: "7px",
+ boxShadow: "3px 3px 1.5px grey",
+ borderBottom: "8px solid white", borderRight: "8px solid white"
}}>
{this.targetDocView}
</div>;
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss
index c6a83b662..f2ab37984 100644
--- a/src/client/views/nodes/PDFBox.scss
+++ b/src/client/views/nodes/PDFBox.scss
@@ -5,24 +5,26 @@
height: 100%;
width: 100%;
overflow: hidden;
- cursor:auto;
+ cursor: auto;
transform-origin: top left;
z-index: 0;
+
.pdfBox-ui {
position: absolute;
- width: 100%;
- height: 100%;
- z-index: 1;
- pointer-events: none;
+ width: 100%;
+ height: 100%;
+ z-index: 1;
+ pointer-events: none;
- .pdfBox-pageNums {
+ .pdfBox-pageNums {
display: flex;
flex-direction: row;
height: 25px;
position: absolute;
left: 5px;
top: 5px;
- .pdfBox-overlayButton-fwd,
+
+ .pdfBox-overlayButton-fwd,
.pdfBox-overlayButton-back {
background: #121721;
height: 25px;
@@ -34,29 +36,29 @@
border-radius: 3px;
pointer-events: all;
}
- }
-
- .pdfBox-overlayButton {
- border-bottom-left-radius: 50%;
- display: flex;
- justify-content: space-evenly;
- align-items: center;
- height: 20px;
- background: none;
- padding: 0;
- position: absolute;
- pointer-events: all;
-
- .pdfBox-overlayButton-arrow {
- width: 0;
- height: 0;
- border-top: 10px solid transparent;
- border-bottom: 10px solid transparent;
- border-right: 15px solid #121721;
- transition: all 0.5s;
- }
-
- .pdfBox-overlayButton-iconCont {
+ }
+
+ .pdfBox-overlayButton {
+ border-bottom-left-radius: 50%;
+ display: flex;
+ justify-content: space-evenly;
+ align-items: center;
+ height: 20px;
+ background: none;
+ padding: 0;
+ position: absolute;
+ pointer-events: all;
+
+ .pdfBox-overlayButton-arrow {
+ width: 0;
+ height: 0;
+ border-top: 10px solid transparent;
+ border-bottom: 10px solid transparent;
+ border-right: 15px solid #121721;
+ transition: all 0.5s;
+ }
+
+ .pdfBox-overlayButton-iconCont {
background: #121721;
height: 20px;
width: 25px;
@@ -66,11 +68,11 @@
justify-content: center;
border-radius: 3px;
pointer-events: all;
- }
+ }
}
- .pdfBox-nextIcon,
- .pdfBox-prevIcon {
+ .pdfBox-nextIcon,
+ .pdfBox-prevIcon {
background: #121721;
height: 20px;
width: 25px;
@@ -81,96 +83,97 @@
border-radius: 3px;
pointer-events: all;
padding: 0px;
- }
-
- .pdfBox-overlayButton:hover {
- background: none;
- }
-
-
- .pdfBox-settingsCont {
- position: absolute;
- right: 0;
- top: 3;
- pointer-events: all;
-
- .pdfBox-settingsButton {
- border-bottom-left-radius: 50%;
- display: flex;
- justify-content: space-evenly;
- align-items: center;
- height: 20px;
- background: none;
- padding: 0;
-
- .pdfBox-settingsButton-arrow {
- width: 0;
- height: 0;
- border-top: 10px solid transparent;
- border-bottom: 10px solid transparent;
- border-right: 15px solid #121721;
- transition: all 0.5s;
- }
-
- .pdfBox-settingsButton-iconCont {
- background: #121721;
- height: 20px;
- width: 25px;
- display: flex;
- justify-content: center;
- align-items: center;
- margin-left: -2px;
- border-radius: 3px;
- }
- }
-
- .pdfBox-settingsButton:hover {
- background: none;
- }
-
- .pdfBox-settingsFlyout {
- position: absolute;
- background: #323232;
- box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
- right: 20px;
- border-radius: 7px;
- padding: 20px;
- display: flex;
- flex-direction: column;
- font-size: 14px;
- transition: all 0.5s;
-
- .pdfBox-settingsFlyout-title {
- color: white;
- }
-
- .pdfBox-settingsFlyout-kvpInput {
- margin-top: 20px;
- display: grid;
- grid-template-columns: 47.5% 5% 47.5%;
- }
- }
- }
-
- .pdfBox-overlayCont {
- position: absolute;
- width: calc(100% - 40px);
- height: 20px;
- background: #121721;
- bottom: 0;
- display: flex;
- justify-content: center;
- align-items: center;
- overflow: hidden;
- transition: left .5s;
- pointer-events: all;
-
- .pdfBox-searchBar {
- width: 70%;
- font-size: 14px;
- }
- }
+ }
+
+ .pdfBox-overlayButton:hover {
+ background: none;
+ }
+
+
+ .pdfBox-settingsCont {
+ position: absolute;
+ right: 0;
+ top: 3;
+ pointer-events: all;
+
+ .pdfBox-settingsButton {
+ border-bottom-left-radius: 50%;
+ display: flex;
+ justify-content: space-evenly;
+ align-items: center;
+ height: 20px;
+ background: none;
+ padding: 0;
+
+ .pdfBox-settingsButton-arrow {
+ width: 0;
+ height: 0;
+ border-top: 10px solid transparent;
+ border-bottom: 10px solid transparent;
+ border-right: 15px solid #121721;
+ transition: all 0.5s;
+ }
+
+ .pdfBox-settingsButton-iconCont {
+ background: #121721;
+ height: 20px;
+ width: 25px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-left: -2px;
+ border-radius: 3px;
+ }
+ }
+
+ .pdfBox-settingsButton:hover {
+ background: none;
+ }
+
+ .pdfBox-settingsFlyout {
+ position: absolute;
+ background: #323232;
+ box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
+ right: 20px;
+ border-radius: 7px;
+ padding: 20px;
+ display: flex;
+ flex-direction: column;
+ font-size: 14px;
+ transition: all 0.5s;
+
+ .pdfBox-settingsFlyout-title {
+ color: white;
+ }
+
+ .pdfBox-settingsFlyout-kvpInput {
+ margin-top: 20px;
+ display: grid;
+ grid-template-columns: 47.5% 5% 47.5%;
+ }
+ }
+ }
+
+ .pdfBox-overlayCont {
+ position: absolute;
+ width: calc(100% - 40px);
+ height: 20px;
+ background: #121721;
+ bottom: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ overflow: hidden;
+ transition: left .5s;
+ pointer-events: all;
+
+ .pdfBox-searchBar {
+ width: 70%;
+ font-size: 14px;
+ }
+ }
}
+
.pdfBox-title-outer {
width: 150%;
height: 100%;
@@ -179,9 +182,9 @@
z-index: 0;
background: lightslategray;
transform-origin: top left;
-
+
.pdfBox-title {
- color:lightgray;
+ color: lightgray;
margin-top: auto;
margin-bottom: auto;
transform-origin: 42% 15%;
@@ -217,4 +220,76 @@
}
}
}
-} \ No newline at end of file
+}
+
+// CSS adjusted for mobile devices
+@media only screen and (max-device-width: 480px) {
+
+ .pdfBox .pdfBox-ui .pdfBox-settingsCont .pdfBox-settingsButton,
+ .pdfBox-interactive .pdfBox-ui .pdfBox-settingsCont .pdfBox-settingsButton {
+ height: 60px;
+
+ .pdfBox-settingsButton-iconCont {
+ height: 60px;
+ width: 75px;
+ font-size: 30px;
+ }
+
+ .pdfBox-settingsButton-arrow {
+ height: 60;
+ border-top: 30px solid transparent;
+ border-bottom: 30px solid transparent;
+ border-right: 30px solid #121721;
+ }
+ }
+
+
+
+ .pdfBox .pdfBox-ui .pdfBox-settingsCont .pdfBox-settingsFlyout,
+ .pdfBox-interactive .pdfBox-ui .pdfBox-settingsCont .pdfBox-settingsFlyout {
+ font-size: 30px;
+ }
+
+
+
+ .pdfBox .pdfBox-ui .pdfBox-overlayCont,
+ .pdfBox-interactive .pdfBox-ui .pdfBox-overlayCont {
+ height: 60px;
+
+ .pdfBox-searchBar {
+ font-size: 40px;
+ }
+ }
+
+ .pdfBox .pdfBox-ui .pdfBox-overlayButton,
+ .pdfBox-interactive .pdfBox-ui .pdfBox-overlayButton {
+ height: 60px;
+
+ .pdfBox-overlayButton-iconCont {
+ height: 60px;
+ width: 75px;
+ font-size: 30;
+ }
+
+ .pdfBox-overlayButton-arrow {
+ border-top: 30px solid transparent;
+ border-bottom: 30px solid transparent;
+ border-right: 30px solid #121721;
+ }
+ }
+
+ button.pdfBox-search {
+ font-size: 30px;
+ width: 50px;
+ height: 50px;
+ }
+
+ .pdfBox .pdfBox-ui .pdfBox-nextIcon,
+ .pdfBox .pdfBox-ui .pdfBox-prevIcon,
+ .pdfBox-interactive .pdfBox-ui .pdfBox-nextIcon,
+ .pdfBox-interactive .pdfBox-ui .pdfBox-prevIcon {
+ height: 50px;
+ width: 50px;
+ font-size: 30px;
+ }
+}
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index eb2a85eeb..323da1233 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -157,7 +157,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum
<div className="pdfBox-overlayCont" key="cont" onPointerDown={(e) => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}>
<button className="pdfBox-overlayButton" title={searchTitle} />
<input className="pdfBox-searchBar" placeholder="Search" ref={this._searchRef} onChange={this.searchStringChanged} onKeyDown={e => e.keyCode === KeyCodes.ENTER && this.search(this._searchString, !e.shiftKey)} />
- <button title="Search" onClick={e => this.search(this._searchString, !e.shiftKey)}>
+ <button className="pdfBox-search" title="Search" onClick={e => this.search(this._searchString, !e.shiftKey)}>
<FontAwesomeIcon icon="search" size="sm" color="white" /></button>
<button className="pdfBox-prevIcon " title="Previous Annotation" onClick={this.prevAnnotation} >
<FontAwesomeIcon style={{ color: "white" }} icon={"arrow-up"} size="lg" />
@@ -189,7 +189,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum
<FontAwesomeIcon style={{ color: "white" }} icon="cog" size="lg" />
</div>
</button>
- <div className="pdfBox-settingsFlyout" style={{ right: `${this._flyout ? 20 : -600}px` }} >
+ <div className="pdfBox-settingsFlyout" style={{ right: `${this._flyout ? 20 : -1000}px` }} >
<div className="pdfBox-settingsFlyout-title">
Annotation View Settings
</div>
@@ -229,7 +229,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum
const classname = "pdfBox" + (this.active() ? "-interactive" : "");
return <div className={classname} style={{
width: !this.props.Document._fitWidth ? this.Document._nativeWidth || 0 : `${100 / this.contentScaling}%`,
- height: !this.props.Document._fitWidth ? this.Document._nativeHeight || 0 : `${100 / this.contentScaling}%`,
+ //height adjusted for mobile (window.screen.width > 600)
+ height: !this.props.Document._fitWidth && (window.screen.width > 600) ? this.Document._nativeHeight || 0 : `${100 / this.contentScaling}%`,
transform: `scale(${this.contentScaling})`
}} >
<div className="pdfBox-title-outer">
@@ -241,7 +242,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum
isChildActive = (outsideReaction?: boolean) => this._isChildActive;
@computed get renderPdfView() {
const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
- return <div className={"pdfBox"} onContextMenu={this.specificContextMenu} style={{ height: this.props.Document._scrollTop && !this.Document._fitWidth ? NumCast(this.Document._height) * this.props.PanelWidth() / NumCast(this.Document._width) : undefined }}>
+ return <div className={"pdfBox"} onContextMenu={this.specificContextMenu} style={{ height: this.props.Document._scrollTop && !this.Document._fitWidth && (window.screen.width > 600) ? NumCast(this.Document._height) * this.props.PanelWidth() / NumCast(this.Document._width) : undefined }}>
<PDFViewer {...this.props} pdf={this._pdf!} url={pdfUrl!.url.pathname} active={this.props.active} loaded={this.loaded}
setPdfViewer={this.setPdfViewer} ContainingCollectionView={this.props.ContainingCollectionView}
renderDepth={this.props.renderDepth} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth}
@@ -267,7 +268,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum
if (!this._pdfjsRequested) {
this._pdfjsRequested = true;
const promise = Pdfjs.getDocument(pdfUrl.url.href).promise;
- promise.then(action(pdf => { this._pdf = pdf; console.log("promise"); }));
+ promise.then(action(pdf => this._pdf = pdf));
}
}
diff --git a/src/client/views/nodes/PresBox.scss b/src/client/views/nodes/PresBox.scss
index d48000e16..9f6af1bde 100644
--- a/src/client/views/nodes/PresBox.scss
+++ b/src/client/views/nodes/PresBox.scss
@@ -16,6 +16,7 @@
height: calc(100% - 25px);
width: 100%;
}
+
.presBox-buttons {
width: 100%;
background: gray;
@@ -24,6 +25,7 @@
display: grid;
grid-column-end: 4;
grid-column-start: 1;
+
.presBox-viewPicker {
height: 25;
position: relative;
@@ -31,10 +33,12 @@
grid-column: 1/2;
min-width: 15px;
}
+
select {
background: #323232;
color: white;
}
+
.presBox-button {
margin-right: 2.5%;
margin-left: 2.5%;
@@ -44,10 +48,12 @@
align-items: center;
background: #323232;
color: white;
+
svg {
margin: auto;
}
}
+
.collectionViewBaseChrome-viewPicker {
min-width: 50;
width: 5%;
@@ -56,17 +62,68 @@
display: inline-block;
}
}
- .presBox-backward, .presBox-forward {
+
+ .presBox-backward,
+ .presBox-forward {
width: 25px;
border-radius: 5px;
- top:50%;
+ top: 50%;
position: absolute;
display: inline-block;
}
+
.presBox-backward {
- left:5;
+ left: 5;
}
+
.presBox-forward {
- right:5;
+ right: 5;
+ }
+}
+
+// CSS adjusted for mobile devices
+@media only screen and (max-device-width: 480px) {
+ .presBox-cont .presBox-buttons {
+ position: absolute;
+ top: 70%;
+ left: 50%;
+ transform: translate(-50%, 0);
+ width: max-content;
+ height: 15%;
+ z-index: 2;
+ align-items: center;
+ background: rgba(0, 0, 0, 0);
+ display: inline-flex;
+
+ .presBox-button {
+ margin-top: 5%;
+ height: 250;
+ width: 300;
+ font-size: 100;
+ display: flex;
+ align-items: center;
+ background: #323232;
+ color: white;
+ }
+
+ .presBox-viewPicker {
+ top: -70;
+ left: 2.5%;
+ height: 50;
+ width: 95%;
+ font-size: 30px;
+ position: absolute;
+ min-width: 50px;
+ }
+ }
+
+ .presBox-cont .presBox-listCont {
+ top: 50;
+ height: calc(100% - 80px);
+ }
+
+ .input,
+ .select {
+ font-size: 100%;
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index 8818d375e..a304ced18 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -297,7 +297,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
(this.layoutDoc.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false)
render() {
- // console.log("render = " + this.layoutDoc.title + " " + this.layoutDoc.presStatus);
// const presOrderedDocs = DocListCast(this.rootDoc.presOrderedDocs);
// if (presOrderedDocs.length != this.childDocs.length || presOrderedDocs.some((pd, i) => pd !== this.childDocs[i])) {
// this.rootDoc.presOrderedDocs = new List<Doc>(this.childDocs.slice());
diff --git a/src/client/views/nodes/RadialMenu.tsx b/src/client/views/nodes/RadialMenu.tsx
index ddfdb67b4..7f0956e51 100644
--- a/src/client/views/nodes/RadialMenu.tsx
+++ b/src/client/views/nodes/RadialMenu.tsx
@@ -1,7 +1,6 @@
import React = require("react");
import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
-import MobileInterface from "../../../mobile/MobileInterface";
import "./RadialMenu.scss";
import { RadialMenuItem, RadialMenuProps } from "./RadialMenuItem";
@@ -26,7 +25,6 @@ export class RadialMenu extends React.Component {
catchTouch = (te: React.TouchEvent) => {
- console.log("caught");
te.stopPropagation();
te.preventDefault();
}
@@ -38,7 +36,6 @@ export class RadialMenu extends React.Component {
this._mouseY = e.clientY;
this.used = false;
document.addEventListener("pointermove", this.onPointerMove);
-
}
@observable
@@ -92,7 +89,6 @@ export class RadialMenu extends React.Component {
@action
componentDidMount = () => {
- console.log(this._pageX);
document.addEventListener("pointerdown", this.onPointerDown);
document.addEventListener("pointerup", this.onPointerUp);
this.previewcircle();
@@ -176,7 +172,6 @@ export class RadialMenu extends React.Component {
@action
openMenu = (x: number, y: number) => {
-
this._pageX = x;
this._pageY = y;
this._shouldDisplay;
@@ -216,7 +211,7 @@ export class RadialMenu extends React.Component {
render() {
- if (!this._display || MobileInterface.Instance) {
+ if (!this._display) {
return null;
}
const style = this._yRelativeToTop ? { left: this._pageX - 130, top: this._pageY - 130 } :
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index f7dee0896..1cd29d795 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -112,7 +112,7 @@ export class ScreenshotBox extends ViewBoxBaseComponent<FieldViewProps, Screensh
return returnedUri;
} catch (e) {
- console.log(e);
+ console.log("ScreenShotBox:" + e);
}
}
@observable _screenCapture = false;
diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx
index 04ac34cc2..1a5edc1d9 100644
--- a/src/client/views/nodes/ScriptingBox.tsx
+++ b/src/client/views/nodes/ScriptingBox.tsx
@@ -32,7 +32,7 @@ const ScriptingDocument = makeInterface(ScriptingSchema, documentSchema);
export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps, ScriptingDocument>(ScriptingDocument) {
private dropDisposer?: DragManager.DragDropDisposer;
- protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer | undefined;
+ protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer | undefined;
public static LayoutString(fieldStr: string) { return FieldView.LayoutString(ScriptingBox, fieldStr); }
private _overlayDisposer?: () => void;
private _caretPos = 0;
@@ -109,7 +109,6 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps, Sc
@action
resetSuggestionPos(caret: any) {
if (!this._suggestionRef.current || !this._scriptTextRef.current) return;
- console.log('(top, left, height) = (%s, %s, %s)', caret.top, caret.left, caret.height);
const suggestionWidth = this._suggestionRef.current.offsetWidth;
const scriptWidth = this._scriptTextRef.current.offsetWidth;
const top = caret.top;
diff --git a/src/client/views/nodes/SliderBox.tsx b/src/client/views/nodes/SliderBox.tsx
index 9a1aefba9..45cdfc5ad 100644
--- a/src/client/views/nodes/SliderBox.tsx
+++ b/src/client/views/nodes/SliderBox.tsx
@@ -56,7 +56,7 @@ export class SliderBox extends ViewBoxBaseComponent<FieldViewProps, SliderDocume
style={{ boxShadow: this.layoutDoc.opacity === 0 ? undefined : StrCast(this.layoutDoc.boxShadow, "") }}>
<div className="sliderBox-mainButton" onContextMenu={this.specificContextMenu} style={{
background: StrCast(this.layoutDoc.backgroundColor), color: StrCast(this.layoutDoc.color, "black"),
- fontSize: NumCast(this.layoutDoc._fontSize), letterSpacing: StrCast(this.layoutDoc.letterSpacing)
+ fontSize: StrCast(this.layoutDoc._fontSize), letterSpacing: StrCast(this.layoutDoc.letterSpacing)
}} >
<Slider
mode={2}
diff --git a/src/client/views/nodes/TaskCompletedBox.scss b/src/client/views/nodes/TaskCompletedBox.scss
new file mode 100644
index 000000000..80b750b39
--- /dev/null
+++ b/src/client/views/nodes/TaskCompletedBox.scss
@@ -0,0 +1,20 @@
+.taskCompletedBox-fade {
+ border: 1px solid rgb(100, 100, 100);
+
+ width: auto;
+ position: absolute;
+
+ height: auto;
+ z-index: 10000;
+ border-radius: 13px;
+ font-size: 13px;
+ white-space: nowrap;
+
+ color: rgb(100, 100, 100);
+ background-color: rgba(250, 250, 250, 0.85);
+ padding-top: 6.5px;
+ padding-bottom: 6.5px;
+ font-weight: bold;
+ padding-left: 9px;
+ padding-right: 9px;
+} \ No newline at end of file
diff --git a/src/client/views/nodes/TaskCompletedBox.tsx b/src/client/views/nodes/TaskCompletedBox.tsx
new file mode 100644
index 000000000..89602f219
--- /dev/null
+++ b/src/client/views/nodes/TaskCompletedBox.tsx
@@ -0,0 +1,32 @@
+import React = require("react");
+import { observer } from "mobx-react";
+import { documentSchema } from "../../../fields/documentSchemas";
+import { makeInterface } from "../../../fields/Schema";
+import "./TaskCompletedBox.scss";
+import { observable, action } from "mobx";
+import { Fade } from "@material-ui/core";
+
+
+@observer
+export class TaskCompletionBox extends React.Component<{}> {
+
+ @observable public static taskCompleted: boolean = false;
+ @observable public static popupX: number = 500;
+ @observable public static popupY: number = 150;
+ @observable public static textDisplayed: string;
+
+ @action
+ public static toggleTaskCompleted = () => {
+ TaskCompletionBox.taskCompleted = !TaskCompletionBox.taskCompleted;
+ }
+
+ render() {
+ return <Fade in={TaskCompletionBox.taskCompleted}>
+ <div className="taskCompletedBox-fade"
+ style={{
+ left: TaskCompletionBox.popupX ? TaskCompletionBox.popupX : 500,
+ top: TaskCompletionBox.popupY ? TaskCompletionBox.popupY : 150,
+ }}>{TaskCompletionBox.textDisplayed}</div>
+ </Fade>;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index a5c6c4a48..ee92e517c 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -205,7 +205,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
return returnedUri;
} catch (e) {
- console.log(e);
+ console.log("VideoBox :" + e);
}
}
@observable _screenCapture = false;
diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss
index 4623444b9..875142169 100644
--- a/src/client/views/nodes/WebBox.scss
+++ b/src/client/views/nodes/WebBox.scss
@@ -1,135 +1,155 @@
@import "../globalCssVariables.scss";
-.webBox-container, .webBox-container-dragging {
- transform-origin: top left;
- width: 100%;
- height: 100%;
+.webBox {
+ height:100%;
+ position: relative;
+ display: flex;
- .webBox-htmlSpan {
+ .pdfViewerDash-dragAnnotationBox {
+ position:absolute;
+ background-color: transparent;
+ opacity: 0.1;
+ }
+ .webBox-annotationLayer {
position: absolute;
+ transform-origin: left top;
top: 0;
- left: 0;
- }
- .webBox-cont {
+ width: 100%;
pointer-events: none;
+ mix-blend-mode: multiply; // bcz: makes text fuzzy!
}
- .webBox-cont, .webBox-cont-interactive {
- padding: 0vw;
+ .webBox-annotationBox {
position: absolute;
- top: 0;
- left: 0;
+ background-color: rgba(245, 230, 95, 0.616);
+ }
+ .webBox-container {
+ transform-origin: top left;
width: 100%;
height: 100%;
- transform-origin: top left;
- overflow: auto;
- .webBox-iframe {
+
+ .webBox-htmlSpan {
+ position: absolute;
+ top: 0;
+ left: 0;
+ }
+ .webBox-cont {
+ pointer-events: none;
+ }
+ .webBox-cont, .webBox-cont-interactive {
+ padding: 0vw;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ transform-origin: top left;
+ overflow: auto;
+ .webBox-iframe {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ top:0;
+ }
+ }
+ .webBox-cont-interactive {
+ span {
+ user-select: text !important;
+ }
+ }
+ .webBox-outerContent {
width: 100%;
height: 100%;
position: absolute;
- top:0;
+ top: 0;
+ left: 0;
+ overflow: auto;
}
- }
- .webBox-cont-interactive {
- span {
- user-select: text !important;
+ div.webBox-outerContent::-webkit-scrollbar-thumb {
+ display:none;
}
}
- .webBox-outerContent {
+
+
+ .webBox-overlay {
width: 100%;
height: 100%;
position: absolute;
- top: 0;
- left: 0;
- overflow: auto;
- .webBox-innerContent {
- width:100%;
- }
- }
- div.webBox-outerContent::-webkit-scrollbar-thumb {
- display:none;
}
-}
-
-
-.webBox-overlay {
- width: 100%;
- height: 100%;
- position: absolute;
-}
-.webBox-buttons {
- margin-left: 44;
- background:lightGray;
- width: 100%;
-}
-.webBox-freeze {
- display: flex;
- align-items: center;
- justify-content: center;
- margin-right: 5px;
- width: 30px;
-}
-
-.webBox-urlEditor {
- position: relative;
- opacity: 0.9;
- z-index: 9001;
- transition: top .5s;
-
- .urlEditor {
- display: grid;
- grid-template-columns: 1fr auto;
- padding-bottom: 10px;
- overflow: hidden;
-
- .editorBase {
- display: flex;
-
- .editor-collapse {
- transition: all .5s, opacity 0.3s;
- position: absolute;
- width: 40px;
- transform-origin: top left;
- }
+ .webBox-buttons {
+ margin-left: 44;
+ background:lightGray;
+ width: 100%;
+ }
+ .webBox-freeze {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 5px;
+ width: 30px;
+ }
- .switchToText {
- color: $main-accent;
+ .webBox-urlEditor {
+ position: relative;
+ opacity: 0.9;
+ z-index: 9001;
+ transition: top .5s;
+
+ .urlEditor {
+ display: grid;
+ grid-template-columns: 1fr auto;
+ padding-bottom: 10px;
+ overflow: hidden;
+
+ .editorBase {
+ display: flex;
+
+ .editor-collapse {
+ transition: all .5s, opacity 0.3s;
+ position: absolute;
+ width: 40px;
+ transform-origin: top left;
+ }
+
+ .switchToText {
+ color: $main-accent;
+ }
+
+ .switchToText:hover {
+ color: $dark-color;
+ }
}
- .switchToText:hover {
- color: $dark-color;
+ button:hover {
+ transform: scale(1);
}
}
+ }
- button:hover {
- transform: scale(1);
- }
+ .webpage-urlInput {
+ padding: 12px 10px 11px 10px;
+ border: 0px;
+ color: grey;
+ letter-spacing: 2px;
+ outline-color: black;
+ background: rgb(238, 238, 238);
+ width: 100%;
+ margin-right: 10px;
+ height: 100%;
}
-}
-
-.webpage-urlInput {
- padding: 12px 10px 11px 10px;
- border: 0px;
- color: grey;
- letter-spacing: 2px;
- outline-color: black;
- background: rgb(238, 238, 238);
- width: 100%;
- margin-right: 10px;
- height: 100%;
-}
-
-.touch-iframe-overlay {
- width: 100%;
- height: 100%;
- position: absolute;
-
- .indicator {
+
+ .touch-iframe-overlay {
+ width: 100%;
+ height: 100%;
position: absolute;
- &.active {
- background-color: rgba(0, 0, 0, 0.1);
+ .indicator {
+ position: absolute;
+
+ &.active {
+ background-color: rgba(0, 0, 0, 0.1);
+ }
}
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 05355caba..d30f1499e 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -1,53 +1,70 @@
-import { library } from "@fortawesome/fontawesome-svg-core";
-import { faStickyNote, faPen, faMousePointer } from '@fortawesome/free-solid-svg-icons';
-import { action, computed, observable, trace, IReactionDisposer, reaction, runInAction } from "mobx";
+import { faMousePointer, faPen, faStickyNote } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
-import { Doc, FieldResult, DocListCast } from "../../../fields/Doc";
+import { Dictionary } from "typescript-collections";
+import * as WebRequest from 'web-request';
+import { Doc, DocListCast, Opt } from "../../../fields/Doc";
import { documentSchema } from "../../../fields/documentSchemas";
+import { Id } from "../../../fields/FieldSymbols";
import { HtmlField } from "../../../fields/HtmlField";
import { InkTool } from "../../../fields/InkField";
-import { makeInterface, listSpec } from "../../../fields/Schema";
-import { Cast, NumCast, BoolCast, StrCast } from "../../../fields/Types";
+import { List } from "../../../fields/List";
+import { listSpec, makeInterface } from "../../../fields/Schema";
+import { Cast, NumCast, StrCast } from "../../../fields/Types";
import { WebField } from "../../../fields/URLField";
-import { Utils, returnOne, emptyFunction, returnZero } from "../../../Utils";
-import { Docs } from "../../documents/Documents";
+import { TraceMobx } from "../../../fields/util";
+import { addStyleSheet, clearStyleSheetRules, emptyFunction, returnOne, returnZero, Utils, returnTrue } from "../../../Utils";
+import { Docs, DocUtils } from "../../documents/Documents";
import { DragManager } from "../../util/DragManager";
import { ImageUtils } from "../../util/Import & Export/ImageUtils";
+import { undoBatch } from "../../util/UndoManager";
+import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
+import { ContextMenu } from "../ContextMenu";
+import { ContextMenuProps } from "../ContextMenuItem";
import { ViewBoxAnnotatableComponent } from "../DocComponent";
import { DocumentDecorations } from "../DocumentDecorations";
+import Annotation from "../pdf/Annotation";
+import PDFMenu from "../pdf/PDFMenu";
+import { PdfViewerMarquee } from "../pdf/PDFViewer";
import { FieldView, FieldViewProps } from './FieldView';
import "./WebBox.scss";
+import "../pdf/PDFViewer.scss";
import React = require("react");
-import * as WebRequest from 'web-request';
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
-import { ContextMenu } from "../ContextMenu";
-import { ContextMenuProps } from "../ContextMenuItem";
-import { undoBatch } from "../../util/UndoManager";
-import { List } from "../../../fields/List";
const htmlToText = require("html-to-text");
-library.add(faStickyNote);
-
type WebDocument = makeInterface<[typeof documentSchema]>;
const WebDocument = makeInterface(documentSchema);
@observer
export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocument>(WebDocument) {
+ private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
+ static _annotationStyle: any = addStyleSheet();
+ private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
+ private _startX: number = 0;
+ private _startY: number = 0;
+ @observable private _marqueeX: number = 0;
+ @observable private _marqueeY: number = 0;
+ @observable private _marqueeWidth: number = 0;
+ @observable private _marqueeHeight: number = 0;
+ @observable private _marqueeing: boolean = false;
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(WebBox, fieldKey); }
get _collapsed() { return StrCast(this.layoutDoc._chromeStatus) !== "enabled"; }
set _collapsed(value) { this.layoutDoc._chromeStatus = !value ? "enabled" : "disabled"; }
@observable private _url: string = "hello";
@observable private _pressX: number = 0;
@observable private _pressY: number = 0;
+ @observable private _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>();
+ private _selectionReactionDisposer?: IReactionDisposer;
+ private _scrollReactionDisposer?: IReactionDisposer;
+ private _moveReactionDisposer?: IReactionDisposer;
private _keyInput = React.createRef<HTMLInputElement>();
private _longPressSecondsHack?: NodeJS.Timeout;
private _outerRef = React.createRef<HTMLDivElement>();
private _iframeRef = React.createRef<HTMLIFrameElement>();
private _iframeIndicatorRef = React.createRef<HTMLDivElement>();
private _iframeDragRef = React.createRef<HTMLDivElement>();
- private _reactionDisposer?: IReactionDisposer;
private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void);
iframeLoaded = action((e: any) => {
@@ -60,21 +77,24 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
iframe.contentDocument.children[0].scrollTop = NumCast(this.layoutDoc._scrollTop);
iframe.contentDocument.children[0].scrollLeft = NumCast(this.layoutDoc._scrollLeft);
}
- this._reactionDisposer?.();
- this._reactionDisposer = reaction(() => ({ y: this.layoutDoc._scrollY, x: this.layoutDoc._scrollX }),
- ({ x, y }) => {
- if (y !== undefined) {
- this._outerRef.current!.scrollTop = y;
- this.layoutDoc._scrollY = undefined;
- }
- if (x !== undefined) {
- this._outerRef.current!.scrollLeft = x;
- this.layoutDoc.scrollX = undefined;
- }
- },
+ this._scrollReactionDisposer?.();
+ this._scrollReactionDisposer = reaction(() => ({ y: this.layoutDoc._scrollY, x: this.layoutDoc._scrollX }),
+ ({ x, y }) => this.updateScroll(x, y),
{ fireImmediately: true }
);
});
+
+ updateScroll = (x: Opt<number>, y: Opt<number>) => {
+ if (y !== undefined) {
+ this._outerRef.current!.scrollTop = y;
+ this.layoutDoc._scrollY = undefined;
+ }
+ if (x !== undefined) {
+ this._outerRef.current!.scrollLeft = x;
+ this.layoutDoc.scrollX = undefined;
+ }
+ }
+
setPreviewCursor = (func?: (x: number, y: number, drag: boolean) => void) => this._setPreviewCursor = func;
iframedown = (e: PointerEvent) => {
this._setPreviewCursor?.(e.screenX, e.screenY, false);
@@ -89,6 +109,19 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
const urlField = Cast(this.dataDoc[this.props.fieldKey], WebField);
runInAction(() => this._url = urlField?.url.toString() || "");
+ this._moveReactionDisposer = reaction(() => this.layoutDoc.x || this.layoutDoc.y,
+ () => this.updateScroll(this.layoutDoc._scrollLeft, this.layoutDoc._scrollTop));
+
+ this._selectionReactionDisposer = reaction(() => this.props.isSelected(),
+ selected => {
+ if (!selected) {
+ this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove()));
+ this._savedAnnotations.keys().forEach(k => this._savedAnnotations.setValue(k, []));
+ PDFMenu.Instance.fadeOut(true);
+ }
+ },
+ { fireImmediately: true });
+
document.addEventListener("pointerup", this.onLongPressUp);
document.addEventListener("pointermove", this.onLongPressMove);
const field = Cast(this.rootDoc[this.props.fieldKey], WebField);
@@ -112,7 +145,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
}
componentWillUnmount() {
- this._reactionDisposer?.();
+ this._moveReactionDisposer?.();
+ this._selectionReactionDisposer?.();
+ this._scrollReactionDisposer?.();
document.removeEventListener("pointerup", this.onLongPressUp);
document.removeEventListener("pointermove", this.onLongPressMove);
this._iframeRef.current?.contentDocument?.removeEventListener('pointerdown', this.iframedown);
@@ -189,7 +224,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
this.dataDoc[this.fieldKey] = new WebField(URLy);
this.dataDoc[this.annotationKey] = new List<Doc>([]);
} catch (e) {
- console.log("Error in URL :" + this._url);
+ console.log("WebBox URL error:" + this._url);
}
}
@@ -267,11 +302,14 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
);
}
+
@action
toggleCollapse = () => {
this._collapsed = !this._collapsed;
}
+
+
_ignore = 0;
onPreWheel = (e: React.WheelEvent) => {
this._ignore = e.timeStamp;
@@ -284,6 +322,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
e.stopPropagation();
}
}
+
onPostWheel = (e: React.WheelEvent) => {
if (this._ignore !== e.timeStamp) {
e.stopPropagation();
@@ -399,7 +438,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
specificContextMenu = (e: React.MouseEvent): void => {
const cm = ContextMenu.Instance;
const funcs: ContextMenuProps[] = [];
- funcs.push({ description: (!this.layoutDoc._nativeWidth || !this.layoutDoc._nativeHeight ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" });
+ funcs.push({ description: (this.layoutDoc.UseCors ? "Don't Use" : "Use") + " Cors", event: () => this.layoutDoc.UseCors = !this.layoutDoc.UseCors, icon: "snowflake" });
cm.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" });
}
@@ -430,6 +469,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
return (<>
<div className={"webBox-cont" + (this.props.isSelected() && Doc.GetSelectedTool() === InkTool.None && !decInteracting ? "-interactive" : "")}
+ style={{ width: Number.isFinite(this.props.ContentScaling()) ? `${Math.max(100, 100 / this.props.ContentScaling())}% ` : "100%" }}
onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}>
{view}
</div>;
@@ -444,60 +484,259 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
{this.urlEditor()}
</>);
}
+
+
+
+ @computed get allAnnotations() { return DocListCast(this.dataDoc[this.props.fieldKey + "-annotations"]); }
+ @computed get nonDocAnnotations() { return this.allAnnotations.filter(a => a.annotations); }
+
+ @undoBatch
+ @action
+ makeAnnotationDocument = (color: string): Opt<Doc> => {
+ if (this._savedAnnotations.size() === 0) return undefined;
+ const anno = this._savedAnnotations.values()[0][0];
+ const annoDoc = Docs.Create.FreeformDocument([], { backgroundColor: color, annotationOn: this.props.Document, title: "Annotation on " + this.Document.title });
+ if (anno.style.left) annoDoc.x = parseInt(anno.style.left);
+ if (anno.style.top) annoDoc.y = NumCast(this.layoutDoc._scrollTop) + parseInt(anno.style.top);
+ if (anno.style.height) annoDoc._height = parseInt(anno.style.height);
+ if (anno.style.width) annoDoc._width = parseInt(anno.style.width);
+ anno.remove();
+ this._savedAnnotations.clear();
+ return annoDoc;
+ }
+ @computed get annotationLayer() {
+ TraceMobx();
+ return <div className="webBox-annotationLayer" style={{ height: NumCast(this.Document._nativeHeight) }} ref={this._annotationLayer}>
+ {this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map(anno =>
+ <Annotation {...this.props} focus={this.props.focus} dataDoc={this.dataDoc} fieldKey={this.props.fieldKey} anno={anno} key={`${anno[Id]}-annotation`} />)
+ }
+ </div>;
+ }
+ @action
+ createAnnotation = (div: HTMLDivElement, page: number) => {
+ if (this._annotationLayer.current) {
+ if (div.style.top) {
+ div.style.top = (parseInt(div.style.top)).toString();
+ }
+ this._annotationLayer.current.append(div);
+ div.style.backgroundColor = "#ACCEF7";
+ div.style.opacity = "0.5";
+ const savedPage = this._savedAnnotations.getValue(page);
+ if (savedPage) {
+ savedPage.push(div);
+ this._savedAnnotations.setValue(page, savedPage);
+ }
+ else {
+ this._savedAnnotations.setValue(page, [div]);
+ }
+ }
+ }
+
+ @action
+ highlight = (color: string) => {
+ // creates annotation documents for current highlights
+ const annotationDoc = this.makeAnnotationDocument(color);
+ annotationDoc && Doc.AddDocToList(this.props.Document, this.annotationKey, annotationDoc);
+ return annotationDoc;
+ }
+ /**
+ * This is temporary for creating annotations from highlights. It will
+ * start a drag event and create or put the necessary info into the drag event.
+ */
+ @action
+ startDrag = async (e: PointerEvent, ele: HTMLElement) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const clipDoc = Doc.MakeAlias(this.dataDoc);
+ clipDoc._fitWidth = true;
+ clipDoc._width = this.marqueeWidth();
+ clipDoc._height = this.marqueeHeight();
+ clipDoc._scrollTop = this.marqueeY();
+ const targetDoc = Docs.Create.TextDocument("", { _width: 125, _height: 125, title: "Note linked to " + this.props.Document.title });
+ Doc.GetProto(targetDoc).data = new List<Doc>([clipDoc]);
+ clipDoc.rootDocument = targetDoc;
+ targetDoc.layoutKey = "layout";
+ const annotationDoc = this.highlight("rgba(146, 245, 95, 0.467)"); // yellowish highlight color when dragging out a text selection
+ if (annotationDoc) {
+ DragManager.StartPdfAnnoDrag([ele], new DragManager.PdfAnnoDragData(this.props.Document, annotationDoc, targetDoc), e.pageX, e.pageY, {
+ dragComplete: e => {
+ if (!e.aborted && e.annoDragData && !e.annoDragData.linkedToDoc) {
+ DocUtils.MakeLink({ doc: annotationDoc }, { doc: e.annoDragData.dropDocument }, "Annotation");
+ annotationDoc.isLinkButton = true;
+ }
+ }
+ });
+ }
+ }
+ @action
+ onMarqueeDown = (e: React.PointerEvent) => {
+ this._marqueeing = false;
+ if (!e.altKey && e.button === 0 && this.active(true)) {
+ // clear out old marquees and initialize menu for new selection
+ PDFMenu.Instance.StartDrag = this.startDrag;
+ PDFMenu.Instance.Highlight = this.highlight;
+ PDFMenu.Instance.Status = "pdf";
+ PDFMenu.Instance.fadeOut(true);
+ this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove()));
+ this._savedAnnotations.keys().forEach(k => this._savedAnnotations.setValue(k, []));
+ if ((e.target as any)?.parentElement.className === "textLayer") {
+ // start selecting text if mouse down on textLayer spans
+ }
+ else if (this._mainCont.current) {
+ // set marquee x and y positions to the spatially transformed position
+ const boundingRect = this._mainCont.current.getBoundingClientRect();
+ const boundingHeight = (this.Document._nativeHeight || 1) / (this.Document._nativeWidth || 1) * boundingRect.width;
+ this._startX = (e.clientX - boundingRect.left) / boundingRect.width * (this.Document._nativeWidth || 1);
+ this._startY = (e.clientY - boundingRect.top) / boundingHeight * (this.Document._nativeHeight || 1);
+ this._marqueeHeight = this._marqueeWidth = 0;
+ this._marqueeing = true;
+ }
+ document.removeEventListener("pointermove", this.onSelectMove);
+ document.addEventListener("pointermove", this.onSelectMove);
+ document.removeEventListener("pointerup", this.onSelectEnd);
+ document.addEventListener("pointerup", this.onSelectEnd);
+ }
+ }
+ @action
+ onSelectMove = (e: PointerEvent): void => {
+ if (this._marqueeing && this._mainCont.current) {
+ // transform positions and find the width and height to set the marquee to
+ const boundingRect = this._mainCont.current.getBoundingClientRect();
+ const boundingHeight = (this.Document._nativeHeight || 1) / (this.Document._nativeWidth || 1) * boundingRect.width;
+ const curX = (e.clientX - boundingRect.left) / boundingRect.width * (this.Document._nativeWidth || 1);
+ const curY = (e.clientY - boundingRect.top) / boundingHeight * (this.Document._nativeHeight || 1);
+ this._marqueeWidth = curX - this._startX;
+ this._marqueeHeight = curY - this._startY;
+ this._marqueeX = Math.min(this._startX, this._startX + this._marqueeWidth);
+ this._marqueeY = Math.min(this._startY, this._startY + this._marqueeHeight);
+ this._marqueeWidth = Math.abs(this._marqueeWidth);
+ this._marqueeHeight = Math.abs(this._marqueeHeight);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ else if (e.target && (e.target as any).parentElement === this._mainCont.current) {
+ e.stopPropagation();
+ }
+ }
+
+ @action
+ onSelectEnd = (e: PointerEvent): void => {
+ clearStyleSheetRules(WebBox._annotationStyle);
+ this._savedAnnotations.clear();
+ if (this._marqueeWidth > 10 || this._marqueeHeight > 10) {
+ const marquees = this._mainCont.current!.getElementsByClassName("pdfViewerDash-dragAnnotationBox");
+ if (marquees?.length) { // copy the marquee and convert it to a permanent annotation.
+ const style = (marquees[0] as HTMLDivElement).style;
+ const copy = document.createElement("div");
+ copy.style.left = style.left;
+ copy.style.top = style.top;
+ copy.style.width = style.width;
+ copy.style.height = style.height;
+ copy.style.border = style.border;
+ copy.style.opacity = style.opacity;
+ (copy as any).marqueeing = true;
+ copy.className = "webBox-annotationBox";
+ this.createAnnotation(copy, 0);
+ }
+
+ if (!e.ctrlKey) {
+ PDFMenu.Instance.Marquee = { left: this._marqueeX, top: this._marqueeY, width: this._marqueeWidth, height: this._marqueeHeight };
+ }
+ PDFMenu.Instance.jumpTo(e.clientX, e.clientY);
+ }
+ //this._marqueeing = false;
+
+ if (PDFMenu.Instance.Highlighting) {// when highlighter has been toggled when menu is pinned, we auto-highlight immediately on mouse up
+ this.highlight("rgba(245, 230, 95, 0.616)"); // yellowish highlight color for highlighted text (should match PDFMenu's highlight color)
+ }
+ else {
+ PDFMenu.Instance.StartDrag = this.startDrag;
+ PDFMenu.Instance.Highlight = this.highlight;
+ }
+ document.removeEventListener("pointermove", this.onSelectMove);
+ document.removeEventListener("pointerup", this.onSelectEnd);
+ }
+ marqueeWidth = () => this._marqueeWidth;
+ marqueeHeight = () => this._marqueeHeight;
+ marqueeX = () => this._marqueeX;
+ marqueeY = () => this._marqueeY;
+ marqueeing = () => this._marqueeing;
+ visibleHeiht = () => {
+ if (this._mainCont.current) {
+ const boundingRect = this._mainCont.current.getBoundingClientRect();
+ const scalin = (this.Document._nativeWidth || 0) / boundingRect.width;
+ return Math.min(boundingRect.height * scalin, this.props.PanelHeight() * scalin);
+ }
+ return this.props.PanelHeight();
+ }
scrollXf = () => this.props.ScreenToLocalTransform().translate(NumCast(this.layoutDoc._scrollLeft), NumCast(this.layoutDoc._scrollTop));
render() {
- return (<div className={`webBox-container`}
- style={{
- transform: `scale(${this.props.ContentScaling()})`,
- width: Number.isFinite(this.props.ContentScaling()) ? `${100 / this.props.ContentScaling()}%` : "100%",
- height: Number.isFinite(this.props.ContentScaling()) ? `${100 / this.props.ContentScaling()}%` : "100%",
- pointerEvents: this.layoutDoc.isBackground ? "none" : undefined
- }}
- onContextMenu={this.specificContextMenu}>
- <base target="_blank" />
- {this.content}
- <div className={"webBox-outerContent"} ref={this._outerRef}
- style={{ pointerEvents: this.layoutDoc.isAnnotating && !this.layoutDoc.isBackground ? "all" : "none" }}
- onWheel={e => e.stopPropagation()}
- onScroll={e => {
- const iframe = this._iframeRef?.current?.contentDocument;
- const outerFrame = this._outerRef.current;
- if (iframe && outerFrame) {
- if (iframe.children[0].scrollTop !== outerFrame.scrollTop) {
- iframe.children[0].scrollTop = outerFrame.scrollTop;
- }
- if (iframe.children[0].scrollLeft !== outerFrame.scrollLeft) {
- iframe.children[0].scrollLeft = outerFrame.scrollLeft;
+ return (<div className="webBox" ref={this._mainCont} >
+ <div className={`webBox-container`}
+ style={{
+ position: undefined,
+ transform: `scale(${this.props.ContentScaling()})`,
+ width: Number.isFinite(this.props.ContentScaling()) ? `${100 / this.props.ContentScaling()}% ` : "100%",
+ height: Number.isFinite(this.props.ContentScaling()) ? `${100 / this.props.ContentScaling()}% ` : "100%",
+ pointerEvents: this.layoutDoc.isBackground ? "none" : undefined
+ }}
+ onContextMenu={this.specificContextMenu}>
+ <base target="_blank" />
+ {this.content}
+ <div className={"webBox-outerContent"} ref={this._outerRef}
+ style={{
+ width: Number.isFinite(this.props.ContentScaling()) ? `${Math.max(100, 100 / this.props.ContentScaling())}% ` : "100%",
+ pointerEvents: this.layoutDoc.isAnnotating && !this.layoutDoc.isBackground ? "all" : "none"
+ }}
+ onWheel={e => e.stopPropagation()}
+ onPointerDown={this.onMarqueeDown}
+ onScroll={e => {
+ const iframe = this._iframeRef?.current?.contentDocument;
+ const outerFrame = this._outerRef.current;
+ if (iframe && outerFrame) {
+ if (iframe.children[0].scrollTop !== outerFrame.scrollTop) {
+ iframe.children[0].scrollTop = outerFrame.scrollTop;
+ }
+ if (iframe.children[0].scrollLeft !== outerFrame.scrollLeft) {
+ iframe.children[0].scrollLeft = outerFrame.scrollLeft;
+ }
}
- }
- //this._outerRef.current!.scrollTop !== this._scrollTop && (this._outerRef.current!.scrollTop = this._scrollTop)
- }}>
- <div className={"webBox-innerContent"} style={{ height: NumCast(this.layoutDoc.scrollHeight), width: 4000 }}>
- <CollectionFreeFormView {...this.props}
- PanelHeight={this.props.PanelHeight}
- PanelWidth={this.props.PanelWidth}
- annotationsKey={this.annotationKey}
- NativeHeight={returnZero}
- NativeWidth={returnZero}
- focus={this.props.focus}
- setPreviewCursor={this.setPreviewCursor}
- isSelected={this.props.isSelected}
- isAnnotationOverlay={true}
- select={emptyFunction}
- active={this.active}
- ContentScaling={returnOne}
- whenActiveChanged={this.whenActiveChanged}
- removeDocument={this.removeDocument}
- moveDocument={this.moveDocument}
- addDocument={this.addDocument}
- CollectionView={undefined}
- ScreenToLocalTransform={this.scrollXf}
- renderDepth={this.props.renderDepth + 1}
- docFilters={this.props.docFilters}
- ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
- </CollectionFreeFormView>
+ //this._outerRef.current!.scrollTop !== this._scrollTop && (this._outerRef.current!.scrollTop = this._scrollTop)
+ }}>
+ <div className={"webBox-innerContent"} style={{
+ height: NumCast(this.layoutDoc.scrollHeight),
+ pointerEvents: this.layoutDoc.isBackground ? "none" : undefined
+ }}>
+ <CollectionFreeFormView {...this.props}
+ PanelHeight={this.props.PanelHeight}
+ PanelWidth={this.props.PanelWidth}
+ annotationsKey={this.annotationKey}
+ NativeHeight={returnZero}
+ NativeWidth={returnZero}
+ VisibleHeight={this.visibleHeiht}
+ focus={this.props.focus}
+ setPreviewCursor={this.setPreviewCursor}
+ isSelected={this.props.isSelected}
+ isAnnotationOverlay={true}
+ select={emptyFunction}
+ active={this.active}
+ ContentScaling={returnOne}
+ whenActiveChanged={this.whenActiveChanged}
+ removeDocument={this.removeDocument}
+ moveDocument={this.moveDocument}
+ addDocument={this.addDocument}
+ CollectionView={undefined}
+ ScreenToLocalTransform={this.scrollXf}
+ renderDepth={this.props.renderDepth + 1}
+ docFilters={this.props.docFilters}
+ ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
+ </CollectionFreeFormView>
+ </div>
</div>
- </div>
- </div >);
+ {this.annotationLayer}
+ <PdfViewerMarquee isMarqueeing={this.marqueeing} width={this.marqueeWidth} height={this.marqueeHeight} x={this.marqueeX} y={this.marqueeY} />
+ </div >
+ </div>);
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx
index 5c3f3dcc9..212da3f3d 100644
--- a/src/client/views/nodes/formattedText/DashDocView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocView.tsx
@@ -209,7 +209,7 @@ export class DashDocView extends React.Component<IDashDocView> {
try { // bcz: an exception will be thrown if two aliases are open at the same time when a doc view comment is made
view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: dashDoc._width + "px", height: dashDoc._height + "px" }));
} catch (e) {
- console.log(e);
+ console.log("DashDocView:" + e);
}
}
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index 8718bf329..958a37568 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -205,9 +205,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
{this._fieldKey}
</span>}
- {/* <div className="dashFieldView-fieldSpan"> */}
- {this.fieldValueContent}
- {/* </div> */}
+ {this.props.fieldKey.startsWith("#") ? (null) : this.fieldValueContent}
{!this._showEnumerables ? (null) : <div className="dashFieldView-enumerables" onPointerDown={this.onPointerDownEnumerables} />}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss
index 678494b27..afdd8fea2 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss
@@ -37,6 +37,7 @@
position: absolute;
}
}
+
.formattedTextBox-outer {
position: relative;
overflow: auto;
@@ -72,7 +73,7 @@
.collectionfreeformview-container {
position: relative;
}
-
+
>.formattedTextBox-sidebar-handle {
right: unset;
left: -5;
@@ -95,7 +96,7 @@
.formattedTextBox-inner-rounded, .formattedTextBox-inner-rounded-selected,
.formattedTextBox-inner, .formattedTextBox-inner-selected {
height: 100%;
- white-space: pre-wrap;
+ white-space: pre-wrap;
.ProseMirror:hover {
background: rgba(200,200,200,0.8);
}
@@ -262,19 +263,19 @@ footnote::after {
border:unset;
padding:0px;
}
-
+
.prosemirror-links a {
float: left;
color: white;
text-decoration: none;
border-radius: 3px;
}
-
+
.prosemirror-links a:hover {
background-color: #eee;
color: black;
}
-
+
.prosemirror-anchor:hover .prosemirror-links {
display: grid;
}
@@ -302,27 +303,26 @@ footnote::after {
font-family: inherit;
}
ol {
- margin-left: 1em;
font-family: inherit;
}
- .bullet { p {display: inline-block; font-family: inherit} margin-left: 0; }
- .bullet1 { p {display: inline-block; font-family: inherit} }
- .bullet2,.bullet3,.bullet4,.bullet5,.bullet6 { p {display: inline-block; font-family: inherit} font-size: smaller; }
+ .bullet { p { font-family: inherit} margin-left: 0; }
+ .bullet1 { p { font-family: inherit} }
+ .bullet2,.bullet3,.bullet4,.bullet5,.bullet6 { p { font-family: inherit} font-size: smaller; }
.decimal1-ol { counter-reset: deci1; p {display: inline-block; font-family: inherit} margin-left: 0; }
- .decimal2-ol { counter-reset: deci2; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 1em;}
- .decimal3-ol { counter-reset: deci3; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 2em;}
- .decimal4-ol { counter-reset: deci4; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 3em;}
+ .decimal2-ol { counter-reset: deci2; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 2.1em;}
+ .decimal3-ol { counter-reset: deci3; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 2.85em;}
+ .decimal4-ol { counter-reset: deci4; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 3.85em;}
.decimal5-ol { counter-reset: deci5; p {display: inline-block; font-family: inherit} font-size: smaller; }
.decimal6-ol { counter-reset: deci6; p {display: inline-block; font-family: inherit} font-size: smaller; }
.decimal7-ol { counter-reset: deci7; p {display: inline-block; font-family: inherit} font-size: smaller; }
.multi1-ol { counter-reset: multi1; p {display: inline-block; font-family: inherit} margin-left: 0; padding-left: 1.2em }
- .multi2-ol { counter-reset: multi2; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 1.4em;}
- .multi3-ol { counter-reset: multi3; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 2em;}
- .multi4-ol { counter-reset: multi4; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 3.4em;}
-
- .bullet:before, .bullet1:before, .bullet2:before, .bullet3:before, .bullet4:before, .bullet5:before { transition: 0.5s; display: inline-block; vertical-align: top; margin-left: -1em; width: 1em; content:" " }
+ .multi2-ol { counter-reset: multi2; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 2em;}
+ .multi3-ol { counter-reset: multi3; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 2.85em;}
+ .multi4-ol { counter-reset: multi4; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 3.85em;}
+
+ //.bullet:before, .bullet1:before, .bullet2:before, .bullet3:before, .bullet4:before, .bullet5:before { transition: 0.5s; display: inline-block; vertical-align: top; margin-left: -1em; width: 1em; content:" " }
.decimal1:before { transition: 0.5s;counter-increment: deci1; display: inline-block; vertical-align: top; margin-left: -1em; width: 1em; content: counter(deci1) ". "; }
.decimal2:before { transition: 0.5s;counter-increment: deci2; display: inline-block; vertical-align: top; margin-left: -2.1em; width: 2.1em; content: counter(deci1) "."counter(deci2) ". "; }
@@ -331,8 +331,8 @@ footnote::after {
.decimal5:before { transition: 0.5s;counter-increment: deci5; display: inline-block; vertical-align: top; margin-left: -2em; width: 5em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) ". "; }
.decimal6:before { transition: 0.5s;counter-increment: deci6; display: inline-block; vertical-align: top; margin-left: -2em; width: 6em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) ". "; }
.decimal7:before { transition: 0.5s;counter-increment: deci7; display: inline-block; vertical-align: top; margin-left: -2em; width: 7em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) "."counter(deci7) ". "; }
-
- .multi1:before { transition: 0.5s;counter-increment: multi1; display: inline-block; vertical-align: top; margin-left: -1em; width: 1.2em; content: counter(multi1, upper-alpha) ". "; }
+
+ .multi1:before { transition: 0.5s;counter-increment: multi1; display: inline-block; vertical-align: top; margin-left: -1.3em; width: 1.2em; content: counter(multi1, upper-alpha) ". "; }
.multi2:before { transition: 0.5s;counter-increment: multi2; display: inline-block; vertical-align: top; margin-left: -2em; width: 2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) ". "; }
.multi3:before { transition: 0.5s;counter-increment: multi3; display: inline-block; vertical-align: top; margin-left: -2.85em; width:2.85em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) ". "; }
.multi4:before { transition: 0.5s;counter-increment: multi4; display: inline-block; vertical-align: top; margin-left: -4.2em; width: 4.2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) "."counter(multi4, lower-roman) ". "; }
@@ -346,4 +346,284 @@ footnote::after {
.ProseMirror:hover {
background: unset;
}
-} \ No newline at end of file
+}
+
+@media only screen and (max-width: 1000px) {
+ @import "../../globalCssVariables";
+
+ .ProseMirror {
+ width: 100%;
+ height: 100%;
+ min-height: 100%;
+ }
+
+ .ProseMirror:focus {
+ outline: none !important;
+ }
+
+ .formattedTextBox-cont {
+ touch-action: none;
+ cursor: text;
+ 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;
+ max-height: 100%;
+ display: flex;
+ flex-direction: row;
+ transition: opacity 1s;
+
+ .formattedTextBox-dictation {
+ height: 12px;
+ width: 10px;
+ top: 0px;
+ left: 0px;
+ position: absolute;
+ }
+ }
+
+ .formattedTextBox-outer {
+ position: relative;
+ overflow: auto;
+ display: inline-block;
+ width: 100%;
+ height: 100%;
+ }
+
+ .formattedTextBox-sidebar-handle {
+ position: absolute;
+ top: calc(50% - 17.5px);
+ width: 10px;
+ height: 35px;
+ background: lightgray;
+ border-radius: 20px;
+ cursor:grabbing;
+ }
+
+ .formattedTextBox-cont>.formattedTextBox-sidebar-handle {
+ right: 0;
+ left: unset;
+ }
+
+ .formattedTextBox-sidebar,
+ .formattedTextBox-sidebar-inking {
+ border-left: dashed 1px black;
+ height: 100%;
+ display: inline-block;
+ position: absolute;
+ right: 0;
+
+ .collectionfreeformview-container {
+ position: relative;
+ }
+
+ >.formattedTextBox-sidebar-handle {
+ right: unset;
+ left: -5;
+ }
+ }
+
+ .formattedTextBox-sidebar-inking {
+ pointer-events: all;
+ }
+
+ .formattedTextBox-inner-rounded {
+ height: 70%;
+ width: 85%;
+ position: absolute;
+ overflow: auto;
+ top: 15%;
+ left: 10%;
+ }
+
+ .formattedTextBox-inner-rounded,
+ .formattedTextBox-inner {
+ height: 100%;
+ white-space: pre-wrap;
+ hr {
+ display: block;
+ unicode-bidi: isolate;
+ margin-block-start: 0.5em;
+ margin-block-end: 0.5em;
+ margin-inline-start: auto;
+ margin-inline-end: auto;
+ overflow: hidden;
+ border-style: inset;
+ border-width: 1px;
+ }
+ }
+
+ // .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;
+ // }
+
+ .strong,
+ .heading {
+ font-weight: bold;
+ }
+
+ .em {
+ font-style: italic;
+ }
+
+ .userMarkOpen {
+ background: rgba(255, 255, 0, 0.267);
+ display: inline;
+ }
+
+ .userMark {
+ background: rgba(255, 255, 0, 0.267);
+ font-size: 2px;
+ display: inline-grid;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ width: 10px;
+ min-height: 10px;
+ text-align: center;
+ align-content: center;
+ }
+
+ footnote {
+ display: inline-block;
+ position: relative;
+ cursor: pointer;
+
+ div {
+ padding: 0 !important;
+ }
+ }
+
+ footnote::after {
+ content: counter(prosemirror-footnote);
+ vertical-align: super;
+ font-size: 75%;
+ counter-increment: prosemirror-footnote;
+ }
+
+ .ProseMirror {
+ counter-reset: prosemirror-footnote;
+ }
+
+ .footnote-tooltip {
+ cursor: auto;
+ font-size: 75%;
+ position: absolute;
+ left: -30px;
+ top: calc(100% + 10px);
+ background: silver;
+ padding: 3px;
+ border-radius: 2px;
+ max-width: 100px;
+ min-width: 50px;
+ width: max-content;
+ }
+
+ .prosemirror-attribution {
+ font-size: 8px;
+ }
+
+ .footnote-tooltip::before {
+ border: 5px solid silver;
+ border-top-width: 0px;
+ border-left-color: transparent;
+ border-right-color: transparent;
+ position: absolute;
+ top: -5px;
+ left: 27px;
+ content: " ";
+ height: 0;
+ width: 0;
+ }
+
+
+ .formattedTextBox-inlineComment {
+ position: relative;
+ width: 40px;
+ height: 20px;
+ &::before {
+ content: "→";
+ }
+ &:hover {
+ background: orange;
+ }
+ }
+
+ .formattedTextBox-summarizer {
+ opacity: 0.5;
+ position: relative;
+ width: 40px;
+ height: 20px;
+ &::after {
+ content: "←";
+ }
+ }
+
+ .formattedTextBox-summarizer-collapsed {
+ opacity: 0.5;
+ position: relative;
+ width: 40px;
+ height: 20px;
+ &::after {
+ content: "...";
+ }
+ }
+
+ .ProseMirror {
+ touch-action: none;
+ span {
+ font-family: inherit;
+ }
+
+ ol, ul {
+ counter-reset: deci1 0 multi1 0;
+ padding-left: 1em;
+ font-family: inherit;
+ }
+ ol {
+ margin-left: 1em;
+ font-family: inherit;
+ }
+
+ .decimal1-ol { counter-reset: deci1; p {display: inline; font-family: inherit} margin-left: 0; }
+ .decimal2-ol { counter-reset: deci2; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 1em;}
+ .decimal3-ol { counter-reset: deci3; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 2em;}
+ .decimal4-ol { counter-reset: deci4; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 3em;}
+ .decimal5-ol { counter-reset: deci5; p {display: inline; font-family: inherit} font-size: smaller; }
+ .decimal6-ol { counter-reset: deci6; p {display: inline; font-family: inherit} font-size: smaller; }
+ .decimal7-ol { counter-reset: deci7; p {display: inline; font-family: inherit} font-size: smaller; }
+
+ .multi1-ol { counter-reset: multi1; p {display: inline; font-family: inherit} margin-left: 0; padding-left: 1.2em }
+ .multi2-ol { counter-reset: multi2; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 1.4em;}
+ .multi3-ol { counter-reset: multi3; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 2em;}
+ .multi4-ol { counter-reset: multi4; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 3.4em;}
+
+ .decimal1:before { transition: 0.5s;counter-increment: deci1; display: inline-block; margin-left: -1em; width: 1em; content: counter(deci1) ". "; }
+ .decimal2:before { transition: 0.5s;counter-increment: deci2; display: inline-block; margin-left: -2.1em; width: 2.1em; content: counter(deci1) "."counter(deci2) ". "; }
+ .decimal3:before { transition: 0.5s;counter-increment: deci3; display: inline-block; margin-left: -2.85em;width: 2.85em; content: counter(deci1) "."counter(deci2) "."counter(deci3) ". "; }
+ .decimal4:before { transition: 0.5s;counter-increment: deci4; display: inline-block; margin-left: -3.85em;width: 3.85em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) ". "; }
+ .decimal5:before { transition: 0.5s;counter-increment: deci5; display: inline-block; margin-left: -2em; width: 5em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) ". "; }
+ .decimal6:before { transition: 0.5s;counter-increment: deci6; display: inline-block; margin-left: -2em; width: 6em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) ". "; }
+ .decimal7:before { transition: 0.5s;counter-increment: deci7; display: inline-block; margin-left: -2em; width: 7em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) "."counter(deci7) ". "; }
+
+ .multi1:before { transition: 0.5s;counter-increment: multi1; display: inline-block; margin-left: -1em; width: 1.2em; content: counter(multi1, upper-alpha) ". "; }
+ .multi2:before { transition: 0.5s;counter-increment: multi2; display: inline-block; margin-left: -2em; width: 2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) ". "; }
+ .multi3:before { transition: 0.5s;counter-increment: multi3; display: inline-block; margin-left: -2.85em; width:2.85em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) ". "; }
+ .multi4:before { transition: 0.5s;counter-increment: multi4; display: inline-block; margin-left: -4.2em; width: 4.2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) "."counter(multi4, lower-roman) ". "; }
+ }
+}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 71ba51039..4dbe59e60 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -13,7 +13,7 @@ import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from "
import { ReplaceStep } from 'prosemirror-transform';
import { EditorView } from "prosemirror-view";
import { DateField } from '../../../../fields/DateField';
-import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym, AclSym } from "../../../../fields/Doc";
+import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym, AclEdit, AclAdmin } from "../../../../fields/Doc";
import { documentSchema } from '../../../../fields/documentSchemas';
import applyDevTools = require("prosemirror-dev-tools");
import { removeMarkWithAttrs } from "./prosemirrorPatches";
@@ -24,7 +24,7 @@ import { RichTextField } from "../../../../fields/RichTextField";
import { RichTextUtils } from '../../../../fields/RichTextUtils';
import { createSchema, makeInterface } from "../../../../fields/Schema";
import { Cast, DateCast, NumCast, StrCast, ScriptCast } from "../../../../fields/Types";
-import { TraceMobx, OVERRIDE_ACL } from '../../../../fields/util';
+import { TraceMobx, OVERRIDE_ACL, GetEffectiveAcl } from '../../../../fields/util';
import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnOne, returnZero, Utils, setupMoveUpEvents } from '../../../../Utils';
import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils';
import { DocServer } from "../../../DocServer";
@@ -59,6 +59,7 @@ import { FieldView, FieldViewProps } from "../FieldView";
import "./FormattedTextBox.scss";
import { FormattedTextBoxComment, formattedTextBoxCommentPlugin, findLinkMark } from './FormattedTextBoxComment';
import React = require("react");
+import { DocumentManager } from '../../../util/DocumentManager';
library.add(faEdit);
library.add(faSmile, faTextHeight, faUpload);
@@ -173,19 +174,25 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
linkOnDeselect: Map<string, string> = new Map();
doLinkOnDeselect() {
+
Array.from(this.linkOnDeselect.entries()).map(entry => {
const key = entry[0];
const value = entry[1];
+
const id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key);
DocServer.GetRefField(value).then(doc => {
DocServer.GetRefField(id).then(linkDoc => {
this.dataDoc[key] = doc || Docs.Create.FreeformDocument([], { title: value, _width: 500, _height: 500 }, value);
DocUtils.Publish(this.dataDoc[key] as Doc, value, this.props.addDocument, this.props.removeDocument);
- if (linkDoc) { (linkDoc as Doc).anchor2 = this.dataDoc[key] as Doc; }
- else DocUtils.MakeLink({ doc: this.rootDoc }, { doc: this.dataDoc[key] as Doc }, "link to named target", id);
+ if (linkDoc) {
+ (linkDoc as Doc).anchor2 = this.dataDoc[key] as Doc;
+ } else {
+ DocUtils.MakeLink({ doc: this.rootDoc }, { doc: this.dataDoc[key] as Doc }, "portal link", "link to named target", id);
+ }
});
});
});
+
this.linkOnDeselect.clear();
}
@@ -216,7 +223,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
const state = this._editorView.state.apply(tx);
this._editorView.updateState(state);
- (tx.storedMarks && !this._editorView.state.storedMarks) && (this._editorView.state.storedMarks = tx.storedMarks);
const tsel = this._editorView.state.selection.$from;
tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 1000)));
@@ -225,32 +231,38 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
const curProto = Cast(Cast(this.dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype
const curLayout = this.rootDoc !== this.layoutDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : undefined; // the default text stored in a layout template
const json = JSON.stringify(state.toJSON());
- if (!this.dataDoc[AclSym]) {
+ let unchanged = true;
+ const effectiveAcl = GetEffectiveAcl(this.dataDoc);
+ if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) {
if (!this._applyingChange && json.replace(/"selection":.*/, "") !== curProto?.Data.replace(/"selection":.*/, "")) {
this._applyingChange = true;
(curText !== Cast(this.dataDoc[this.fieldKey], RichTextField)?.Text) && (this.dataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now()))) && (this.dataDoc["lastModified"] = new DateField(new Date(Date.now())));
if ((!curTemp && !curProto) || curText || curLayout?.Data.includes("dash")) { // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended)
- if (json !== curLayout?.Data) {
+ if (json.replace(/"selection":.*/, "") !== curLayout?.Data.replace(/"selection":.*/, "")) {
!curText && tx.storedMarks?.map(m => m.type.name === "pFontSize" && (Doc.UserDoc().fontSize = this.layoutDoc._fontSize = m.attrs.fontSize));
!curText && tx.storedMarks?.map(m => m.type.name === "pFontFamily" && (Doc.UserDoc().fontFamily = this.layoutDoc._fontFamily = m.attrs.fontFamily));
this.dataDoc[this.props.fieldKey] = new RichTextField(json, curText);
this.dataDoc[this.props.fieldKey + "-noTemplate"] = (curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited
ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: curText });
+ unchanged = false;
}
} else { // if we've deleted all the text in a note driven by a template, then restore the template data
this.dataDoc[this.props.fieldKey] = undefined;
this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse((curProto || curTemp).Data)));
this.dataDoc[this.props.fieldKey + "-noTemplate"] = undefined; // mark the data field as not being split from any template it might have
+ unchanged = false;
}
this._applyingChange = false;
+ if (!unchanged) {
+ this.updateTitle();
+ this.tryUpdateHeight();
+ }
}
} else {
const json = JSON.parse(Cast(this.dataDoc[this.fieldKey], RichTextField)?.Data!);
json.selection = state.toJSON().selection;
this._editorView.updateState(EditorState.fromJSON(this.config, json));
}
- this.updateTitle();
- this.tryUpdateHeight();
}
}
@@ -337,7 +349,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
public unhighlightSearchTerms = () => {
- if (this._editorView && (this._editorView as any).docView) {
+ if (window.screen.width < 600) null;
+ else if (this._editorView && (this._editorView as any).docView) {
const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true });
const end = this._editorView.state.doc.nodeSize - 2;
@@ -453,16 +466,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { background: "moccasin" });
}
if (FormattedTextBox._highlights.indexOf("Todo Items") !== -1) {
- addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "todo", { outline: "black solid 1px" });
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "UT-" + "todo", { outline: "black solid 1px" });
}
if (FormattedTextBox._highlights.indexOf("Important Items") !== -1) {
- addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "important", { "font-size": "larger" });
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "UT-" + "important", { "font-size": "larger" });
}
if (FormattedTextBox._highlights.indexOf("Disagree Items") !== -1) {
- addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "disagree", { "text-decoration": "line-through" });
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "UT-" + "disagree", { "text-decoration": "line-through" });
}
if (FormattedTextBox._highlights.indexOf("Ignore Items") !== -1) {
- addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "ignore", { "font-size": "1" });
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "UT-" + "ignore", { "font-size": "1" });
}
if (FormattedTextBox._highlights.indexOf("By Recent Minute") !== -1) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" });
@@ -498,9 +511,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
specificContextMenu = (e: React.MouseEvent): void => {
const cm = ContextMenu.Instance;
- const appearance = ContextMenu.Instance.findByDescription("Appearance...");
- const appearanceItems = appearance && "subitems" in appearance ? appearance.subitems : [];
-
const changeItems: ContextMenuProps[] = [];
const noteTypesDoc = Cast(Doc.UserDoc()["template-notes"], Doc, null);
DocListCast(noteTypesDoc?.data).forEach(note => {
@@ -512,17 +522,34 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
});
});
changeItems.push({ description: "FreeForm", event: () => DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.FreeformDocument, "freeform"), icon: "eye" });
- appearanceItems.push({ description: "Change Perspective...", subitems: changeItems, icon: "external-link-alt" });
+ const highlighting: ContextMenuProps[] = [];
+ ["My Text", "Text from Others", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option =>
+ highlighting.push({
+ description: (FormattedTextBox._highlights.indexOf(option) === -1 ? "Highlight " : "Unhighlight ") + option, event: () => {
+ e.stopPropagation();
+ if (FormattedTextBox._highlights.indexOf(option) === -1) {
+ FormattedTextBox._highlights.push(option);
+ } else {
+ FormattedTextBox._highlights.splice(FormattedTextBox._highlights.indexOf(option), 1);
+ }
+ this.updateHighlights();
+ }, icon: "expand-arrows-alt"
+ }));
+
+
const uicontrols: ContextMenuProps[] = [];
- uicontrols.push({ description: "Toggle Sidebar", event: () => this.layoutDoc._showSidebar = !this.layoutDoc._showSidebar, icon: "expand-arrows-alt" });
- uicontrols.push({ description: "Toggle Dictation Icon", event: () => this.layoutDoc._showAudio = !this.layoutDoc._showAudio, icon: "expand-arrows-alt" });
- uicontrols.push({ description: "Toggle Menubar", event: () => this.toggleMenubar(), icon: "expand-arrows-alt" });
+ uicontrols.push({ description: `${this.layoutDoc._showSidebar ? "Hide" : "Show"} Sidebar`, event: () => this.layoutDoc._showSidebar = !this.layoutDoc._showSidebar, icon: "expand-arrows-alt" });
+ uicontrols.push({ description: `${this.layoutDoc._showAudio ? "Hide" : "Show"} Dictation Icon`, event: () => this.layoutDoc._showAudio = !this.layoutDoc._showAudio, icon: "expand-arrows-alt" });
+ uicontrols.push({ description: "Show Highlights...", noexpand: true, subitems: highlighting, icon: "hand-point-right" });
!Doc.UserDoc().noviceMode && uicontrols.push({
description: "Broadcast Message", event: () => DocServer.GetRefField("rtfProto").then(proto =>
proto instanceof Doc && (proto.BROADCAST_MESSAGE = Cast(this.rootDoc[this.fieldKey], RichTextField)?.Text)), icon: "expand-arrows-alt"
});
+ cm.addItem({ description: "UI Controls...", subitems: uicontrols, icon: "asterisk" });
- appearanceItems.push({ description: "UI Controls...", subitems: uicontrols, icon: "asterisk" });
+ const appearance = cm.findByDescription("Appearance...");
+ const appearanceItems = appearance && "subitems" in appearance ? appearance.subitems : [];
+ appearanceItems.push({ description: "Change Perspective...", noexpand: true, subitems: changeItems, icon: "external-link-alt" });
this.rootDoc.isTemplateDoc && appearanceItems.push({ description: "Make Default Layout", event: async () => Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.rootDoc), icon: "eye" });
Doc.UserDoc().defaultTextLayout && appearanceItems.push({ description: "Reset default note style", event: () => Doc.UserDoc().defaultTextLayout = undefined, icon: "eye" });
appearanceItems.push({
@@ -550,30 +577,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
Doc.AddDocToList(Cast(Doc.UserDoc()["template-notes"], Doc, null), "data", this.rootDoc);
}, icon: "eye"
});
- !appearance && ContextMenu.Instance.addItem({ description: "Appearance...", subitems: appearanceItems, icon: "eye" });
-
- const funcs: ContextMenuProps[] = [];
-
- //funcs.push({ description: `${this.Document._autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight, icon: "plus" });
- funcs.push({ description: (!this.layoutDoc._nativeWidth || !this.layoutDoc._nativeHeight ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" });
- funcs.push({ description: "Toggle Single Line", event: () => this.layoutDoc._singleLine = !this.layoutDoc._singleLine, icon: "expand-arrows-alt" });
-
- const highlighting: ContextMenuProps[] = [];
- ["My Text", "Text from Others", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option =>
- highlighting.push({
- description: (FormattedTextBox._highlights.indexOf(option) === -1 ? "Highlight " : "Unhighlight ") + option, event: () => {
- e.stopPropagation();
- if (FormattedTextBox._highlights.indexOf(option) === -1) {
- FormattedTextBox._highlights.push(option);
- } else {
- FormattedTextBox._highlights.splice(FormattedTextBox._highlights.indexOf(option), 1);
- }
- this.updateHighlights();
- }, icon: "expand-arrows-alt"
- }));
- funcs.push({ description: "highlighting...", subitems: highlighting, icon: "hand-point-right" });
-
- ContextMenu.Instance.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" });
+ cm.addItem({ description: "Appearance...", subitems: appearanceItems, icon: "eye" });
+
+ const options = cm.findByDescription("Options...");
+ const optionItems = options && "subitems" in options ? options.subitems : [];
+ !Doc.UserDoc().noviceMode && optionItems.push({ description: this.Document._singleLine ? "Make Single Line" : "Make Multi Line", event: () => this.layoutDoc._singleLine = !this.layoutDoc._singleLine, icon: "expand-arrows-alt" });
+ optionItems.push({ description: `${this.Document._autoHeight ? "Lock" : "Auto"} Height`, event: () => this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight, icon: "plus" });
+ optionItems.push({ description: `${!this.layoutDoc._nativeWidth || !this.layoutDoc._nativeHeight ? "Lock" : "Unlock"} Aspect`, event: this.toggleNativeDimensions, icon: "snowflake" });
+ !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "eye" });
this._downX = this._downY = Number.NaN;
}
@@ -590,11 +601,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
stopDictation = (abort: boolean) => { DictationManager.Controls.stop(!abort); };
- @action
- toggleMenubar = () => {
- this.layoutDoc._chromeStatus = this.layoutDoc._chromeStatus === "disabled" ? "enabled" : "disabled";
- }
-
recordBullet = async () => {
const completedCue = "end session";
const results = await DictationManager.Controls.listen({
@@ -724,7 +730,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
);
this._disposers.editorState = reaction(
() => {
- if (this.dataDoc[this.props.fieldKey + "-noTemplate"] || !this.layoutDoc[this.props.fieldKey + "-textTemplate"]) {
+ if (this.dataDoc?.[this.props.fieldKey + "-noTemplate"] || !this.layoutDoc[this.props.fieldKey + "-textTemplate"]) {
return Cast(this.dataDoc[this.props.fieldKey], RichTextField, null)?.Data;
}
return Cast(this.layoutDoc[this.props.fieldKey + "-textTemplate"], RichTextField, null)?.Data;
@@ -778,7 +784,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
{ fireImmediately: true });
this._disposers.search2 = reaction(() => this.rootDoc.searchMatch,
search => search ? this.highlightSearchTerms([Doc.SearchQuery()]) : this.unhighlightSearchTerms(),
- { fireImmediately: true });
+ { fireImmediately: this.rootDoc.searchMatch ? true : false });
this._disposers.record = reaction(() => this._recording,
() => {
@@ -986,6 +992,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
frag.forEach(node => nodes.push(marker(node)));
return Fragment.fromArray(nodes);
}
+
+
function addLinkMark(node: Node, title: string, linkId: string) {
if (!node.isText) {
const content = addMarkToFrag(node.content, (node: Node) => addLinkMark(node, title, linkId));
@@ -1029,7 +1037,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
clipboardTextSerializer: this.clipboardTextSerializer,
handlePaste: this.handlePaste,
});
- !Doc.UserDoc().noviceMode && applyDevTools.applyDevTools(this._editorView);
+ // !Doc.UserDoc().noviceMode && applyDevTools.applyDevTools(this._editorView);
const startupText = !rtfField && this._editorView && Field.toString(this.dataDoc[fieldKey] as Field);
if (startupText) {
const { state: { tr }, dispatch } = this._editorView;
@@ -1046,10 +1054,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
FormattedTextBox.SelectOnLoadChar = "";
}
- (selectOnLoad /* || !rtfField?.Text*/) && this._editorView!.focus();
+ selectOnLoad && this._editorView!.focus();
// add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet.
- if (!this._editorView!.state.storedMarks || !this._editorView!.state.storedMarks.some(mark => mark.type === schema.marks.user_mark)) {
- this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })];
+ if (!this._editorView!.state.storedMarks?.some(mark => mark.type === schema.marks.user_mark)) {
+ this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ?? []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })];
}
}
getFont(font: string) {
@@ -1070,10 +1078,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
this._editorView?.destroy();
}
- static _downEvent: any;
+ _downEvent: any;
_downX = 0;
_downY = 0;
_break = false;
+ _collapsed = false;
onPointerDown = (e: React.PointerEvent): void => {
if (this._recording && !e.ctrlKey && e.button === 0) {
this.stopDictation(true);
@@ -1089,7 +1098,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
this._downX = e.clientX;
this._downY = e.clientY;
this.doLinkOnDeselect();
- FormattedTextBox._downEvent = true;
+ this._downEvent = true;
FormattedTextBoxComment.textBox = this;
if (this.props.onClick && e.button === 0 && !this.props.isSelected(false)) {
e.preventDefault();
@@ -1105,8 +1114,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
onPointerUp = (e: React.PointerEvent): void => {
- if (!FormattedTextBox._downEvent) return;
- FormattedTextBox._downEvent = false;
+ if (!this._downEvent) return;
+ this._downEvent = false;
if (!(e.nativeEvent as any).formattedHandled) {
const editor = this._editorView!;
FormattedTextBoxComment.textBox = this;
@@ -1122,6 +1131,39 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
@action
+ onDoubleClick = (e: React.MouseEvent): void => {
+
+ this.doLinkOnDeselect();
+ FormattedTextBoxComment.textBox = this;
+ if (this.props.onClick && e.button === 0 && !this.props.isSelected(false)) {
+ e.preventDefault();
+ }
+ if (e.button === 0 && this.props.isSelected(true) && !e.altKey && !e.ctrlKey && !e.metaKey) {
+ if (e.clientX < this.ProseRef!.getBoundingClientRect().right) { // stop propagation if not in sidebar
+ e.stopPropagation(); // if the text box is selected, then it consumes all down events
+ }
+ }
+ if (e.button === 2 || (e.button === 0 && e.ctrlKey)) {
+ e.preventDefault();
+ }
+ FormattedTextBoxComment.Hide();
+ if (FormattedTextBoxComment.linkDoc) {
+ if (FormattedTextBoxComment.linkDoc.type !== DocumentType.LINK) {
+ this.props.addDocTab(FormattedTextBoxComment.linkDoc, e.ctrlKey ? "inTab" : "onRight");
+ } else {
+ DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, this.props.Document,
+ (doc: Doc, followLinkLocation: string) => this.props.addDocTab(doc, e.ctrlKey ? "inTab" : followLinkLocation));
+ }
+ }
+
+ (e.nativeEvent as any).formattedHandled = true;
+
+ if (e.buttons === 1 && this.props.isSelected(true) && !e.altKey) {
+ e.stopPropagation();
+ }
+ }
+
+ @action
onFocused = (e: React.FocusEvent): void => {
FormattedTextBox.FocusedBox = this;
this.tryUpdateHeight();
@@ -1154,9 +1196,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
static _bulletStyleSheet: any = addStyleSheet();
static _userStyleSheet: any = addStyleSheet();
-
+ _forceUncollapse = true; // if the cursor doesn't move between clicks, then the selection will disappear for some reason. This flags the 2nd click as happening on a selection which allows bullet points to toggle
+ _forceDownNode: Node | undefined;
onClick = (e: React.MouseEvent): void => {
- if ((this._editorView!.root as any).getSelection().isCollapsed) { // this is a hack to allow the cursor to be placed at the end of a document when the document ends in an inline dash comment. Apparently Chrome on Windows has a bug/feature which breaks this when clicking after the end of the text.
+ if (Math.abs(e.clientX - this._downX) > 4 || Math.abs(e.clientY - this._downY) > 4) {
+ this._forceDownNode = undefined;
+ return;
+ }
+ if (!this._forceUncollapse || (this._editorView!.root as any).getSelection().isCollapsed) { // this is a hack to allow the cursor to be placed at the end of a document when the document ends in an inline dash comment. Apparently Chrome on Windows has a bug/feature which breaks this when clicking after the end of the text.
const pcords = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
const node = pcords && this._editorView!.state.doc.nodeAt(pcords.pos); // get what prosemirror thinks the clicked node is (if it's null, then we didn't click on any text)
if (pcords && node?.type === this._editorView!.state.schema.nodes.dashComment) {
@@ -1168,6 +1215,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
if (e.clientY > lastNode?.getBoundingClientRect().bottom) { // if we clicked below the last prosemirror div, then set the selection to be the end of the document
this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, this._editorView!.state.doc.content.size)));
}
+ } else if ([this._editorView!.state.schema.nodes.ordered_list, this._editorView!.state.schema.nodes.listItem].includes(node?.type) &&
+ node !== (this._editorView!.state.selection as NodeSelection)?.node && pcords) {
+ this._editorView!.dispatch(this._editorView!.state.tr.setSelection(NodeSelection.create(this._editorView!.state.doc, pcords.pos)));
}
}
if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); return; }
@@ -1175,12 +1225,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
if (this.props.isSelected(true)) { // if text box is selected, then it consumes all click events
e.stopPropagation();
- this.hitBulletTargets(e.clientX, e.clientY, e.shiftKey, false);
+ this.hitBulletTargets(e.clientX, e.clientY, !this._editorView?.state.selection.empty || this._forceUncollapse, false, this._forceDownNode, e.shiftKey);
}
+ this._forceUncollapse = !(this._editorView!.root as any).getSelection().isCollapsed;
+ this._forceDownNode = (this._editorView!.state.selection as NodeSelection)?.node;
}
// this hackiness handles clicking on the list item bullets to do expand/collapse. the bullets are ::before pseudo elements so there's no real way to hit test against them.
- hitBulletTargets(x: number, y: number, collapse: boolean, highlightOnly: boolean) {
+ hitBulletTargets(x: number, y: number, collapse: boolean, highlightOnly: boolean, downNode: Node | undefined = undefined, selectOrderedList: boolean = false) {
+ this._forceUncollapse = false;
clearStyleSheetRules(FormattedTextBox._bulletStyleSheet);
const clickPos = this._editorView!.posAtCoords({ left: x, top: y });
let olistPos = clickPos?.pos;
@@ -1196,20 +1249,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
$olistPos = this._editorView?.state.doc.resolve(($olistPos as any).path[($olistPos as any).path.length - 4]);
}
}
+ const listPos = this._editorView?.state.doc.resolve(clickPos.pos);
const listNode = this._editorView?.state.doc.nodeAt(clickPos.pos);
- if (olistNode && olistNode.type === this._editorView?.state.schema.nodes.ordered_list) {
- if (!collapse) {
- if (!highlightOnly) {
- this._editorView.dispatch(this._editorView.state.tr.setSelection(new NodeSelection($olistPos!)));
- }
- addStyleSheetRule(FormattedTextBox._bulletStyleSheet, olistNode.attrs.mapStyle + olistNode.attrs.bulletStyle + ":hover:before", { background: "lightgray" });
- } else if (listNode && listNode.type === this._editorView.state.schema.nodes.list_item) {
- if (!highlightOnly) {
+ if (olistNode && olistNode.type === this._editorView?.state.schema.nodes.ordered_list && listNode) {
+ if (!highlightOnly) {
+ if (selectOrderedList || (!collapse && listNode.attrs.visibility)) {
+ this._editorView.dispatch(this._editorView.state.tr.setSelection(new NodeSelection(selectOrderedList ? $olistPos! : listPos!)));
+ } else if (!listNode.attrs.visibility || downNode === listNode) {
this._editorView.dispatch(this._editorView.state.tr.setNodeMarkup(clickPos.pos, listNode.type, { ...listNode.attrs, visibility: !listNode.attrs.visibility }));
this._editorView.dispatch(this._editorView.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, clickPos.pos)));
}
- addStyleSheetRule(FormattedTextBox._bulletStyleSheet, olistNode.attrs.mapStyle + olistNode.attrs.bulletStyle + ":hover:before", { background: "lightgray" });
}
+ addStyleSheetRule(FormattedTextBox._bulletStyleSheet, olistNode.attrs.mapStyle + olistNode.attrs.bulletStyle + ":hover:before", { background: "lightgray" });
}
}
}
@@ -1217,7 +1268,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
e.stopPropagation();
const view = this._editorView as any;
- // this interposes on prosemirror's upHandler to prevent prosemirror's up from invoked multiple times when there
+ // this interposes on prosemirror's upHandler to prevent prosemirror's up from invoked multiple times when there
// are nested prosemirrors. We only want the lowest level prosemirror to be invoked.
if (view.mouseDown) {
const originalUpHandler = view.mouseDown.up;
@@ -1234,7 +1285,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
richTextMenuPlugin() {
return new Plugin({
view(newView) {
- RichTextMenu.Instance && RichTextMenu.Instance.changeView(newView);
+ RichTextMenu.Instance?.changeView(newView);
return RichTextMenu.Instance;
}
});
@@ -1289,9 +1340,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
if (e.key === "Tab" || e.key === "Enter") {
e.preventDefault();
}
- const mark = e.key !== " " && this._lastTimedMark ? this._lastTimedMark : schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) });
- this._lastTimedMark = mark;
- // this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(mark));
+ if (e.key === " " || this._lastTimedMark?.attrs.userid !== Doc.CurrentUserEmail) {
+ const mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) });
+ this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(mark));
+ }
if (!this._undoTyping) {
this.startUndoTypingBatch();
@@ -1341,9 +1393,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
const scale = this.props.ContentScaling() * NumCast(this.layoutDoc._viewScale, 1);
const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : "";
const interactive = Doc.GetSelectedTool() === InkTool.None && !this.layoutDoc.isBackground;
- if (this.props.isSelected()) {
- setTimeout(() => this._editorView && RichTextMenu.Instance.updateFromDash(this._editorView, undefined, this.props), 0);
- } else if (FormattedTextBoxComment.textBox === this) {
+ setTimeout(() => this._editorView && RichTextMenu.Instance.updateFromDash(this._editorView, undefined, this.props), this.props.isSelected() ? 10 : 0); // need to make sure that we update a text box that is selected after updating the one that was deselected
+ if (!this.props.isSelected() && FormattedTextBoxComment.textBox === this) {
setTimeout(() => FormattedTextBoxComment.Hide(), 0);
}
const selPad = this.props.isSelected() ? -10 : 0;
@@ -1366,7 +1417,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
opacity: this.props.hideOnLeave ? (this._entered ? 1 : 0.1) : 1,
color: this.props.color ? this.props.color : StrCast(this.layoutDoc[this.props.fieldKey + "-color"], this.props.hideOnLeave ? "white" : "inherit"),
pointerEvents: interactive ? undefined : "none",
- fontSize: Cast(this.layoutDoc._fontSize, "number", null),
+ fontSize: Cast(this.layoutDoc._fontSize, "string", null),
fontFamily: StrCast(this.layoutDoc._fontFamily, "inherit"),
transition: "opacity 1s"
}}
@@ -1390,14 +1441,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
}
})}
+ onDoubleClick={this.onDoubleClick}
>
<div className={`formattedTextBox-outer`} ref={this._scrollRef}
- style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: !this.props.isSelected() ? "none" : undefined }}
+ style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: !this.props.active() ? "none" : undefined }}
onScroll={this.onscrolled} onDrop={this.ondrop} >
<div className={`formattedTextBox-inner${rounded}${selclass}`} ref={this.createDropTarget}
style={{
padding: this.layoutDoc._textBoxPadding ? StrCast(this.layoutDoc._textBoxPadding) : `${Math.max(0, NumCast(this.layoutDoc._yMargin, this.props.yMargin || 0) + selPad)}px ${NumCast(this.layoutDoc._xMargin, this.props.xMargin || 0) + selPad}px`,
- pointerEvents: !this.props.isSelected() ? ((this.layoutDoc.isLinkButton || this.props.onClick) ? "none" : "all") : undefined
+ pointerEvents: !this.props.active() ? ((this.layoutDoc.isLinkButton || this.props.onClick) ? "none" : undefined) : undefined
}}
/>
</div>
@@ -1436,7 +1488,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
setTimeout(() => this._editorView!.focus(), 500);
e.stopPropagation();
}} >
- <FontAwesomeIcon className="formattedTExtBox-audioFont"
+ <FontAwesomeIcon className="formattedTextBox-audioFont"
style={{ color: this._recording ? "red" : "blue", opacity: this._recording ? 1 : 0.5, display: this.props.isSelected() ? "" : "none" }} icon={"microphone"} size="sm" />
</div>}
</div>
diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss
index 2dd63ec21..6a403cb17 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss
@@ -4,14 +4,80 @@
z-index: 20;
background: white;
border: 1px solid silver;
- border-radius: 2px;
+ border-radius: 7px;
margin-bottom: 7px;
-webkit-transform: translateX(-50%);
transform: translateX(-50%);
- }
- .FormattedTextBox-tooltip:before {
+ box-shadow: 3px 3px 1.5px grey;
+
+ .FormattedTextBoxComment {
+ background-color: white;
+ border: 8px solid white;
+
+ //display: flex;
+ .FormattedTextBoxComment-info {
+
+ margin-bottom: 9px;
+
+ .FormattedTextBoxComment-title {
+ padding-right: 4px;
+ float: left;
+
+ .FormattedTextBoxComment-description {
+ text-decoration: none;
+ font-style: italic;
+ color: rgb(95, 97, 102);
+ font-size: 10px;
+ padding-bottom: 4px;
+ margin-bottom: 5px;
+
+ }
+ }
+
+ .FormattedTextBoxComment-button {
+ display: inline;
+ padding-left: 6px;
+ padding-right: 6px;
+ padding-top: 2.5px;
+ padding-bottom: 2.5px;
+ width: 17px;
+ height: 17px;
+ margin: 0;
+ margin-right: 3px;
+ border-radius: 50%;
+ pointer-events: auto;
+ background-color: rgb(0, 0, 0);
+ color: rgb(255, 255, 255);
+ transition: transform 0.2s;
+ text-align: center;
+ position: relative;
+ font-size: 12px;
+
+ &:hover {
+ background-color: rgb(77, 77, 77);
+ cursor: pointer;
+ }
+ }
+ }
+
+ .FormattedTextBoxComment-preview-wrapper {
+ width: 170px;
+ height: 170px;
+ overflow: hidden;
+ //padding-top: 5px;
+ margin-top: 10px;
+ margin-bottom: 8px;
+ align-content: center;
+ justify-content: center;
+ background-color: rgb(160, 160, 160);
+ }
+ }
+}
+
+.FormattedTextBox-tooltip:before {
content: "";
- height: 0; width: 0;
+ height: 0;
+ width: 0;
position: absolute;
left: 50%;
margin-left: -5px;
@@ -19,10 +85,12 @@
border: 5px solid transparent;
border-bottom-width: 0;
border-top-color: silver;
- }
- .FormattedTextBox-tooltip:after {
+}
+
+.FormattedTextBox-tooltip:after {
content: "";
- height: 0; width: 0;
+ height: 0;
+ width: 0;
position: absolute;
left: 50%;
margin-left: -5px;
@@ -30,4 +98,12 @@
border: 5px solid transparent;
border-bottom-width: 0;
border-top-color: white;
- } \ No newline at end of file
+}
+
+.FormattedTextBoxComment-buttons {
+ display: none;
+ position: absolute;
+ top: 50%;
+ right: 0;
+ transform: translateY(-50%);
+} \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
index 4c90b6afd..6f3984f39 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
@@ -2,8 +2,8 @@ import { Mark, ResolvedPos } from "prosemirror-model";
import { EditorState, Plugin } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import * as ReactDOM from 'react-dom';
-import { Doc, DocCastAsync } from "../../../../fields/Doc";
-import { Cast, FieldValue, NumCast } from "../../../../fields/Types";
+import { Doc, DocCastAsync, Opt } from "../../../../fields/Doc";
+import { Cast, FieldValue, NumCast, StrCast } from "../../../../fields/Types";
import { emptyFunction, returnEmptyString, returnFalse, Utils, emptyPath, returnZero, returnOne, returnEmptyFilter } from "../../../../Utils";
import { DocServer } from "../../../DocServer";
import { DocumentManager } from "../../../util/DocumentManager";
@@ -16,6 +16,13 @@ import React = require("react");
import { Docs } from "../../../documents/Documents";
import wiki from "wikijs";
import { DocumentType } from "../../../documents/DocumentTypes";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { action } from "mobx";
+import { LinkManager } from "../../../util/LinkManager";
+import { LinkDocPreview } from "../LinkDocPreview";
+import { DocumentLinksButton } from "../DocumentLinksButton";
+import { Tooltip } from "@material-ui/core";
+import { undoBatch } from "../../../util/UndoManager";
export let formattedTextBoxCommentPlugin = new Plugin({
view(editorView) { return new FormattedTextBoxComment(editorView); }
@@ -62,6 +69,10 @@ export class FormattedTextBoxComment {
static mark: Mark;
static textBox: FormattedTextBox | undefined;
static linkDoc: Doc | undefined;
+
+ static _deleteRef: Opt<HTMLDivElement | null>;
+ static _followRef: Opt<HTMLDivElement | null>;
+
constructor(view: any) {
if (!FormattedTextBoxComment.tooltip) {
const root = document.getElementById("root");
@@ -75,24 +86,51 @@ export class FormattedTextBoxComment {
FormattedTextBoxComment.tooltip.appendChild(FormattedTextBoxComment.tooltipText);
FormattedTextBoxComment.tooltip.className = "FormattedTextBox-tooltip";
FormattedTextBoxComment.tooltip.style.pointerEvents = "all";
- FormattedTextBoxComment.tooltip.style.maxWidth = "350px";
- FormattedTextBoxComment.tooltip.style.maxHeight = "250px";
+ FormattedTextBoxComment.tooltip.style.maxWidth = "200px";
+ FormattedTextBoxComment.tooltip.style.maxHeight = "235px";
FormattedTextBoxComment.tooltip.style.width = "100%";
FormattedTextBoxComment.tooltip.style.height = "100%";
FormattedTextBoxComment.tooltip.style.overflow = "hidden";
FormattedTextBoxComment.tooltip.style.display = "none";
FormattedTextBoxComment.tooltip.appendChild(FormattedTextBoxComment.tooltipInput);
- FormattedTextBoxComment.tooltip.onpointerdown = (e: PointerEvent) => {
+ FormattedTextBoxComment.tooltip.onpointerdown = async (e: PointerEvent) => {
const keep = e.target && (e.target as any).type === "checkbox" ? true : false;
const textBox = FormattedTextBoxComment.textBox;
if (FormattedTextBoxComment.linkDoc && !keep && textBox) {
if (FormattedTextBoxComment.linkDoc.author) {
- if (FormattedTextBoxComment.linkDoc.type !== DocumentType.LINK) {
- textBox.props.addDocTab(FormattedTextBoxComment.linkDoc, e.ctrlKey ? "inTab" : "onRight");
+
+ if (FormattedTextBoxComment._deleteRef && FormattedTextBoxComment._deleteRef.contains(e.target as any)) {
+ this.deleteLink();
+ } else if (FormattedTextBoxComment._followRef && FormattedTextBoxComment._followRef.contains(e.target as any)) {
+ if (FormattedTextBoxComment.linkDoc.type !== DocumentType.LINK) {
+ textBox.props.addDocTab(FormattedTextBoxComment.linkDoc, e.ctrlKey ? "inTab" : "onRight");
+ } else {
+ const anchor = FieldValue(Doc.AreProtosEqual(FieldValue(Cast(FormattedTextBoxComment.linkDoc.anchor1, Doc)), textBox.dataDoc) ?
+ Cast(FormattedTextBoxComment.linkDoc.anchor2, Doc) : (Cast(FormattedTextBoxComment.linkDoc.anchor1, Doc))
+ || FormattedTextBoxComment.linkDoc);
+ const target = anchor?.annotationOn ? await DocCastAsync(anchor.annotationOn) : anchor;
+
+ if (FormattedTextBoxComment.linkDoc.follow) {
+ if (FormattedTextBoxComment.linkDoc.follow === "Default") {
+ DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, textBox.props.Document, doc => textBox.props.addDocTab(doc, "onRight"), false);
+ } else if (FormattedTextBoxComment.linkDoc.follow === "Always open in right tab") {
+ if (target) { textBox.props.addDocTab(target, "onRight"); }
+ } else if (FormattedTextBoxComment.linkDoc.follow === "Always open in new tab") {
+ if (target) { textBox.props.addDocTab(target, "inTab"); }
+ }
+ } else {
+ DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, textBox.props.Document, doc => textBox.props.addDocTab(doc, "onRight"), false);
+ }
+ }
} else {
- DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, textBox.props.Document,
- (doc: Doc, followLinkLocation: string) => textBox.props.addDocTab(doc, e.ctrlKey ? "inTab" : followLinkLocation));
+ if (FormattedTextBoxComment.linkDoc.type !== DocumentType.LINK) {
+ textBox.props.addDocTab(FormattedTextBoxComment.linkDoc, e.ctrlKey ? "inTab" : "onRight");
+ } else {
+ DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, textBox.props.Document,
+ (doc: Doc, followLinkLocation: string) => textBox.props.addDocTab(doc, e.ctrlKey ? "inTab" : followLinkLocation));
+ }
}
+
}
} else if (textBox && (FormattedTextBoxComment.tooltipText as any).href) {
textBox.props.addDocTab(Docs.Create.WebDocument((FormattedTextBoxComment.tooltipText as any).href, { title: (FormattedTextBoxComment.tooltipText as any).href, _width: 200, _height: 400, UseCors: true }), "onRight");
@@ -106,6 +144,16 @@ export class FormattedTextBoxComment {
}
}
+ @undoBatch
+ @action
+ deleteLink = () => {
+ FormattedTextBoxComment.linkDoc ? LinkManager.Instance.deleteLink(FormattedTextBoxComment.linkDoc) : null;
+ LinkDocPreview.LinkInfo = undefined;
+ DocumentLinksButton.EditLink = undefined;
+ //FormattedTextBoxComment.tooltipText = undefined;
+ FormattedTextBoxComment.Hide();
+ }
+
public static Hide() {
FormattedTextBoxComment.textBox = undefined;
FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = "none");
@@ -210,32 +258,71 @@ export class FormattedTextBoxComment {
}
if (target?.author) {
FormattedTextBoxComment.showCommentbox("", view, nbef);
- ReactDOM.render(<ContentFittingDocumentView
- Document={target}
- LibraryPath={emptyPath}
- fitToBox={true}
- moveDocument={returnFalse}
- rootSelected={returnFalse}
- ScreenToLocalTransform={Transform.Identity}
- parentActive={returnFalse}
- addDocument={returnFalse}
- removeDocument={returnFalse}
- addDocTab={returnFalse}
- pinToPres={returnFalse}
- dontRegisterView={true}
- docFilters={returnEmptyFilter}
- ContainingCollectionDoc={undefined}
- ContainingCollectionView={undefined}
- renderDepth={0}
- PanelWidth={() => Math.min(350, NumCast(target._width, 350))}
- PanelHeight={() => Math.min(250, NumCast(target._height, 250))}
- focus={emptyFunction}
- whenActiveChanged={returnFalse}
- bringToFront={returnFalse}
- ContentScaling={returnOne}
- NativeWidth={returnZero}
- NativeHeight={returnZero}
- />, FormattedTextBoxComment.tooltipText);
+
+ const title = StrCast(target.title).length > 16 ?
+ StrCast(target.title).substr(0, 16) + "..." : target.title;
+
+
+ const docPreview = <div className="FormattedTextBoxComment">
+ <div className="FormattedTextBoxComment-info">
+ <div className="FormattedTextBoxComment-title">
+ {title}
+ {FormattedTextBoxComment.linkDoc.description !== "" ? <p className="FormattedTextBoxComment-description">
+ {StrCast(FormattedTextBoxComment.linkDoc.description)}</p> : null}
+ </div>
+ <div className="wrapper" style={{ float: "right" }}>
+
+ <Tooltip title={<><div className="dash-tooltip">Delete Link</div></>} placement="top">
+ <div className="FormattedTextBoxComment-button"
+ ref={(r) => this._deleteRef = r}>
+ <FontAwesomeIcon className="FormattedTextBoxComment-fa-icon" icon="trash" color="white"
+ size="sm" /></div>
+ </Tooltip>
+
+ <Tooltip title={<><div className="dash-tooltip">Follow Link</div></>} placement="top">
+ <div className="FormattedTextBoxComment-button"
+ ref={(r) => this._followRef = r}>
+ <FontAwesomeIcon className="FormattedTextBoxComment-fa-icon" icon="arrow-right" color="white"
+ size="sm" />
+ </div>
+ </Tooltip>
+ </div> </div>
+ <div className="FormattedTextBoxComment-preview-wrapper">
+ <ContentFittingDocumentView
+ Document={target}
+ LibraryPath={emptyPath}
+ fitToBox={true}
+ moveDocument={returnFalse}
+ rootSelected={returnFalse}
+ ScreenToLocalTransform={Transform.Identity}
+ parentActive={returnFalse}
+ addDocument={returnFalse}
+ removeDocument={returnFalse}
+ addDocTab={returnFalse}
+ pinToPres={returnFalse}
+ dontRegisterView={true}
+ docFilters={returnEmptyFilter}
+ ContainingCollectionDoc={undefined}
+ ContainingCollectionView={undefined}
+ renderDepth={0}
+ PanelWidth={() => 175} //Math.min(350, NumCast(target._width, 350))}
+ PanelHeight={() => 175} //Math.min(250, NumCast(target._height, 250))}
+ focus={emptyFunction}
+ whenActiveChanged={returnFalse}
+ bringToFront={returnFalse}
+ ContentScaling={returnOne}
+ NativeWidth={() => target._nativeWidth ? NumCast(target._nativeWidth) : 0}
+ NativeHeight={() => target._nativeHeight ? NumCast(target._nativeHeight) : 0}
+ />
+ </div>
+ </div>;
+
+
+
+ FormattedTextBoxComment.showCommentbox("", view, nbef);
+
+ ReactDOM.render(docPreview, FormattedTextBoxComment.tooltipText);
+
FormattedTextBoxComment.tooltip.style.width = NumCast(target._width) ? `${NumCast(target._width)}` : "100%";
FormattedTextBoxComment.tooltip.style.height = NumCast(target._height) ? `${NumCast(target._height)}` : "100%";
}
@@ -249,4 +336,4 @@ export class FormattedTextBoxComment {
}
destroy() { }
-}
+} \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
index 9d69f4be7..8faf752b4 100644
--- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
+++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
@@ -11,18 +11,23 @@ import { Doc, DataSym } from "../../../../fields/Doc";
import { FormattedTextBox } from "./FormattedTextBox";
import { Id } from "../../../../fields/FieldSymbols";
import { Docs } from "../../../documents/Documents";
+import { Utils } from "../../../../Utils";
const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false;
export type KeyMap = { [key: string]: any };
-export let updateBullets = (tx2: Transaction, schema: Schema, mapStyle?: string, from?: number, to?: number) => {
+export let updateBullets = (tx2: Transaction, schema: Schema, assignedMapStyle?: string, from?: number, to?: number) => {
+ let mapStyle = assignedMapStyle;
tx2.doc.descendants((node: any, offset: any, index: any) => {
if ((from === undefined || to === undefined || (from <= offset + node.nodeSize && to >= offset)) && (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item)) {
const path = (tx2.doc.resolve(offset) as any).path;
let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty("type") && c.type === schema.nodes.ordered_list ? 1 : 0), 0);
- if (node.type === schema.nodes.ordered_list) depth++;
- tx2.setNodeMarkup(offset, node.type, { ...node.attrs, mapStyle: mapStyle || node.attrs.mapStyle, bulletStyle: depth, }, node.marks);
+ if (node.type === schema.nodes.ordered_list) {
+ if (depth === 0 && !assignedMapStyle) mapStyle = node.attrs.mapStyle;
+ depth++;
+ }
+ tx2.setNodeMarkup(offset, node.type, { ...node.attrs, mapStyle, bulletStyle: depth, }, node.marks);
}
});
return tx2;
@@ -98,7 +103,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any
//Command to create a new Tab with a PDF of all the command shortcuts
bind("Mod-/", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
- const newDoc = Docs.Create.PdfDocument("http://localhost:1050/assets/cheat-sheet.pdf", { _width: 300, _height: 300 });
+ const newDoc = Docs.Create.PdfDocument(Utils.prepend("/assets/cheat-sheet.pdf"), { _fitWidth: true, _width: 300, _height: 300 });
props.addDocTab(newDoc, "onRight");
});
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 95d6c9fac..7ccbfa051 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -11,7 +11,7 @@ import { EditorState, NodeSelection, TextSelection } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { Doc } from "../../../../fields/Doc";
import { DarkPastelSchemaPalette, PastelSchemaPalette } from '../../../../fields/SchemaHeaderField';
-import { Cast, StrCast, BoolCast } from "../../../../fields/Types";
+import { Cast, StrCast, BoolCast, NumCast } from "../../../../fields/Types";
import { unimplementedFunction, Utils } from "../../../../Utils";
import { DocServer } from "../../../DocServer";
import { LinkManager } from "../../../util/LinkManager";
@@ -23,7 +23,8 @@ import { updateBullets } from "./ProsemirrorExampleTransfer";
import "./RichTextMenu.scss";
import { schema } from "./schema_rts";
import { TraceMobx } from "../../../../fields/util";
-import { UndoManager } from "../../../util/UndoManager";
+import { UndoManager, undoBatch } from "../../../util/UndoManager";
+import { Tooltip } from "@material-ui/core";
const { toggleMark } = require("prosemirror-commands");
library.add(faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSuperscript, faSubscript, faOutdent, faIndent, faHandPointLeft, faHandPointRight, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller);
@@ -55,6 +56,7 @@ export default class RichTextMenu extends AntimodeMenu {
@observable private activeFontSize: string = "";
@observable private activeFontFamily: string = "";
@observable private activeListType: string = "";
+ @observable private activeAlignment: string = "left";
@observable private brushIsEmpty: boolean = true;
@observable private brushMarks: Set<Mark> = new Set();
@@ -91,7 +93,7 @@ export default class RichTextMenu extends AntimodeMenu {
{ mark: schema.marks.pFontSize.create({ fontSize: 32 }), title: "Set font size", label: "32pt", command: this.changeFontSize },
{ mark: schema.marks.pFontSize.create({ fontSize: 48 }), title: "Set font size", label: "48pt", command: this.changeFontSize },
{ mark: schema.marks.pFontSize.create({ fontSize: 72 }), title: "Set font size", label: "72pt", command: this.changeFontSize },
- { mark: null, title: "", label: "various", command: unimplementedFunction, hidden: true },
+ { mark: null, title: "", label: "...", command: unimplementedFunction, hidden: true },
{ mark: null, title: "", label: "13pt", command: unimplementedFunction, hidden: true }, // this is here because the default size is 13, but there is no actual 13pt option
];
@@ -110,7 +112,8 @@ export default class RichTextMenu extends AntimodeMenu {
this.listTypeOptions = [
{ node: schema.nodes.ordered_list.create({ mapStyle: "bullet" }), title: "Set list type", label: ":", command: this.changeListType },
{ node: schema.nodes.ordered_list.create({ mapStyle: "decimal" }), title: "Set list type", label: "1.1", command: this.changeListType },
- { node: schema.nodes.ordered_list.create({ mapStyle: "multi" }), title: "Set list type", label: "1.A", command: this.changeListType },
+ { node: schema.nodes.ordered_list.create({ mapStyle: "multi" }), title: "Set list type", label: "A.1", command: this.changeListType },
+ { node: schema.nodes.ordered_list.create({ mapStyle: "" }), title: "Set list type", label: "<none>", command: this.changeListType },
//{ node: undefined, title: "Set list type", label: "Remove", command: this.changeListType },
];
@@ -153,7 +156,9 @@ export default class RichTextMenu extends AntimodeMenu {
@action
changeView(view: EditorView) {
- this.view = view;
+ if ((view as any)?.TextView?.props.isSelected(true)) {
+ this.view = view;
+ }
}
update(view: EditorView, lastState: EditorState | undefined) {
@@ -162,8 +167,7 @@ export default class RichTextMenu extends AntimodeMenu {
@action
public async updateFromDash(view: EditorView, lastState: EditorState | undefined, props: any) {
- if (!view) {
- console.log("no editor? why?");
+ if (!view || !(view as any).TextView?.props.isSelected(true)) {
return;
}
this.view = view;
@@ -178,11 +182,13 @@ export default class RichTextMenu extends AntimodeMenu {
// update active font family and size
const active = this.getActiveFontStylesOnSelection();
- const activeFamilies = active?.get("families");
- const activeSizes = active?.get("sizes");
+ const activeFamilies = active.activeFamilies;
+ const activeSizes = active.activeSizes;
- this.activeFontFamily = !activeFamilies?.length ? "Arial" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various";
- this.activeFontSize = !activeSizes?.length ? "13pt" : activeSizes.length === 1 ? String(activeSizes[0]) : "various";
+ this.activeListType = this.getActiveListStyle();
+ this.activeAlignment = this.getActiveAlignment();
+ this.activeFontFamily = !activeFamilies.length ? "Arial" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various";
+ this.activeFontSize = !activeSizes.length ? "13pt" : activeSizes.length === 1 ? String(activeSizes[0]) : "...";
// update link in current selection
const targetTitle = await this.getTextLinkTargetTitle();
@@ -213,25 +219,56 @@ export default class RichTextMenu extends AntimodeMenu {
}
// finds font sizes and families in selection
+ getActiveAlignment() {
+ if (this.view && this.TextView.props.isSelected(true)) {
+ const path = (this.view.state.selection.$from as any).path;
+ for (let i = path.length - 3; i < path.length && i >= 0; i -= 3) {
+ if (path[i]?.type === this.view.state.schema.nodes.paragraph || path[i]?.type === this.view.state.schema.nodes.heading) {
+ return path[i].attrs.align || "left";
+ }
+ }
+ }
+ return "left";
+ }
+
+ // finds font sizes and families in selection
+ getActiveListStyle() {
+ if (this.view && this.TextView.props.isSelected(true)) {
+ const path = (this.view.state.selection.$from as any).path;
+ for (let i = 0; i < path.length; i += 3) {
+ if (path[i].type === this.view.state.schema.nodes.ordered_list) {
+ return path[i].attrs.mapStyle;
+ }
+ }
+ if (this.view.state.selection.$from.nodeAfter?.type === this.view.state.schema.nodes.ordered_list) {
+ return this.view.state.selection.$from.nodeAfter?.attrs.mapStyle;
+ }
+ }
+ return "";
+ }
+
+ // finds font sizes and families in selection
getActiveFontStylesOnSelection() {
- if (!this.view) return;
+ if (!this.view) return { activeFamilies: [], activeSizes: [] };
const activeFamilies: string[] = [];
const activeSizes: string[] = [];
- const state = this.view.state;
- const pos = this.view.state.selection.$from;
- const ref_node = this.reference_node(pos);
- if (ref_node && ref_node !== this.view.state.doc && ref_node.isText) {
- ref_node.marks.forEach(m => {
- m.type === state.schema.marks.pFontFamily && activeFamilies.push(m.attrs.family);
- m.type === state.schema.marks.pFontSize && activeSizes.push(String(m.attrs.fontSize) + "pt");
- });
+ if (this.TextView.props.isSelected(true)) {
+ const state = this.view.state;
+ const pos = this.view.state.selection.$from;
+ const ref_node = this.reference_node(pos);
+ if (ref_node && ref_node !== this.view.state.doc && ref_node.isText) {
+ ref_node.marks.forEach(m => {
+ m.type === state.schema.marks.pFontFamily && activeFamilies.push(m.attrs.family);
+ m.type === state.schema.marks.pFontSize && activeSizes.push(String(m.attrs.fontSize) + "pt");
+ });
+ }
+ !activeFamilies.length && (activeFamilies.push(StrCast(this.TextView.layoutDoc._fontFamily, StrCast(Doc.UserDoc().fontFamily))));
+ !activeSizes.length && (activeSizes.push(StrCast(this.TextView.layoutDoc._fontSize, StrCast(Doc.UserDoc().fontSize))));
}
-
- const styles = new Map<String, String[]>();
- styles.set("families", activeFamilies);
- styles.set("sizes", activeSizes);
- return styles;
+ !activeFamilies.length && (activeFamilies.push(StrCast(Doc.UserDoc().fontFamily)));
+ !activeSizes.length && (activeSizes.push(StrCast(Doc.UserDoc().fontSize)));
+ return { activeFamilies, activeSizes };
}
getMarksInSelection(state: EditorState<any>) {
@@ -243,14 +280,14 @@ export default class RichTextMenu extends AntimodeMenu {
//finds all active marks on selection in given group
getActiveMarksOnSelection() {
- if (!this.view) return;
+ let activeMarks: MarkType[] = [];
+ if (!this.view || !this.TextView.props.isSelected(true)) return activeMarks;
const markGroup = [schema.marks.strong, schema.marks.em, schema.marks.underline, schema.marks.strikethrough, schema.marks.superscript, schema.marks.subscript];
if (this.view.state.storedMarks) return this.view.state.storedMarks.map(mark => mark.type);
//current selection
const { empty, ranges, $to } = this.view.state.selection as TextSelection;
const state = this.view.state;
- let activeMarks: MarkType[] = [];
if (!empty) {
activeMarks = markGroup.filter(mark => {
const has = false;
@@ -282,7 +319,7 @@ export default class RichTextMenu extends AntimodeMenu {
}
destroy() {
- this.fadeOut(true);
+ !this.TextView?.props.isSelected(true) && this.fadeOut(true);
}
@action
@@ -322,22 +359,20 @@ export default class RichTextMenu extends AntimodeMenu {
}
return (
- <button className={"antimodeMenu-button" + (isActive ? " active" : "")} key={title} title={title} onPointerDown={onClick}>
- <FontAwesomeIcon icon={faIcon as IconProp} size="lg" />
- </button>
+ <Tooltip title={<div className="dash-tooltip">{title}</div>} key={title} placement="bottom">
+ <button className={"antimodeMenu-button" + (isActive ? " active" : "")} onPointerDown={onClick}>
+ <FontAwesomeIcon icon={faIcon as IconProp} size="lg" />
+ </button>
+ </Tooltip>
);
}
- createMarksDropdown(activeOption: string, options: { mark: Mark | null, title: string, label: string, command: (mark: Mark, view: EditorView) => void, hidden?: boolean, style?: {} }[], key: string): JSX.Element {
+ createMarksDropdown(activeOption: string, options: { mark: Mark | null, title: string, label: string, command: (mark: Mark, view: EditorView) => void, hidden?: boolean, style?: {} }[], key: string, setter: (val: string) => {}): JSX.Element {
const items = options.map(({ title, label, hidden, style }) => {
if (hidden) {
- return label === activeOption ?
- <option value={label} title={title} key={label} style={style ? style : {}} selected hidden>{label}</option> :
- <option value={label} title={title} key={label} style={style ? style : {}} hidden>{label}</option>;
+ return <option value={label} title={title} key={label} style={style ? style : {}} hidden>{label}</option>;
}
- return label === activeOption ?
- <option value={label} title={title} key={label} style={style ? style : {}} selected>{label}</option> :
- <option value={label} title={title} key={label} style={style ? style : {}}>{label}</option>;
+ return <option value={label} title={title} key={label} style={style ? style : {}}>{label}</option>;
});
const self = this;
@@ -346,36 +381,47 @@ export default class RichTextMenu extends AntimodeMenu {
e.preventDefault();
self.TextView.endUndoTypingBatch();
options.forEach(({ label, mark, command }) => {
- if (e.target.value === label) {
- UndoManager.RunInBatch(() => self.view && mark && command(mark, self.view), "text mark dropdown");
+ if (e.target.value === label && mark) {
+ if (!self.TextView.props.isSelected(true)) {
+ switch (mark.type) {
+ case schema.marks.pFontFamily: setter(Doc.UserDoc().fontFamily = mark.attrs.family); break;
+ case schema.marks.pFontSize: setter(Doc.UserDoc().fontSize = mark.attrs.fontSize.toString() + "pt"); break;
+ }
+ }
+ else UndoManager.RunInBatch(() => self.view && mark && command(mark, self.view), "text mark dropdown");
}
});
}
- return <select onChange={onChange} key={key}>{items}</select>;
+ return <Tooltip key={key} title={<div className="dash-tooltip">{key}</div>} placement="bottom">
+ <select onChange={onChange} value={activeOption}>{items}</select>
+ </Tooltip>;
}
- createNodesDropdown(activeOption: string, options: { node: NodeType | any | null, title: string, label: string, command: (node: NodeType | any) => void, hidden?: boolean, style?: {} }[], key: string): JSX.Element {
+ createNodesDropdown(activeMap: string, options: { node: NodeType | any | null, title: string, label: string, command: (node: NodeType | any) => void, hidden?: boolean, style?: {} }[], key: string, setter: (val: string) => {}): JSX.Element {
+ const activeOption = activeMap === "bullet" ? ":" : activeMap === "decimal" ? "1.1" : activeMap === "multi" ? "A.1" : "<none>";
const items = options.map(({ title, label, hidden, style }) => {
if (hidden) {
- return label === activeOption ?
- <option value={label} title={title} key={label} style={style ? style : {}} selected hidden>{label}</option> :
- <option value={label} title={title} key={label} style={style ? style : {}} hidden>{label}</option>;
+ return <option value={label} title={title} key={label} style={style ? style : {}} hidden>{label}</option>;
}
- return label === activeOption ?
- <option value={label} title={title} key={label} style={style ? style : {}} selected>{label}</option> :
- <option value={label} title={title} key={label} style={style ? style : {}}>{label}</option>;
+ return <option value={label} title={title} key={label} style={style ? style : {}}>{label}</option>;
});
const self = this;
function onChange(val: string) {
self.TextView.endUndoTypingBatch();
options.forEach(({ label, node, command }) => {
- if (val === label) {
- UndoManager.RunInBatch(() => self.view && node && command(node), "nodes dropdown");
+ if (val === label && node) {
+ if (self.TextView.props.isSelected(true)) {
+ UndoManager.RunInBatch(() => self.view && node && command(node), "nodes dropdown");
+ setter(val);
+ }
}
});
}
- return <select onChange={e => onChange(e.target.value)} key={key}>{items}</select>;
+
+ return <Tooltip key={key} title={<div className="dash-tooltip">{key}</div>} placement="bottom">
+ <select value={activeOption} onChange={e => onChange(e.target.value)}>{items}</select>
+ </Tooltip>;
}
changeFontSize = (mark: Mark, view: EditorView) => {
@@ -389,10 +435,21 @@ export default class RichTextMenu extends AntimodeMenu {
// TODO: remove doesn't work
//remove all node type and apply the passed-in one to the selected text
changeListType = (nodeType: Node | undefined) => {
- if (!this.view) return;
+ if (!this.view || (nodeType as any)?.attrs.mapStyle === "") return;
+
+ const nextIsOL = this.view.state.selection.$from.nodeAfter?.type === schema.nodes.ordered_list;
+ let inList: any = undefined;
+ let fromList = -1;
+ const path: any = Array.from((this.view.state.selection.$from as any).path);
+ for (let i = 0; i < path.length; i++) {
+ if (path[i]?.type === schema.nodes.ordered_list) {
+ inList = path[i];
+ fromList = path[i - 1];
+ }
+ }
const marks = this.view.state.storedMarks || (this.view.state.selection.$to.parentOffset && this.view.state.selection.$from.marks());
- if (!wrapInList(schema.nodes.ordered_list)(this.view.state, (tx2: any) => {
+ if (inList || !wrapInList(schema.nodes.ordered_list)(this.view.state, (tx2: any) => {
const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle, this.view!.state.selection.from - 1, this.view!.state.selection.to + 1);
marks && tx3.ensureMarks([...marks]);
marks && tx3.setStoredMarks([...marks]);
@@ -400,12 +457,12 @@ export default class RichTextMenu extends AntimodeMenu {
this.view!.dispatch(tx2);
})) {
const tx2 = this.view.state.tr;
- if (nodeType && this.view.state.selection.$from.nodeAfter?.type === schema.nodes.ordered_list) {
- const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle, this.view.state.selection.from, this.view.state.selection.to);
+ if (nodeType && (inList || nextIsOL)) {
+ const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle, inList ? fromList : this.view.state.selection.from,
+ inList ? fromList + inList.nodeSize : this.view.state.selection.to);
marks && tx3.ensureMarks([...marks]);
marks && tx3.setStoredMarks([...marks]);
-
- this.view.dispatch(tx3.setSelection(new NodeSelection(tx3.doc.resolve(this.view.state.selection.$from.pos))));
+ this.view.dispatch(tx3);
}
}
}
@@ -421,19 +478,19 @@ export default class RichTextMenu extends AntimodeMenu {
return true;
}
alignCenter = (state: EditorState<any>, dispatch: any) => {
- return this.alignParagraphs(state, "center", dispatch);
+ return this.TextView.props.isSelected(true) && this.alignParagraphs(state, "center", dispatch);
}
alignLeft = (state: EditorState<any>, dispatch: any) => {
- return this.alignParagraphs(state, "left", dispatch);
+ return this.TextView.props.isSelected(true) && this.alignParagraphs(state, "left", dispatch);
}
alignRight = (state: EditorState<any>, dispatch: any) => {
- return this.alignParagraphs(state, "right", dispatch);
+ return this.TextView.props.isSelected(true) && this.alignParagraphs(state, "right", dispatch);
}
alignParagraphs(state: EditorState<any>, align: "left" | "right" | "center", dispatch: any) {
var tr = state.tr;
state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => {
- if (node.type === schema.nodes.paragraph) {
+ if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) {
tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, align }, node.marks);
return false;
}
@@ -446,7 +503,7 @@ export default class RichTextMenu extends AntimodeMenu {
insetParagraph(state: EditorState<any>, dispatch: any) {
var tr = state.tr;
state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => {
- if (node.type === schema.nodes.paragraph) {
+ if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) {
const inset = (node.attrs.inset ? Number(node.attrs.inset) : 0) + 10;
tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, inset }, node.marks);
return false;
@@ -459,7 +516,7 @@ export default class RichTextMenu extends AntimodeMenu {
outsetParagraph(state: EditorState<any>, dispatch: any) {
var tr = state.tr;
state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => {
- if (node.type === schema.nodes.paragraph) {
+ if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) {
const inset = Math.max(0, (node.attrs.inset ? Number(node.attrs.inset) : 0) - 10);
tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, inset }, node.marks);
return false;
@@ -472,8 +529,9 @@ export default class RichTextMenu extends AntimodeMenu {
indentParagraph(state: EditorState<any>, dispatch: any) {
var tr = state.tr;
+ let headin = false;
state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => {
- if (node.type === schema.nodes.paragraph) {
+ if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) {
const nodeval = node.attrs.indent ? Number(node.attrs.indent) : undefined;
const indent = !nodeval ? 25 : nodeval < 0 ? 0 : nodeval + 25;
tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, indent }, node.marks);
@@ -481,14 +539,14 @@ export default class RichTextMenu extends AntimodeMenu {
}
return true;
});
- dispatch?.(tr);
+ !headin && dispatch?.(tr);
return true;
}
hangingIndentParagraph(state: EditorState<any>, dispatch: any) {
var tr = state.tr;
state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => {
- if (node.type === schema.nodes.paragraph) {
+ if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) {
const nodeval = node.attrs.indent ? Number(node.attrs.indent) : undefined;
const indent = !nodeval ? -25 : nodeval > 0 ? 0 : nodeval - 10;
tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, indent }, node.marks);
@@ -547,10 +605,11 @@ export default class RichTextMenu extends AntimodeMenu {
label = "No marks are currently stored";
}
- const button =
- <button className="antimodeMenu-button" title="" onPointerDown={onBrushClick} style={this.brushMarks?.size > 0 ? { backgroundColor: "121212" } : {}}>
+ const button = <Tooltip title={<div className="dash-tooltip">style brush</div>} placement="bottom">
+ <button className="antimodeMenu-button" onPointerDown={onBrushClick} style={this.brushMarks?.size > 0 ? { backgroundColor: "121212" } : {}}>
<FontAwesomeIcon icon="paint-roller" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.brushMarks?.size > 0 ? 45 : 0}deg)` }} />
- </button>;
+ </button>
+ </Tooltip>;
const dropdownContent =
<div className="dropdown">
@@ -599,7 +658,7 @@ export default class RichTextMenu extends AntimodeMenu {
@action toggleColorDropdown() { this.showColorDropdown = !this.showColorDropdown; }
@action setActiveColor(color: string) { this.activeFontColor = color; }
- get TextView() { return (this.view as any).TextView as FormattedTextBox; }
+ get TextView() { return (this.view as any)?.TextView as FormattedTextBox; }
createColorButton() {
const self = this;
@@ -619,11 +678,12 @@ export default class RichTextMenu extends AntimodeMenu {
self.TextView.EditorView!.focus();
}
- const button =
- <button className="antimodeMenu-button color-preview-button" title="" onPointerDown={onColorClick}>
+ const button = <Tooltip title={<div className="dash-tooltip">set font color</div>} placement="bottom">
+ <button className="antimodeMenu-button color-preview-button" onPointerDown={onColorClick}>
<FontAwesomeIcon icon="palette" size="lg" />
<div className="color-preview" style={{ backgroundColor: this.activeFontColor }}></div>
- </button>;
+ </button>
+ </Tooltip>;
const dropdownContent =
<div className="dropdown" >
@@ -672,11 +732,12 @@ export default class RichTextMenu extends AntimodeMenu {
UndoManager.RunInBatch(() => self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch), "rt highlighter");
}
- const button =
- <button className="antimodeMenu-button color-preview-button" title="" key="highilghter-button" onPointerDown={onHighlightClick}>
+ const button = <Tooltip title={<div className="dash-tooltip">set highlight color</div>} placement="bottom">
+ <button className="antimodeMenu-button color-preview-button" key="highilghter-button" onPointerDown={onHighlightClick}>
<FontAwesomeIcon icon="highlighter" size="lg" />
<div className="color-preview" style={{ backgroundColor: this.activeHighlightColor }}></div>
- </button>;
+ </button>
+ </Tooltip>;
const dropdownContent =
<div className="dropdown">
@@ -715,7 +776,9 @@ export default class RichTextMenu extends AntimodeMenu {
const link = this.currentLink ? this.currentLink : "";
- const button = <FontAwesomeIcon icon="link" size="lg" />;
+ const button = <Tooltip title={<div className="dash-tooltip">set hyperlink</div>} placement="bottom">
+ <div><FontAwesomeIcon icon="link" size="lg" /> </div>
+ </Tooltip>;
const dropdownContent =
<div className="dropdown link-menu">
@@ -726,9 +789,7 @@ export default class RichTextMenu extends AntimodeMenu {
<button className="remove-button" onPointerDown={e => this.deleteLink()}>Remove link</button>
</div>;
- return (
- <ButtonDropdown view={this.view} key={"link button"} button={button} dropdownContent={dropdownContent} openDropdownOnButton={true} />
- );
+ return <ButtonDropdown view={this.view} key={"link button"} button={button} dropdownContent={dropdownContent} openDropdownOnButton={true} />;
}
async getTextLinkTargetTitle() {
@@ -767,10 +828,13 @@ export default class RichTextMenu extends AntimodeMenu {
}
// TODO: should check for valid URL
+ @undoBatch
makeLinkToURL = (target: string, lcoation: string) => {
((this.view as any)?.TextView as FormattedTextBox).makeLinkToSelection("", target, "onRight", "", target);
}
+ @undoBatch
+ @action
deleteLink = () => {
if (this.view) {
const link = this.view.state.selection.$from.nodeAfter?.marks.find(m => m.type === this.view!.state.schema.marks.linkAnchor);
@@ -855,45 +919,45 @@ export default class RichTextMenu extends AntimodeMenu {
render() {
TraceMobx();
- const row1 = <div className="antimodeMenu-row" key="row1" style={{ display: this.collapsed ? "none" : undefined }}>{[
+ const row1 = <div className="antimodeMenu-row" key="row 1" style={{ display: this.collapsed ? "none" : undefined }}>{[
!this.collapsed ? this.getDragger() : (null),
- !this.Pinned ? (null) : <> {[
+ !this.Pinned ? (null) : <div key="frag1"> {[
this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)),
this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)),
this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)),
this.createButton("strikethrough", "Strikethrough", this.strikethroughActive, toggleMark(schema.marks.strikethrough)),
this.createButton("superscript", "Superscript", this.superscriptActive, toggleMark(schema.marks.superscript)),
this.createButton("subscript", "Subscript", this.subscriptActive, toggleMark(schema.marks.subscript)),
- <div className="richTextMenu-divider" />
- ]}</>,
+ <div className="richTextMenu-divider" key="divider" />
+ ]}</div>,
this.createColorButton(),
this.createHighlighterButton(),
this.createLinkButton(),
this.createBrushButton(),
- <div className="richTextMenu-divider" />,
- this.createButton("align-left", "Align Left", undefined, this.alignLeft),
- this.createButton("align-center", "Align Center", undefined, this.alignCenter),
- this.createButton("align-right", "Align Right", undefined, this.alignRight),
+ <div className="richTextMenu-divider" key="divider 2" />,
+ this.createButton("align-left", "Align Left", this.activeAlignment === "left", this.alignLeft),
+ this.createButton("align-center", "Align Center", this.activeAlignment === "center", this.alignCenter),
+ this.createButton("align-right", "Align Right", this.activeAlignment === "right", this.alignRight),
this.createButton("indent", "Inset More", undefined, this.insetParagraph),
this.createButton("outdent", "Inset Less", undefined, this.outsetParagraph),
this.createButton("hand-point-left", "Hanging Indent", undefined, this.hangingIndentParagraph),
this.createButton("hand-point-right", "Indent", undefined, this.indentParagraph),
]}</div>;
- const row2 = <div className="antimodeMenu-row row-2" key="antimodemenu row2">
+ const row2 = <div className="antimodeMenu-row row-2" key="row2">
{this.collapsed ? this.getDragger() : (null)}
- <div key="row" style={{ display: this.collapsed ? "none" : undefined }}>
- <div className="richTextMenu-divider" />,
- {[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions, "font size"),
- this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family"),
- <div className="richTextMenu-divider" />,
- this.createNodesDropdown(this.activeListType, this.listTypeOptions, "nodes"),
+ <div key="row 2" style={{ display: this.collapsed ? "none" : undefined }}>
+ <div className="richTextMenu-divider" key="divider 3" />,
+ {[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions, "font size", action((val: string) => this.activeFontSize = val)),
+ this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family", action((val: string) => this.activeFontFamily = val)),
+ <div className="richTextMenu-divider" key="divider 4" />,
+ this.createNodesDropdown(this.activeListType, this.listTypeOptions, "list type", action((val: string) => this.activeListType = val)),
this.createButton("sort-amount-down", "Summarize", undefined, this.insertSummarizer),
this.createButton("quote-left", "Blockquote", undefined, this.insertBlockquote),
this.createButton("minus", "Horizontal Rule", undefined, this.insertHorizontalRule),
- <div className="richTextMenu-divider" />,]}
+ <div className="richTextMenu-divider" key="divider 5" />,]}
</div>
- <div key="button">
+ <div key="collapser">
{/* <div key="collapser">
<button className="antimodeMenu-button" key="collapse menu" title="Collapse menu" onClick={this.toggleCollapse} style={{ backgroundColor: this.collapsed ? "#121212" : "", width: 25 }}>
<FontAwesomeIcon icon="chevron-left" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.3s", transform: `rotate(${this.collapsed ? 180 : 0}deg)` }} />
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index ca30dde9d..dc1d8a2c8 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -90,7 +90,7 @@ export class RichTextRules {
textDoc.inlineTextCount = numInlines + 1;
const inlineFieldKey = "inline" + numInlines; // which field on the text document this annotation will write to
const inlineLayoutKey = "layout_" + inlineFieldKey; // the field holding the layout string that will render the inline annotation
- const textDocInline = Docs.Create.TextDocument("", { layoutKey: inlineLayoutKey, _width: 75, _height: 35, annotationOn: textDoc, _autoHeight: true, _fontSize: 9, title: "inline comment" });
+ const textDocInline = Docs.Create.TextDocument("", { layoutKey: inlineLayoutKey, _width: 75, _height: 35, annotationOn: textDoc, _autoHeight: true, _fontSize: "9pt", title: "inline comment" });
textDocInline.title = inlineFieldKey; // give the annotation its own title
textDocInline.customTitle = true; // And make sure that it's 'custom' so that editing text doesn't change the title of the containing doc
textDocInline.isTemplateForField = inlineFieldKey; // this is needed in case the containing text doc is converted to a template at some point
@@ -317,13 +317,12 @@ export class RichTextRules {
// create an inline view of a tag stored under the '#' field
new InputRule(
- new RegExp(/#([a-zA-Z_\-]+[a-zA-Z_;\-0-9]*)\s$/),
+ new RegExp(/#([a-zA-Z_\-]+[a-zA-Z_\-0-9]*)\s$/),
(state, match, start, end) => {
const tag = match[1];
if (!tag) return state.tr;
- const multiple = tag.split(";");
- this.Document[DataSym]["#"] = multiple.length > 1 ? new List(multiple) : tag;
- const fieldView = state.schema.nodes.dashField.create({ fieldKey: "#" });
+ this.Document[DataSym]["#" + tag] = ".";
+ const fieldView = state.schema.nodes.dashField.create({ fieldKey: "#" + tag });
return state.tr.deleteRange(start, end).insert(start, fieldView);
}),
diff --git a/src/client/views/nodes/formattedText/RichTextSchema.tsx b/src/client/views/nodes/formattedText/RichTextSchema.tsx
index a989abd6a..33a080fe4 100644
--- a/src/client/views/nodes/formattedText/RichTextSchema.tsx
+++ b/src/client/views/nodes/formattedText/RichTextSchema.tsx
@@ -54,6 +54,8 @@ export class DashDocView {
this._dashSpan.style.height = node.attrs.height;
this._dashSpan.style.position = "absolute";
this._dashSpan.style.display = "inline-block";
+ this._dashSpan.style.left = "0";
+ this._dashSpan.style.top = "0";
this._dashSpan.style.whiteSpace = "normal";
this._dashSpan.onpointerleave = () => {
@@ -160,9 +162,15 @@ export class DashDocView {
if (node.attrs.width !== dashDoc._width + "px" || node.attrs.height !== dashDoc._height + "px") {
try { // bcz: an exception will be thrown if two aliases are open at the same time when a doc view comment is made
- view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: dashDoc._width + "px", height: dashDoc._height + "px" }));
+ if (getPos() !== undefined) {
+ const node = view.state.tr.doc.nodeAt(getPos());
+ if (node.attrs.width !== dashDoc._width + "px" ||
+ node.attrs.height !== dashDoc._height + "px") {
+ view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: dashDoc._width + "px", height: dashDoc._height + "px" }));
+ }
+ }
} catch (e) {
- console.log(e);
+ console.log("RichTextSchema: " + e);
}
}
};
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
index 3d7d71b14..bcd6f716b 100644
--- a/src/client/views/nodes/formattedText/marks_rts.ts
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -40,7 +40,7 @@ export const marks: { [index: string]: MarkSpec } = {
return node.attrs.docref && node.attrs.title ?
["div", ["span", `"`], ["span", 0], ["span", `"`], ["br"], ["a", { ...node.attrs, href: node.attrs.allLinks[0].href, class: "prosemirror-attribution" }, node.attrs.title], ["br"]] :
node.attrs.allLinks.length === 1 ?
- ["a", { ...node.attrs, class: linkids, targetids, title: `${node.attrs.title}`, href: node.attrs.allLinks[0].href }, 0] :
+ ["a", { ...node.attrs, class: linkids, targetids, style: `text-decoration: ${linkids === " " ? "underline" : undefined}`, title: `${node.attrs.title}`, href: node.attrs.allLinks[0].href }, 0] :
["div", { class: "prosemirror-anchor" },
["span", { class: "prosemirror-linkBtn" },
["a", { ...node.attrs, class: linkids, targetids, title: `${node.attrs.title}` }, 0],
@@ -258,9 +258,7 @@ export const marks: { [index: string]: MarkSpec } = {
},
parseDOM: [{ style: 'background: yellow' }],
toDOM(node: any) {
- return ['span', {
- style: `background: ${node.attrs.selected ? "orange" : "yellow"}`
- }];
+ return ['span', { style: `background: ${node.attrs.selected ? "orange" : "yellow"}` }];
}
},
@@ -277,8 +275,8 @@ export const marks: { [index: string]: MarkSpec } = {
const min = Math.round(node.attrs.modified / 12);
const hr = Math.round(min / 60);
const day = Math.round(hr / 60 / 24);
- const remote = node.attrs.userid !== Doc.CurrentUserEmail ? " userMark-remote" : "";
- return ['span', { class: "userMark-" + uid + remote + " userMark-min-" + min + " userMark-hr-" + hr + " userMark-day-" + day }, 0];
+ const remote = node.attrs.userid !== Doc.CurrentUserEmail ? " UM-remote" : "";
+ return ['span', { class: "UM-" + uid + remote + " UM-min-" + min + " UM-hr-" + hr + " UM-day-" + day }, 0];
}
},
// the id of the user who entered the text
@@ -292,7 +290,7 @@ export const marks: { [index: string]: MarkSpec } = {
inclusive: false,
toDOM(node: any) {
const uid = node.attrs.userid.replace(".", "").replace("@", "");
- return ['span', { class: "userTag-" + uid + " userTag-" + node.attrs.tag }, 0];
+ return ['span', { class: "UT-" + uid + " UT-" + node.attrs.tag }, 0];
}
},
diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts
index f83cff9b9..0eca6d753 100644
--- a/src/client/views/nodes/formattedText/nodes_rts.ts
+++ b/src/client/views/nodes/formattedText/nodes_rts.ts
@@ -66,9 +66,11 @@ export const nodes: { [index: string]: NodeSpec } = {
// should hold the number 1 to 6. Parsed and serialized as `<h1>` to
// `<h6>` elements.
heading: {
- attrs: { level: { default: 1 } },
- content: "inline*",
- group: "block",
+ ...ParagraphNodeSpec,
+ attrs: {
+ ...ParagraphNodeSpec.attrs,
+ level: { default: 1 },
+ },
defining: true,
parseDOM: [{ tag: "h1", attrs: { level: 1 } },
{ tag: "h2", attrs: { level: 2 } },
@@ -76,7 +78,18 @@ export const nodes: { [index: string]: NodeSpec } = {
{ tag: "h4", attrs: { level: 4 } },
{ tag: "h5", attrs: { level: 5 } },
{ tag: "h6", attrs: { level: 6 } }],
- toDOM(node: any) { return ["h" + node.attrs.level, 0]; }
+ toDOM(node) {
+ var dom = toParagraphDOM(node) as any;
+ var level = node.attrs.level || 1;
+ dom[0] = 'h' + level;
+ return dom;
+ },
+ getAttrs(dom: any) {
+ var attrs = getParagraphNodeAttrs(dom) as any;
+ var level = Number(dom.nodeName.substring(1)) || 1;
+ attrs.level = level;
+ return attrs;
+ }
},
// :: NodeSpec A code listing. Disallows marks or non-text inline
@@ -310,9 +323,9 @@ export const nodes: { [index: string]: NodeSpec } = {
}],
toDOM(node: any) {
const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : "";
- return node.attrs.visibility ?
- ["li", { class: `${map}`, "data-mapStyle": node.attrs.mapStyle, "data-bulletStyle": node.attrs.bulletStyle }, 0] :
- ["li", { class: `${map}`, "data-mapStyle": node.attrs.mapStyle, "data-bulletStyle": node.attrs.bulletStyle }, `${node.firstChild?.textContent}...`];
+ return ["li", { class: `${map}`, "data-mapStyle": node.attrs.mapStyle, "data-bulletStyle": node.attrs.bulletStyle }, node.attrs.visibility ? 0 :
+ ["span", { style: `position: relative; width: 100%; height: 1.5em; overflow: hidden; display: ${node.attrs.mapStyle !== "bullet" ? "inline-block" : "list-item"}; text-overflow: ellipsis; white-space: pre` },
+ `${node.firstChild?.textContent}...`]];
}
},
}; \ No newline at end of file
diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx
index 00c56d73e..c3e1ae22f 100644
--- a/src/client/views/pdf/PDFMenu.tsx
+++ b/src/client/views/pdf/PDFMenu.tsx
@@ -47,7 +47,7 @@ export default class PDFMenu extends AntimodeMenu {
public AddTag: (key: string, value: string) => boolean = returnFalse;
public PinToPres: () => void = unimplementedFunction;
public Marquee: { left: number; top: number; width: number; height: number; } | undefined;
- public get Active() { return this._opacity ? true : false; }
+ public get Active() { return this._left > 0; }
constructor(props: Readonly<{}>) {
super(props);
@@ -84,11 +84,6 @@ export default class PDFMenu extends AntimodeMenu {
e.preventDefault();
}
- togglePin = action((e: React.MouseEvent) => {
- this.Pinned = !this.Pinned;
- !this.Pinned && (this.Highlighting = false);
- });
-
@action
highlightClicked = (e: React.MouseEvent) => {
if (!this.Highlight(this.highlightColor) && this.Pinned) {
@@ -161,8 +156,6 @@ export default class PDFMenu extends AntimodeMenu {
this.highlighter,
<button key="2" className="antimodeMenu-button" title="Drag to Annotate" ref={this._commentCont} onPointerDown={this.pointerDown}>
<FontAwesomeIcon icon="comment-alt" size="lg" /></button>,
- <button key="4" className="antimodeMenu-button" title="Pin Menu" onClick={this.togglePin} style={this.Pinned ? { backgroundColor: "#121212" } : {}}>
- <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transition: "transform 0.1s", transform: this.Pinned ? "rotate(45deg)" : "" }} /></button>,
] : [
<button key="5" className="antimodeMenu-button" title="Delete Anchor" onPointerDown={this.deleteClicked}>
<FontAwesomeIcon icon="trash-alt" size="lg" /></button>,
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 98f64edec..94a052824 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -97,7 +97,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
@observable private _zoomed = 1;
private _pdfViewer: any;
- private _retries = 0; // number of times tried to create the PDF viewer
+ private _retries = 0; // number of times tried to create the PDF viewer
private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void);
private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
private _reactionDisposer?: IReactionDisposer;
@@ -263,7 +263,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
const eventBus = new PDFJSViewer.EventBus(true);
eventBus._on("pagesinit", this.pagesinit);
eventBus._on("pagerendered", action(() => this._showCover = this._showWaiting = false));
- const pdfLinkService = new PDFJSViewer.PDFLinkService();
+ const pdfLinkService = new PDFJSViewer.PDFLinkService({ eventBus });
const pdfFindController = new PDFJSViewer.PDFFindController({ linkService: pdfLinkService, eventBus });
this._pdfViewer = new PDFJSViewer.PDFViewer({
container: this._mainCont.current,
@@ -353,7 +353,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
@action
scrollToAnnotation = (scrollToAnnotation: Doc) => {
if (scrollToAnnotation) {
- const offset = this.visibleHeight() / 2 * 96 / 72;
+ const offset = this.visibleHeight() / 2;
this._mainCont.current && smoothScroll(500, this._mainCont.current, NumCast(scrollToAnnotation.y) - offset);
Doc.linkFollowHighlight(scrollToAnnotation);
}
@@ -399,7 +399,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
@action
search = (searchString: string, fwd: boolean, clear: boolean = false) => {
if (clear) {
- this._pdfViewer.findController.executeCommand('reset', {});
+ this._pdfViewer.findController.executeCommand('reset', { query: "" });
} else if (!searchString) {
fwd ? this.nextAnnotation() : this.prevAnnotation();
} else if (this._pdfViewer.pageViewsReady) {
@@ -533,7 +533,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
if (this._marqueeing) {
if (this._marqueeWidth > 10 || this._marqueeHeight > 10) {
const marquees = this._mainCont.current!.getElementsByClassName("pdfViewerDash-dragAnnotationBox");
- if (marquees && marquees.length) { // copy the marquee and convert it to a permanent annotation.
+ if (marquees?.length) { // copy the marquee and convert it to a permanent annotation.
const style = (marquees[0] as HTMLDivElement).style;
const copy = document.createElement("div");
copy.style.left = style.left;
@@ -556,7 +556,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
}
else {
const sel = window.getSelection();
- if (sel && sel.type === "Range") {
+ if (sel?.type === "Range") {
const selRange = sel.getRangeAt(0);
this.createTextAnnotation(sel, selRange);
PDFMenu.Instance.jumpTo(e.clientX, e.clientY);
@@ -649,7 +649,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
@action
onZoomWheel = (e: React.WheelEvent) => {
- if (this.active()) {
+ if (this.active(true)) {
e.stopPropagation();
if (e.ctrlKey) {
const curScale = Number(this._pdfViewer.currentScaleValue);
@@ -703,7 +703,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
</div>;
}
@computed get pdfViewerDiv() {
- return <div className={"pdfViewerDash-text" + ((!DocumentDecorations.Instance.Interacting && (this.props.isSelected() || this.props.isChildActive())) ? "-selected" : "")} ref={this._viewer} />;
+ return <div className={"pdfViewerDash-text" + ((!DocumentDecorations.Instance?.Interacting && (this.props.isSelected() || this.props.isChildActive())) ? "-selected" : "")} ref={this._viewer} />;
}
@computed get contentScaling() { return this.props.ContentScaling(); }
@computed get standinViews() {
@@ -717,7 +717,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
marqueeX = () => this._marqueeX;
marqueeY = () => this._marqueeY;
marqueeing = () => this._marqueeing;
- visibleHeight = () => this.props.PanelHeight() / this.props.ContentScaling() * 72 / 96;
+ visibleHeight = () => this.props.PanelHeight() / this.props.ContentScaling();
contentZoom = () => this._zoomed;
render() {
TraceMobx();
@@ -725,8 +725,8 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
onScroll={this.onScroll} onWheel={this.onZoomWheel} onPointerDown={this.onPointerDown} onClick={this.onClick}
style={{
overflowX: this._zoomed !== 1 ? "scroll" : undefined,
- width: !this.props.Document._fitWidth ? NumCast(this.props.Document._nativeWidth) : `${100 / this.contentScaling}%`,
- height: !this.props.Document._fitWidth ? NumCast(this.props.Document._nativeHeight) : `${100 / this.contentScaling}%`,
+ width: !this.props.Document._fitWidth && (window.screen.width > 600) ? NumCast(this.props.Document._nativeWidth) : `${100 / this.contentScaling}%`,
+ height: !this.props.Document._fitWidth && (window.screen.width > 600) ? NumCast(this.props.Document._nativeHeight) : `${100 / this.contentScaling}%`,
transform: `scale(${this.props.ContentScaling()})`
}} >
{this.pdfViewerDiv}
@@ -738,7 +738,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
}
}
-interface PdfViewerMarqueeProps {
+export interface PdfViewerMarqueeProps {
isMarqueeing: () => boolean;
width: () => number;
height: () => number;
@@ -747,7 +747,7 @@ interface PdfViewerMarqueeProps {
}
@observer
-class PdfViewerMarquee extends React.Component<PdfViewerMarqueeProps> {
+export class PdfViewerMarquee extends React.Component<PdfViewerMarqueeProps> {
render() {
return !this.props.isMarqueeing() ? (null) : <div className="pdfViewerDash-dragAnnotationBox"
style={{
@@ -758,4 +758,4 @@ class PdfViewerMarquee extends React.Component<PdfViewerMarqueeProps> {
}}>
</div>;
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/presentationview/PresElementBox.scss b/src/client/views/presentationview/PresElementBox.scss
index ccd2e8947..dbe6b0d4f 100644
--- a/src/client/views/presentationview/PresElementBox.scss
+++ b/src/client/views/presentationview/PresElementBox.scss
@@ -13,9 +13,10 @@
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
- transition: all .1s;
+ transition: all .1s;
padding: 0px;
padding-bottom: 3px;
+
.documentView-node {
position: absolute;
z-index: 1;
@@ -45,7 +46,7 @@
.presElementBox-closeIcon {
border-radius: 20px;
- transform:scale(0.7);
+ transform: scale(0.7);
position: absolute;
right: 0;
top: 0;
@@ -58,6 +59,7 @@
position: relative;
width: 100%;
height: auto;
+
.presElementBox-interaction {
color: gray;
float: left;
@@ -65,6 +67,7 @@
width: 20px;
height: 20px;
}
+
.presElementBox-interaction-selected {
color: white;
float: left;
@@ -76,7 +79,7 @@
}
.presElementBox-name {
- font-size: 12pxππ;
+ font-size: 12px;
position: absolute;
display: inline-block;
width: calc(100% - 45px);
@@ -90,15 +93,58 @@
display: flex;
width: auto;
justify-content: center;
- margin:auto;
+ margin: auto;
}
.presElementBox-embeddedMask {
- width:100%;
- height:100%;
+ width: 100%;
+ height: 100%;
position: absolute;
- left:0;
- top:0;
+ left: 0;
+ top: 0;
background: transparent;
- z-index:2;
+ z-index: 2;
+}
+
+@media only screen and (max-device-width: 480px) {
+ .presElementBox-buttons {
+ display: inline-flex;
+ position: absolute;
+ top: 0;
+ right: 0;
+ z-index: 3;
+ width: 50%;
+
+ .presElementBox-interaction {
+ width: 50;
+ height: 50;
+ }
+
+ .presElementBox-interaction-selected {
+ width: 50;
+ height: 50;
+ }
+ }
+
+ .presElementBox-item {
+ display: inline-flex;
+ overflow: hidden;
+ }
+
+ .presElementBox-closeIcon {
+ transform: scale(1.5);
+ right: 10;
+ top: 10;
+ }
+
+ .presElementBox-name {
+ font-size: 30px;
+ top: 10px;
+ z-index: 3;
+ width: 50%;
+ }
+
+ .presElementBox-embedded {
+ transform: translate(0, 90px) scale(1.5);
+ }
} \ No newline at end of file
diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx
index 6b59a0563..4c0972736 100644
--- a/src/client/views/presentationview/PresElementBox.tsx
+++ b/src/client/views/presentationview/PresElementBox.tsx
@@ -15,6 +15,7 @@ import { FieldView, FieldViewProps } from '../nodes/FieldView';
import "./PresElementBox.scss";
import React = require("react");
import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView";
+import { DocumentType } from "../../documents/DocumentTypes";
export const presSchema = createSchema({
presentationTargetDoc: Doc,
@@ -49,7 +50,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
componentDidMount() {
this._heightDisposer = reaction(() => [this.rootDoc.presExpandInlineButton, this.collapsedHeight],
- params => this.layoutDoc._height = NumCast(params[1]) + (Number(params[0]) ? 100 : 0), { fireImmediately: true });
+ params => this.layoutDoc._height = NumCast(params[1]) + (Number(params[0]) ? 200 : 0), { fireImmediately: true });
}
componentWillUnmount() {
this._heightDisposer?.();
@@ -100,7 +101,8 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
e.stopPropagation();
this.rootDoc.presProgressivize = !this.rootDoc.presProgressivize;
const rootTarget = Cast(this.rootDoc.presentationTargetDoc, Doc, null);
- const docs = DocListCast(rootTarget[Doc.LayoutFieldKey(rootTarget)]);
+ const docs = rootTarget.type === DocumentType.COL ? DocListCast(rootTarget[Doc.LayoutFieldKey(rootTarget)]) :
+ DocListCast(rootTarget[Doc.LayoutFieldKey(rootTarget) + "-annotations"]);
if (this.rootDoc.presProgressivize) {
rootTarget.currentFrame = 0;
CollectionFreeFormDocumentView.setupKeyframes(docs, docs.length, true);
diff --git a/src/client/views/search/FilterBox.tsx b/src/client/views/search/FilterBox.tsx
index 4b53963a5..eb61f9a14 100644
--- a/src/client/views/search/FilterBox.tsx
+++ b/src/client/views/search/FilterBox.tsx
@@ -352,7 +352,6 @@ export class FilterBox extends React.Component {
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">
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 005f7d1af..72cb3a04e 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -250,7 +250,7 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc
return returnedUri;
} catch (e) {
- console.log(e);
+ console.log("SearchBox:" + e);
}
}
@@ -910,7 +910,6 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc
// when the next css transition finishes (which should be the one we just triggered)
element.addEventListener('transitionend', function handler(e) {
// remove this event listener so it only gets triggered once
- console.log("autoset");
element.removeEventListener('transitionend', handler);
// remove "height" from the element's inline styles, so it can return to its initial value
@@ -925,7 +924,6 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc
autoset(thing: string) {
const element = document.getElementById(thing)!;
- console.log("autoset");
element.removeEventListener('transitionend', function (e) { });
// remove "height" from the element's inline styles, so it can return to its initial value
diff --git a/src/client/views/search/ToggleBar.tsx b/src/client/views/search/ToggleBar.tsx
index e4d7f2fd5..466822eba 100644
--- a/src/client/views/search/ToggleBar.tsx
+++ b/src/client/views/search/ToggleBar.tsx
@@ -58,7 +58,6 @@ 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/fields/Doc.ts b/src/fields/Doc.ts
index b2fc10b99..b78d5611c 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1,6 +1,6 @@
-import { action, computed, observable, ObservableMap, runInAction } from "mobx";
+import { action, computed, observable, ObservableMap, runInAction, untracked } from "mobx";
import { computedFn } from "mobx-utils";
-import { alias, map, serializable } from "serializr";
+import { alias, map, serializable, list } from "serializr";
import { DocServer } from "../client/DocServer";
import { DocumentType } from "../client/documents/DocumentTypes";
import { Scripting, scriptingGlobal } from "../client/util/Scripting";
@@ -14,11 +14,15 @@ import { ObjectField } from "./ObjectField";
import { PrefetchProxy, ProxyField } from "./Proxy";
import { FieldId, RefField } from "./RefField";
import { RichTextField } from "./RichTextField";
+import { ImageField, VideoField, WebField, AudioField, PdfField } from "./URLField";
+import { DateField } from "./DateField";
import { listSpec } from "./Schema";
-import { ComputedField } from "./ScriptField";
+import { ComputedField, ScriptField } from "./ScriptField";
import { Cast, FieldValue, NumCast, StrCast, ToConstructor } from "./Types";
-import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction } from "./util";
+import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction, GetEffectiveAcl, SharingPermissions } from "./util";
import { LinkManager } from "../client/util/LinkManager";
+import JSZip = require("jszip");
+import { saveAs } from "file-saver";
export namespace Field {
export function toKeyValueString(doc: Doc, key: string): string {
@@ -92,31 +96,33 @@ export const WidthSym = Symbol("Width");
export const HeightSym = Symbol("Height");
export const DataSym = Symbol("Data");
export const LayoutSym = Symbol("Layout");
+export const FieldsSym = Symbol("Fields");
export const AclSym = Symbol("Acl");
export const AclPrivate = Symbol("AclOwnerOnly");
export const AclReadonly = Symbol("AclReadOnly");
export const AclAddonly = Symbol("AclAddonly");
-export const AclReadWrite = Symbol("AclReadWrite");
+export const AclEdit = Symbol("AclEdit");
+export const AclAdmin = Symbol("AclAdmin");
export const UpdatingFromServer = Symbol("UpdatingFromServer");
-const CachedUpdates = Symbol("Cached updates");
+export const CachedUpdates = Symbol("Cached updates");
+const AclMap = new Map<string, symbol>([
+ [SharingPermissions.None, AclPrivate],
+ [SharingPermissions.View, AclReadonly],
+ [SharingPermissions.Add, AclAddonly],
+ [SharingPermissions.Edit, AclEdit],
+ [SharingPermissions.Admin, AclAdmin]
+]);
export function fetchProto(doc: Doc) {
- if (doc.author !== Doc.CurrentUserEmail) {
- const acl = Doc.Get(doc, "ACL", true);
- switch (acl) {
- case "ownerOnly":
- doc[AclSym] = AclPrivate;
- return undefined;
- case "readOnly":
- doc[AclSym] = AclReadonly;
- break;
- case "addOnly":
- doc[AclSym] = AclAddonly;
- break;
- case "write":
- doc[AclSym] = AclReadWrite;
- }
+ const permissions: { [key: string]: symbol } = {};
+
+ Object.keys(doc).filter(key => key.startsWith("ACL")).forEach(key => permissions[key] = AclMap.get(StrCast(doc[key]))!);
+
+ if (Object.keys(permissions).length) doc[AclSym] = permissions;
+
+ if (GetEffectiveAcl(doc) === AclPrivate) {
+ runInAction(() => doc[FieldsSym](true));
}
if (doc.proto instanceof Promise) {
@@ -134,10 +140,10 @@ export class Doc extends RefField {
set: setter,
get: getter,
// getPrototypeOf: (target) => Cast(target[SelfProxy].proto, Doc) || null, // TODO this might be able to replace the proto logic in getter
- has: (target, key) => target[AclSym] !== AclPrivate && key in target.__fields,
+ has: (target, key) => GetEffectiveAcl(target) !== AclPrivate && key in target.__fields,
ownKeys: target => {
const obj = {} as any;
- if (target[AclSym] !== AclPrivate) Object.assign(obj, target.___fields);
+ if (GetEffectiveAcl(target) !== AclPrivate) Object.assign(obj, target.___fieldKeys);
runInAction(() => obj.__LAYOUT__ = target.__LAYOUT__);
return Object.keys(obj);
},
@@ -145,11 +151,11 @@ export class Doc extends RefField {
if (prop.toString() === "__LAYOUT__") {
return Reflect.getOwnPropertyDescriptor(target, prop);
}
- if (prop in target.__fields) {
+ if (prop in target.__fieldKeys) {
return {
configurable: true,//TODO Should configurable be true?
enumerable: true,
- value: target.__fields[prop]
+ value: 0//() => target.__fields[prop])
};
}
return Reflect.getOwnPropertyDescriptor(target, prop);
@@ -173,16 +179,23 @@ export class Doc extends RefField {
this.___fields = value;
for (const key in value) {
const field = value[key];
+ field && (this.__fieldKeys[key] = true);
if (!(field instanceof ObjectField)) continue;
field[Parent] = this[Self];
field[OnUpdate] = updateFunction(this[Self], key, field, this[SelfProxy]);
}
}
+ private get __fieldKeys() { return this.___fieldKeys; }
+ private set __fieldKeys(value) {
+ this.___fieldKeys = value;
+ }
@observable
- //{ [key: string]: Field | FieldWaiting | undefined }
private ___fields: any = {};
+ @observable
+ private ___fieldKeys: any = {};
+
private [UpdatingFromServer]: boolean = false;
private [Update] = (diff: any) => {
@@ -191,11 +204,19 @@ export class Doc extends RefField {
private [Self] = this;
private [SelfProxy]: any;
- public [AclSym]: any = undefined;
+ public [FieldsSym] = (clear?: boolean) => {
+ if (clear) {
+ this.___fields = {};
+ this.___fieldKeys = {};
+ }
+ return this.___fields;
+ }
+ @observable
+ public [AclSym]: { [key: string]: symbol };
public [WidthSym] = () => NumCast(this[SelfProxy]._width);
public [HeightSym] = () => NumCast(this[SelfProxy]._height);
public [ToScriptString]() { return `DOC-"${this[Self][Id]}"-`; }
- public [ToString]() { return `Doc(${this[AclSym] === AclPrivate ? "-inaccessible-" : this.title})`; }
+ public [ToString]() { return `Doc(${GetEffectiveAcl(this) === AclPrivate ? "-inaccessible-" : this.title})`; }
public get [LayoutSym]() { return this[SelfProxy].__LAYOUT__; }
public get [DataSym]() {
const self = this[SelfProxy];
@@ -215,8 +236,8 @@ export class Doc extends RefField {
return Cast(this[SelfProxy][renderFieldKey + "-layout[" + templateLayoutDoc[Id] + "]"], Doc, null) || templateLayoutDoc;
}
return undefined;
- }
+ }
private [CachedUpdates]: { [key: string]: () => void | Promise<any> } = {};
public static CurrentUserEmail: string = "";
@@ -231,11 +252,18 @@ export class Doc extends RefField {
const fKey = key.substring(7);
const fn = async () => {
const value = await SerializationHelper.Deserialize(set[key]);
+ const prev = GetEffectiveAcl(this);
this[UpdatingFromServer] = true;
this[fKey] = value;
+ if (fKey.startsWith("ACL")) {
+ fetchProto(this);
+ }
this[UpdatingFromServer] = false;
+ if (prev === AclPrivate && GetEffectiveAcl(this) !== AclPrivate) {
+ DocServer.GetRefField(this[Id], true);
+ }
};
- if (sameAuthor || DocServer.getFieldWriteMode(fKey) !== DocServer.WriteMode.Playground) {
+ if (sameAuthor || fKey.startsWith("ACL") || DocServer.getFieldWriteMode(fKey) !== DocServer.WriteMode.Playground) {
delete this[CachedUpdates][fKey];
await fn();
} else {
@@ -483,27 +511,28 @@ export namespace Doc {
return alias;
}
-
-
- export function makeClone(doc: Doc, cloneMap: Map<string, Doc>, rtfs: { copy: Doc, key: string, field: RichTextField }[]): Doc {
+ export async function makeClone(doc: Doc, cloneMap: Map<string, Doc>, rtfs: { copy: Doc, key: string, field: RichTextField }[], exclusions: string[], dontCreate: boolean): Promise<Doc> {
if (Doc.IsBaseProto(doc)) return doc;
if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!;
- const copy = new Doc(undefined, true);
+ const copy = dontCreate ? doc : new Doc(undefined, true);
cloneMap.set(doc[Id], copy);
if (LinkManager.Instance.getAllLinks().includes(doc) && LinkManager.Instance.getAllLinks().indexOf(copy) === -1) LinkManager.Instance.addLink(copy);
- const exclude = Cast(doc.excludeFields, listSpec("string"), []);
- Object.keys(doc).forEach(key => {
- if (exclude.includes(key)) return;
+ const filter = Cast(doc.cloneFieldFilter, listSpec("string"), exclusions);
+ await Promise.all(Object.keys(doc).map(async key => {
+ if (filter.includes(key)) return;
+ const assignKey = (val: any) => !dontCreate && (copy[key] = val);
const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
const field = ProxyField.WithoutProxy(() => doc[key]);
- const copyObjectField = (field: ObjectField) => {
- const list = Cast(doc[key], listSpec(Doc));
- if (list !== undefined && !(list instanceof Promise)) {
- copy[key] = new List<Doc>(list.filter(d => d instanceof Doc).map(d => Doc.makeClone(d as Doc, cloneMap, rtfs)));
+ const copyObjectField = async (field: ObjectField) => {
+ const list = await Cast(doc[key], listSpec(Doc));
+ const docs = list && (await DocListCastAsync(list))?.filter(d => d instanceof Doc);
+ if (docs !== undefined && docs.length) {
+ const clones = await Promise.all(docs.map(async d => await Doc.makeClone(d as Doc, cloneMap, rtfs, exclusions, dontCreate)));
+ !dontCreate && assignKey(new List<Doc>(clones));
} else if (doc[key] instanceof Doc) {
- copy[key] = key.includes("layout[") ? undefined : Doc.makeClone(doc[key] as Doc, cloneMap, rtfs); // reference documents except copy documents that are expanded teplate fields
+ assignKey(key.includes("layout[") ? undefined : key.startsWith("layout") ? doc[key] as Doc : await Doc.makeClone(doc[key] as Doc, cloneMap, rtfs, exclusions, dontCreate)); // reference documents except copy documents that are expanded teplate fields
} else {
- copy[key] = ObjectField.MakeCopy(field);
+ assignKey(ObjectField.MakeCopy(field));
if (field instanceof RichTextField) {
if (field.Data.includes('"docid":') || field.Data.includes('"targetId":') || field.Data.includes('"linkId":')) {
rtfs.push({ copy, key, field });
@@ -513,32 +542,34 @@ export namespace Doc {
};
if (key === "proto") {
if (doc[key] instanceof Doc) {
- copy[key] = Doc.makeClone(doc[key]!, cloneMap, rtfs);
+ assignKey(await Doc.makeClone(doc[key]!, cloneMap, rtfs, exclusions, dontCreate));
}
} else {
if (field instanceof RefField) {
- copy[key] = field;
+ assignKey(field);
} else if (cfield instanceof ComputedField) {
- copy[key] = ComputedField.MakeFunction(cfield.script.originalScript);
- (key === "links" && field instanceof ObjectField) && copyObjectField(field);
+ !dontCreate && assignKey(ComputedField.MakeFunction(cfield.script.originalScript));
+ (key === "links" && field instanceof ObjectField) && await copyObjectField(field);
} else if (field instanceof ObjectField) {
- copyObjectField(field);
+ await copyObjectField(field);
} else if (field instanceof Promise) {
debugger; //This shouldn't happend...
} else {
- copy[key] = field;
+ assignKey(field);
}
}
- });
- Doc.SetInPlace(copy, "title", "CLONE: " + doc.title, true);
- copy.cloneOf = doc;
- cloneMap.set(doc[Id], copy);
+ }));
+ if (!dontCreate) {
+ Doc.SetInPlace(copy, "title", "CLONE: " + doc.title, true);
+ copy.cloneOf = doc;
+ cloneMap.set(doc[Id], copy);
+ }
return copy;
}
- export function MakeClone(doc: Doc): Doc {
+ export async function MakeClone(doc: Doc, dontCreate: boolean = false) {
const cloneMap = new Map<string, Doc>();
const rtfMap: { copy: Doc, key: string, field: RichTextField }[] = [];
- const copy = Doc.makeClone(doc, cloneMap, rtfMap);
+ const copy = await Doc.makeClone(doc, cloneMap, rtfMap, ["context", "annotationOn", "cloneOf"], dontCreate);
rtfMap.map(({ copy, key, field }) => {
const replacer = (match: any, attr: string, id: string, offset: any, string: any) => {
const mapped = cloneMap.get(id);
@@ -552,9 +583,61 @@ export namespace Doc {
const re = new RegExp(regex, "g");
copy[key] = new RichTextField(field.Data.replace(/("docid":|"targetId":|"linkId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text);
});
- return copy;
- }
+ return { clone: copy, map: cloneMap };
+ }
+
+ export async function Zip(doc: Doc) {
+ // 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();
+ const { clone, map } = await Doc.MakeClone(doc, true);
+ function replacer(key: any, value: any) {
+ if (["cloneOf", "context", "cursors"].includes(key)) return undefined;
+ else if (value instanceof Doc) {
+ if (key !== "field" && Number.isNaN(Number(key))) {
+ const __fields = value[FieldsSym]();
+ return { id: value[Id], __type: "Doc", fields: __fields };
+ } else {
+ return { fieldId: value[Id], __type: "proxy" };
+ }
+ }
+ else if (value instanceof ScriptField) return { script: value.script, __type: "script" };
+ else if (value instanceof RichTextField) return { Data: value.Data, Text: value.Text, __type: "RichTextField" };
+ else if (value instanceof ImageField) return { url: value.url.href, __type: "image" };
+ else if (value instanceof PdfField) return { url: value.url.href, __type: "pdf" };
+ else if (value instanceof AudioField) return { url: value.url.href, __type: "audio" };
+ else if (value instanceof VideoField) return { url: value.url.href, __type: "video" };
+ else if (value instanceof WebField) return { url: value.url.href, __type: "web" };
+ else if (value instanceof DateField) return { date: value.toString(), __type: "date" };
+ else if (value instanceof ProxyField) return { fieldId: value.fieldId, __type: "proxy" };
+ else if (value instanceof Array && key !== "fields") return { fields: value, __type: "list" };
+ else if (value instanceof ComputedField) return { script: value.script, __type: "computed" };
+ else return value;
+ }
+
+ const docs: { [id: string]: any } = {};
+ Array.from(map.entries()).forEach(f => docs[f[0]] = f[1]);
+ const docString = JSON.stringify({ id: doc[Id], docs }, replacer);
+
+ var zip = new JSZip();
+ zip.file("doc.json", docString);
+
+ // // Generate a directory within the Zip file structure
+ // var img = zip.folder("images");
+
+ // // Add a file to the directory, in this case an image with data URI as contents
+ // img.file("smile.gif", imgData, {base64: true});
+
+ // Generate the zip file asynchronously
+ zip.generateAsync({ type: "blob" })
+ .then((content: any) => {
+ // Force down of the Zip file
+ saveAs(content, "download.zip");
+ });
+ }
//
// Determines whether the layout needs to be expanded (as a template).
// template expansion is rquired when the layout is a template doc/field and there's a datadoc which isn't equal to the layout template
@@ -657,7 +740,7 @@ export namespace Doc {
export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string): Doc {
const copy = new Doc(copyProtoId, true);
- const exclude = Cast(doc.excludeFields, listSpec("string"), []);
+ const exclude = Cast(doc.cloneFieldFilter, listSpec("string"), []);
Object.keys(doc).forEach(key => {
if (exclude.includes(key)) return;
const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
@@ -823,7 +906,7 @@ export namespace Doc {
}
// don't bother memoizing (caching) the result if called from a non-reactive context. (plus this avoids a warning message)
export function IsBrushedDegreeUnmemoized(doc: Doc) {
- if (!doc || doc[AclSym] === AclPrivate || Doc.GetProto(doc)[AclSym] === AclPrivate) return 0;
+ if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return 0;
return brushManager.BrushedDoc.has(doc) ? 2 : brushManager.BrushedDoc.has(Doc.GetProto(doc)) ? 1 : 0;
}
export function IsBrushedDegree(doc: Doc) {
@@ -832,13 +915,13 @@ export namespace Doc {
})(doc);
}
export function BrushDoc(doc: Doc) {
- if (!doc || doc[AclSym] === AclPrivate || Doc.GetProto(doc)[AclSym] === AclPrivate) return doc;
+ if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return doc;
brushManager.BrushedDoc.set(doc, true);
brushManager.BrushedDoc.set(Doc.GetProto(doc), true);
return doc;
}
export function UnBrushDoc(doc: Doc) {
- if (!doc || doc[AclSym] === AclPrivate || Doc.GetProto(doc)[AclSym] === AclPrivate) return doc;
+ if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return doc;
brushManager.BrushedDoc.delete(doc);
brushManager.BrushedDoc.delete(Doc.GetProto(doc));
return doc;
@@ -868,7 +951,7 @@ export namespace Doc {
}
const highlightManager = new HighlightBrush();
export function IsHighlighted(doc: Doc) {
- if (!doc || doc[AclSym] === AclPrivate || Doc.GetProto(doc)[AclSym] === AclPrivate) return false;
+ if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return false;
return highlightManager.HighlightedDoc.get(doc) || highlightManager.HighlightedDoc.get(Doc.GetProto(doc));
}
export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true) {
@@ -897,9 +980,12 @@ export namespace Doc {
}
export function getDocTemplate(doc?: Doc) {
- return doc?.isTemplateDoc ? doc :
- Cast(doc?.dragFactory, Doc, null)?.isTemplateDoc ? doc?.dragFactory :
- Cast(doc?.layout, Doc, null)?.isTemplateDoc ? doc?.layout : undefined;
+ return !doc ? undefined :
+ doc.isTemplateDoc ? doc :
+ Cast(doc.dragFactory, Doc, null)?.isTemplateDoc ? doc.dragFactory :
+ Cast(Doc.Layout(doc), Doc, null)?.isTemplateDoc ?
+ (Cast(Doc.Layout(doc), Doc, null).resolvedDataDoc ? Doc.Layout(doc).proto : Doc.Layout(doc)) :
+ undefined;
}
export function matchFieldValue(doc: Doc, key: string, value: any): boolean {
@@ -1154,7 +1240,7 @@ Scripting.addGlobal(function getProto(doc: any) { return Doc.GetProto(doc); });
Scripting.addGlobal(function getDocTemplate(doc?: any) { return Doc.getDocTemplate(doc); });
Scripting.addGlobal(function getAlias(doc: any) { return Doc.MakeAlias(doc); });
Scripting.addGlobal(function getCopy(doc: any, copyProto: any) { return doc.isTemplateDoc ? Doc.ApplyTemplate(doc) : Doc.MakeCopy(doc, copyProto); });
-Scripting.addGlobal(function copyField(field: any) { return ObjectField.MakeCopy(field); });
+Scripting.addGlobal(function copyField(field: any) { return field instanceof ObjectField ? ObjectField.MakeCopy(field) : field; });
Scripting.addGlobal(function aliasDocs(field: any) { return Doc.aliasDocs(field); });
Scripting.addGlobal(function docList(field: any) { return DocListCast(field); });
Scripting.addGlobal(function setInPlace(doc: any, field: any, value: any) { return Doc.SetInPlace(doc, field, value, false); });
diff --git a/src/fields/Proxy.ts b/src/fields/Proxy.ts
index 555faaad0..62734d3d2 100644
--- a/src/fields/Proxy.ts
+++ b/src/fields/Proxy.ts
@@ -9,7 +9,12 @@ import { Id, Copy, ToScriptString, ToString } from "./FieldSymbols";
import { scriptingGlobal } from "../client/util/Scripting";
import { Plugins } from "./util";
-@Deserializable("proxy")
+function deserializeProxy(field: any) {
+ if (!field.cache) {
+ field.cache = DocServer.GetCachedRefField(field.fieldId) as any;
+ }
+}
+@Deserializable("proxy", deserializeProxy)
export class ProxyField<T extends RefField> extends ObjectField {
constructor();
constructor(value: T);
@@ -17,6 +22,7 @@ export class ProxyField<T extends RefField> extends ObjectField {
constructor(value?: T | string) {
super();
if (typeof value === "string") {
+ this.cache = DocServer.GetCachedRefField(value) as any;
this.fieldId = value;
} else if (value) {
this.cache = value;
diff --git a/src/fields/Schema.ts b/src/fields/Schema.ts
index 98ef3e087..23ac50f74 100644
--- a/src/fields/Schema.ts
+++ b/src/fields/Schema.ts
@@ -31,7 +31,7 @@ export function makeInterface<T extends Interface[]>(...schemas: T): InterfaceFu
}
const proto = new Proxy({}, {
get(target: any, prop, receiver) {
- const field = receiver.doc[prop];
+ const field = receiver.doc?.[prop];
if (prop in schema) {
const desc = prop === "proto" ? Doc : (schema as any)[prop]; // bcz: proto doesn't appear in schemas ... maybe it should?
if (typeof desc === "object" && "defaultVal" in desc && "type" in desc) {//defaultSpec
@@ -52,7 +52,7 @@ export function makeInterface<T extends Interface[]>(...schemas: T): InterfaceFu
return field;
},
set(target: any, prop, value, receiver) {
- receiver.doc[prop] = value;
+ receiver.doc && (receiver.doc[prop] = value); // receiver.doc may be undefined as the result of a change in ACLs
return true;
}
});
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
index 11b3b0524..f55483a5b 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -3,7 +3,7 @@ import { CompiledScript, CompileScript, scriptingGlobal, ScriptOptions, CompileE
import { Copy, ToScriptString, ToString, Parent, SelfProxy } from "./FieldSymbols";
import { serializable, createSimpleSchema, map, primitive, object, deserialize, PropSchema, custom, SKIP } from "serializr";
import { Deserializable, autoObject } from "../client/util/SerializationHelper";
-import { Doc, Field } from "./Doc";
+import { Doc, Field, Opt } from "./Doc";
import { Plugins, setter } from "./util";
import { computedFn } from "mobx-utils";
import { ProxyField } from "./Proxy";
@@ -38,6 +38,21 @@ const scriptSchema = createSimpleSchema({
});
async function deserializeScript(script: ScriptField) {
+ if (script.script.originalScript === 'getCopy(this.dragFactory, true)') {
+ return (script as any).script = (ScriptField.GetCopyOfDragFactory ?? (ScriptField.GetCopyOfDragFactory = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')))?.script;
+ }
+ if (script.script.originalScript === 'links(self)') {
+ return (script as any).script = (ScriptField.LinksSelf ?? (ScriptField.LinksSelf = ComputedField.MakeFunction('links(self)')))?.script;
+ }
+ if (script.script.originalScript === 'openOnRight(getCopy(this.dragFactory, true))') {
+ return (script as any).script = (ScriptField.OpenOnRight ?? (ScriptField.OpenOnRight = ComputedField.MakeFunction('openOnRight(getCopy(this.dragFactory, true))')))?.script;
+ }
+ if (script.script.originalScript === 'deiconifyView(self)') {
+ return (script as any).script = (ScriptField.DeiconifyView ?? (ScriptField.DeiconifyView = ComputedField.MakeFunction('deiconifyView(self)')))?.script;
+ }
+ if (script.script.originalScript === 'convertToButtons(dragData)') {
+ return (script as any).script = (ScriptField.ConvertToButtons ?? (ScriptField.ConvertToButtons = ComputedField.MakeFunction('convertToButtons(dragData)', { dragData: "DocumentDragData" })))?.script;
+ }
const captures: ProxyField<Doc> = (script as any).captures;
if (captures) {
const doc = (await captures.value())!;
@@ -65,6 +80,11 @@ export class ScriptField extends ObjectField {
@serializable(autoObject())
private captures?: ProxyField<Doc>;
+ public static GetCopyOfDragFactory: Opt<ScriptField>;
+ public static LinksSelf: Opt<ScriptField>;
+ public static OpenOnRight: Opt<ScriptField>;
+ public static DeiconifyView: Opt<ScriptField>;
+ public static ConvertToButtons: Opt<ScriptField>;
constructor(script: CompiledScript, setterscript?: CompiledScript) {
super();
@@ -163,6 +183,10 @@ Scripting.addGlobal(function getIndexVal(list: any[], index: number) {
return list.reduce((p, x, i) => (i <= index && x !== undefined) || p === undefined ? x : p, undefined as any);
}, "returns the value at a given index of a list", "(list: any[], index: number)");
+Scripting.addGlobal(function makeScript(script: string) {
+ return ScriptField.MakeScript(script);
+}, "returns the value at a given index of a list", "(list: any[], index: number)");
+
export namespace ComputedField {
let useComputed = true;
export function DisableComputedFields() {
diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts
index 97f62c9d4..8cf8f47b7 100644
--- a/src/fields/documentSchemas.ts
+++ b/src/fields/documentSchemas.ts
@@ -3,7 +3,6 @@ import { ScriptField } from "./ScriptField";
import { Doc } from "./Doc";
import { DateField } from "./DateField";
import { SchemaHeaderField } from "./SchemaHeaderField";
-import { Schema } from "prosemirror-model";
export const documentSchema = createSchema({
// content properties
@@ -55,7 +54,7 @@ export const documentSchema = createSchema({
_columnsHideIfEmpty: "boolean", // whether empty stacking view column headings should be hidden
_columnHeaders: listSpec(SchemaHeaderField), // header descriptions for stacking/masonry
_schemaHeaders: listSpec(SchemaHeaderField), // header descriptions for schema views
- _fontSize: "number",
+ _fontSize: "string",
_fontFamily: "string",
_sidebarWidthPercent: "string", // percent of text window width taken up by sidebar
@@ -66,6 +65,7 @@ export const documentSchema = createSchema({
color: "string", // foreground color of document
fitToBox: "boolean", // whether freeform view contents should be zoomed/panned to fill the area of the document view
fontSize: "string",
+ hidden: "boolean", // whether a document should not be displayed
isInkMask: "boolean", // is the document a mask (ie, sits on top of other documents, has an unbounded width/height that is dark, and content uses 'hard-light' mix-blend-mode to let other documents pop through)
layout: "string", // this is the native layout string for the document. templates can be added using other fields and setting layoutKey below
layoutKey: "string", // holds the field key for the field that actually holds the current lyoat
@@ -73,6 +73,9 @@ export const documentSchema = createSchema({
opacity: "number", // opacity of document
strokeWidth: "number",
strokeBezier: "number",
+ strokeStartMarker: "string",
+ strokeEndMarker: "string",
+ strokeDash: "string",
textTransform: "string",
treeViewOpen: "boolean", // flag denoting whether the documents sub-tree (contents) is visible or hidden
treeViewExpandedView: "string", // name of field whose contents are being displayed as the document's subtree
@@ -85,6 +88,9 @@ export const documentSchema = createSchema({
onPointerUp: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
onDragStart: ScriptField, // script to run when document is dragged (without being selected). the script should return the Doc to be dropped.
followLinkLocation: "string",// flag for where to place content when following a click interaction (e.g., onRight, inPlace, inTab, )
+ hideLinkButton: "boolean", // whether the blue link counter button should be hidden
+ hideAllLinks: "boolean", // whether all individual blue anchor dots should be hidden
+ linkDisplay: "boolean", // whether a link connection should be shown between link anchor endpoints.
isInPlaceContainer: "boolean",// whether the marked object will display addDocTab() calls that target "inPlace" destinations
isLinkButton: "boolean", // whether document functions as a link follow button to follow the first link on the document when clicked
isBackground: "boolean", // whether document is a background element and ignores input events (can only select with marquee)
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 2dc21c987..a62795e64 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -1,14 +1,15 @@
import { UndoManager } from "../client/util/UndoManager";
-import { Doc, Field, FieldResult, UpdatingFromServer, LayoutSym, AclSym, AclPrivate } from "./Doc";
+import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, CachedUpdates, DataSym, DocListCast, AclAdmin, FieldsSym, HeightSym, WidthSym } from "./Doc";
import { SerializationHelper } from "../client/util/SerializationHelper";
import { ProxyField, PrefetchProxy } from "./Proxy";
import { RefField } from "./RefField";
import { ObjectField } from "./ObjectField";
import { action, trace } from "mobx";
-import { Parent, OnUpdate, Update, Id, SelfProxy, Self } from "./FieldSymbols";
+import { Parent, OnUpdate, Update, Id, SelfProxy, Self, HandleUpdate } from "./FieldSymbols";
import { DocServer } from "../client/DocServer";
import { ComputedField } from "./ScriptField";
-import { ScriptCast } from "./Types";
+import { ScriptCast, StrCast } from "./Types";
+import { returnZero } from "../Utils";
function _readOnlySetter(): never {
@@ -34,7 +35,6 @@ export namespace Plugins {
}
const _setterImpl = action(function (target: any, prop: string | symbol | number, value: any, receiver: any): boolean {
- //console.log("-set " + target[SelfProxy].title + "(" + target[SelfProxy][prop] + ")." + prop.toString() + " = " + value);
if (SerializationHelper.IsSerializing()) {
target[prop] = value;
return true;
@@ -67,18 +67,25 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
delete curValue[Parent];
delete curValue[OnUpdate];
}
+
+ const effectiveAcl = GetEffectiveAcl(target);
+
const writeMode = DocServer.getFieldWriteMode(prop as string);
const fromServer = target[UpdatingFromServer];
const sameAuthor = fromServer || (receiver.author === Doc.CurrentUserEmail);
- const writeToDoc = sameAuthor || (writeMode !== DocServer.WriteMode.LiveReadonly);
- const writeToServer = sameAuthor || (writeMode === DocServer.WriteMode.Default);
+ const writeToDoc = sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || (writeMode !== DocServer.WriteMode.LiveReadonly);
+ const writeToServer = (sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || writeMode === DocServer.WriteMode.Default) && !playgroundMode;
+
if (writeToDoc) {
if (value === undefined) {
+ target.__fieldKeys && (delete target.__fieldKeys[prop]);
delete target.__fields[prop];
} else {
+ target.__fieldKeys && (target.__fieldKeys[prop] = true);
target.__fields[prop] = value;
}
//if (typeof value === "object" && !(value instanceof ObjectField)) debugger;
+
if (writeToServer) {
if (value === undefined) target[Update]({ '$unset': { ["fields." + prop]: "" } });
else target[Update]({ '$set': { ["fields." + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : (value === undefined ? null : value) } });
@@ -89,8 +96,9 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
redo: () => receiver[prop] = value,
undo: () => receiver[prop] = curValue
});
+ return true;
}
- return true;
+ return false;
});
let _setter: (target: any, prop: string | symbol | number, value: any, receiver: any) => boolean = _setterImpl;
@@ -107,11 +115,117 @@ export function OVERRIDE_ACL(val: boolean) {
_overrideAcl = val;
}
+let playgroundMode = false;
+
+export function togglePlaygroundMode() {
+ playgroundMode = !playgroundMode;
+}
+
+export function getPlaygroundMode() {
+ return playgroundMode;
+}
+
+let currentUserGroups: string[] = [];
+
+export function setGroups(groups: string[]) {
+ currentUserGroups = groups;
+}
+
+export enum SharingPermissions {
+ Admin = "Admin",
+ Edit = "Can Edit",
+ Add = "Can Add",
+ View = "Can View",
+ None = "Not Shared"
+}
+
+export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number): symbol {
+ if (in_prop === UpdatingFromServer || target[UpdatingFromServer]) return AclAdmin;
+
+ if (target[AclSym] && Object.keys(target[AclSym]).length) {
+
+ if (target.__fields?.author === Doc.CurrentUserEmail || target.author === Doc.CurrentUserEmail || currentUserGroups.includes("admin")) return AclAdmin;
+
+ if (_overrideAcl || (in_prop && DocServer.PlaygroundFields?.includes(in_prop.toString()))) return AclEdit;
+
+ let effectiveAcl = AclPrivate;
+ let aclPresent = false;
+
+ const HierarchyMapping = new Map<symbol, number>([
+ [AclPrivate, 0],
+ [AclReadonly, 1],
+ [AclAddonly, 2],
+ [AclEdit, 3],
+ [AclAdmin, 4]
+ ]);
+
+ for (const [key, value] of Object.entries(target[AclSym])) {
+ if (currentUserGroups.includes(key.substring(4)) || Doc.CurrentUserEmail === key.substring(4).replace("_", ".")) {
+ if (HierarchyMapping.get(value as symbol)! >= HierarchyMapping.get(effectiveAcl)!) {
+ aclPresent = true;
+ effectiveAcl = value as symbol;
+ if (effectiveAcl === AclEdit) break;
+ }
+ }
+ }
+ return aclPresent ? effectiveAcl : AclEdit;
+ }
+ return AclAdmin;
+}
+
+export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, inheritingFromCollection?: boolean) {
+
+ const HierarchyMapping = new Map<string, number>([
+ ["Not Shared", 0],
+ ["Can View", 1],
+ ["Can Add", 2],
+ ["Can Edit", 3],
+ ["Admin", 4]
+ ]);
+
+ const dataDoc = target[DataSym];
+
+ if (!inheritingFromCollection || !target[key] || HierarchyMapping.get(StrCast(target[key]))! > HierarchyMapping.get(acl)!) target[key] = acl;
+
+ if (dataDoc && (!inheritingFromCollection || !dataDoc[key] || HierarchyMapping.get(StrCast(dataDoc[key]))! > HierarchyMapping.get(acl)!)) {
+ dataDoc[key] = acl;
+
+ DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc)]).map(d => {
+ if (d.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) {
+ distributeAcls(key, acl, d);
+ d[key] = acl;
+ }
+ const data = d[DataSym];
+ if (data && data.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) {
+ distributeAcls(key, acl, data);
+ data[key] = acl;
+ }
+ });
+
+ DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + "-annotations"]).map(d => {
+ if (d.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) {
+ distributeAcls(key, acl, d);
+ d[key] = acl;
+ }
+ const data = d[DataSym];
+ if (data && data.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) {
+ distributeAcls(key, acl, data);
+ data[key] = acl;
+ }
+ });
+ }
+}
+
const layoutProps = ["panX", "panY", "width", "height", "nativeWidth", "nativeHeight", "fitWidth", "fitToBox",
"chromeStatus", "viewType", "gridGap", "xMargin", "yMargin", "autoHeight"];
export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean {
let prop = in_prop;
- if (target[AclSym] && !_overrideAcl && !DocServer.PlaygroundFields.includes(in_prop.toString())) return true;
+ const effectiveAcl = GetEffectiveAcl(target, in_prop);
+ if (effectiveAcl !== AclEdit && effectiveAcl !== AclAdmin) return true;
+
+ if (typeof prop === "string" && prop.startsWith("ACL") && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined].includes(value))) return true;
+ // if (typeof prop === "string" && prop.startsWith("ACL") && !["Can Edit", "Can Add", "Can View", "Not Shared", undefined].includes(value)) return true;
+
if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" && (prop.startsWith("_") || layoutProps.includes(prop))) {
if (!prop.startsWith("_")) {
console.log(prop + " is deprecated - switch to _" + prop);
@@ -130,8 +244,10 @@ export function setter(target: any, in_prop: string | symbol | number, value: an
export function getter(target: any, in_prop: string | symbol | number, receiver: any): any {
let prop = in_prop;
+
+ if (in_prop === FieldsSym || in_prop === Id || in_prop === HandleUpdate || in_prop === CachedUpdates) return target.__fields[prop] || target[prop];
if (in_prop === AclSym) return _overrideAcl ? undefined : target[AclSym];
- if (target[AclSym] === AclPrivate && !_overrideAcl) return undefined;
+ if (GetEffectiveAcl(target) === AclPrivate && !_overrideAcl) return prop === HeightSym || prop === WidthSym ? returnZero : undefined;
if (prop === LayoutSym) {
return target.__LAYOUT__;
}
@@ -168,7 +284,7 @@ function getFieldImpl(target: any, prop: string | number, receiver: any, ignoreP
}
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
- if (proto instanceof Doc && proto[AclSym] !== AclPrivate) {
+ if (proto instanceof Doc && GetEffectiveAcl(proto) !== AclPrivate) {
return getFieldImpl(proto[Self], prop, receiver, ignoreProto);
}
return undefined;
diff --git a/src/mobile/AudioUpload.scss b/src/mobile/AudioUpload.scss
new file mode 100644
index 000000000..6e64d9e2e
--- /dev/null
+++ b/src/mobile/AudioUpload.scss
@@ -0,0 +1,61 @@
+@import "../client/views/globalCssVariables.scss";
+
+.audioUpload_cont {
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+ align-items: center;
+ margin-top: 10px;
+ height: 400px;
+ width: 600px;
+}
+
+.upload_label {
+ position: relative;
+ font-weight: 700;
+ color: black;
+ background-color: rgba(0, 0, 0, 0);
+ border: solid 3px black;
+ margin: 10px;
+ font-size: 30;
+ height: 70px;
+ width: 60%;
+ display: inline-flex;
+ font-family: sans-serif;
+ text-transform: uppercase;
+ justify-content: center;
+ flex-direction: column;
+ border-radius: 10px;
+}
+
+.restart_label {
+ position: relative;
+ font-weight: 700;
+ color: black;
+ background-color: rgba(0, 0, 0, 0);
+ border: solid 3px black;
+ margin: 10px;
+ font-size: 30;
+ height: 70px;
+ width: 60%;
+ display: inline-flex;
+ font-family: sans-serif;
+ text-transform: uppercase;
+ justify-content: center;
+ flex-direction: column;
+ border-radius: 10px;
+}
+
+.audio-upload {
+ top: 100%;
+ opacity: 0;
+}
+
+.audio-upload.active {
+ top: 0;
+ position: absolute;
+ z-index: 999;
+ height: 100vh;
+ width: 100vw;
+ opacity: 1;
+} \ No newline at end of file
diff --git a/src/mobile/AudioUpload.tsx b/src/mobile/AudioUpload.tsx
new file mode 100644
index 000000000..b75102e43
--- /dev/null
+++ b/src/mobile/AudioUpload.tsx
@@ -0,0 +1,153 @@
+import { Docs } from '../client/documents/Documents';
+import "./ImageUpload.scss";
+import React = require('react');
+import { observer } from 'mobx-react';
+import { observable, action, computed } from 'mobx';
+import { Utils, emptyPath, returnFalse, emptyFunction, returnOne, returnZero, returnTrue, returnEmptyFilter } from '../Utils';
+import { Doc, Opt } from '../fields/Doc';
+import { Cast, FieldValue } from '../fields/Types';
+import { listSpec } from '../fields/Schema';
+import MainViewModal from '../client/views/MainViewModal';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { nullAudio } from '../fields/URLField';
+import { Transform } from '../client/util/Transform';
+import { DocumentView } from '../client/views/nodes/DocumentView';
+import { MobileInterface } from './MobileInterface';
+import { DictationOverlay } from '../client/views/DictationOverlay';
+import RichTextMenu from '../client/views/nodes/formattedText/RichTextMenu';
+import { ContextMenu } from '../client/views/ContextMenu';
+
+@observer
+export class AudioUpload extends React.Component {
+ @observable public _audioCol: Doc = FieldValue(Cast(Docs.Create.FreeformDocument([Cast(Docs.Create.AudioDocument(nullAudio, { title: "mobile audio", _width: 500, _height: 100 }), Doc) as Doc], { title: "mobile audio", _width: 300, _height: 300, _fitToBox: true, boxShadow: "0 0" }), Doc)) as Doc;
+
+ /**
+ * Handles the onclick functionality for the 'Restart' button
+ * Resets the document to its default view
+ */
+ @action
+ clearUpload = () => {
+ for (let i = 1; i < 8; i++) {
+ this.setOpacity(i, "0.2");
+ }
+ this._audioCol = FieldValue(Cast(
+ Docs.Create.FreeformDocument(
+ [Cast(Docs.Create.AudioDocument(nullAudio, {
+ title: "mobile audio",
+ _width: 500,
+ _height: 100
+ }), Doc) as Doc], { title: "mobile audio", _width: 300, _height: 300, _fitToBox: true, boxShadow: "0 0" }), Doc)) as Doc;
+ }
+
+ /**
+ * Handles the onClick of the 'Close' button
+ * Reset upload interface and toggle audio
+ */
+ closeUpload = () => {
+ this.clearUpload();
+ MobileInterface.Instance.toggleAudio();
+ }
+
+ /**
+ * Handles the on click of the 'Upload' button.
+ * Pushing the audio doc onto Dash Web through the right side bar
+ */
+ uploadAudio = () => {
+ const audioRightSidebar = Cast(Doc.UserDoc().rightSidebarCollection, Doc) as Doc;
+ const audioDoc = this._audioCol;
+ const data = Cast(audioRightSidebar.data, listSpec(Doc));
+ for (let i = 1; i < 8; i++) {
+ setTimeout(() => this.setOpacity(i, "1"), i * 200);
+ }
+ if (data) {
+ data.push(audioDoc);
+ }
+ // Resets uploader after 3 seconds
+ setTimeout(this.clearUpload, 3000);
+ }
+
+ // Returns the upload audio menu
+ private get uploadInterface() {
+ return (
+ <>
+ <ContextMenu />
+ <DictationOverlay />
+ <div style={{ display: "none" }}><RichTextMenu key="rich" /></div>
+ <div className="closeUpload" onClick={() => this.closeUpload()}>
+ <FontAwesomeIcon icon="window-close" size={"lg"} />
+ </div>
+ <FontAwesomeIcon icon="microphone" size="lg" style={{ fontSize: "130" }} />
+ <div className="audioUpload_cont">
+ <DocumentView
+ Document={this._audioCol}
+ DataDoc={undefined}
+ LibraryPath={emptyPath}
+ addDocument={undefined}
+ addDocTab={returnFalse}
+ pinToPres={emptyFunction}
+ rootSelected={returnTrue}
+ removeDocument={undefined}
+ docFilters={returnEmptyFilter}
+ onClick={undefined}
+ ScreenToLocalTransform={Transform.Identity}
+ ContentScaling={returnOne}
+ PanelWidth={() => 600}
+ PanelHeight={() => 400}
+ NativeHeight={returnZero}
+ NativeWidth={returnZero}
+ renderDepth={0}
+ focus={emptyFunction}
+ backgroundColor={() => "rgba(0,0,0,0)"}
+ parentActive={returnTrue}
+ whenActiveChanged={emptyFunction}
+ bringToFront={emptyFunction}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
+ />
+ </div>
+ <div className="restart_label" onClick={this.clearUpload}>
+ Restart
+ </div>
+ <div className="upload_label" onClick={this.uploadAudio}>
+ Upload
+ </div>
+ <div className="loadingImage">
+ <div className="loadingSlab" id="slab01" />
+ <div className="loadingSlab" id="slab02" />
+ <div className="loadingSlab" id="slab03" />
+ <div className="loadingSlab" id="slab04" />
+ <div className="loadingSlab" id="slab05" />
+ <div className="loadingSlab" id="slab06" />
+ <div className="loadingSlab" id="slab07" />
+ </div>
+ </>
+ );
+ }
+
+ // Handles the setting of the loading bar
+ setOpacity = (index: number, opacity: string) => {
+ const slab = document.getElementById("slab0" + index);
+ if (slab) {
+ slab.style.opacity = opacity;
+ }
+ }
+
+ @observable private dialogueBoxOpacity = 1;
+ @observable private overlayOpacity = 0.4;
+
+ render() {
+ return (
+ <MainViewModal
+ contents={this.uploadInterface}
+ isDisplayed={true}
+ interactive={true}
+ dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity}
+ overlayDisplayedOpacity={this.overlayOpacity}
+ closeOnExternalClick={this.closeUpload}
+ />
+ );
+ }
+
+}
+
+
diff --git a/src/mobile/ImageUpload.scss b/src/mobile/ImageUpload.scss
index eea69b81c..890258918 100644
--- a/src/mobile/ImageUpload.scss
+++ b/src/mobile/ImageUpload.scss
@@ -5,8 +5,32 @@
justify-content: center;
flex-direction: column;
align-items: center;
- width: 100vw;
- height: 100vh;
+ max-width: 400px;
+ min-width: 400px;
+
+ .upload_label {
+ font-weight: 700;
+ color: black;
+ background-color: rgba(0, 0, 0, 0);
+ border: solid 3px black;
+ margin: 10px;
+ font-size: 30;
+ height: 70px;
+ width: 80%;
+ display: flex;
+ font-family: sans-serif;
+ text-transform: uppercase;
+ justify-content: center;
+ flex-direction: column;
+ border-radius: 10px;
+ }
+
+ .file {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ direction: ltr;
+ }
.button_file {
text-align: center;
@@ -17,18 +41,98 @@
font-size: 3em;
}
- .input_file {
- display: none;
+ .inputfile {
+ width: 0.1px;
+ height: 0.1px;
+ opacity: 0;
+ overflow: hidden;
+ position: absolute;
+ z-index: -1;
}
- .upload_label,
- .upload_button {
- background: $dark-color;
- font-size: 500%;
- font-family: $sans-serif;
- text-align: center;
- padding: 5vh;
- margin-bottom: 20px;
- color: white;
+ .inputfile+label {
+ font-weight: 700;
+ color: black;
+ background-color: rgba(0, 0, 0, 0);
+ border: solid 3px black;
+ margin: 10px;
+ font-size: 30;
+ height: 70px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin-top: 30px;
+ width: 80%;
+ display: flex;
+ font-family: sans-serif;
+ text-transform: uppercase;
+ justify-content: center;
+ flex-direction: column;
+ border-radius: 10px;
+ }
+
+ .inputfile.active+label {
+ font-style: italic;
+ color: black;
+ background-color: lightgreen;
+ border: solid 3px darkgreen;
+ }
+
+ .status {
+ font-size: 2em;
}
+
+}
+
+.image-upload {
+ top: 100%;
+ opacity: 0;
+}
+
+.image-upload.active {
+ top: 0;
+ position: absolute;
+ z-index: 999;
+ height: 100vh;
+ width: 100vw;
+ opacity: 1;
+}
+
+.uploadContainer {
+ top: 40;
+ position: absolute;
+ z-index: 1000;
+ height: 20vh;
+ width: 80vw;
+ opacity: 1;
+}
+
+.closeUpload {
+ position: absolute;
+ border-radius: 10px;
+ top: 3;
+ color: black;
+ font-size: 30;
+ right: 3;
+ z-index: 1002;
+ padding: 0px 3px;
+ background: rgba(0, 0, 0, 0);
+ transition: 0.5s ease all;
+ border: 0px solid;
+}
+
+.loadingImage {
+ display: inline-flex;
+ width: max-content;
+}
+
+.loadingSlab {
+ position: relative;
+ width: 30px;
+ height: 30px;
+ margin: 10;
+ border-radius: 20px;
+ opacity: 0.2;
+ background-color: black;
+ transition: all 2s, opacity 1.5s;
} \ No newline at end of file
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index b15042f9f..c6420bab5 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -1,128 +1,180 @@
-import * as ReactDOM from 'react-dom';
import * as rp from 'request-promise';
import { Docs } from '../client/documents/Documents';
import "./ImageUpload.scss";
import React = require('react');
import { DocServer } from '../client/DocServer';
-import { Opt, Doc } from '../fields/Doc';
+import { observer } from 'mobx-react';
+import { observable, action } from 'mobx';
+import { Utils } from '../Utils';
+import { Networking } from '../client/Network';
+import { Doc, Opt } from '../fields/Doc';
import { Cast } from '../fields/Types';
import { listSpec } from '../fields/Schema';
import { List } from '../fields/List';
-import { observer } from 'mobx-react';
-import { observable } from 'mobx';
-import { Utils } from '../Utils';
-import MobileInterface from './MobileInterface';
-import { CurrentUserUtils } from '../client/util/CurrentUserUtils';
-import { resolvedPorts } from '../client/views/Main';
+import MainViewModal from '../client/views/MainViewModal';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { MobileInterface } from './MobileInterface';
+
+export interface ImageUploadProps {
+ Document: Doc; // Target document for upload (upload location)
+}
-// const onPointerDown = (e: React.TouchEvent) => {
-// let imgInput = document.getElementById("input_image_file");
-// if (imgInput) {
-// imgInput.click();
-// }
-// }
const inputRef = React.createRef<HTMLInputElement>();
@observer
-class Uploader extends React.Component {
+export class Uploader extends React.Component<ImageUploadProps> {
@observable error: string = "";
- @observable status: string = "";
+ @observable nm: string = "Choose files"; // Text of 'Choose Files' button
+ @observable process: string = ""; // Current status of upload
onClick = async () => {
- console.log("uploader click");
try {
- this.status = "initializing protos";
+ const col = this.props.Document;
await Docs.Prototypes.initialize();
const imgPrev = document.getElementById("img_preview");
+ this.setOpacity(1, "1"); // Slab 1
if (imgPrev) {
const files: FileList | null = inputRef.current!.files;
+ this.setOpacity(2, "1"); // Slab 2
if (files && files.length !== 0) {
- console.log(files[0]);
- const name = files[0].name;
- const formData = new FormData();
- formData.append("file", files[0]);
-
- const upload = window.location.origin + "/uploadFormData";
- this.status = "uploading image";
- console.log("uploading image", formData);
- const res = await fetch(upload, {
- method: 'POST',
- body: formData
- });
- this.status = "upload image, getting json";
- const json = await res.json();
- json.map(async (file: any) => {
- const path = window.location.origin + file;
- const doc = Docs.Create.ImageDocument(path, { _nativeWidth: 200, _width: 200, title: name });
-
- this.status = "getting user document";
-
- const res = await rp.get(Utils.prepend("/getUserDocumentId"));
- if (!res) {
- throw new Error("No user id returned");
- }
- const field = await DocServer.GetRefField(res);
- let pending: Opt<Doc>;
- if (field instanceof Doc) {
- pending = await Cast(field.rightSidebarCollection, Doc);
- }
- if (pending) {
- this.status = "has pending docs";
- const data = await Cast(pending.data, listSpec(Doc));
- if (data) {
- data.push(doc);
+ this.process = "Uploading Files";
+ for (let index = 0; index < files.length; ++index) {
+ const file = files[index];
+ const res = await Networking.UploadFilesToServer(file);
+ this.setOpacity(3, "1"); // Slab 3
+ // For each item that the user has selected
+ res.map(async ({ result }) => {
+ const name = file.name;
+ if (result instanceof Error) {
+ return;
+ }
+ const path = Utils.prepend(result.accessPaths.agnostic.client);
+ let doc = null;
+ // Case 1: File is a video
+ if (file.type === "video/mp4") {
+ doc = Docs.Create.VideoDocument(path, { _nativeWidth: 400, _width: 400, title: name });
+ // Case 2: File is a PDF document
+ } else if (file.type === "application/pdf") {
+ doc = Docs.Create.PdfDocument(path, { _nativeWidth: 400, _width: 400, _fitWidth: true, title: name });
+ // Case 3: File is another document type (most likely Image)
} else {
- pending.data = new List([doc]);
+ doc = Docs.Create.ImageDocument(path, { _nativeWidth: 400, _width: 400, title: name });
}
- this.status = "finished";
- }
- });
-
- // console.log(window.location.origin + file[0])
-
- //imgPrev.setAttribute("src", window.location.origin + files[0].name)
+ this.setOpacity(4, "1"); // Slab 4
+ const res = await rp.get(Utils.prepend("/getUserDocumentId"));
+ if (!res) {
+ throw new Error("No user id returned");
+ }
+ const field = await DocServer.GetRefField(res);
+ let pending: Opt<Doc>;
+ if (field instanceof Doc) {
+ pending = col;
+ }
+ if (pending) {
+ const data = await Cast(pending.data, listSpec(Doc));
+ if (data) data.push(doc);
+ else pending.data = new List([doc]);
+ this.setOpacity(5, "1"); // Slab 5
+ this.process = "File " + (index + 1).toString() + " Uploaded";
+ this.setOpacity(6, "1"); // Slab 6
+ }
+ if ((index + 1) === files.length) {
+ this.process = "Uploads Completed";
+ this.setOpacity(7, "1"); // Slab 7
+ }
+ });
+ }
+ // Case in which the user pressed upload and no files were selected
+ } else {
+ this.process = "No file selected";
}
+ // Three seconds after upload the menu will reset
+ setTimeout(this.clearUpload, 3000);
}
} catch (error) {
this.error = JSON.stringify(error);
}
}
- render() {
+ // Updates label after a files is selected (so user knows a file is uploaded)
+ inputLabel = async () => {
+ const files: FileList | null = inputRef.current!.files;
+ await files;
+ if (files && files.length === 1) {
+ this.nm = files[0].name;
+ } else if (files && files.length > 1) {
+ this.nm = files.length.toString() + " files selected";
+ }
+ }
+
+ // Loops through load icons, and resets buttons
+ @action
+ clearUpload = () => {
+ for (let i = 1; i < 8; i++) {
+ this.setOpacity(i, "0.2");
+ }
+ this.nm = "Choose files";
+
+ if (inputRef.current) {
+ inputRef.current.value = "";
+ }
+ this.process = "";
+ }
+
+ // Clears the upload and closes the upload menu
+ closeUpload = () => {
+ this.clearUpload();
+ MobileInterface.Instance.toggleUpload();
+ }
+
+ // Handles the setting of the loading bar
+ setOpacity = (index: number, opacity: string) => {
+ const slab = document.getElementById("slab" + index);
+ if (slab) slab.style.opacity = opacity;
+ }
+
+ // Returns the upload interface for mobile
+ private get uploadInterface() {
return (
<div className="imgupload_cont">
- <label htmlFor="input_image_file" className="upload_label">Choose an Image</label>
- <input type="file" accept="image/*" className="input_file" id="input_image_file" ref={inputRef}></input>
- <button onClick={this.onClick} className="upload_button">Upload</button>
+ <div className="closeUpload" onClick={() => this.closeUpload()}>
+ <FontAwesomeIcon icon="window-close" size={"lg"} />
+ </div>
+ <FontAwesomeIcon icon="upload" size="lg" style={{ fontSize: "130" }} />
+ <input type="file" accept="application/pdf, video/*,image/*" className={`inputFile ${this.nm !== "Choose files" ? "active" : ""}`} id="input_image_file" ref={inputRef} onChange={this.inputLabel} multiple></input>
+ <label className="file" id="label" htmlFor="input_image_file">{this.nm}</label>
+ <div className="upload_label" onClick={this.onClick}>
+ Upload
+ </div>
<img id="img_preview" src=""></img>
- <p>{this.status}</p>
- <p>{this.error}</p>
+ <div className="loadingImage">
+ <div className="loadingSlab" id="slab1" />
+ <div className="loadingSlab" id="slab2" />
+ <div className="loadingSlab" id="slab3" />
+ <div className="loadingSlab" id="slab4" />
+ <div className="loadingSlab" id="slab5" />
+ <div className="loadingSlab" id="slab6" />
+ <div className="loadingSlab" id="slab7" />
+ </div>
+ <p className="status">{this.process}</p>
</div>
);
}
-}
+ @observable private dialogueBoxOpacity = 1;
+ @observable private overlayOpacity = 0.4;
-
-// DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, "image upload");
-(async () => {
- const info = await CurrentUserUtils.loadCurrentUser();
- DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, info.email + "mobile");
- await Docs.Prototypes.initialize();
- if (info.id !== "__guest__") {
- // a guest will not have an id registered
- await CurrentUserUtils.loadUserDocument(info);
+ render() {
+ return (
+ <MainViewModal
+ contents={this.uploadInterface}
+ isDisplayed={true}
+ interactive={true}
+ dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity}
+ overlayDisplayedOpacity={this.overlayOpacity}
+ closeOnExternalClick={this.closeUpload}
+ />
+ );
}
- document.getElementById('root')!.addEventListener('wheel', event => {
- if (event.ctrlKey) {
- event.preventDefault();
- }
- }, true);
- ReactDOM.render((
- // <Uploader />
- <MobileInterface />
- ),
- document.getElementById('root')
- );
+
}
-)(); \ No newline at end of file
diff --git a/src/mobile/MobileInkOverlay.tsx b/src/mobile/MobileInkOverlay.tsx
index 59c73ed27..d668d134e 100644
--- a/src/mobile/MobileInkOverlay.tsx
+++ b/src/mobile/MobileInkOverlay.tsx
@@ -4,11 +4,9 @@ import { MobileInkOverlayContent, GestureContent, UpdateMobileInkOverlayPosition
import { observable, action } from "mobx";
import { GestureUtils } from "../pen-gestures/GestureUtils";
import "./MobileInkOverlay.scss";
-import { StrCast, Cast } from '../fields/Types';
import { DragManager } from "../client/util/DragManager";
import { DocServer } from '../client/DocServer';
-import { Doc, DocListCastAsync } from '../fields/Doc';
-import { listSpec } from '../fields/Schema';
+import { Doc } from '../fields/Doc';
@observer
diff --git a/src/mobile/MobileInterface.scss b/src/mobile/MobileInterface.scss
index 4d86e208f..4b32c3da0 100644
--- a/src/mobile/MobileInterface.scss
+++ b/src/mobile/MobileInterface.scss
@@ -1,19 +1,445 @@
-.mobileInterface-inkInterfaceButtons {
- position: absolute;
+$navbar-height: 120px;
+$pathbar-height: 50px;
+
+@media only screen and (max-device-width: 480px) {
+ * {
+ margin: 0px;
+ padding: 0px;
+ box-sizing: border-box;
+ font-family: sans-serif;
+ }
+}
+
+body {
+ overflow: hidden;
+}
+
+.mobileInterface-container {
+ height: 100%;
+ position: relative;
+ touch-action: none;
+ width: 100%;
+
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+}
+
+// Topbar of Dash Mobile
+.navbar {
+ position: fixed;
top: 0px;
+ left: 0px;
+ width: 100vw;
+ height: $navbar-height;
+ background-color: whitesmoke;
+ z-index: 150;
+
+ .cover {
+ position: absolute;
+ right: 0px;
+ top: 0px;
+ height: 120px;
+ width: 120px;
+ background-color: whitesmoke;
+ z-index: 200;
+ }
+
+ .toggle-btn {
+ position: absolute;
+ right: 20px;
+ top: 30px;
+ height: 70px;
+ width: 70px;
+ transition: all 400ms ease-in-out 200ms;
+ z-index: 180;
+ }
+
+ .background {
+ position: absolute;
+ right: 0px;
+ top: 0px;
+ height: 120px;
+ width: 120px;
+ //border: 1px solid black;
+ }
+
+ .background.active {
+ background-color: lightgrey;
+ }
+
+ .toggle-btn-home {
+ right: -200px;
+ }
+
+ .header {
+ position: absolute;
+ top: 50%;
+ top: calc(9px + 50%);
+ right: 50%;
+ transform: translate(50%, -50%);
+ font-size: 40;
+ font-weight: 700;
+ text-align: center;
+ user-select: none;
+ text-transform: uppercase;
+ font-family: Arial, Helvetica, sans-serif;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ direction: ltr;
+ width: 600px;
+ }
+
+ .toggle-btn span {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 70%;
+ height: 4px;
+ background: black;
+ transition: all 200ms ease;
+ z-index: 180;
+ }
+
+ .toggle-btn span:nth-child(1) {
+ transition: top 200ms ease-in-out;
+ top: 30%;
+ }
+
+ .toggle-btn span:nth-child(3) {
+ transition: top 200ms ease-in-out;
+ top: 70%;
+ }
+
+ .toggle-btn.active {
+ transition: transform 200ms ease-in-out 200ms;
+ transform: rotate(135deg);
+ }
+
+ .toggle-btn.active span:nth-child(1) {
+ top: 50%;
+ }
+
+ .toggle-btn.active span:nth-child(2) {
+ transform: translate(-50%, -50%) rotate(90deg);
+ }
+
+ .toggle-btn.active span:nth-child(3) {
+ top: 50%;
+ }
+}
+
+.sidebar {
+ position: fixed;
+ top: 120px;
+ opacity: 0;
+ right: -100%;
+ width: 80%;
+ height: calc(80% - (120px));
+ z-index: 101;
+ background-color: whitesmoke;
+ transition: all 400ms ease 50ms;
+ padding: 20px;
+ box-shadow: 0 0 5px 5px grey;
+
+ .item {
+ width: 100%;
+ padding: 13px 12px;
+ border-bottom: 1px solid rgba(200, 200, 200, 0.7);
+ font-family: Arial, Helvetica, sans-serif;
+ font-style: normal;
+ font-weight: normal;
+ user-select: none;
+ display: inline-flex;
+ font-size: 35px;
+ text-transform: uppercase;
+ color: black;
+ }
+
+ .ink:focus {
+ outline: 1px solid blue;
+ }
+
+ .sidebarButtons {
+ top: 80px;
+ position: relative;
+ }
+}
+
+
+
+
+
+
+.blanket {
+ position: fixed;
+ top: 120px;
+ opacity: 0.5;
+ right: -100%;
+ width: 100%;
+ height: calc(100% - (120px));
+ z-index: 101;
+ background-color: grey;
+ padding: 20px;
+}
+
+.blanket.active {
+ position: absolute;
+ right: 0%;
+ z-index: 100;
+}
+
+.home {
+ position: absolute;
+ top: 30px;
+ left: 30px;
+ font-size: 60;
+ user-select: none;
+ text-transform: uppercase;
+ font-family: Arial, Helvetica, sans-serif;
+ z-index: 200;
+}
+
+.item-type {
+ display: inline;
+ text-transform: lowercase;
+ margin-left: 20px;
+ font-size: 35px;
+ font-style: italic;
+ color: rgb(28, 28, 28);
+}
+
+.item-title {
+ max-width: 70%;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.right {
+ margin-left: 20px;
+ z-index: 200;
+}
+
+.open {
+ right: 20px;
+ font-size: 35;
+ position: absolute;
+}
+
+.left {
+ width: 100%;
+ height: 100%;
+}
+
+
+
+.sidebar.active {
+ position: absolute;
+ right: 0%;
+ opacity: 1;
+ z-index: 101;
+}
+
+.back {
+ position: absolute;
+ left: 42px;
+ top: 0;
+ background: #1a1a1a;
+ width: 50px;
+ height: 100%;
display: flex;
- justify-content: space-between;
+ justify-content: center;
+ text-align: center;
+ flex-direction: column;
+ align-items: center;
+ border-radius: 10px;
+ font-size: 25px;
+ user-select: none;
+ z-index: 100;
+}
+
+.pathbar {
+ position: fixed;
+ top: 118px;
+ left: 0px;
+ background: #1a1a1a;
+ z-index: 120;
+ border-radius: 0px;
width: 100%;
- z-index: 9999;
- height: 50px;
+ height: 80px;
+ overflow: hidden;
+
+ .pathname {
+ position: relative;
+ font-size: 25;
+ top: 50%;
+ width: 86%;
+ left: 12%;
+ color: whitesmoke;
+ transform: translate(0%, -50%);
+ z-index: 20;
+ font-family: sans-serif;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ direction: rtl;
+ text-align: left;
+ text-transform: uppercase;
+ }
+
+ .scrollmenu {
+ overflow: auto;
+ width: 100%;
+ height: 100%;
+ white-space: nowrap;
+ display: inline-flex;
+ }
- .mobileInterface-button {
+ .hidePath {
+ position: absolute;
height: 100%;
+ width: 200px;
+ left: 0px;
+ top: 0px;
+ background-image: linear-gradient(to right, #1a1a1a, rgba(0, 0, 0, 0));
+ text-align: center;
+ user-select: none;
+ z-index: 99;
+ pointer-events: none;
+ }
+
+ .pathbarItem {
+ position: relative;
+ display: flex;
+ align-items: center;
+ color: whitesmoke;
+ text-align: center;
+ justify-content: center;
+ user-select: none;
+ transform: translate(100px, 0px);
+ font-size: 30px;
+ padding: 10px;
+ text-transform: uppercase;
+
+ .pathbarText {
+ font-family: sans-serif;
+ text-align: center;
+ height: 50px;
+ padding: 10px;
+ font-size: 30px;
+ border-radius: 10px;
+ text-transform: uppercase;
+ margin-left: 20px;
+ position: relative;
+ }
+
+ .pathIcon {
+ transform: translate(0px, 0px);
+ position: relative;
+ }
}
}
-.mobileInterface-container {
- height: 100%;
+
+/**
+* docButton appears at the bottom of mobile document
+* Buttons include: pin to presentation, download, upload, reload
+*/
+.docButton {
position: relative;
- touch-action: none;
+ width: 100px;
+ display: flex;
+ height: 100px;
+ font-size: 70px;
+ text-align: center;
+ border: 3px solid black;
+ margin: 20px;
+ z-index: 100;
+ border-radius: 100%;
+ justify-content: center;
+ flex-direction: column;
+ align-items: center;
+}
+
+.docButtonContainer {
+ top: 80%;
+ position: absolute;
+ display: flex;
+ transform: translate(-50%, 0);
+ left: 50%;
+ z-index: 100;
+}
+
+.toolbar {
+ left: 50%;
+ transform: translate(-50%);
+ position: absolute;
+ height: max-content;
+ top: 0px;
+ border-radius: 20px;
+ background-color: lightgrey;
+ opacity: 0;
+ transition: all 400ms ease 50ms;
+}
+
+.toolbar.active {
+ display: inline-block;
+ width: 300px;
+ padding: 5px;
+ opacity: 1;
+ height: max-content;
+ top: -450px;
+}
+
+.colorSelector {
+ position: absolute;
+ top: 550px;
+ left: 280px;
+ transform: translate(-50%, 0);
+ z-index: 100;
+ display: inline-flex;
+ width: max-content;
+ height: max-content;
+ pointer-events: all;
+ font-size: 80px;
+ user-select: none;
+}
+
+// Menu buttons for toggling between list and icon view
+.homeSwitch {
+ position: fixed;
+ top: 212;
+ right: 36px;
+ display: inline-flex;
+ width: max-content;
+ z-index: 99;
+ height: 70px;
+
+ .list {
+ width: 70px;
+ height: 70px;
+ margin: 5;
+ padding: 10;
+ align-items: center;
+ text-align: center;
+ font-size: 50;
+ border-style: solid;
+ border-width: 3;
+ border-color: black;
+ background: whitesmoke;
+ align-self: center;
+ border-radius: 10px;
+ }
+
+ .list.active {
+ color: darkred;
+ border-color: darkred;
+ }
} \ No newline at end of file
diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx
index a50ec103e..3a889b0db 100644
--- a/src/mobile/MobileInterface.tsx
+++ b/src/mobile/MobileInterface.tsx
@@ -1,47 +1,69 @@
-import React = require('react');
+import * as React from "react";
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faEraser, faHighlighter, faLongArrowAltLeft, faMousePointer, faPenNib } from '@fortawesome/free-solid-svg-icons';
+import {
+ faTasks, faReply, faQuoteLeft, faHandPointLeft, faFolderOpen, faAngleDoubleLeft, faExternalLinkSquareAlt, faMobile, faThLarge, faWindowClose, faEdit, faTrashAlt, faPalette, faAngleRight, faBell, faTrash, faCamera, faExpand, faCaretDown, faCaretLeft, faCaretRight, faCaretSquareDown, faCaretSquareRight, faArrowsAltH, faPlus, faMinus,
+ faTerminal, faToggleOn, faFile as fileSolid, faExternalLinkAlt, faLocationArrow, faSearch, faFileDownload, faStop, faCalculator, faWindowMaximize, faAddressCard,
+ faQuestionCircle, faArrowLeft, faArrowRight, faArrowDown, faArrowUp, faBolt, faBullseye, faCaretUp, faCat, faCheck, faChevronRight, faClipboard, faClone, faCloudUploadAlt,
+ faCommentAlt, faCompressArrowsAlt, faCut, faEllipsisV, faEraser, faExclamation, faFileAlt, faFileAudio, faFilePdf, faFilm, faFilter, faFont, faGlobeAsia, faHighlighter,
+ faLongArrowAltRight, faMicrophone, faMousePointer, faMusic, faObjectGroup, faPause, faPen, faPenNib, faPhone, faPlay, faPortrait, faRedoAlt, faStamp, faStickyNote,
+ faThumbtack, faTree, faTv, faBook, faUndoAlt, faVideo, faAsterisk, faBrain, faImage, faPaintBrush, faTimes, faEye, faHome, faLongArrowAltLeft, faBars, faTh, faChevronLeft,
+ faAlignRight, faAlignLeft
+} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable } from 'mobx';
+import { action, computed, observable, reaction, trace, runInAction } from 'mobx';
import { observer } from 'mobx-react';
-import { DocServer } from '../client/DocServer';
-import { Docs } from '../client/documents/Documents';
-import { DocumentManager } from '../client/util/DocumentManager';
-import RichTextMenu from '../client/views/nodes/formattedText/RichTextMenu';
+import { Doc, DocListCast } from '../fields/Doc';
+import { CurrentUserUtils } from '../client/util/CurrentUserUtils';
+import { emptyFunction, emptyPath, returnFalse, returnOne, returnTrue, returnZero, returnEmptyFilter } from '../Utils';
+import { Docs, DocumentOptions } from '../client/documents/Documents';
import { Scripting } from '../client/util/Scripting';
-import { Transform } from '../client/util/Transform';
-import { DocumentDecorations } from '../client/views/DocumentDecorations';
-import GestureOverlay from '../client/views/GestureOverlay';
import { DocumentView } from '../client/views/nodes/DocumentView';
-import { RadialMenu } from '../client/views/nodes/RadialMenu';
-import { PreviewCursor } from '../client/views/PreviewCursor';
-import { Doc, DocListCast, FieldResult } from '../fields/Doc';
-import { Id } from '../fields/FieldSymbols';
+import { Transform } from '../client/util/Transform';
+import "./MobileInterface.scss";
+import "./ImageUpload.scss";
+import "./AudioUpload.scss";
+import SettingsManager from '../client/util/SettingsManager';
+import { Uploader } from "./ImageUpload";
+import { DockedFrameRenderer } from '../client/views/collections/CollectionDockingView';
import { InkTool } from '../fields/InkField';
-import { listSpec } from '../fields/Schema';
+import GestureOverlay from "../client/views/GestureOverlay";
+import { ScriptField } from "../fields/ScriptField";
+import { RadialMenu } from "../client/views/nodes/RadialMenu";
+import { UndoManager } from "../client/util/UndoManager";
+import { List } from "../fields/List";
+import { AudioUpload } from "./AudioUpload";
import { Cast, FieldValue } from '../fields/Types';
-import { WebField } from "../fields/URLField";
-import { CurrentUserUtils } from '../client/util/CurrentUserUtils';
-import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero, returnEmptyFilter } from '../Utils';
-import "./MobileInterface.scss";
-import { CollectionView } from '../client/views/collections/CollectionView';
-import { InkingStroke } from '../client/views/InkingStroke';
+import RichTextMenu from "../client/views/nodes/formattedText/RichTextMenu";
+import { AudioBox } from "../client/views/nodes/AudioBox";
+import { CollectionViewType } from "../client/views/collections/CollectionView";
+import { DocumentType } from "../client/documents/DocumentTypes";
+import { CollectionFreeFormViewChrome } from "../client/views/collections/CollectionMenu";
-library.add(faLongArrowAltLeft);
+library.add(faTasks, faReply, faQuoteLeft, faHandPointLeft, faFolderOpen, faAngleDoubleLeft, faExternalLinkSquareAlt, faMobile, faThLarge, faWindowClose, faEdit, faTrashAlt, faPalette, faAngleRight, faBell, faTrash, faCamera, faExpand, faCaretDown, faCaretLeft, faCaretRight, faCaretSquareDown, faCaretSquareRight, faArrowsAltH, faPlus, faMinus,
+ faTerminal, faToggleOn, fileSolid, faExternalLinkAlt, faLocationArrow, faSearch, faFileDownload, faStop, faCalculator, faWindowMaximize, faAddressCard,
+ faQuestionCircle, faArrowLeft, faArrowRight, faArrowDown, faArrowUp, faBolt, faBullseye, faCaretUp, faCat, faCheck, faChevronRight, faClipboard, faClone, faCloudUploadAlt,
+ faCommentAlt, faCompressArrowsAlt, faCut, faEllipsisV, faEraser, faExclamation, faFileAlt, faFileAudio, faFilePdf, faFilm, faFilter, faFont, faGlobeAsia, faHighlighter,
+ faLongArrowAltRight, faMicrophone, faMousePointer, faMusic, faObjectGroup, faPause, faPen, faPenNib, faPhone, faPlay, faPortrait, faRedoAlt, faStamp, faStickyNote,
+ faThumbtack, faTree, faTv, faUndoAlt, faBook, faVideo, faAsterisk, faBrain, faImage, faPaintBrush, faTimes, faEye, faHome, faLongArrowAltLeft, faBars, faTh, faChevronLeft,
+ faAlignLeft, faAlignRight);
@observer
-export default class MobileInterface extends React.Component {
- @observable static Instance: MobileInterface;
- @computed private get userDoc() { return Doc.UserDoc(); }
- @computed private get mainContainer() { return this.userDoc ? FieldValue(Cast(this.userDoc.activeMobile, Doc)) : CurrentUserUtils.GuestMobile; }
- // @observable private currentView: "main" | "ink" | "upload" = "main";
- private mainDoc: any = CurrentUserUtils.setupMobileDoc(this.userDoc);
- @observable private renderView?: () => JSX.Element;
+export class MobileInterface extends React.Component {
+ static Instance: MobileInterface;
+ private _library: Doc = CurrentUserUtils.setupLibrary(Doc.UserDoc()); // to access documents in Dash Web
+ private _mainDoc: any = CurrentUserUtils.setupActiveMobileMenu(Doc.UserDoc());
+ @observable private _sidebarActive: boolean = false; //to toggle sidebar display
+ @observable private _imageUploadActive: boolean = false; //to toggle image upload
+ @observable private _audioUploadActive: boolean = false;
+ @observable private _menuListView: boolean = false; //to switch between menu view (list / icon)
+ @observable private _ink: boolean = false; //toggle whether ink is being dispalyed
+ @observable private _homeMenu: boolean = true; // to determine whether currently at home menu
+ @observable private _child: Doc | null = null; // currently selected document
+ @observable private _activeDoc: Doc = this._mainDoc; // doc updated as the active mobile page is updated (initially home menu)
+ @observable private _homeDoc: Doc = this._mainDoc; // home menu as a document
+ @observable private _parents: Array<Doc> = []; // array of parent docs (for pathbar)
- // private inkDoc?: Doc;
- public drawingInk: boolean = false;
-
- // private uploadDoc?: Doc;
+ @computed private get mainContainer() { return Doc.UserDoc() ? FieldValue(Cast(Doc.UserDoc().activeMobile, Doc)) : CurrentUserUtils.GuestMobile; }
constructor(props: Readonly<{}>) {
super(props);
@@ -50,298 +72,613 @@ export default class MobileInterface extends React.Component {
@action
componentDidMount = () => {
- library.add(...[faPenNib, faHighlighter, faEraser, faMousePointer]);
+ // if the home menu is in list view -> adjust the menu toggle appropriately
+ this._menuListView = this._homeDoc._viewType === "stacking" ? true : false;
+ Doc.SetSelectedTool(InkTool.None); // ink should intially be set to none
+ Doc.UserDoc().activeMobile = this._homeDoc; // active mobile set to home
+ AudioBox.Enabled = true;
- if (this.userDoc && !this.mainContainer) {
- this.userDoc.activeMobile = this.mainDoc;
- }
+ // remove double click to avoid mobile zoom in
+ document.removeEventListener("dblclick", this.onReactDoubleClick);
+ document.addEventListener("dblclick", this.onReactDoubleClick);
}
@action
- switchCurrentView = (doc: (userDoc: Doc) => Doc, renderView?: () => JSX.Element, onSwitch?: () => void) => {
- if (!this.userDoc) return;
+ componentWillUnmount = () => {
+ document.removeEventListener('dblclick', this.onReactDoubleClick);
+ }
+
+ // Prevent zooming in when double tapping the screen
+ onReactDoubleClick = (e: MouseEvent) => {
+ e.stopPropagation();
+ }
- this.userDoc.activeMobile = doc(this.userDoc);
- onSwitch && onSwitch();
+ // Switch the mobile view to the given doc
+ @action
+ switchCurrentView = (doc: Doc, renderView?: () => JSX.Element, onSwitch?: () => void) => {
+ if (!Doc.UserDoc()) return;
+ if (this._activeDoc === this._homeDoc) {
+ this._parents.push(this._activeDoc);
+ this._homeMenu = false;
+ }
+ this._activeDoc = doc;
+ Doc.UserDoc().activeMobile = doc;
+ onSwitch?.();
- this.renderView = renderView;
+ // Ensures that switching to home is not registed
+ UndoManager.undoStack.length = 0;
+ UndoManager.redoStack.length = 0;
}
- onSwitchInking = () => {
- Doc.SetSelectedTool(InkTool.Pen);
- MobileInterface.Instance.drawingInk = true;
+ // For toggling the hamburger menu
+ @action
+ toggleSidebar = () => {
+ this._sidebarActive = !this._sidebarActive;
- DocServer.Mobile.dispatchOverlayTrigger({
- enableOverlay: true,
- width: window.innerWidth,
- height: window.innerHeight
- });
+ if (this._ink) {
+ this.onSwitchInking();
+ }
+ }
+ /**
+ * Method called when 'Library' button is pressed on the home screen
+ */
+ switchToLibrary = async () => {
+ this.switchCurrentView(this._library);
+ runInAction(() => this._homeMenu = false);
+ this.toggleSidebar();
}
- onSwitchUpload = async () => {
- let width = 300;
- let height = 300;
-
- // get width and height of the collection doc
- if (this.mainContainer) {
- const data = Cast(this.mainContainer.data, listSpec(Doc));
- if (data) {
- const collectionDoc = await data[1]; // this should be the collection doc since the positions should be locked
- const docView = DocumentManager.Instance.getDocumentView(collectionDoc);
- if (docView) {
- width = docView.nativeWidth ? docView.nativeWidth : 300;
- height = docView.nativeHeight ? docView.nativeHeight : 300;
- }
- }
+ /**
+ * Back method for navigating through items
+ */
+ @action
+ back = () => {
+ const header = document.getElementById("header") as HTMLElement;
+ const doc = Cast(this._parents.pop(), Doc) as Doc; // Parent document
+ // Case 1: Parent document is 'workspaces'
+ if (doc === Cast(this._library, Doc) as Doc) {
+ this._child = null;
+ this.switchCurrentView(this._library);
+ // Case 2: Parent document is the 'home' menu (root node)
+ } else if (doc === Cast(this._homeDoc, Doc) as Doc) {
+ this._homeMenu = true;
+ this._parents = [];
+ this._child = null;
+ this.switchCurrentView(this._homeDoc);
+ // Case 3: Parent document is any document
+ } else if (doc) {
+ this._child = doc;
+ this.switchCurrentView(doc);
+ this._homeMenu = false;
+ header.textContent = String(doc.title);
}
- DocServer.Mobile.dispatchOverlayTrigger({
- enableOverlay: true,
- width: width,
- height: height,
- text: "Documents uploaded from mobile will show here",
- });
+ this._ink = false; // turns ink off
}
- renderDefaultContent = () => {
- if (this.mainContainer) {
- return <DocumentView
- Document={this.mainContainer}
- DataDoc={undefined}
- LibraryPath={emptyPath}
- addDocument={returnFalse}
- addDocTab={returnFalse}
- pinToPres={emptyFunction}
- rootSelected={returnFalse}
- removeDocument={undefined}
- onClick={undefined}
- ScreenToLocalTransform={Transform.Identity}
- ContentScaling={returnOne}
- NativeHeight={returnZero}
- NativeWidth={returnZero}
- PanelWidth={() => window.screen.width}
- PanelHeight={() => window.screen.height}
- renderDepth={0}
- focus={emptyFunction}
- backgroundColor={returnEmptyString}
- parentActive={returnTrue}
- whenActiveChanged={emptyFunction}
- bringToFront={emptyFunction}
- docFilters={returnEmptyFilter}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined} />;
+ /**
+ * Return 'Home", which implies returning to 'Home' menu buttons
+ */
+ @action
+ returnHome = () => {
+ if (!this._homeMenu || this._sidebarActive) {
+ this._homeMenu = true;
+ this._parents = [];
+ this._child = null;
+ this.switchCurrentView(this._homeDoc);
+ }
+ if (this._sidebarActive) {
+ this.toggleSidebar();
}
- return "hello";
}
- onBack = (e: React.MouseEvent) => {
- this.switchCurrentView((userDoc: Doc) => this.mainDoc);
- Doc.SetSelectedTool(InkTool.None); // TODO: switch to previous tool
+ /**
+ * Return to primary Workspace in library (Workspaces Doc)
+ */
+ @action
+ returnMain = () => {
+ this._parents = [this._homeDoc];
+ this.switchCurrentView(this._library);
+ this._homeMenu = false;
+ this._child = null;
+ }
+
+ /**
+ * Note: window.innerWidth and window.screen.width compute different values.
+ * window.screen.width is the display size, however window.innerWidth is the
+ * display resolution which computes differently.
+ */
+ returnWidth = () => window.innerWidth; //The windows width
+ returnHeight = () => (window.innerHeight - 300); //Calculating the windows height (-300 to account for topbar)
+ whitebackground = () => "white";
+ /**
+ * DocumentView for graphic display of all documents
+ */
+ @computed get displayWorkspaces() {
+ return !this.mainContainer ? (null) :
+ <div style={{ position: "relative", top: '198px', height: `calc(100% - 350px)`, width: "100%", left: "0%" }}>
+ <DocumentView
+ Document={this.mainContainer}
+ DataDoc={undefined}
+ LibraryPath={emptyPath}
+ addDocument={returnFalse}
+ addDocTab={returnFalse}
+ pinToPres={emptyFunction}
+ rootSelected={returnFalse}
+ removeDocument={undefined}
+ onClick={undefined}
+ ScreenToLocalTransform={Transform.Identity}
+ ContentScaling={returnOne}
+ PanelWidth={this.returnWidth}
+ PanelHeight={this.returnHeight}
+ NativeHeight={returnZero}
+ NativeWidth={returnZero}
+ renderDepth={0}
+ focus={emptyFunction}
+ backgroundColor={this.whitebackground}
+ parentActive={returnTrue}
+ whenActiveChanged={emptyFunction}
+ bringToFront={emptyFunction}
+ docFilters={returnEmptyFilter}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
+ />
+ </div>;
+ }
- DocServer.Mobile.dispatchOverlayTrigger({
- enableOverlay: false,
- width: window.innerWidth,
- height: window.innerHeight
+ /**
+ * Handles the click functionality in the library panel.
+ * Navigates to the given doc and updates the sidebar.
+ * @param doc: doc for which the method is called
+ */
+ handleClick = async (doc: Doc) => {
+ runInAction(() => {
+ if (doc.type !== "collection" && this._sidebarActive) {
+ this._parents.push(this._activeDoc);
+ this.switchCurrentView(doc);
+ this._homeMenu = false;
+ this.toggleSidebar();
+ }
+ else {
+ this._parents.push(this._activeDoc);
+ this.switchCurrentView(doc);
+ this._homeMenu = false;
+ this._child = doc;
+ }
});
+ }
- // this.inkDoc = undefined;
- this.drawingInk = false;
+ /**
+ * Called when an item in the library is clicked and should
+ * be opened (open icon on RHS of all menu items)
+ * @param doc doc to be opened
+ */
+ @action
+ openFromSidebar = (doc: Doc) => {
+ this._parents.push(this._activeDoc);
+ this.switchCurrentView(doc);
+ this._homeMenu = false;
+ this._child = doc;
+ this.toggleSidebar();
}
- shiftLeft = (e: React.MouseEvent) => {
- DocServer.Mobile.dispatchOverlayPositionUpdate({
- dx: -10
- });
- e.preventDefault();
- e.stopPropagation();
+ // Renders the graphical pathbar
+ renderPathbar = () => {
+ const docPath = [...this._parents, this._activeDoc];
+ const items = docPath.map((doc: Doc, index: any) =>
+ <div className="pathbarItem" key={index}>
+ {index === 0 ? (null) : <FontAwesomeIcon key="icon" className="pathIcon" icon="angle-right" size="lg" />}
+ <div className="pathbarText"
+ style={{ backgroundColor: this._homeMenu || doc === this._activeDoc ? "rgb(119,17,37)" : undefined }}
+ onClick={() => this.handlePathClick(doc, index)}>{doc.title}
+ </div>
+ </div>);
+ return (<div className="pathbar">
+ <div className="scrollmenu">
+ {items}
+ </div>
+ {!this._parents.length ? (null) :
+ <div className="back" >
+ <FontAwesomeIcon onClick={this.back} icon={"chevron-left"} color="white" size={"2x"} />
+ </div>}
+ <div className="hidePath" />
+ </div>);
}
- shiftRight = (e: React.MouseEvent) => {
- DocServer.Mobile.dispatchOverlayPositionUpdate({
- dx: 10
- });
- e.preventDefault();
- e.stopPropagation();
+ // Handles when user clicks on a document in the pathbar
+ @action
+ handlePathClick = (doc: Doc, index: number) => {
+ if (doc === this._library) {
+ this._child = null;
+ this.switchCurrentView(doc);
+ this._parents.length = index;
+ } else if (doc === this._homeDoc) {
+ this.returnHome();
+ } else {
+ this._child = doc;
+ this.switchCurrentView(doc);
+ this._parents.length = index;
+ }
}
- panelHeight = () => window.innerHeight;
- panelWidth = () => window.innerWidth;
- renderInkingContent = () => {
- console.log("rendering inking content");
- // TODO: support panning and zooming
- // TODO: handle moving of ink strokes
- if (this.mainContainer) {
+ // Renders the contents of the menu and sidebar
+ @computed get renderDefaultContent() {
+ if (this._homeMenu) {
return (
- <div className="mobileInterface">
- <div className="mobileInterface-inkInterfaceButtons">
- <div className="navButtons">
- <button className="mobileInterface-button cancel" onClick={this.onBack} title="Cancel drawing">BACK</button>
- </div>
- <div className="inkSettingButtons">
- <button className="mobileInterface-button cancel" onClick={this.onBack} title="Cancel drawing"><FontAwesomeIcon icon="long-arrow-alt-left" /></button>
- </div>
- <div className="navButtons">
- <button className="mobileInterface-button" onClick={this.shiftLeft} title="Shift left">left</button>
- <button className="mobileInterface-button" onClick={this.shiftRight} title="Shift right">right</button>
+ <div>
+ <div className="navbar">
+ <FontAwesomeIcon className="home" icon="home" onClick={this.returnHome} />
+ <div className="header" id="header">{this._homeDoc.title}</div>
+ <div className="cover" id="cover" onClick={e => e.stopPropagation()}></div>
+ <div className="toggle-btn" id="menuButton" onClick={this.toggleSidebar}>
+ <span></span>
+ <span></span>
+ <span></span>
</div>
</div>
- <CollectionView
- Document={this.mainContainer}
- DataDoc={undefined}
- LibraryPath={emptyPath}
- filterAddDocument={returnTrue}
- fieldKey={""}
- dropAction={"alias"}
- bringToFront={emptyFunction}
- addDocTab={returnFalse}
- pinToPres={emptyFunction}
- PanelWidth={this.panelWidth}
- PanelHeight={this.panelHeight}
- NativeHeight={returnZero}
- NativeWidth={returnZero}
- focus={emptyFunction}
- isSelected={returnFalse}
- select={emptyFunction}
- active={returnFalse}
- ContentScaling={returnOne}
- whenActiveChanged={returnFalse}
- ScreenToLocalTransform={Transform.Identity}
- renderDepth={0}
- docFilters={returnEmptyFilter}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
- rootSelected={returnTrue}>
- </CollectionView>
+ {this.renderPathbar()}
</div>
);
}
+ // stores workspace documents as 'workspaces' variable
+ let workspaces = Cast(Doc.UserDoc().myWorkspaces, Doc) as Doc;
+ if (this._child) {
+ workspaces = this._child;
+ }
+ // returns a list of navbar buttons as 'buttons'
+ const buttons = DocListCast(workspaces.data).map((doc: Doc, index: any) => {
+ if (doc.type !== "ink") {
+ return (
+ <div
+ className="item"
+ key={index}>
+ <div className="item-title" onClick={() => this.handleClick(doc)}> {doc.title} </div>
+ <div className="item-type" onClick={() => this.handleClick(doc)}>{doc.type}</div>
+ <FontAwesomeIcon onClick={() => this.handleClick(doc)} className="right" icon="angle-right" size="lg" style={{ display: `${doc.type === "collection" ? "block" : "none"}` }} />
+ <FontAwesomeIcon className="open" onClick={() => this.openFromSidebar(doc)} icon="external-link-alt" size="lg" />
+ </div>
+ );
+ }
+ });
+
+ return (
+ <div>
+ <div className="navbar">
+ <FontAwesomeIcon className="home" icon="home" onClick={this.returnHome} />
+ <div className="header" id="header">{this._sidebarActive ? "library" : this._activeDoc.title}</div>
+ <div className={`toggle-btn ${this._sidebarActive ? "active" : ""}`} onClick={this.toggleSidebar}>
+ <span></span>
+ <span></span>
+ <span></span>
+ </div>
+ <div className={`background ${this._sidebarActive ? "active" : ""}`} onClick={this.toggleSidebar}></div>
+ </div>
+ {this.renderPathbar()}
+ <div className={`sidebar ${this._sidebarActive ? "active" : ""}`}>
+ <div className="sidebarButtons">
+ {this._child ?
+ <>
+ {buttons}
+ <div
+ className="item" key="home"
+ onClick={this.returnMain}
+ style={{ opacity: 0.7 }}>
+ <FontAwesomeIcon className="right" icon="angle-double-left" size="lg" />
+ <div className="item-type">Return to workspaces</div>
+ </div>
+ </> :
+ <>
+ {buttons}
+ <div
+ className="item"
+ style={{ opacity: 0.7 }}
+ onClick={() => this.createNewWorkspace()}>
+ <FontAwesomeIcon className="right" icon="plus" size="lg" />
+ <div className="item-type">Create New Workspace</div>
+ </div>
+ </>
+ }
+ </div>
+ </div>
+ <div className={`blanket ${this._sidebarActive ? "active" : ""}`} onClick={this.toggleSidebar}>
+ </div>
+ </div>
+ );
}
- upload = async (e: React.MouseEvent) => {
- if (this.mainContainer) {
- const data = Cast(this.mainContainer.data, listSpec(Doc));
- if (data) {
- const collectionDoc = await data[1]; // this should be the collection doc since the positions should be locked
- const children = DocListCast(collectionDoc.data);
- const uploadDoc = children.length === 1 ? children[0] : Docs.Create.StackingDocument(children, {
- title: "Mobile Upload Collection", backgroundColor: "white", lockedPosition: true, _width: 300, _height: 300
- });
- if (uploadDoc) {
- DocServer.Mobile.dispatchMobileDocumentUpload({
- docId: uploadDoc[Id],
- });
- }
- }
- }
- e.stopPropagation();
- e.preventDefault();
+ /**
+ * Handles the 'Create New Workspace' button in the menu (taken from MainView.tsx)
+ */
+ @action
+ createNewWorkspace = async (id?: string) => {
+ const workspaces = Cast(Doc.UserDoc().myWorkspaces, Doc) as Doc;
+ const workspaceCount = DocListCast(workspaces.data).length + 1;
+ const freeformOptions: DocumentOptions = {
+ x: 0,
+ y: 400,
+ title: "Collection " + workspaceCount,
+ };
+ const freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions);
+ const workspaceDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600, path: [Doc.UserDoc().myCatalog as Doc] }], { title: `Workspace ${workspaceCount}` }, id, "row");
+
+ const toggleTheme = ScriptField.MakeScript(`self.darkScheme = !self.darkScheme`);
+ const toggleComic = ScriptField.MakeScript(`toggleComicMode()`);
+ const cloneWorkspace = ScriptField.MakeScript(`cloneWorkspace()`);
+ workspaceDoc.contextMenuScripts = new List<ScriptField>([toggleTheme!, toggleComic!, cloneWorkspace!]);
+ workspaceDoc.contextMenuLabels = new List<string>(["Toggle Theme Colors", "Toggle Comic Mode", "New Workspace Layout"]);
+
+ Doc.AddDocToList(workspaces, "data", workspaceDoc);
}
- addWebToCollection = async () => {
- let url = "https://en.wikipedia.org/wiki/Hedgehog";
- if (this.mainContainer) {
- const data = Cast(this.mainContainer.data, listSpec(Doc));
- if (data) {
- const webDoc = await data[0];
- const urlField: FieldResult<WebField> = Cast(webDoc.data, WebField);
- url = urlField ? urlField.url.toString() : "https://en.wikipedia.org/wiki/Hedgehog";
+ // Button for switching between pen and ink mode
+ @action
+ onSwitchInking = () => {
+ const button = document.getElementById("inkButton") as HTMLElement;
+ button.style.backgroundColor = this._ink ? "white" : "black";
+ button.style.color = this._ink ? "black" : "white";
- }
- }
- Docs.Create.WebDocument(url, { _width: 300, _height: 300, title: "Mobile Upload Web Doc" });
- }
-
- clearUpload = async () => {
- if (this.mainContainer) {
- const data = Cast(this.mainContainer.data, listSpec(Doc));
- if (data) {
- const collectionDoc = await data[1];
- const children = DocListCast(collectionDoc.data);
- children.forEach(doc => {
- });
- // collectionDoc[data] = new List<Doc>();
- }
+ if (!this._ink) {
+ Doc.SetSelectedTool(InkTool.Pen);
+ this._ink = true;
+ } else {
+ Doc.SetSelectedTool(InkTool.None);
+ this._ink = false;
}
}
- renderUploadContent() {
- if (this.mainContainer) {
+ // The static ink menu that appears at the top
+ @computed get inkMenu() {
+ return this._activeDoc._viewType !== CollectionViewType.Docking || !this._ink ? (null) :
+ <div className="colorSelector">
+ {/* <CollectionFreeFormViewChrome /> */}
+ </div>;
+ }
+
+ // DocButton that uses UndoManager and handles the opacity change if CanUndo is true
+ @computed get undo() {
+ if (this.mainContainer && this._activeDoc.type === "collection" && this._activeDoc !== this._homeDoc &&
+ this._activeDoc !== Doc.UserDoc().rightSidebarCollection && this._activeDoc.title !== "WORKSPACES") {
return (
- <div className="mobileInterface" onDragOver={this.onDragOver}>
- <div className="mobileInterface-inkInterfaceButtons">
- <button className="mobileInterface-button cancel" onClick={this.onBack} title="Back">BACK</button>
- {/* <button className="mobileInterface-button" onClick={this.clearUpload} title="Clear Upload">CLEAR</button> */}
- {/* <button className="mobileInterface-button" onClick={this.addWeb} title="Add Web Doc to Upload Collection"></button> */}
- <button className="mobileInterface-button" onClick={this.upload} title="Upload">UPLOAD</button>
- </div>
- <DocumentView
- Document={this.mainContainer}
- DataDoc={undefined}
- LibraryPath={emptyPath}
- addDocument={returnFalse}
- addDocTab={returnFalse}
- pinToPres={emptyFunction}
- rootSelected={returnFalse}
- removeDocument={undefined}
- onClick={undefined}
- ScreenToLocalTransform={Transform.Identity}
- ContentScaling={returnOne}
- NativeHeight={returnZero}
- NativeWidth={returnZero}
- PanelWidth={() => window.screen.width}
- PanelHeight={() => window.screen.height}
- renderDepth={0}
- focus={emptyFunction}
- backgroundColor={returnEmptyString}
- parentActive={returnTrue}
- whenActiveChanged={emptyFunction}
- bringToFront={emptyFunction}
- docFilters={returnEmptyFilter}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined} />
+ <div className="docButton"
+ style={{ backgroundColor: "black", color: "white", fontSize: "60", opacity: UndoManager.CanUndo() ? "1" : "0.4", }}
+ id="undoButton"
+ title="undo"
+ onClick={(e: React.MouseEvent) => {
+ UndoManager.Undo();
+ e.stopPropagation();
+ }}>
+ <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="undo-alt" />
+ </div>);
+ } else return (null);
+ }
+
+ // DocButton that uses UndoManager and handles the opacity change if CanRedo is true
+ @computed get redo() {
+ if (this.mainContainer && this._activeDoc.type === "collection" && this._activeDoc !== this._homeDoc &&
+ this._activeDoc !== Doc.UserDoc().rightSidebarCollection && this._activeDoc.title !== "WORKSPACES") {
+ return (
+ <div className="docButton"
+ style={{ backgroundColor: "black", color: "white", fontSize: "60", opacity: UndoManager.CanRedo() ? "1" : "0.4", }}
+ id="undoButton"
+ title="redo"
+ onClick={(e: React.MouseEvent) => {
+ UndoManager.Redo();
+ e.stopPropagation();
+ }}>
+ <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="redo-alt" />
+ </div>);
+ } else return (null);
+ }
+
+ // DocButton for switching into ink mode
+ @computed get drawInk() {
+ return !this.mainContainer || this._activeDoc._viewType !== CollectionViewType.Docking ? (null) :
+ <div className="docButton"
+ id="inkButton"
+ title={Doc.isDocPinned(this._activeDoc) ? "Pen on" : "Pen off"}
+ onClick={this.onSwitchInking}>
+ <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="pen-nib" />
+ </div>;
+ }
+
+ // DocButton: Button that appears on the bottom of the screen to initiate image upload
+ @computed get uploadImageButton() {
+ if (this._activeDoc.type === DocumentType.COL && this._activeDoc !== this._homeDoc && this._activeDoc._viewType !== CollectionViewType.Docking && this._activeDoc.title !== "WORKSPACES") {
+ return <div className="docButton"
+ id="imageButton"
+ title={Doc.isDocPinned(this._activeDoc) ? "Pen on" : "Pen off"}
+ onClick={this.toggleUpload}>
+ <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="upload" />
+ </div>;
+ } else return (null);
+ }
+
+ // DocButton to download images on the mobile
+ @computed get downloadDocument() {
+ if (this._activeDoc.type === "image" || this._activeDoc.type === "pdf" || this._activeDoc.type === "video") {
+ return <div className="docButton"
+ title={"Download Image"}
+ style={{ backgroundColor: "white", color: "black" }}
+ onClick={e => window.open(this._activeDoc["data-path"]?.toString())}> {/* daa-path holds the url */}
+ <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="download" />
+ </div>;
+ } else return (null);
+ }
+
+ // DocButton for pinning images to presentation
+ @computed get pinToPresentation() {
+ // Only making button available if it is an image
+ if (!(this._activeDoc.type === "collection" || this._activeDoc.type === "presentation")) {
+ const isPinned = this._activeDoc && Doc.isDocPinned(this._activeDoc);
+ return <div className="docButton"
+ title={Doc.isDocPinned(this._activeDoc) ? "Unpin from presentation" : "Pin to presentation"}
+ style={{ backgroundColor: isPinned ? "black" : "white", color: isPinned ? "white" : "black" }}
+ onClick={e => DockedFrameRenderer.PinDoc(this._activeDoc, isPinned)}>
+ <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" />
+ </div>;
+ } else return (null);
+ }
+
+ // Buttons for switching the menu between large and small icons
+ @computed get switchMenuView() {
+ return this._activeDoc.title !== this._homeDoc.title ? (null) :
+ <div className="homeSwitch">
+ <div className={`list ${!this._menuListView ? "active" : ""}`} onClick={this.changeToIconView}>
+ <FontAwesomeIcon size="sm" icon="th-large" />
</div>
- );
+ <div className={`list ${this._menuListView ? "active" : ""}`} onClick={this.changeToListView}>
+ <FontAwesomeIcon size="sm" icon="bars" />
+ </div>
+ </div>;
+ }
+
+ // Logic for switching the menu into the icons
+ @action
+ changeToIconView = () => {
+ if (this._homeDoc._viewType = "stacking") {
+ this._menuListView = false;
+ this._homeDoc._viewType = "masonry";
+ this._homeDoc.columnWidth = 300;
+ this._homeDoc._columnWidth = 300;
+ const menuButtons = DocListCast(this._homeDoc.data);
+ menuButtons.map(doc => {
+ const buttonData = DocListCast(doc.data);
+ buttonData[1]._nativeWidth = 0.1;
+ buttonData[1]._width = 0.1;
+ buttonData[1]._dimMagnitude = 0;
+ buttonData[1]._opacity = 0;
+ doc._nativeWidth = 400;
+ });
}
}
+ // Logic for switching the menu into the stacking view
+ @action
+ changeToListView = () => {
+ if (this._homeDoc._viewType = "masonry") {
+ this._homeDoc._viewType = "stacking";
+ this._menuListView = true;
+ const menuButtons = DocListCast(this._homeDoc.data);
+ menuButtons.map(doc => {
+ const buttonData = DocListCast(doc.data);
+ buttonData[1]._nativeWidth = 450;
+ buttonData[1]._dimMagnitude = 2;
+ buttonData[1]._opacity = 1;
+ doc._nativeWidth = 900;
+ });
+ }
+ }
+
+ // For setting up the presentation document for the home menu
+ @action
+ setupDefaultPresentation = () => {
+ const presentation = Cast(Doc.UserDoc().activePresentation, Doc) as Doc;
+
+ if (presentation) {
+ this.switchCurrentView(presentation);
+ this._homeMenu = false;
+ }
+ }
+
+ // For toggling image upload pop up
+ @action
+ toggleUpload = () => this._imageUploadActive = !this._imageUploadActive
+
+ // For toggling audio record and dictate pop up
+ @action
+ toggleAudio = () => this._audioUploadActive = !this._audioUploadActive
+
+ // Button for toggling the upload pop up in a collection
+ @action
+ toggleUploadInCollection = () => {
+ const button = document.getElementById("imageButton") as HTMLElement;
+ button.style.backgroundColor = this._imageUploadActive ? "white" : "black";
+ button.style.color = this._imageUploadActive ? "black" : "white";
+
+ this._imageUploadActive = !this._imageUploadActive;
+ }
+
+ // For closing the image upload pop up
+ @action
+ closeUpload = () => {
+ this._imageUploadActive = false;
+ }
+
+ // Returns the image upload pop up
+ @computed get uploadImage() {
+ const doc = !this._homeMenu ? this._activeDoc : Cast(Doc.UserDoc().rightSidebarCollection, Doc) as Doc;
+ return <Uploader Document={doc} />;
+ }
+
+ // Radial menu can only be used if it is a colleciton and it is not a homeDoc
+ // (and cannot be used on Workspace to avoid pin to presentation opening on right)
+ @computed get displayRadialMenu() {
+ return this._activeDoc.type === "collection" && this._activeDoc !== this._homeDoc &&
+ this._activeDoc._viewType !== CollectionViewType.Docking ? <RadialMenu /> : (null);
+ }
+
onDragOver = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
}
+ /**
+ * MENU BUTTON
+ * Switch view from mobile menu to access the mobile uploads
+ * Global function name: openMobileUploads()
+ */
+ @action
+ switchToMobileUploads = () => {
+ const mobileUpload = Cast(Doc.UserDoc().rightSidebarCollection, Doc) as Doc;
+ this.switchCurrentView(mobileUpload);
+ this._homeMenu = false;
+ }
+
render() {
- // const content = this.currentView === "main" ? this.mainContent :
- // this.currentView === "ink" ? this.inkContent :
- // this.currentView === "upload" ? this.uploadContent : <></>;
return (
<div className="mobileInterface-container" onDragOver={this.onDragOver}>
- {/* <DocumentDecorations />
- <GestureOverlay>
- {this.renderView ? this.renderView() : this.renderDefaultContent()}
- </GestureOverlay> */}
-
- {/* <DictationOverlay />
- <SharingManager />
- <GoogleAuthenticationManager /> */}
- <DocumentDecorations />
+ <SettingsManager />
+ <div className={`image-upload ${this._imageUploadActive ? "active" : ""}`}>
+ {this.uploadImage}
+ </div>
+ <div className={`audio-upload ${this._audioUploadActive ? "active" : ""}`}>
+ <AudioUpload />
+ </div>
+ {this.switchMenuView}
+ {this.inkMenu}
<GestureOverlay>
- {this.renderView ? this.renderView() : this.renderDefaultContent()}
+ <div style={{ display: "none" }}><RichTextMenu key="rich" /></div>
+ <div className="docButtonContainer">
+ {this.pinToPresentation}
+ {this.downloadDocument}
+ {this.undo}
+ {this.redo}
+ {this.drawInk}
+ {this.uploadImageButton}
+ </div>
+ {this.displayWorkspaces}
+ {this.renderDefaultContent}
</GestureOverlay>
- <PreviewCursor />
- {/* <ContextMenu /> */}
- <RadialMenu />
- <RichTextMenu />
- {/* <PDFMenu />
- <MarqueeOptionsMenu />
- <OverlayView /> */}
+ {this.displayRadialMenu}
</div>
);
}
}
-Scripting.addGlobal(function switchMobileView(doc: (userDoc: Doc) => Doc, renderView?: () => JSX.Element, onSwitch?: () => void) { return MobileInterface.Instance.switchCurrentView(doc, renderView, onSwitch); });
-Scripting.addGlobal(function onSwitchMobileInking() { return MobileInterface.Instance.onSwitchInking(); });
-Scripting.addGlobal(function renderMobileInking() { return MobileInterface.Instance.renderInkingContent(); });
-Scripting.addGlobal(function onSwitchMobileUpload() { return MobileInterface.Instance.onSwitchUpload(); });
-Scripting.addGlobal(function renderMobileUpload() { return MobileInterface.Instance.renderUploadContent(); });
-Scripting.addGlobal(function addWebToMobileUpload() { return MobileInterface.Instance.addWebToCollection(); });
+//Global functions for mobile menu
+Scripting.addGlobal(function switchToMobileLibrary() { return MobileInterface.Instance.switchToLibrary(); },
+ "opens the library to navigate through workspaces on Dash Mobile");
+Scripting.addGlobal(function openMobileUploads() { return MobileInterface.Instance.toggleUpload(); },
+ "opens the upload files menu for Dash Mobile");
+Scripting.addGlobal(function switchToMobileUploadCollection() { return MobileInterface.Instance.switchToMobileUploads(); },
+ "opens the mobile uploads collection on Dash Mobile");
+Scripting.addGlobal(function openMobileAudio() { return MobileInterface.Instance.toggleAudio(); },
+ "opens the record and dictate menu on Dash Mobile");
+Scripting.addGlobal(function switchToMobilePresentation() { return MobileInterface.Instance.setupDefaultPresentation(); },
+ "opens the presentation on Dash Mobile");
+Scripting.addGlobal(function openMobileSettings() { return SettingsManager.Instance.open(); },
+ "opens settings on Dash Mobile");
+
+// Other global functions for mobile
+Scripting.addGlobal(function switchMobileView(doc: Doc, renderView?: () => JSX.Element, onSwitch?: () => void) { return MobileInterface.Instance.switchCurrentView(doc, renderView, onSwitch); },
+ "changes the active document displayed on the Dash Mobile", "(doc: any)");
diff --git a/src/mobile/MobileMain.tsx b/src/mobile/MobileMain.tsx
new file mode 100644
index 000000000..3d4680d58
--- /dev/null
+++ b/src/mobile/MobileMain.tsx
@@ -0,0 +1,25 @@
+import { MobileInterface } from "./MobileInterface";
+import { Docs } from "../client/documents/Documents";
+import { CurrentUserUtils } from "../client/util/CurrentUserUtils";
+import * as ReactDOM from 'react-dom';
+import * as React from 'react';
+import { DocServer } from "../client/DocServer";
+import { AssignAllExtensions } from "../extensions/General/Extensions";
+
+AssignAllExtensions();
+
+(async () => {
+ const info = await CurrentUserUtils.loadCurrentUser();
+ DocServer.init(window.location.protocol, window.location.hostname, 4321, info.email + " (mobile)");
+ await Docs.Prototypes.initialize();
+ if (info.id !== "__guest__") {
+ // a guest will not have an id registered
+ await CurrentUserUtils.loadUserDocument(info);
+ }
+ document.getElementById('root')!.addEventListener('wheel', event => {
+ if (event.ctrlKey) {
+ event.preventDefault();
+ }
+ }, true);
+ ReactDOM.render(<MobileInterface />, document.getElementById('root'));
+})(); \ No newline at end of file
diff --git a/src/mobile/MobileMenu.scss b/src/mobile/MobileMenu.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/mobile/MobileMenu.scss
diff --git a/src/pen-gestures/ndollar.ts b/src/pen-gestures/ndollar.ts
index 9d42035d1..ecd8df3e7 100644
--- a/src/pen-gestures/ndollar.ts
+++ b/src/pen-gestures/ndollar.ts
@@ -552,7 +552,7 @@ function Distance(p1: any, p2: any) // distance between two points
}
function CalcStartUnitVector(points: any, index: any) // start angle from points[0] to points[index] normalized as a unit vector
{
- const v = new Point(points[index].X - points[0].X, points[index].Y - points[0].Y);
+ const v = new Point(points[index]?.X - points[0]?.X, points[index]?.Y - points[0]?.Y);
const len = Math.sqrt(v.X * v.X + v.Y * v.Y);
return new Point(v.X / len, v.Y / len);
}
diff --git a/src/server/ApiManagers/DownloadManager.ts b/src/server/ApiManagers/DownloadManager.ts
index c5f3ca717..0d4472fdc 100644
--- a/src/server/ApiManagers/DownloadManager.ts
+++ b/src/server/ApiManagers/DownloadManager.ts
@@ -80,20 +80,14 @@ async function getDocs(id: string) {
}
const ids: string[] = [];
for (const key in doc.fields) {
- if (!doc.fields.hasOwnProperty(key)) {
- continue;
- }
+ if (!doc.fields.hasOwnProperty(key)) { continue; }
const field = doc.fields[key];
- if (field === undefined || field === null) {
- continue;
- }
+ 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);
- }
+ field.captures && ids.push(field.captures.fieldId);
} else if (field.__type === "list") {
ids.push(...fn(field));
} else if (typeof field === "string") {
diff --git a/src/server/ApiManagers/HypothesisManager.ts b/src/server/ApiManagers/HypothesisManager.ts
new file mode 100644
index 000000000..33badbc42
--- /dev/null
+++ b/src/server/ApiManagers/HypothesisManager.ts
@@ -0,0 +1,44 @@
+import ApiManager, { Registration } from "./ApiManager";
+import { Method, _permission_denied } from "../RouteManager";
+import { GoogleApiServerUtils } from "../apis/google/GoogleApiServerUtils";
+import { Database } from "../database";
+import { writeFile, readFile, readFileSync, existsSync } from "fs";
+import { serverPathToFile, Directory } from "./UploadManager";
+
+export default class HypothesisManager extends ApiManager {
+
+ protected initialize(register: Registration): void {
+
+ register({
+ method: Method.GET,
+ subscription: "/readHypothesisAccessToken",
+ secureHandler: async ({ user, res }) => {
+ if (existsSync(serverPathToFile(Directory.hypothesis, user.id))) {
+ const read = readFileSync(serverPathToFile(Directory.hypothesis, user.id), "base64") || "";
+ console.log("READ = " + read);
+ res.send(read);
+ } else res.send("");
+ }
+ });
+
+ register({
+ method: Method.POST,
+ subscription: "/writeHypothesisAccessToken",
+ secureHandler: async ({ user, req, res }) => {
+ const write = req.body.authenticationCode;
+ console.log("WRITE = " + write);
+ res.send(await writeFile(serverPathToFile(Directory.hypothesis, user.id), write, "base64", () => { }));
+ }
+ });
+
+ register({
+ method: Method.GET,
+ subscription: "/revokeHypothesisAccessToken",
+ secureHandler: async ({ user, res }) => {
+ await Database.Auxiliary.GoogleAccessToken.Revoke("dash-hyp-" + user.id);
+ res.send();
+ }
+ });
+
+ }
+} \ No newline at end of file
diff --git a/src/server/ApiManagers/PDFManager.ts b/src/server/ApiManagers/PDFManager.ts
index e028d628d..2b4212588 100644
--- a/src/server/ApiManagers/PDFManager.ts
+++ b/src/server/ApiManagers/PDFManager.ts
@@ -114,4 +114,4 @@ class NodeCanvasFactory {
canvasAndContext.canvas = null;
canvasAndContext.context = null;
}
-} \ No newline at end of file
+}
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 696ad0288..515fbe4ff 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -15,6 +15,7 @@ import RouteSubscriber from "../RouteSubscriber";
const imageDataUri = require('image-data-uri');
import { isWebUri } from "valid-url";
import { Opt } from "../../fields/Doc";
+import { SolrManager } from "./SearchManager";
export enum Directory {
parsed_files = "parsed_files",
@@ -23,7 +24,8 @@ export enum Directory {
pdfs = "pdfs",
text = "text",
pdf_thumbnails = "pdf_thumbnails",
- audio = "audio"
+ audio = "audio",
+ hypothesis = "hypothesis"
}
export function serverPathToFile(directory: Directory, filename: string) {
@@ -137,13 +139,9 @@ export default class UploadManager extends ApiManager {
doc.id = getId(doc.id);
}
for (const key in doc.fields) {
- if (!doc.fields.hasOwnProperty(key)) {
- continue;
- }
+ if (!doc.fields.hasOwnProperty(key)) { continue; }
const field = doc.fields[key];
- if (field === undefined || field === null) {
- continue;
- }
+ if (field === undefined || field === null) { continue; }
if (field.__type === "proxy" || field.__type === "prefetch_proxy") {
field.fieldId = getId(field.fieldId);
@@ -206,11 +204,8 @@ export default class UploadManager extends ApiManager {
} catch (e) { console.log(e); }
unlink(path_2, () => { });
}
- if (id) {
- res.send(JSON.stringify(getId(id)));
- } else {
- res.send(JSON.stringify("error"));
- }
+ SolrManager.update();
+ res.send(JSON.stringify(id ? getId(id) : "error"));
} catch (e) { console.log(e); }
resolve();
});
diff --git a/src/server/Recommender.ts b/src/server/Recommender.ts
index 423ce9b46..935ec3871 100644
--- a/src/server/Recommender.ts
+++ b/src/server/Recommender.ts
@@ -21,7 +21,6 @@
// private choice: string = ""; // Tensorflow or Word2Vec
// constructor() {
-// console.log("creating recommender...");
// Recommender.Instance = this;
// }
@@ -70,7 +69,6 @@
// if (this._model) {
// if (this.choice === "WV") {
// let similarity = this._model.similarity('father', 'mother');
-// console.log(similarity);
// }
// else if (this.choice === "TF") {
// const model = this._model as use.UniversalSentenceEncoder;
@@ -119,7 +117,6 @@
// }
// // public async trainModel() {
-// // console.log("phrasing...");
// // w2v.word2vec("./node_modules/word2vec/examples/eng_news-typical_2016_1M-sentences.txt", './node_modules/word2vec/examples/my_phrases.txt', {
// // cbow: 1,
// // size: 200,
@@ -131,7 +128,6 @@
// // iter: 200,
// // minCount: 2
// // });
-// // console.log("phrased!!!");
// // }
// }
diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts
index 20f96f432..b0157a85f 100644
--- a/src/server/apis/google/GoogleApiServerUtils.ts
+++ b/src/server/apis/google/GoogleApiServerUtils.ts
@@ -39,7 +39,8 @@ export namespace GoogleApiServerUtils {
*/
export enum Service {
Documents = "Documents",
- Slides = "Slides"
+ Slides = "Slides",
+ Hypothesis = "Hypothesis"
}
/**
diff --git a/src/server/downsize.ts b/src/server/downsize.ts
index cd0d83812..5cd709fa3 100644
--- a/src/server/downsize.ts
+++ b/src/server/downsize.ts
@@ -7,7 +7,7 @@ const jpgTypes = ["jpg", "JPG", "jpeg", "JPEG"];
const smallResizer = sharp().resize(100);
fs.readdir(folder, async (err, files) => {
if (err) {
- console.log(err);
+ console.log("readdir:" + err);
return;
}
// files.forEach(file => {
diff --git a/src/server/index.ts b/src/server/index.ts
index 590affd06..9af4b00bc 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -18,10 +18,10 @@ import PDFManager from "./ApiManagers/PDFManager";
import UploadManager from "./ApiManagers/UploadManager";
import { log_execution } from "./ActionUtilities";
import GeneralGoogleManager from "./ApiManagers/GeneralGoogleManager";
+import HypothesisManager from "./ApiManagers/HypothesisManager";
import GooglePhotosManager from "./ApiManagers/GooglePhotosManager";
import { Logger } from "./ProcessFactory";
import { yellow } from "colors";
-import { DashSessionAgent } from "./DashSession/DashSessionAgent";
import SessionManager from "./ApiManagers/SessionManager";
import { AppliedSessionAgent } from "./DashSession/Session/agents/applied_session_agent";
@@ -72,6 +72,7 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }:
new DeleteManager(),
new UtilManager(),
new GeneralGoogleManager(),
+ new HypothesisManager(),
new GooglePhotosManager(),
];
@@ -104,6 +105,7 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }:
const serve: PublicHandler = ({ req, res }) => {
const detector = new mobileDetect(req.headers['user-agent'] || "");
const filename = detector.mobile() !== null ? 'mobile/image.html' : 'index.html';
+ console.log(detector.is("iPhone"));
res.sendFile(path.join(__dirname, '../../deploy/' + filename));
};
diff --git a/src/server/websocket.ts b/src/server/websocket.ts
index 19c98454c..c80056579 100644
--- a/src/server/websocket.ts
+++ b/src/server/websocket.ts
@@ -180,7 +180,14 @@ export namespace WebSocket {
function barReceived(socket: SocketIO.Socket, userEmail: string) {
clients[userEmail] = new Client(userEmail.toString());
- console.log(green(`user ${userEmail} has connected to the web socket`));
+ const currentdate = new Date();
+ const datetime = currentdate.getDate() + "/"
+ + (currentdate.getMonth() + 1) + "/"
+ + currentdate.getFullYear() + " @ "
+ + currentdate.getHours() + ":"
+ + currentdate.getMinutes() + ":"
+ + currentdate.getSeconds();
+ console.log(green(`user ${userEmail} has connected to the web socket at: ${datetime}`));
socketMap.set(socket, userEmail);
}