aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--package-lock.json6
-rw-r--r--src/Utils.ts71
-rw-r--r--src/client/DocServer.ts9
-rw-r--r--src/client/apis/google_docs/GooglePhotosClientUtils.ts2
-rw-r--r--src/client/documents/Documents.ts186
-rw-r--r--src/client/documents/Gitlike.ts36
-rw-r--r--src/client/util/CurrentUserUtils.ts466
-rw-r--r--src/client/util/DocumentManager.ts11
-rw-r--r--src/client/util/DragManager.ts21
-rw-r--r--src/client/util/GroupMemberView.tsx1
-rw-r--r--src/client/util/HypothesisUtils.ts2
-rw-r--r--src/client/util/InteractionUtils.tsx7
-rw-r--r--src/client/util/LinkManager.ts101
-rw-r--r--src/client/util/Scripting.ts8
-rw-r--r--src/client/util/SettingsManager.scss5
-rw-r--r--src/client/util/SettingsManager.tsx3
-rw-r--r--src/client/util/SharingManager.tsx95
-rw-r--r--src/client/views/.DS_Storebin10244 -> 10244 bytes
-rw-r--r--src/client/views/ContextMenu.scss10
-rw-r--r--src/client/views/ContextMenuItem.tsx2
-rw-r--r--src/client/views/DocComponent.tsx34
-rw-r--r--src/client/views/DocumentButtonBar.scss1
-rw-r--r--src/client/views/DocumentButtonBar.tsx7
-rw-r--r--src/client/views/DocumentDecorations.scss6
-rw-r--r--src/client/views/DocumentDecorations.tsx2
-rw-r--r--src/client/views/EditableView.scss8
-rw-r--r--src/client/views/EditableView.tsx3
-rw-r--r--src/client/views/GestureOverlay.tsx58
-rw-r--r--src/client/views/GlobalKeyHandler.ts8
-rw-r--r--src/client/views/InkControls.tsx71
-rw-r--r--src/client/views/InkHandles.tsx17
-rw-r--r--src/client/views/InkStrokeProperties.ts61
-rw-r--r--src/client/views/InkingStroke.tsx61
-rw-r--r--src/client/views/Main.tsx1
-rw-r--r--src/client/views/MainView.scss5
-rw-r--r--src/client/views/MainView.tsx40
-rw-r--r--src/client/views/MainViewModal.scss4
-rw-r--r--src/client/views/MarqueeAnnotator.tsx22
-rw-r--r--src/client/views/PreviewCursor.tsx2
-rw-r--r--src/client/views/PropertiesView.scss37
-rw-r--r--src/client/views/PropertiesView.tsx16
-rw-r--r--src/client/views/SidebarAnnos.tsx35
-rw-r--r--src/client/views/StyleProvider.tsx11
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx54
-rw-r--r--src/client/views/collections/CollectionMenu.scss1255
-rw-r--r--src/client/views/collections/CollectionMenu.tsx116
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx6
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx4
-rw-r--r--src/client/views/collections/CollectionSubView.tsx79
-rw-r--r--src/client/views/collections/CollectionTimeView.tsx14
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx15
-rw-r--r--src/client/views/collections/CollectionView.tsx45
-rw-r--r--src/client/views/collections/TabDocView.scss30
-rw-r--r--src/client/views/collections/TabDocView.tsx41
-rw-r--r--src/client/views/collections/TreeView.tsx54
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx11
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx1
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx31
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx25
-rw-r--r--src/client/views/collections/collectionFreeForm/index.ts7
-rw-r--r--src/client/views/collections/collectionGrid/index.ts2
-rw-r--r--src/client/views/collections/collectionLinearView/CollectionLinearView.scss (renamed from src/client/views/collections/CollectionLinearView.scss)62
-rw-r--r--src/client/views/collections/collectionLinearView/CollectionLinearView.tsx (renamed from src/client/views/collections/CollectionLinearView.tsx)86
-rw-r--r--src/client/views/collections/collectionLinearView/index.ts1
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx4
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx4
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx10
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx6
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaView.scss120
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaView.tsx4
-rw-r--r--src/client/views/collections/collectionSchema/SchemaTable.tsx5
-rw-r--r--src/client/views/global/globalCssVariables.scss31
-rw-r--r--src/client/views/global/globalEnums.tsx5
-rw-r--r--src/client/views/linking/LinkEditor.scss18
-rw-r--r--src/client/views/linking/LinkEditor.tsx103
-rw-r--r--src/client/views/linking/LinkMenu.tsx2
-rw-r--r--src/client/views/linking/LinkMenuGroup.tsx21
-rw-r--r--src/client/views/linking/LinkPopup.scss2
-rw-r--r--src/client/views/linking/LinkPopup.tsx45
-rw-r--r--src/client/views/linking/LinkRelationshipSearch.tsx63
-rw-r--r--src/client/views/nodes/AudioBox.tsx6
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx2
-rw-r--r--src/client/views/nodes/DocumentLinksButton.scss1
-rw-r--r--src/client/views/nodes/DocumentLinksButton.tsx28
-rw-r--r--src/client/views/nodes/DocumentView.tsx66
-rw-r--r--src/client/views/nodes/FieldView.tsx11
-rw-r--r--src/client/views/nodes/FilterBox.tsx34
-rw-r--r--src/client/views/nodes/FontIconBox.scss103
-rw-r--r--src/client/views/nodes/FontIconBox.tsx96
-rw-r--r--src/client/views/nodes/ImageBox.tsx27
-rw-r--r--src/client/views/nodes/LinkDescriptionPopup.scss65
-rw-r--r--src/client/views/nodes/LinkDescriptionPopup.tsx4
-rw-r--r--src/client/views/nodes/LinkDocPreview.tsx4
-rw-r--r--src/client/views/nodes/PDFBox.tsx112
-rw-r--r--src/client/views/nodes/ScreenshotBox.tsx30
-rw-r--r--src/client/views/nodes/VideoBox.scss67
-rw-r--r--src/client/views/nodes/VideoBox.tsx99
-rw-r--r--src/client/views/nodes/WebBox.tsx185
-rw-r--r--src/client/views/nodes/button/ButtonScripts.ts14
-rw-r--r--src/client/views/nodes/button/FontIconBadge.tsx36
-rw-r--r--src/client/views/nodes/button/FontIconBox.scss383
-rw-r--r--src/client/views/nodes/button/FontIconBox.tsx761
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx71
-rw-r--r--src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts73
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx9
-rw-r--r--src/client/views/nodes/trails/PresBox.tsx2
-rw-r--r--src/client/views/pdf/AnchorMenu.tsx24
-rw-r--r--src/client/views/pdf/PDFViewer.tsx54
-rw-r--r--src/client/views/search/SearchBox.scss194
-rw-r--r--src/client/views/search/SearchBox.tsx733
-rw-r--r--src/client/views/topbar/TopBar.scss32
-rw-r--r--src/client/views/topbar/TopBar.tsx2
-rw-r--r--src/fields/Doc.ts105
-rw-r--r--src/fields/InkField.ts6
-rw-r--r--src/fields/List.ts19
-rw-r--r--src/fields/URLField.ts15
-rw-r--r--src/fields/util.ts53
-rw-r--r--src/mobile/ImageUpload.tsx2
-rw-r--r--src/server/server_Initialization.ts3
-rw-r--r--src/server/websocket.ts2
120 files changed, 4586 insertions, 2881 deletions
diff --git a/package-lock.json b/package-lock.json
index 58cdf8805..7810e3120 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7694,14 +7694,14 @@
"resolved": "https://registry.npmjs.org/image-size-stream/-/image-size-stream-1.1.0.tgz",
"integrity": "sha1-Ivou2mbG31AQh0bacUkmSy0l+Gs=",
"requires": {
- "image-size": "image-size@github:netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1",
+ "image-size": "github:netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1",
"readable-stream": "^1.0.33",
"tryit": "^1.0.1"
},
"dependencies": {
"image-size": {
- "version": "git+ssh://git@github.com/netroy/image-size.git#da2c863807a3e9602617bdd357b0de3ab4a064c1",
- "from": "image-size@github:netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1"
+ "version": "github:netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1",
+ "from": "github:netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1"
},
"isarray": {
"version": "0.0.1",
diff --git a/src/Utils.ts b/src/Utils.ts
index d87c3cc6b..b7e6b83a0 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -3,6 +3,8 @@ import v5 = require("uuid/v5");
import { ColorState } from 'react-color';
import { Socket } from 'socket.io';
import { Message } from './server/Message';
+import { Colors } from './client/views/global/globalEnums';
+import Color = require('color');
export namespace Utils {
export let DRAG_THRESHOLD = 4;
@@ -67,7 +69,6 @@ export namespace Utils {
export function prepend(extension: string): string {
return window.location.origin + extension;
}
-
export function fileUrl(filename: string): string {
return prepend(`/files/${filename}`);
}
@@ -116,6 +117,24 @@ export namespace Utils {
return { r: r, g: g, b: b, a: a };
}
+ const isTransparentFunctionHack = "isTransparent(__value__)";
+ const noRecursionHack = "__noRecursion";
+ export function IsRecursiveFilter(val: string) {
+ return !val.includes(noRecursionHack);
+ }
+ export function HasTransparencyFilter(val: string) {
+ return val.includes(isTransparentFunctionHack);
+ }
+ export function IsTransparentFilter() {
+ // bcz: isTransparent(__value__) is a hack. it would be nice to have acual functions be parsed, but now Doc.matchFieldValue is hardwired to recognize just this one
+ return `backgroundColor:${isTransparentFunctionHack},${noRecursionHack}:check`;// bcz: hack. noRecursion should probably be either another ':' delimited field, or it should be a modifier to the comparision (eg., check, x, etc) field
+ }
+ export function IsOpaqueFilter() {
+ // bcz: isTransparent(__value__) is a hack. it would be nice to have acual functions be parsed, but now Doc.matchFieldValue is hardwired to recognize just this one
+ return `backgroundColor:${isTransparentFunctionHack},${noRecursionHack}:x`;// bcz: hack. noRecursion should probably be either another ':' delimited field, or it should be a modifier to the comparision (eg., check, x, etc) field
+ }
+
+
export function toRGBAstr(col: { r: number, g: number, b: number, a?: number }) {
return "rgba(" + col.r + "," + col.g + "," + col.b + (col.a !== undefined ? "," + col.a : "") + ")";
}
@@ -391,8 +410,7 @@ export function formatTime(time: number) {
const hours = Math.floor(time / 60 / 60);
const minutes = Math.floor(time / 60) - (hours * 60);
const seconds = time % 60;
-
- return hours.toString().padStart(2, '0') + ':' + minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0');
+ return (hours ? hours.toString() + ":" : "") + minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0');
}
export function aggregateBounds(boundsList: { x: number, y: number, width?: number, height?: number }[], xpad: number, ypad: number) {
@@ -440,6 +458,7 @@ export function emptyFunction() { }
export function unimplementedFunction() { throw new Error("This function is not implemented, but should be."); }
+
export type Without<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
export type Predicate<K, V> = (entry: [K, V]) => boolean;
@@ -557,46 +576,12 @@ export function simulateMouseClick(element: Element | null | undefined, x: numbe
}
export function lightOrDark(color: any) {
-
- // Variables for red, green, blue values
- var r, g, b, hsp;
-
- // Check the format of the color, HEX or RGB?
- if (color.match(/^rgb/)) {
-
- // If RGB --> store the red, green, blue values in separate variables
- color = color.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/);
-
- r = color[1];
- g = color[2];
- b = color[3];
- }
- else {
-
- // If hex --> Convert it to RGB: http://gist.github.com/983661
- color = +("0x" + color.slice(1).replace(
- color.length < 5 && /./g, '$&$&'));
-
- r = color >> 16;
- g = color >> 8 & 255;
- b = color & 255;
- }
-
- // HSP (Highly Sensitive Poo) equation from http://alienryderflex.com/hsp.html
- hsp = Math.sqrt(
- 0.299 * (r * r) +
- 0.587 * (g * g) +
- 0.114 * (b * b)
- );
-
- // Using the HSP value, determine whether the color is light or dark
- if (hsp > 127.5) {
- return 'light';
- }
- else {
-
- return 'dark';
- }
+ const nonAlphaColor = color.startsWith("#") ? (color as string).substring(0, 7) :
+ color.startsWith("rgba") ? color.replace(/,.[^,]*\)/, ")").replace("rgba", "rgb") : color;
+ const col = Color(nonAlphaColor).rgb();
+ const colsum = (col.red() + col.green() + col.blue());
+ if (colsum / col.alpha() > 400 || col.alpha() < 0.25) return Colors.DARK_GRAY;
+ else return Colors.WHITE;
}
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index 1d7497cf8..e498a7cca 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -1,6 +1,6 @@
import * as io from 'socket.io-client';
import { MessageStore, YoutubeQueryTypes, GestureContent, MobileInkOverlayContent, UpdateMobileInkOverlayPositionContent, MobileDocumentUploadContent } from "./../server/Message";
-import { Opt, Doc, UpdatingFromServer } from '../fields/Doc';
+import { Opt, Doc, UpdatingFromServer, updateCachedAcls } from '../fields/Doc';
import { Utils, emptyFunction } from '../Utils';
import { SerializationHelper } from './util/SerializationHelper';
import { RefField } from '../fields/RefField';
@@ -61,6 +61,9 @@ export namespace DocServer {
DocServer.PlaygroundFields = livePlaygroundFields;
livePlaygroundFields.forEach(f => DocServer.setFieldWriteMode(f, DocServer.WriteMode.Playground));
}
+ export function IsPlaygroundField(field: string) {
+ return DocServer.PlaygroundFields?.includes(field.replace(/^_/, ""));
+ }
export function setFieldWriteMode(field: string, writeMode: WriteMode) {
fieldWriteModes[field] = writeMode;
@@ -225,7 +228,7 @@ export namespace DocServer {
* the server if the document has not been cached.
* @param id the id of the requested document
*/
- const _GetRefFieldImpl = (id: string, force: boolean = false): Promise<Opt<RefField>> => {
+ const _GetRefFieldImpl = async (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
@@ -395,7 +398,7 @@ export namespace DocServer {
(_cache[field.id] as any).then((f: any) => fieldMap[field.id] = f);
} else if (field) {
proms.push(_cache[field.id] as any);
- fieldMap[field.id] = field;
+ fieldMap[field.id] = DocServer.GetCachedRefField(field.id) || field;
}
}
});
diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index 899e65a16..ff9460b62 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -285,7 +285,7 @@ export namespace GooglePhotos {
const photos = await endpoint();
const albumId = StrCast(collection.albumId);
if (albumId && albumId.length) {
- const enrichment = new photos.TextEnrichment(content || Utils.prepend("/doc/" + collection[Id]));
+ const enrichment = new photos.TextEnrichment(content || Doc.globalServerPath(collection));
const position = new photos.AlbumPosition(photos.AlbumPosition.POSITIONS.FIRST_IN_ALBUM);
const enrichmentItem = await photos.albums.addEnrichment(albumId, enrichment, position);
if (enrichmentItem) {
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index fce5e76f5..8ef31362c 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1,7 +1,7 @@
import { action, runInAction } from "mobx";
-import { basename, extname } from "path";
+import { basename } from "path";
import { DateField } from "../../fields/DateField";
-import { Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym, Initializing } from "../../fields/Doc";
+import { Doc, DocListCast, DocListCastAsync, Field, HeightSym, Initializing, Opt, updateCachedAcls, WidthSym } from "../../fields/Doc";
import { Id } from "../../fields/FieldSymbols";
import { HtmlField } from "../../fields/HtmlField";
import { InkField } from "../../fields/InkField";
@@ -13,20 +13,20 @@ import { ComputedField, ScriptField } from "../../fields/ScriptField";
import { Cast, NumCast, StrCast } from "../../fields/Types";
import { AudioField, ImageField, PdfField, VideoField, WebField, YoutubeField } from "../../fields/URLField";
import { SharingPermissions } from "../../fields/util";
-import { MessageStore } from "../../server/Message";
import { Upload } from "../../server/SharedMediaTypes";
import { OmitKeys, Utils } from "../../Utils";
import { YoutubeBox } from "../apis/youtube/YoutubeBox";
import { DocServer } from "../DocServer";
import { Networking } from "../Network";
+import { CurrentUserUtils } from "../util/CurrentUserUtils";
import { DocumentManager } from "../util/DocumentManager";
import { dropActionType } from "../util/DragManager";
import { DirectoryImportBox } from "../util/Import & Export/DirectoryImportBox";
import { LinkManager } from "../util/LinkManager";
import { Scripting } from "../util/Scripting";
import { undoBatch, UndoManager } from "../util/UndoManager";
-import { DimUnit } from "../views/collections/collectionMulticolumn/CollectionMulticolumnView";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
+import { DimUnit } from "../views/collections/collectionMulticolumn/CollectionMulticolumnView";
import { CollectionView, CollectionViewType } from "../views/collections/CollectionView";
import { ContextMenu } from "../views/ContextMenu";
import { ContextMenuProps } from "../views/ContextMenuItem";
@@ -36,30 +36,29 @@ import { AudioBox } from "../views/nodes/AudioBox";
import { ColorBox } from "../views/nodes/ColorBox";
import { ComparisonBox } from "../views/nodes/ComparisonBox";
import { DocFocusOptions } from "../views/nodes/DocumentView";
+import { EquationBox } from "../views/nodes/EquationBox";
+import { FieldViewProps } from "../views/nodes/FieldView";
import { FilterBox } from "../views/nodes/FilterBox";
-import { FontIconBox } from "../views/nodes/FontIconBox";
+import { FontIconBox } from "../views/nodes/button/FontIconBox";
import { FormattedTextBox } from "../views/nodes/formattedText/FormattedTextBox";
+import { FunctionPlotBox } from "../views/nodes/FunctionPlotBox";
import { ImageBox } from "../views/nodes/ImageBox";
import { KeyValueBox } from "../views/nodes/KeyValueBox";
import { LabelBox } from "../views/nodes/LabelBox";
import { LinkBox } from "../views/nodes/LinkBox";
import { LinkDescriptionPopup } from "../views/nodes/LinkDescriptionPopup";
import { PDFBox } from "../views/nodes/PDFBox";
-import { PresBox } from "../views/nodes/trails/PresBox";
import { ScreenshotBox } from "../views/nodes/ScreenshotBox";
import { ScriptingBox } from "../views/nodes/ScriptingBox";
import { SliderBox } from "../views/nodes/SliderBox";
import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox";
+import { PresBox } from "../views/nodes/trails/PresBox";
+import { PresElementBox } from "../views/nodes/trails/PresElementBox";
import { VideoBox } from "../views/nodes/VideoBox";
import { WebBox } from "../views/nodes/WebBox";
-import { PresElementBox } from "../views/nodes/trails/PresElementBox";
import { SearchBox } from "../views/search/SearchBox";
import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo";
import { DocumentType } from "./DocumentTypes";
-import { EquationBox } from "../views/nodes/EquationBox";
-import { FunctionPlotBox } from "../views/nodes/FunctionPlotBox";
-import { CurrentUserUtils } from "../util/CurrentUserUtils";
-import { FieldViewProps } from "../views/nodes/FieldView";
const path = require('path');
const defaultNativeImageDim = Number(DFLT_IMAGE_NATIVE_DIM.replace("px", ""));
@@ -215,7 +214,29 @@ export class DocumentOptions {
annotationOn?: Doc;
isPushpin?: boolean;
_removeDropProperties?: List<string>; // list of properties that should be removed from a document when it is dropped. e.g., a creator button may be forceActive to allow it be dragged, but the forceActive property can be removed from the dropped document
+
+ //BUTTONS
iconShape?: string; // shapes of the fonticon border
+ btnType?: string;
+ btnList?: List<string>;
+ docColorBtn?: string;
+ userColorBtn?: string;
+ canClick?: string;
+ script?: string;
+ numBtnType?: string;
+ numBtnMax?: number;
+ numBtnMin?: number;
+ switchToggle?: boolean;
+
+ //LINEAR VIEW
+ linearViewIsExpanded?: boolean; // is linear view expanded
+ linearViewExpandable?: boolean; // can linear view be expanded
+ linearViewToggleButton?: string; // button to open close linear view group
+ linearViewSubMenu?: boolean;
+ linearViewFloating?: boolean;
+ flexGap?: number; // Linear view flex gap
+ flexDirection?: "unset" | "row" | "column" | "row-reverse" | "column-reverse";
+
layout_linkView?: Doc; // view template for a link document
layout_keyValue?: string; // view tempalte for key value docs
linkRelationship?: string; // type of relatinoship a link represents
@@ -262,14 +283,14 @@ export class DocumentOptions {
text?: string;
textTransform?: string; // is linear view expanded
letterSpacing?: string; // is linear view expanded
- flexDirection?: "unset" | "row" | "column" | "row-reverse" | "column-reverse";
selectedIndex?: number; // which item in a linear view has been selected using the "thumb doc" ui
clipboard?: Doc;
searchQuery?: string; // for quersyBox
- linearViewIsExpanded?: boolean; // is linear view expanded
useLinkSmallAnchor?: boolean; // whether links to this document should use a miniature linkAnchorBox
border?: string; //for searchbox
hoverBackgroundColor?: string; // background color of a label when hovered
+ linkRelationshipList?: List<string>; // for storing different link relationships (when set by user in the link editor)
+ linkColorList?: List<string>; // colors of links corresponding to specific link relationships
}
export namespace Docs {
@@ -550,84 +571,6 @@ export namespace Docs {
export namespace Create {
/**
- * Synchronously returns a collection into which
- * the device documents will be put. This is initially empty,
- * but gets populated by updates from the web socket. When everything is over,
- * this function cleans up after itself.
- * s
- * Look at Websocket.ts for the server-side counterpart to this
- * function.
- */
- export function Buxton() {
- let responded = false;
- const loading = new Doc;
- loading.title = "Please wait for the import script...";
- const parent = TreeDocument([loading], {
- title: "The Buxton Collection",
- _width: 400,
- _height: 400
- });
- const parentProto = Doc.GetProto(parent);
- const { _socket } = DocServer;
-
- // just in case, clean up
- _socket.off(MessageStore.BuxtonDocumentResult.Message);
- _socket.off(MessageStore.BuxtonImportComplete.Message);
-
- // this is where the client handles the receipt of a new valid parsed document
- Utils.AddServerHandler(_socket, MessageStore.BuxtonDocumentResult, ({ device, invalid: errors }) => {
- if (!responded) {
- responded = true;
- parentProto.data = new List<Doc>();
- }
- if (device) {
- const { title, __images, additionalMedia } = device;
- delete device.__images;
- delete device.additionalMedia;
- const { ImageDocument, StackingDocument } = Docs.Create;
- const constructed = __images.map(({ url, nativeWidth, nativeHeight }) => ({ url: Utils.prepend(url), nativeWidth, nativeHeight }));
- const deviceImages = constructed.map(({ url, nativeWidth, nativeHeight }, i) => {
- const imageDoc = ImageDocument(url, {
- title: `image${i}.${extname(url)}`,
- _nativeWidth: nativeWidth,
- _nativeHeight: nativeHeight
- });
- const media = additionalMedia[i];
- if (media) {
- for (const key of Object.keys(media)) {
- imageDoc[`additionalMedia_${key}`] = Utils.prepend(`/files/${key}/buxton/${media[key]}`);
- }
- }
- return imageDoc;
- });
- // the main document we create
- const doc = StackingDocument(deviceImages, { title, hero: new ImageField(constructed[0].url) });
- doc.nameAliases = new List<string>([title.toLowerCase()]);
- // add the parsed attributes to this main document
- Doc.Get.FromJson({ data: device, appendToExisting: { targetDoc: Doc.GetProto(doc) } });
- Doc.AddDocToList(parentProto, "data", doc);
- } else if (errors) {
- console.log("Documents:" + errors);
- } else {
- alert("A Buxton document import was completely empty (??)");
- }
- });
-
- // when the import is complete, we stop listening for these creation
- // and termination events and alert the user
- Utils.AddServerHandler(_socket, MessageStore.BuxtonImportComplete, ({ deviceCount, errorCount }) => {
- _socket.off(MessageStore.BuxtonDocumentResult.Message);
- _socket.off(MessageStore.BuxtonImportComplete.Message);
- alert(`Successfully imported ${deviceCount} device${deviceCount === 1 ? "" : "s"}, with ${errorCount} error${errorCount === 1 ? "" : "s"}, in ${(Date.now() - startTime) / 1000} seconds.`);
- });
- const startTime = Date.now();
- Utils.Emit(_socket, MessageStore.BeginBuxtonImport, ""); // signal the server to start importing
- return parent; // synchronously return the collection, to be populateds
- }
-
- Scripting.addGlobal(Buxton);
-
- /**
* This function receives the relevant document prototype and uses
* it to create a new of that base-level prototype, or the
* underlying data document, which it then delegates again
@@ -655,8 +598,10 @@ export namespace Docs {
dataProps.creationDate = new DateField;
dataProps[`${fieldKey}-lastModified`] = new DateField;
dataProps["acl-Override"] = "None";
- dataProps["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Add;
+ dataProps["acl-Public"] = options["acl-Public"] ? options["acl-Public"] : Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
+
dataProps[fieldKey] = data;
+
// 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.
dataProps[fieldKey + "-annotations"] = new List<Doc>();
@@ -664,18 +609,20 @@ export namespace Docs {
viewProps.author = Doc.CurrentUserEmail;
viewProps["acl-Override"] = "None";
- viewProps["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Add;
+ viewProps["acl-Public"] = options["_acl-Public"] ? options["_acl-Public"] : Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
const viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewProps, true, true);
![DocumentType.LINK, DocumentType.MARKER, DocumentType.LABEL].includes(viewDoc.type as any) && DocUtils.MakeLinkToActiveAudio(() => viewDoc);
!Doc.IsSystem(dataDoc) && ![DocumentType.MARKER, DocumentType.KVP, DocumentType.LINK, DocumentType.LINKANCHOR].includes(proto.type as any) &&
!dataDoc.isFolder && !dataProps.annotationOn && Doc.AddDocToList(Cast(Doc.UserDoc().myFileOrphans, Doc, null), "data", dataDoc);
+ updateCachedAcls(dataDoc);
+ updateCachedAcls(viewDoc);
return viewDoc;
}
export function ImageDocument(url: string, options: DocumentOptions = {}) {
- const imgField = new ImageField(new URL(url));
+ const imgField = new ImageField(url);
return InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: path.basename(url), ...options });
}
@@ -689,11 +636,11 @@ export namespace Docs {
}
export function VideoDocument(url: string, options: DocumentOptions = {}) {
- return InstanceFromProto(Prototypes.get(DocumentType.VID), new VideoField(new URL(url)), options);
+ return InstanceFromProto(Prototypes.get(DocumentType.VID), new VideoField(url), options);
}
export function YoutubeDocument(url: string, options: DocumentOptions = {}) {
- return InstanceFromProto(Prototypes.get(DocumentType.YOUTUBE), new YoutubeField(new URL(url)), options);
+ return InstanceFromProto(Prototypes.get(DocumentType.YOUTUBE), new YoutubeField(url), options);
}
export function WebCamDocument(url: string, options: DocumentOptions = {}) {
@@ -709,7 +656,7 @@ export namespace Docs {
}
export function AudioDocument(url: string, options: DocumentOptions = {}) {
- return InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(new URL(url)),
+ return InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(url),
{ ...options, backgroundColor: ComputedField.MakeFunction("this._mediaState === 'playing' ? 'green':'gray'") as any });
}
@@ -752,14 +699,14 @@ export namespace Docs {
return linkDoc;
}
- export function InkDocument(color: string, tool: string, strokeWidth: string, strokeBezier: string, fillColor: string, arrowStart: string, arrowEnd: string, dash: string, points: { X: number, Y: number }[], options: DocumentOptions = {}) {
+ export function InkDocument(color: string, tool: string, strokeWidth: number, strokeBezier: string, fillColor: string, arrowStart: string, arrowEnd: string, dash: string, points: { X: number, Y: number }[], options: DocumentOptions = {}) {
const I = new Doc();
I[Initializing] = true;
I.type = DocumentType.INK;
I.layout = InkingStroke.LayoutString("data");
I.color = color;
I.fillColor = fillColor;
- I.strokeWidth = Number(strokeWidth);
+ I.strokeWidth = strokeWidth;
I.strokeBezier = strokeBezier;
I.strokeStartMarker = arrowStart;
I.strokeEndMarker = arrowEnd;
@@ -768,25 +715,25 @@ export namespace Docs {
I.title = "ink";
I.x = options.x;
I.y = options.y;
- I._backgroundColor = "transparent";
I._width = options._width as number;
I._height = options._height as number;
I._fontFamily = "cursive";
I.author = Doc.CurrentUserEmail;
I.rotation = 0;
I.data = new InkField(points);
- I["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Add;
+ I["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
I["acl-Override"] = "None";
+ I.links = ComputedField.MakeFunction("links(self)") as any;
I[Initializing] = false;
return I;
}
export function PdfDocument(url: string, options: DocumentOptions = {}) {
- return InstanceFromProto(Prototypes.get(DocumentType.PDF), new PdfField(new URL(url)), options);
+ return InstanceFromProto(Prototypes.get(DocumentType.PDF), new PdfField(url), options);
}
export function WebDocument(url: string, options: DocumentOptions = {}) {
- return InstanceFromProto(Prototypes.get(DocumentType.WEB), url ? new WebField(new URL(url)) : undefined, options);
+ return InstanceFromProto(Prototypes.get(DocumentType.WEB), url ? new WebField(url) : undefined, options);
}
export function HtmlDocument(html: string, options: DocumentOptions = {}) {
@@ -797,15 +744,20 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.KVP), document, { title: document.title + ".kvp", ...options });
}
- export function TextanchorDocument(options: DocumentOptions = {}, id?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.MARKER), undefined, options, id);
- }
-
export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Freeform }, id);
documents.map(d => d.context = inst);
return inst;
}
+
+ export function WebanchorDocument(url?: string, options: DocumentOptions = {}, id?: string) {
+ return InstanceFromProto(Prototypes.get(DocumentType.MARKER), url, options, id);
+ }
+
+ export function TextanchorDocument(options: DocumentOptions = {}, id?: string) {
+ return InstanceFromProto(Prototypes.get(DocumentType.MARKER), options?.data, options, id);
+ }
+
export function HTMLAnchorDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
return InstanceFromProto(Prototypes.get(DocumentType.MARKER), new List(documents), options, id);
}
@@ -834,8 +786,8 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { schemaHeaders: new List(schemaHeaders), ...options, _viewType: CollectionViewType.Schema });
}
- export function TreeDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Tree }, id);
+ export function TreeDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) {
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Tree }, id, undefined, protoId);
}
export function StackingDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) {
@@ -885,8 +837,8 @@ export namespace Docs {
}
export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) {
- const tabs = TreeDocument(documents, { title: "On-Screen Tabs", childDontRegisterViews: true, freezeChildren: "remove|add", treeViewExpandedViewLock: true, treeViewExpandedView: "data", _fitWidth: true, system: true });
- const all = TreeDocument([], { title: "Off-Screen Tabs", childDontRegisterViews: true, freezeChildren: "add", treeViewExpandedViewLock: true, treeViewExpandedView: "data", system: true });
+ const tabs = TreeDocument(documents, { title: "On-Screen Tabs", childDontRegisterViews: true, freezeChildren: "remove|add", treeViewExpandedViewLock: true, treeViewExpandedView: "data", _fitWidth: true, system: true, isFolder: true });
+ const all = TreeDocument([], { title: "Off-Screen Tabs", childDontRegisterViews: true, freezeChildren: "add", treeViewExpandedViewLock: true, treeViewExpandedView: "data", system: true, isFolder: true });
return InstanceFromProto(Prototypes.get(DocumentType.COL), new List([tabs, all]), { freezeChildren: "remove|add", treeViewExpandedViewLock: true, treeViewExpandedView: "data", ...options, _viewType: CollectionViewType.Docking, dockingConfig: config }, id);
}
@@ -1109,8 +1061,8 @@ export namespace DocUtils {
title: ComputedField.MakeFunction("generateLinkTitle(self)") as any,
"anchor1-useLinkSmallAnchor": source.doc.useLinkSmallAnchor ? true : undefined,
"anchor2-useLinkSmallAnchor": target.doc.useLinkSmallAnchor ? true : undefined,
- "acl-Public": SharingPermissions.Add,
- "_acl-Public": SharingPermissions.Add,
+ "acl-Public": SharingPermissions.Augment,
+ "_acl-Public": SharingPermissions.Augment,
layout_linkView: Cast(Cast(Doc.UserDoc()["template-button-link"], Doc, null).dragFactory, Doc, null),
linkDisplay: true, hidden: true,
linkRelationship,
@@ -1204,6 +1156,7 @@ export namespace DocUtils {
description: ":" + StrCast(note.title),
event: undoBatch((args: { x: number, y: number }) => {
const textDoc = Docs.Create.TextDocument("", {
+ _fontFamily: StrCast(Doc.UserDoc()._fontFamily), _fontSize: StrCast(Doc.UserDoc()._fontSize),
_width: 200, x, y, _autoHeight: note._autoHeight !== false,
title: StrCast(note.title) + "#" + (note.aliasCount = NumCast(note.aliasCount) + 1)
});
@@ -1230,7 +1183,7 @@ export namespace DocUtils {
}, icon: "compress-arrows-alt"
});
ContextMenu.Instance.addItem({
- description: "Add Template Doc ...",
+ description: "Create document",
subitems: DocListCast(Cast(Doc.UserDoc().myItemCreators, Doc, null)?.data).filter(btnDoc => !btnDoc.hidden).map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null)).filter(doc => doc && doc !== Doc.UserDoc().emptyPresentation).map((dragDoc, i) => ({
description: ":" + StrCast(dragDoc.title),
event: undoBatch((args: { x: number, y: number }) => {
@@ -1243,7 +1196,7 @@ export namespace DocUtils {
docAdder?.(newDoc);
}
}),
- icon: "eye"
+ icon: Doc.toIcon(Cast(dragDoc.type, Doc, null)),
})) as ContextMenuProps[],
icon: "eye"
});
@@ -1428,4 +1381,7 @@ Scripting.addGlobal(function generateLinkTitle(self: Doc) {
const anchor2title = self.anchor2 && self.anchor2 !== self ? Cast(self.anchor2, Doc, null).title : "<?>";
const relation = self.linkRelationship || "to";
return `${anchor1title} (${relation}) ${anchor2title}`;
+});
+Scripting.addGlobal(function openTabAlias(tab: Doc) {
+ CollectionDockingView.AddSplit(Doc.MakeAlias(tab), "right");
}); \ No newline at end of file
diff --git a/src/client/documents/Gitlike.ts b/src/client/documents/Gitlike.ts
index fddf317bc..575c984f5 100644
--- a/src/client/documents/Gitlike.ts
+++ b/src/client/documents/Gitlike.ts
@@ -1,7 +1,8 @@
-import { Doc, DocListCast, DocListCastAsync } from "../../fields/Doc";
+import { Doc, DocListCast, DocListCastAsync, Field } from "../../fields/Doc";
import { List } from "../../fields/List";
-import { ObjectField } from "../../fields/ObjectField";
import { Cast, DateCast } from "../../fields/Types";
+import { DateField } from "../../fields/DateField";
+import { Id } from "../../fields/FieldSymbols";
// synchs matching documents on the two branches that are being merged/pulled
// currently this just synchs the main 'fieldKey' component of the data since
@@ -10,11 +11,22 @@ function GitlikeSynchDocs(bd: Doc, md: Doc) {
const fieldKey = Doc.LayoutFieldKey(md);
const bdate = DateCast(bd[`${fieldKey}-lastModified`])?.date;
const mdate = DateCast(md[`${fieldKey}-lastModified`])?.date;
- if (bdate === mdate || bdate > mdate) return;
const bdproto = bd && Doc.GetProto(bd);
+ if (bdate !== mdate && bdate <= mdate) {
+ if (bdproto && md) {
+ bdproto[fieldKey] = Field.Copy(md[fieldKey]);
+ bdproto[`${fieldKey}-lastModified`] = new DateField();
+ }
+ }
+ const bldate = DateCast(bd._lastModified)?.date;
+ const mldate = DateCast(md._lastModified)?.date;
+ if (bldate === mldate || bldate > mldate) return;
if (bdproto && md) {
- bdproto[fieldKey] = ObjectField.MakeCopy(md[fieldKey] as ObjectField);
- bdproto[`${fieldKey}-lastModified`] = ObjectField.MakeCopy(md[`${fieldKey}-lastModified`] as ObjectField);
+ bd.x = Field.Copy(md.x);
+ bd.y = Field.Copy(md.y);
+ bd.width = Field.Copy(md.width);
+ bd.height = Field.Copy(md.height);
+ bdproto._lastModified = new DateField();
}
}
@@ -36,8 +48,9 @@ async function GitlikePullFromMaster(branch: Doc, suffix = "") {
const bd = branchMainDocs?.find(bd => (Cast(bd.branchOf, Doc, null) || bd) === md);
bd && GitlikeSynchDocs(bd, md);
});
+ const cloneMap = new Map<string, Doc>(); cloneMap.set(masterMain[Id], branch);
// make branch clones of them, then add them to the branch
- const newlyBranchedDocs = await Promise.all(newDocsFromMaster?.map(async md => (await Doc.MakeClone(md, false, true)).clone) || []);
+ const newlyBranchedDocs = await Promise.all(newDocsFromMaster?.map(async md => (await Doc.MakeClone(md, false, true, cloneMap)).clone) || []);
newlyBranchedDocs.forEach(nd => {
Doc.AddDocToList(branch, Doc.LayoutFieldKey(branch) + suffix, nd);
nd.context = branch;
@@ -56,22 +69,26 @@ async function GitlikeMergeWithMaster(master: Doc, suffix = "") {
branches?.map(async branch => {
const branchChildren = await DocListCastAsync(branch[Doc.LayoutFieldKey(branch) + suffix]);
branchChildren && await Promise.all(branchChildren.map(async bd => {
+ const cloneMap = new Map<string, Doc>(); cloneMap.set(master[Id], branch);
// see if the branch's child exists on master.
- const masterChild = Cast(bd.branchOf, Doc, null) || (await Doc.MakeClone(bd, false, true)).clone;
+ const masterChild = Cast(bd.branchOf, Doc, null) || (await Doc.MakeClone(bd, false, true, cloneMap)).clone;
// if the branch's child didn't exist on master, we make a branch clone of the child to add to master.
// however, since master is supposed to have the "main" clone, and branches, the "branch" clones, we have to reverse the fields
// on the branch child and master clone.
if (masterChild.branchOf) {
const branchDocProto = Doc.GetProto(bd);
const masterChildProto = Doc.GetProto(masterChild);
- masterChildProto.branchOf = undefined; // the master child should not be a branch of the branch child, so unset 'branchOf'
+ const branchTitle = bd.title;
+ branchDocProto.title = masterChildProto.title;
+ masterChildProto.title = branchTitle;
+ masterChildProto.branchOf = masterChild.branchOf = undefined; // the master child should not be a branch of the branch child, so unset 'branchOf'
masterChildProto.branches = new List<Doc>([bd]); // the master child's branches needs to include the branch child
Doc.RemoveDocFromList(branchDocProto, "branches", masterChildProto); // the branch child should not have the master child in its branch list.
branchDocProto.branchOf = masterChild; // the branch child is now a branch of the master child
}
Doc.AddDocToList(master, Doc.LayoutFieldKey(master) + suffix, masterChild); // add the masterChild to master (if it's already there, this is a no-op)
masterChild.context = master;
- GitlikeSynchDocs(Doc.GetProto(masterChild), bd);
+ GitlikeSynchDocs(masterChild, bd);//Doc.GetProto(masterChild), bd);
}));
const masterChildren = await DocListCastAsync(master[Doc.LayoutFieldKey(master) + suffix]);
masterChildren?.forEach(mc => { // see if any master children
@@ -93,6 +110,7 @@ export async function BranchTask(target: Doc, action: "pull" | "merge") {
const func = action === "pull" ? GitlikePullFromMaster : GitlikeMergeWithMaster;
await func(target, "");
await DocListCast(target[Doc.LayoutFieldKey(target)]).forEach(async targetChild => func(targetChild, "-annotations"));
+ await DocListCast(target[Doc.LayoutFieldKey(target)]).forEach(async targetChild => func(targetChild, "-sidebar"));
}
export async function BranchCreate(target: Doc) {
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index bf768a401..cd2803373 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -1,14 +1,15 @@
-import { computed, observable, reaction, action } from "mobx";
+import { computed, observable, reaction } from "mobx";
import * as rp from 'request-promise';
-import { DataSym, Doc, DocListCast, DocListCastAsync, AclReadonly } from "../../fields/Doc";
+import { DataSym, Doc, DocListCast, DocListCastAsync } from "../../fields/Doc";
import { Id } from "../../fields/FieldSymbols";
+import { InkTool } from "../../fields/InkField";
import { List } from "../../fields/List";
import { PrefetchProxy } from "../../fields/Proxy";
import { RichTextField } from "../../fields/RichTextField";
import { listSpec } from "../../fields/Schema";
import { SchemaHeaderField } from "../../fields/SchemaHeaderField";
import { ComputedField, ScriptField } from "../../fields/ScriptField";
-import { BoolCast, Cast, NumCast, PromiseValue, StrCast, DateCast } from "../../fields/Types";
+import { BoolCast, Cast, DateCast, NumCast, PromiseValue, StrCast } from "../../fields/Types";
import { nullAudio } from "../../fields/URLField";
import { SharingPermissions } from "../../fields/util";
import { Utils } from "../../Utils";
@@ -19,6 +20,7 @@ import { Networking } from "../Network";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
import { DimUnit } from "../views/collections/collectionMulticolumn/CollectionMulticolumnView";
import { CollectionView, CollectionViewType } from "../views/collections/CollectionView";
+import { Colors } from "../views/global/globalEnums";
import { MainView } from "../views/MainView";
import { FormattedTextBox } from "../views/nodes/formattedText/FormattedTextBox";
import { LabelBox } from "../views/nodes/LabelBox";
@@ -31,13 +33,28 @@ import { LinkManager } from "./LinkManager";
import { Scripting } from "./Scripting";
import { SearchUtil } from "./SearchUtil";
import { SelectionManager } from "./SelectionManager";
-import { UndoManager } from "./UndoManager";
-import { SnappingManager } from "./SnappingManager";
-import { InkTool } from "../../fields/InkField";
-import { computedFn } from "mobx-utils";
import { ColorScheme } from "./SettingsManager";
-import { Colors } from "../views/global/globalEnums";
-
+import { SharingManager } from "./SharingManager";
+import { SnappingManager } from "./SnappingManager";
+import { UndoManager } from "./UndoManager";
+import { ButtonType, NumButtonType } from "../views/nodes/button/FontIconBox";
+import { IconName } from "@fortawesome/fontawesome-svg-core";
+
+interface Button {
+ title?: string;
+ toolTip?: string;
+ icon?: string;
+ btnType?: ButtonType;
+ click?: string;
+ numBtnType?: NumButtonType;
+ numBtnMin?: number;
+ numBtnMax?: number;
+ switchToggle?: boolean;
+ script?: string;
+ width?: number;
+ list?: string[];
+ ignoreClick?: boolean;
+}
export let resolvedPorts: { server: number, socket: number };
const headerViewVersion = "0.1";
@@ -46,6 +63,7 @@ export class CurrentUserUtils {
//TODO tfs: these should be temporary...
private static mainDocId: string | undefined;
+ public static searchBtn: Doc;
public static get id() { return this.curr_id; }
public static get MainDocId() { return this.mainDocId; }
public static set MainDocId(id: string | undefined) { this.mainDocId = id; }
@@ -55,6 +73,7 @@ export class CurrentUserUtils {
@observable public static GuestDashboard: Doc | undefined;
@observable public static GuestMobile: Doc | undefined;
@observable public static propertiesWidth: number = 0;
+ @observable public static searchPanelWidth: number = 0;
// sets up the default User Templates - slideView, headerView
static setupUserTemplateButtons(doc: Doc) {
@@ -67,13 +86,14 @@ export class CurrentUserUtils {
[this.ficon({
ignoreClick: true,
icon: "mobile",
+ btnType: ButtonType.ToolButton,
backgroundColor: "transparent"
}),
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('copyDragFactory(this.dragFactory)'),
- dragFactory: new PrefetchProxy(queryTemplate) as any as Doc, title: "mobile button", icon: "mobile"
+ dragFactory: new PrefetchProxy(queryTemplate) as any as Doc, title: "mobile button", icon: "mobile", btnType: ButtonType.ToolButton,
});
}
@@ -81,14 +101,15 @@ export class CurrentUserUtils {
const slideTemplate = Docs.Create.MultirowDocument(
[
Docs.Create.MulticolumnDocument([], { title: "data", _height: 200, system: true }),
- Docs.Create.TextDocument("", { title: "text", _height: 100, system: true })
+ Docs.Create.TextDocument("", { title: "text", _height: 100, system: true, _fontFamily: StrCast(Doc.UserDoc()._fontFamily), _fontSize: StrCast(Doc.UserDoc()._fontSize) })
],
{ _width: 400, _height: 300, title: "slideView", _xMargin: 3, _yMargin: 3, system: true }
);
slideTemplate.isTemplateDoc = makeTemplate(slideTemplate);
doc["template-button-slides"] = CurrentUserUtils.ficon({
onDragStart: ScriptField.MakeFunction('copyDragFactory(this.dragFactory)'),
- dragFactory: new PrefetchProxy(slideTemplate) as any as Doc, title: "presentation slide", icon: "address-card"
+ dragFactory: new PrefetchProxy(slideTemplate) as any as Doc, title: "presentation slide", icon: "address-card",
+ btnType: ButtonType.ToolButton
});
}
@@ -134,7 +155,8 @@ export class CurrentUserUtils {
doc["template-button-link"] = CurrentUserUtils.ficon({
onDragStart: ScriptField.MakeFunction('copyDragFactory(this.dragFactory)'),
- dragFactory: new PrefetchProxy(linkTemplate) as any as Doc, title: "link view", icon: "window-maximize", system: true
+ dragFactory: new PrefetchProxy(linkTemplate) as any as Doc, title: "link view", icon: "window-maximize", system: true,
+ btnType: ButtonType.ToolButton
});
}
@@ -165,7 +187,8 @@ export class CurrentUserUtils {
doc["template-button-switch"] = CurrentUserUtils.ficon({
onDragStart: ScriptField.MakeFunction('copyDragFactory(this.dragFactory)'),
- dragFactory: new PrefetchProxy(box) as any as Doc, title: "data switch", icon: "toggle-on", system: true
+ dragFactory: new PrefetchProxy(box) as any as Doc, title: "data switch", icon: "toggle-on", system: true,
+ btnType: ButtonType.ToolButton
});
}
@@ -214,7 +237,11 @@ export class CurrentUserUtils {
doc["template-button-detail"] = CurrentUserUtils.ficon({
onDragStart: ScriptField.MakeFunction('copyDragFactory(this.dragFactory)'),
- dragFactory: new PrefetchProxy(detailView) as any as Doc, title: "detailView", icon: "window-maximize", system: true
+ dragFactory: new PrefetchProxy(detailView) as any as Doc,
+ title: "detailView",
+ icon: "window-maximize",
+ system: true,
+ btnType: ButtonType.ToolButton,
});
}
@@ -229,7 +256,7 @@ export class CurrentUserUtils {
doc["template-buttons"] = new PrefetchProxy(Docs.Create.MasonryDocument(requiredTypes, {
title: "Advanced Item Prototypes", _xMargin: 0, _showTitle: "title", _chromeHidden: true,
hidden: ComputedField.MakeFunction("IsNoviceMode()") as any,
- _stayInCollection: true, _hideContextMenu: true,
+ _stayInCollection: true, _hideContextMenu: true, _forceActive: true,
_autoHeight: true, _width: 500, _height: 300, _fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true,
dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), system: true
}));
@@ -246,24 +273,34 @@ export class CurrentUserUtils {
// setup the different note type skins
static setupNoteTemplates(doc: Doc) {
if (doc["template-note-Note"] === undefined) {
- const noteView = Docs.Create.TextDocument("", { title: "text", isTemplateDoc: true, backgroundColor: "yellow", system: true });
+ const noteView = Docs.Create.TextDocument("", {
+ title: "text", isTemplateDoc: true, backgroundColor: "yellow", system: true,
+ _fontFamily: StrCast(Doc.UserDoc()._fontFamily), _fontSize: StrCast(Doc.UserDoc()._fontSize),
+ });
noteView.isTemplateDoc = makeTemplate(noteView, true, "Note");
doc["template-note-Note"] = new PrefetchProxy(noteView);
}
if (doc["template-note-Idea"] === undefined) {
- const noteView = Docs.Create.TextDocument("", { title: "text", backgroundColor: "pink", system: true });
+ const noteView = Docs.Create.TextDocument("", {
+ title: "text", backgroundColor: "pink", system: true,
+ _fontFamily: StrCast(Doc.UserDoc()._fontFamily), _fontSize: StrCast(Doc.UserDoc()._fontSize),
+ });
noteView.isTemplateDoc = makeTemplate(noteView, true, "Idea");
doc["template-note-Idea"] = new PrefetchProxy(noteView);
}
if (doc["template-note-Topic"] === undefined) {
- const noteView = Docs.Create.TextDocument("", { title: "text", backgroundColor: "lightblue", system: true });
+ const noteView = Docs.Create.TextDocument("", {
+ title: "text", backgroundColor: "lightblue", system: true,
+ _fontFamily: StrCast(Doc.UserDoc()._fontFamily), _fontSize: StrCast(Doc.UserDoc()._fontSize),
+ });
noteView.isTemplateDoc = makeTemplate(noteView, true, "Topic");
doc["template-note-Topic"] = new PrefetchProxy(noteView);
}
if (doc["template-note-Todo"] === undefined) {
const noteView = Docs.Create.TextDocument("", {
title: "text", backgroundColor: "orange", _autoHeight: false, _height: 100, _showCaption: "caption",
- layout: FormattedTextBox.LayoutString("Todo"), caption: RichTextField.DashField("taskStatus"), system: true
+ layout: FormattedTextBox.LayoutString("Todo"), caption: RichTextField.DashField("taskStatus"), system: true,
+ _fontFamily: StrCast(Doc.UserDoc()._fontFamily), _fontSize: StrCast(Doc.UserDoc()._fontSize),
});
noteView.isTemplateDoc = makeTemplate(noteView, true, "Todo");
doc["template-note-Todo"] = new PrefetchProxy(noteView);
@@ -447,7 +484,11 @@ export class CurrentUserUtils {
((doc.emptyAudio as Doc).proto as Doc)["dragFactory-count"] = 0;
}
if (doc.emptyNote === undefined) {
- doc.emptyNote = Docs.Create.TextDocument("", { _width: 200, title: "text note", _autoHeight: true, system: true, cloneFieldFilter: new List<string>(["system"]) });
+ doc.emptyNote = Docs.Create.TextDocument("", {
+ _width: 200, title: "text note", _autoHeight: true, system: true,
+ _fontFamily: StrCast(Doc.UserDoc()._fontFamily), _fontSize: StrCast(Doc.UserDoc()._fontSize),
+ cloneFieldFilter: new List<string>(["system"])
+ });
((doc.emptyNote as Doc).proto as Doc)["dragFactory-count"] = 0;
}
if (doc.emptyImage === undefined) {
@@ -458,7 +499,7 @@ export class CurrentUserUtils {
((doc.emptyButton as Doc).proto as Doc)["dragFactory-count"] = 0;
}
if (doc.emptyWebpage === undefined) {
- doc.emptyWebpage = Docs.Create.WebDocument("", { title: "webpage", _nativeWidth: 850, isTemplateDoc: true, _height: 512, _width: 400, useCors: true, system: true, cloneFieldFilter: new List<string>(["system"]) });
+ doc.emptyWebpage = Docs.Create.WebDocument("", { title: "webpage", _nativeWidth: 850, _height: 512, _width: 400, useCors: true, system: true, cloneFieldFilter: new List<string>(["system"]) });
}
if (doc.activeMobileMenu === undefined) {
this.setupActiveMobileMenu(doc);
@@ -500,11 +541,13 @@ export class CurrentUserUtils {
icon,
title,
toolTip,
+ btnType: ButtonType.ToolButton,
ignoreClick,
_dropAction: "alias",
onDragStart: drag ? ScriptField.MakeFunction(drag) : undefined,
onClick: click ? ScriptField.MakeScript(click) : undefined,
- backgroundColor,
+ backgroundColor: backgroundColor ? backgroundColor : Colors.DARK_GRAY,
+ color: Colors.WHITE,
_hideContextMenu: true,
_removeDropProperties: new List<string>(["_stayInCollection"]),
_stayInCollection: true,
@@ -517,7 +560,7 @@ export class CurrentUserUtils {
if (dragCreatorSet === undefined) {
doc.myItemCreators = new PrefetchProxy(Docs.Create.MasonryDocument(creatorBtns, {
title: "Basic Item Creators", _showTitle: "title", _xMargin: 0, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true,
- _autoHeight: true, _width: 500, _height: 300, _fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true,
+ _forceActive: true, _autoHeight: true, _width: 500, _height: 300, _fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true,
dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), system: true
}));
} else {
@@ -529,34 +572,26 @@ export class CurrentUserUtils {
static async menuBtnDescriptions(doc: Doc) {
return [
{ title: "Dashboards", target: Cast(doc.myDashboards, Doc, null), icon: "desktop", click: 'selectMainMenu(self)' },
+ { title: "Search", target: Cast(doc.mySearchPanel, Doc, null), icon: "search", click: 'selectMainMenu(self)' },
{ title: "My Files", target: Cast(doc.myFilesystem, Doc, null), icon: "file", click: 'selectMainMenu(self)' },
{ title: "Tools", target: Cast(doc.myTools, Doc, null), icon: "wrench", click: 'selectMainMenu(self)' },
{ title: "Import", target: Cast(doc.myImportPanel, Doc, null), icon: "upload", click: 'selectMainMenu(self)' },
{ title: "Recently Closed", target: Cast(doc.myRecentlyClosedDocs, Doc, null), icon: "archive", click: 'selectMainMenu(self)' },
{ title: "Sharing", target: Cast(doc.mySharedDocs, Doc, null), icon: "users", click: 'selectMainMenu(self)', watchedDocuments: doc.mySharedDocs as Doc },
- // { title: "Filter", target: Cast(doc.currentFilter, Doc, null), icon: "filter", click: 'selectMainMenu(self)' },
- { title: "Pres. Trails", target: Cast(doc.myPresentations, Doc, null), icon: "pres-trail", click: 'selectMainMenu(self)' },
+ // { title: "Pres. Trails", target: Cast(doc.myPresentations, Doc, null), icon: "pres-trail", click: 'selectMainMenu(self)' },
// { title: "Help", target: undefined as any, icon: "question-circle", click: 'selectMainMenu(self)' },
// { title: "Settings", target: undefined as any, icon: "cog", click: 'selectMainMenu(self)' },
{ title: "User Doc", target: Cast(doc.myUserDoc, Doc, null), icon: "address-card", click: 'selectMainMenu(self)' },
];
}
- static setupSearchPanel(doc: Doc) {
- if (doc.mySearchPanelDoc === undefined) {
- doc.mySearchPanelDoc = new PrefetchProxy(Docs.Create.SearchDocument({
- _width: 500, _height: 300, backgroundColor: "dimGray", ignoreClick: true, _searchDoc: true,
- childDropAction: "alias", _lockedPosition: true, _viewType: CollectionViewType.Schema, title: "sidebar search stack", system: true
- })) as any as Doc;
- }
- }
static async setupMenuPanel(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) {
if (doc.menuStack === undefined) {
await this.setupSharingSidebar(doc, sharingDocumentId, linkDatabaseId); // sets up the right sidebar collection for mobile upload documents and sharing
const menuBtns = (await CurrentUserUtils.menuBtnDescriptions(doc)).map(({ title, target, icon, click, watchedDocuments }) =>
Docs.Create.FontIconDocument({
icon,
- iconShape: "square",
+ btnType: ButtonType.MenuButton,
_stayInCollection: true,
_hideContextMenu: true,
system: true,
@@ -569,10 +604,23 @@ export class CurrentUserUtils {
_height: 60,
watchedDocuments,
onClick: ScriptField.MakeScript(click, { scriptContext: "any" })
- }));
+ })
+ );
+
+ menuBtns.forEach(menuBtn => {
+ if (menuBtn.title === "Search") {
+ this.searchBtn = menuBtn;
+ }
+ });
// hack -- last button is assumed to be the userDoc
menuBtns[menuBtns.length - 1].hidden = ComputedField.MakeFunction("IsNoviceMode()");
+ menuBtns.forEach(menuBtn => {
+ if (menuBtn.title === "Search") {
+ doc.searchBtn = menuBtn;
+ }
+ });
+
doc.menuStack = new PrefetchProxy(Docs.Create.StackingDocument(menuBtns, {
title: "menuItemPanel",
childDropAction: "alias",
@@ -637,7 +685,7 @@ export class CurrentUserUtils {
onClick: data.click ? ScriptField.MakeScript(data.click) : undefined,
backgroundColor: data.backgroundColor, system: true
},
- [this.ficon({ ignoreClick: true, icon: data.icon, backgroundColor: "rgba(0,0,0,0)", system: true }), this.mobileTextContainer({}, [this.mobileButtonText({}, data.title), this.mobileButtonInfo({}, data.info)])])
+ [this.ficon({ ignoreClick: true, icon: data.icon, backgroundColor: "rgba(0,0,0,0)", system: true, btnType: ButtonType.ClickButton, }), this.mobileTextContainer({}, [this.mobileButtonText({}, data.title), this.mobileButtonInfo({}, data.info)])])
);
}
@@ -747,8 +795,8 @@ export class CurrentUserUtils {
if (doc.myTools === undefined) {
const toolsStack = new PrefetchProxy(Docs.Create.StackingDocument([doc.myCreators as Doc, doc.myColorPicker as Doc], {
- title: "My Tools", _width: 500, _yMargin: 20, ignoreClick: true, _lockedPosition: true, _forceActive: true,
- system: true, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true,
+ title: "My Tools", _showTitle: "title", _width: 500, _yMargin: 20, ignoreClick: true, _lockedPosition: true, _forceActive: true,
+ system: true, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true, boxShadow: "0 0",
})) as any as Doc;
doc.myTools = toolsStack;
@@ -763,11 +811,16 @@ export class CurrentUserUtils {
title: "My Dashboards", _showTitle: "title", _height: 400, childHideLinkButton: true,
treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, childDropAction: "alias",
treeViewTruncateTitleWidth: 150, ignoreClick: true,
- _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true
+ _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", treeViewType: "fileSystem", isFolder: true, system: true
}));
const newDashboard = ScriptField.MakeScript(`createNewDashboard(Doc.UserDoc())`);
- (doc.myDashboards as any as Doc).contextMenuScripts = new List<ScriptField>([newDashboard!]);
- (doc.myDashboards as any as Doc).contextMenuLabels = new List<string>(["Create New Dashboard"]);
+ const toggleTheme = ScriptField.MakeScript(`Doc.UserDoc().darkScheme = !Doc.UserDoc().darkScheme`);
+ const toggleComic = ScriptField.MakeScript(`toggleComicMode()`);
+ const snapshotDashboard = ScriptField.MakeScript(`snapshotDashboard()`);
+ const shareDashboard = ScriptField.MakeScript(`shareDashboard(self)`);
+ const removeDashboard = ScriptField.MakeScript('removeDashboard(self)');
+ (doc.myDashboards as any as Doc).childContextMenuScripts = new List<ScriptField>([newDashboard!, toggleTheme!, toggleComic!, snapshotDashboard!, shareDashboard!, removeDashboard!]);
+ (doc.myDashboards as any as Doc).childContextMenuLabels = new List<string>(["Create New Dashboard", "Toggle Theme Colors", "Toggle Comic Mode", "Snapshot Dashboard", "Share Dashboard", "Remove Dashboard"]);
}
return doc.myDashboards as any as Doc;
}
@@ -819,6 +872,7 @@ export class CurrentUserUtils {
(doc.myRecentlyClosedDocs as any as Doc).contextMenuLabels = new List<string>(["Clear All"]);
}
}
+
static setupFilterDocs(doc: Doc) {
// setup Filter item
if (doc.currentFilter === undefined) {
@@ -865,14 +919,13 @@ export class CurrentUserUtils {
CurrentUserUtils.setupPresentations(doc);
CurrentUserUtils.setupFilesystem(doc);
CurrentUserUtils.setupRecentlyClosedDocs(doc);
- // CurrentUserUtils.setupFilterDocs(doc);
CurrentUserUtils.setupUserDoc(doc);
}
- 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,
+ static linearButtonList = (opts: DocumentOptions, docs: Doc[]) => new PrefetchProxy(Docs.Create.LinearDocument(docs, {
+ ...opts, _gridGap: 0, _xMargin: 5, _yMargin: 5, boxShadow: "0 0", _forceActive: true,
dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }),
- backgroundColor: "black", _lockedPosition: true, linearViewIsExpanded: true, system: true
+ _lockedPosition: true, linearViewIsExpanded: true, system: true, flexDirection: "row"
})) as any as Doc
static ficon = (opts: DocumentOptions) => new PrefetchProxy(Docs.Create.FontIconDocument({
@@ -882,18 +935,206 @@ export class CurrentUserUtils {
/// 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-undo"] === undefined) {
- doc["dockedBtn-undo"] = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("undo()"), dontUndo: true, _stayInCollection: true, _dropAction: "alias", _hideContextMenu: true, _removeDropProperties: new List<string>(["dropAction", "_hideContextMenu", "stayInCollection"]), toolTip: "click to undo", title: "undo", icon: "undo-alt", system: true });
+ doc["dockedBtn-undo"] = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("undo()"), dontUndo: true, _stayInCollection: true, btnType: ButtonType.ToolButton, _dropAction: "alias", _hideContextMenu: true, _removeDropProperties: new List<string>(["dropAction", "_hideContextMenu", "stayInCollection"]), toolTip: "Click to undo", title: "undo", icon: "undo-alt", system: true });
}
if (doc["dockedBtn-redo"] === undefined) {
- doc["dockedBtn-redo"] = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("redo()"), dontUndo: true, _stayInCollection: true, _dropAction: "alias", _hideContextMenu: true, _removeDropProperties: new List<string>(["dropAction", "_hideContextMenu", "stayInCollection"]), toolTip: "click to redo", title: "redo", icon: "redo-alt", system: true });
+ doc["dockedBtn-redo"] = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("redo()"), dontUndo: true, _stayInCollection: true, btnType: ButtonType.ToolButton, _dropAction: "alias", _hideContextMenu: true, _removeDropProperties: new List<string>(["dropAction", "_hideContextMenu", "stayInCollection"]), toolTip: "Click to redo", title: "redo", icon: "redo-alt", system: true });
}
if (doc.dockedBtns === undefined) {
- doc.dockedBtns = CurrentUserUtils.blist({ title: "docked buttons", ignoreClick: true }, [doc["dockedBtn-undo"] as Doc, doc["dockedBtn-redo"] as Doc]);
+ doc.dockedBtns = CurrentUserUtils.linearButtonList({ title: "docked buttons", _height: 40, flexGap: 0, linearViewExpandable: true, ignoreClick: true }, [doc["dockedBtn-undo"] as Doc, doc["dockedBtn-redo"] as Doc]);
}
(doc["dockedBtn-undo"] as Doc).dontUndo = true;
(doc["dockedBtn-redo"] as Doc).dontUndo = true;
}
+ static textTools(doc: Doc) {
+ const tools: Button[] =
+ [
+ {
+ title: "Font", toolTip: "Font", width: 100, btnType: ButtonType.DropdownList, ignoreClick: true,
+ list: ["Roboto", "Roboto Mono", "Nunito", "Times New Roman", "Arial", "Georgia",
+ "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"],
+ script: 'setFont'
+ },
+ { title: "Font size", toolTip: "Font size", width: 75, btnType: ButtonType.NumberButton, numBtnMax: 200, numBtnMin: 0, numBtnType: NumButtonType.DropdownOptions, ignoreClick: true, script: 'setFontSize' },
+ { title: "Font color", toolTip: "Font color", btnType: ButtonType.ColorButton, icon: "font", ignoreClick: true, script: 'setFontColor' },
+ { title: "Bold", toolTip: "Bold (Ctrl+B)", btnType: ButtonType.ToggleButton, icon: "bold", click: 'toggleBold()', script: 'toggleBold' },
+ { title: "Italic", toolTip: "Italic (Ctrl+I)", btnType: ButtonType.ToggleButton, icon: "italic", click: 'toggleItalic()', script: 'toggleItalic' },
+ { title: "Underline", toolTip: "Underline (Ctrl+U)", btnType: ButtonType.ToggleButton, icon: "underline", click: 'toggleUnderline()', script: 'toggleUnderline' },
+ // { title: "Strikethrough", tooltip: "Strikethrough", btnType: ButtonType.ToggleButton, icon: "strikethrough", click: 'toggleStrikethrough()'},
+ // { title: "Superscript", tooltip: "Superscript", btnType: ButtonType.ToggleButton, icon: "superscript", click: 'toggleSuperscript()'},
+ // { title: "Subscript", tooltip: "Subscript", btnType: ButtonType.ToggleButton, icon: "subscript", click: 'toggleSubscript()'},
+ { title: "Left align", toolTip: "Left align", btnType: ButtonType.ToggleButton, icon: "align-left", ignoreClick: true, script: 'setAlignment("left")' },
+ { title: "Center align", toolTip: "Center align", btnType: ButtonType.ToggleButton, icon: "align-center", ignoreClick: true, script: 'setAlignment("center")' },
+ { title: "Right align", toolTip: "Right align", btnType: ButtonType.ToggleButton, icon: "align-right", ignoreClick: true, script: 'setAlignment("right")' },
+ ];
+ return tools;
+ }
+
+ static inkTools(doc: Doc) {
+ const tools: Button[] = [
+ { title: "Pen", toolTip: "Pen (Ctrl+P)", btnType: ButtonType.ToggleButton, icon: "pen", click: 'setActiveInkTool("pen")', script: 'setActiveInkTool("pen" , true)' },
+ { title: "Highlighter", toolTip: "Highlighter (Ctrl+H)", btnType: ButtonType.ToggleButton, icon: "highlighter", click: 'setActiveInkTool("highlighter")', script: 'setActiveInkTool("highlighter", true)' },
+ { title: "Circle", toolTip: "Circle (Ctrl+Shift+C)", btnType: ButtonType.ToggleButton, icon: "circle", click: 'setActiveInkTool("circle")', script: 'setActiveInkTool("circle" , true)' },
+ { title: "Square", toolTip: "Square (Ctrl+Shift+S)", btnType: ButtonType.ToggleButton, icon: "square", click: 'setActiveInkTool("square")', script: 'setActiveInkTool("square" , true)' },
+ { title: "Line", toolTip: "Line (Ctrl+Shift+L)", btnType: ButtonType.ToggleButton, icon: "minus", click: 'setActiveInkTool("line")', script: 'setActiveInkTool("line" , true)' },
+ { title: "Stroke width", toolTip: "Stroke width", btnType: ButtonType.NumberButton, numBtnType: NumButtonType.Slider, ignoreClick: true, script: 'setStrokeWidth' },
+ { title: "Stroke color", toolTip: "Stroke color", btnType: ButtonType.ColorButton, icon: "minus", ignoreClick: true, script: 'setStrokeColor' },
+
+ ];
+ return tools;
+ }
+
+ static schemaTools(doc: Doc) {
+ const tools: Button[] =
+ [
+ {
+ title: "Show preview",
+ toolTip: "Show preview of selected document",
+ btnType: ButtonType.ToggleButton,
+ width: 50,
+ switchToggle: true,
+ icon: "eye",
+ ignoreClick: true,
+ script: 'toggleSchemaShow'
+ },
+ ];
+ return tools;
+ }
+
+ static webTools(doc: Doc) {
+ const tools: Button[] =
+ [
+ { title: "Back", toolTip: "Go back", btnType: ButtonType.ClickButton, icon: "arrow-left", click: 'webBack()' },
+ { title: "Forward", toolTip: "Go forward", btnType: ButtonType.ClickButton, icon: "arrow-right", click: 'webForward()' },
+ { title: "Reload", toolTip: "Reload webpage", btnType: ButtonType.ClickButton, icon: "redo-alt", click: 'webReload()' },
+ { title: "URL", toolTip: "URL", width: 150, btnType: ButtonType.EditableText, icon: "lock", ignoreClick: true, script: 'webSetURL' },
+ ];
+
+ return tools;
+ }
+
+ static async contextMenuTools(doc: Doc) {
+ return [
+ {
+ title: "Perspective", toolTip: "View", width: 100, btnType: ButtonType.DropdownList, ignoreClick: true,
+ list: [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Tree,
+ CollectionViewType.Stacking, CollectionViewType.Masonry, CollectionViewType.Multicolumn,
+ CollectionViewType.Multirow, CollectionViewType.Time, CollectionViewType.Carousel,
+ CollectionViewType.Carousel3D, CollectionViewType.Linear, CollectionViewType.Map,
+ CollectionViewType.Grid],
+ script: 'setView',
+ }, // Always show
+ {
+ title: "Background", toolTip: "Background", btnType: ButtonType.ColorButton, ignoreClick: true, icon: "fill-drip",
+ script: "setBackgroundColor", hidden: 'selectedDocumentType()'
+ }, // Only when a document is selected
+ { title: "Overlay", toolTip: "Overlay", btnType: ButtonType.ToggleButton, icon: "layer-group", click: 'toggleOverlay()', script: 'toggleOverlay', hidden: 'selectedDocumentType(undefined, "freeform", true)' }, // Only when floating document is selected in freeform
+ { title: "Alias", btnType: ButtonType.ClickButton, icon: "copy", hidden: 'selectedDocumentType()' }, // Only when a document is selected
+ { title: "Text", type: "textTools", subMenu: true, expanded: 'selectedDocumentType("rtf")' }, // Always available
+ { title: "Ink", type: "inkTools", subMenu: true, expanded: 'selectedDocumentType("ink")' }, // Always available
+ { title: "Web", type: "webTools", subMenu: true, hidden: 'selectedDocumentType("web")' }, // Only when Web is selected
+ { title: "Schema", type: "schemaTools", subMenu: true, hidden: 'selectedDocumentType(undefined, "schema")' } // Only when Schema is selected
+ ];
+ }
+
+ // Sets up the default context menu buttons
+ static async setupContextMenuButtons(doc: Doc) {
+ const docList: Doc[] = [];
+
+ (await CurrentUserUtils.contextMenuTools(doc)).map(({ title, width, list, toolTip, ignoreClick, icon, type, btnType, click, script, subMenu, hidden, expanded }) => {
+ const menuDocList: Doc[] = [];
+ if (subMenu) {
+ // default is textTools
+ let tools: Button[];
+ switch (type) {
+ case "inkTools":
+ tools = CurrentUserUtils.inkTools(doc);
+ break;
+ case "schemaTools":
+ tools = CurrentUserUtils.schemaTools(doc);
+ break;
+ case "webTools":
+ tools = CurrentUserUtils.webTools(doc);
+ break;
+ case "textTools":
+ tools = CurrentUserUtils.textTools(doc);
+ break;
+ default:
+ tools = CurrentUserUtils.textTools(doc);
+ break;
+ }
+ tools.map(({ title, toolTip, icon, btnType, numBtnType, click, script, width, list, ignoreClick, switchToggle }) => {
+ menuDocList.push(Docs.Create.FontIconDocument({
+ _nativeWidth: width ? width : 25,
+ _nativeHeight: 25,
+ _width: width ? width : 25,
+ _height: 25,
+ icon,
+ toolTip,
+ numBtnType,
+ script,
+ btnType: btnType,
+ btnList: new List<string>(list),
+ ignoreClick: ignoreClick,
+ _stayInCollection: true,
+ _hideContextMenu: true,
+ _lockedPosition: true,
+ system: true,
+ dontUndo: true,
+ title,
+ switchToggle,
+ color: Colors.WHITE,
+ backgroundColor: "transparent",
+ _dropAction: "alias",
+ _removeDropProperties: new List<string>(["dropAction", "_stayInCollection"]),
+ onClick: click ? ScriptField.MakeScript(click, { doc: Doc.name }) : undefined
+ }));
+ });
+ docList.push(CurrentUserUtils.linearButtonList({
+ linearViewSubMenu: true,
+ flexGap: 0,
+ ignoreClick: true,
+ linearViewExpandable: true,
+ icon: title,
+ _height: 30,
+ backgroundColor: "transparent",
+ linearViewIsExpanded: expanded ? ComputedField.MakeFunction(expanded) as any : undefined,
+ hidden: hidden ? ComputedField.MakeFunction(hidden) as any : undefined,
+ }, menuDocList));
+ } else {
+ docList.push(Docs.Create.FontIconDocument({
+ _nativeWidth: width ? width : 25,
+ _nativeHeight: 25,
+ _width: width ? width : 25,
+ _height: 25,
+ icon,
+ toolTip,
+ script,
+ btnType: btnType,
+ btnList: new List<string>(list),
+ ignoreClick: ignoreClick,
+ _stayInCollection: true,
+ _hideContextMenu: true,
+ _lockedPosition: true,
+ system: true,
+ dontUndo: true,
+ title,
+ color: Colors.WHITE,
+ backgroundColor: "transparent",
+ _dropAction: "alias",
+ hidden: hidden ? ComputedField.MakeFunction(hidden) as any : undefined,
+ _removeDropProperties: new List<string>(["dropAction", "_stayInCollection"]),
+ onClick: click ? ScriptField.MakeScript(click, { scriptContext: "any" }) : undefined
+ }));
+ }
+ });
+
+ if (doc.contextMenuBtns === undefined) {
+ doc.contextMenuBtns = CurrentUserUtils.linearButtonList({ title: "menu buttons", flexGap: 0, ignoreClick: true, linearViewExpandable: false, _height: 35 }, docList);
+ }
+ }
+
// sets up the default set of documents to be shown in the Overlay layer
static setupOverlays(doc: Doc) {
if (doc.myOverlayDocs === undefined) {
@@ -916,24 +1157,30 @@ export class CurrentUserUtils {
let linkDocs = Docs.newAccount ? undefined : await DocServer.GetRefField(linkDatabaseId);
if (!linkDocs) {
linkDocs = new Doc(linkDatabaseId, true);
+ (linkDocs as Doc).title = "LINK DATABASE: " + Doc.CurrentUserEmail;
(linkDocs as Doc).author = Doc.CurrentUserEmail;
(linkDocs as Doc).data = new List<Doc>([]);
- (linkDocs as Doc)["acl-Public"] = SharingPermissions.Add;
+ (linkDocs as Doc)["acl-Public"] = SharingPermissions.Augment;
}
doc.myLinkDatabase = new PrefetchProxy(linkDocs);
}
if (doc.mySharedDocs === undefined) {
let sharedDocs = Docs.newAccount ? undefined : await DocServer.GetRefField(sharingDocumentId + "outer");
if (!sharedDocs) {
- sharedDocs = Docs.Create.StackingDocument([], {
- title: "My SharedDocs", childDropAction: "alias", system: true, contentPointerEvents: "none", childLimitHeight: 0, _yMargin: 50, _gridGap: 15,
- _showTitle: "title", ignoreClick: true, _lockedPosition: true, "acl-Public": SharingPermissions.Add, "_acl-Public": SharingPermissions.Add,
+ sharedDocs = Docs.Create.TreeDocument([], {
+ title: "My SharedDocs", childDropAction: "alias", system: true, contentPointerEvents: "all", childLimitHeight: 0, _yMargin: 50, _gridGap: 15,
+ _showTitle: "title", ignoreClick: true, _lockedPosition: true, "acl-Public": SharingPermissions.Augment, "_acl-Public": SharingPermissions.Augment,
_chromeHidden: true, boxShadow: "0 0",
}, sharingDocumentId + "outer", sharingDocumentId);
- (sharedDocs as Doc)["acl-Public"] = (sharedDocs as Doc)[DataSym]["acl-Public"] = SharingPermissions.Add;
+ (sharedDocs as Doc)["acl-Public"] = (sharedDocs as Doc)[DataSym]["acl-Public"] = SharingPermissions.Augment;
}
if (sharedDocs instanceof Doc) {
Doc.GetProto(sharedDocs).userColor = sharedDocs.userColor || "rgb(202, 202, 202)";
+ const addToDashboards = ScriptField.MakeScript(`addToDashboards(self)`);
+ const dashboardFilter = ScriptField.MakeFunction(`doc._viewType === '${CollectionViewType.Docking}'`, { doc: Doc.name });
+ sharedDocs.childContextMenuFilters = new List<ScriptField>([dashboardFilter!,]);
+ sharedDocs.childContextMenuScripts = new List<ScriptField>([addToDashboards!,]);
+ sharedDocs.childContextMenuLabels = new List<string>(["Add to Dashboards",]);
}
doc.mySharedDocs = new PrefetchProxy(sharedDocs);
}
@@ -949,11 +1196,21 @@ export class CurrentUserUtils {
}
if (doc.myImportPanel === undefined) {
const uploads = Cast(doc.myImportDocs, Doc, null);
- const newUpload = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("importDocument()"), toolTip: "Import External document", _stayInCollection: true, _hideContextMenu: true, title: "Import", icon: "upload", system: true });
+ const newUpload = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("importDocument()"), toolTip: "Import External document", _stayInCollection: true, _hideContextMenu: true, title: "Import", btnType: ButtonType.ToolButton, icon: "upload", system: true });
doc.myImportPanel = new PrefetchProxy(Docs.Create.StackingDocument([newUpload, uploads], { title: "My ImportPanel", _yMargin: 20, _showTitle: "title", ignoreClick: true, _chromeHidden: true, _stayInCollection: true, _hideContextMenu: true, _lockedPosition: true, system: true, boxShadow: "0 0" }));
}
}
+ // Search sidebar is where searches within the document are performed
+ static setupSearchSidebar(doc: Doc) {
+ if (doc.mySearchPanel === undefined) {
+ doc.mySearchPanel = new PrefetchProxy(Docs.Create.SearchDocument({
+ backgroundColor: "dimGray", ignoreClick: true, _searchDoc: true,
+ childDropAction: "alias", _lockedPosition: true, _viewType: CollectionViewType.Schema, title: "Search Sidebar", system: true
+ })) as any as Doc;
+ }
+ }
+
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
@@ -1019,8 +1276,8 @@ export class CurrentUserUtils {
doc._raiseWhenDragged = true;
doc._showLabel = false;
doc._showMenuLabel = true;
- doc.activeInkColor = StrCast(doc.activeInkColor, "rgb(0, 0, 0)");
- doc.activeInkWidth = StrCast(doc.activeInkWidth, "1");
+ doc.activeInkColor = StrCast(doc.activeInkColor, "rgb(0, 0, 0, 0)");
+ doc.activeInkWidth = NumCast(doc.activeInkWidth, 1);
doc.activeInkBezier = StrCast(doc.activeInkBezier, "0");
doc.activeFillColor = StrCast(doc.activeFillColor, "");
doc.activeArrowStart = StrCast(doc.activeArrowStart, "");
@@ -1030,7 +1287,7 @@ export class CurrentUserUtils {
doc.fontFamily = StrCast(doc.fontFamily, "Arial");
doc.fontColor = StrCast(doc.fontColor, "black");
doc.fontHighlight = StrCast(doc.fontHighlight, "");
- doc.defaultAclPrivate = BoolCast(doc.defaultAclPrivate, true);
+ doc.defaultAclPrivate = BoolCast(doc.defaultAclPrivate, false);
doc.activeCollectionBackground = StrCast(doc.activeCollectionBackground, "white");
doc.activeCollectionNestedBackground = Cast(doc.activeCollectionNestedBackground, "string", null);
doc.noviceMode = BoolCast(doc.noviceMode, true);
@@ -1041,10 +1298,11 @@ export class CurrentUserUtils {
doc.filterDocCount = 0;
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.setupImportSidebar(doc);
+ this.setupImportSidebar(doc); // sets up the import sidebar
+ this.setupSearchSidebar(doc); // sets up the search sidebar
this.setupActiveMobileMenu(doc); // sets up the current mobile menu for Dash Mobile
- this.setupSearchPanel(doc);
this.setupOverlays(doc); // documents in overlay layer
+ this.setupContextMenuButtons(doc); // set up context menu buttons
this.setupDockedButtons(doc); // the bottom bar of font icons
await this.setupSidebarButtons(doc); // the pop-out left sidebar of tools/panels
await this.setupMenuPanel(doc, sharingDocumentId, linkDatabaseId);
@@ -1207,14 +1465,28 @@ export class CurrentUserUtils {
};
const freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions);
const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: `Dashboard ${dashboardCount}` }, id, "row");
+ freeformDoc.context = dashboardDoc;
+
+ // switching the tabs from the datadoc to the regular doc
+ const dashboardTabs = dashboardDoc[DataSym].data;
+ dashboardDoc[DataSym].data = new List<Doc>();
+ dashboardDoc.data = dashboardTabs;
+
+ // collating all docs on the dashboard to make a data-all field
+ const allDocs = new List<Doc>();
+ const allDocs2 = new List<Doc>(); // Array.from, spread, splice all cause so stack or acl issues for some reason
+ DocListCast(dashboardTabs).forEach(doc => {
+ const tabDocs = DocListCast(doc.data);
+ allDocs.push(...tabDocs);
+ allDocs2.push(...tabDocs);
+ });
+ dashboardDoc[DataSym]["data-all"] = allDocs;
+ dashboardDoc["data-all"] = allDocs2;
+ DocListCast(dashboardDoc.data).forEach(doc => doc.dashboard = dashboardDoc);
+ DocListCast(dashboardDoc.data)[1].data = ComputedField.MakeFunction(`dynamicOffScreenDocs(self.dashboard)`) as any;
+
Doc.AddDocToList(myPresentations, "data", presentation);
userDoc.activePresentation = presentation;
- const toggleTheme = ScriptField.MakeScript(`Doc.UserDoc().darkScheme = !Doc.UserDoc().darkScheme`);
- const toggleComic = ScriptField.MakeScript(`toggleComicMode()`);
- const snapshotDashboard = ScriptField.MakeScript(`snapshotDashboard()`);
- const createDashboard = ScriptField.MakeScript(`createNewDashboard()`);
- dashboardDoc.contextMenuScripts = new List<ScriptField>([toggleTheme!, toggleComic!, snapshotDashboard!, createDashboard!]);
- dashboardDoc.contextMenuLabels = new List<string>(["Toggle Theme Colors", "Toggle Comic Mode", "Snapshot Dashboard", "Create Dashboard"]);
Doc.AddDocToList(dashboards, "data", dashboardDoc);
CurrentUserUtils.openDashboard(userDoc, dashboardDoc);
@@ -1265,4 +1537,64 @@ Scripting.addGlobal(function createNewPresentation() { return MainView.Instance.
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 importDocument() { return CurrentUserUtils.importDocument(); },
- "imports files from device directly into the import sidebar"); \ No newline at end of file
+ "imports files from device directly into the import sidebar");
+Scripting.addGlobal(function shareDashboard(dashboard: Doc) {
+ SharingManager.Instance.open(undefined, dashboard);
+},
+ "opens sharing dialog for Dashboard");
+Scripting.addGlobal(async function removeDashboard(dashboard: Doc) {
+ const dashboards = await DocListCastAsync(CurrentUserUtils.MyDashboards.data);
+ if (dashboards && dashboards.length > 1) {
+ if (dashboard === CurrentUserUtils.ActiveDashboard) CurrentUserUtils.openDashboard(Doc.UserDoc(), dashboards.find(doc => doc !== dashboard)!);
+ Doc.RemoveDocFromList(CurrentUserUtils.MyDashboards, "data", dashboard);
+ }
+},
+ "Remove Dashboard from Dashboards");
+Scripting.addGlobal(async function addToDashboards(dashboard: Doc) {
+ const dashboardAlias = Doc.MakeAlias(dashboard);
+
+ const allDocs = await DocListCastAsync(dashboard[DataSym]["data-all"]);
+
+ // moves the data-all field from the datadoc to the layoutdoc, necessary for off screen docs tab to function properly
+ // dashboard["data-all"] = new List<Doc>(allDocs);
+ // dashboardAlias["data-all"] = new List<Doc>((allDocs || []).map(doc => Doc.MakeAlias(doc)));
+
+ // const dockingConfig = JSON.parse(StrCast(dashboardAlias.dockingConfig));
+ // dashboardAlias.dockingConfig = JSON.stringify(dockingConfig);
+
+ dashboardAlias.data = new List<Doc>(DocListCast(dashboard.data).map(tabFolder => Doc.MakeAlias(tabFolder)));
+ DocListCast(dashboardAlias.data).forEach(doc => doc.dashboard = dashboardAlias);
+ //new List<Doc>();
+ DocListCast(dashboardAlias.data)[1].data = ComputedField.MakeFunction(`dynamicOffScreenDocs(self.dashboard)`) as any;
+ Doc.AddDocToList(CurrentUserUtils.MyDashboards, "data", dashboardAlias);
+ CurrentUserUtils.openDashboard(Doc.UserDoc(), dashboardAlias);
+},
+ "adds Dashboard to set of Dashboards");
+
+/**
+ * Dynamically computes which docs should be rendered in the off-screen tabs tree of a dashboard.
+ */
+Scripting.addGlobal(function dynamicOffScreenDocs(dashboard: Doc) {
+ if (dashboard[DataSym] instanceof Doc) {
+ const allDocs = DocListCast(dashboard["data-all"]);
+ const onScreenTab = DocListCast(dashboard.data)[0];
+ const onScreenDocs = DocListCast(onScreenTab.data);
+ return new List<Doc>(allDocs.reduce((result: Doc[], doc) => {
+ !onScreenDocs.includes(doc) && !onScreenDocs.includes(doc.aliasOf as Doc) && (result.push(doc));
+ return result;
+ }, []));
+ }
+ return [];
+});
+Scripting.addGlobal(function selectedDocumentType(docType?: DocumentType, colType?: CollectionViewType, checkParent?: boolean) {
+ let selected = SelectionManager.Views().length ? SelectionManager.Views()[0].rootDoc : undefined;
+ if (selected && checkParent) {
+ const parentDoc: Doc = Cast(selected.context, Doc, null);
+ selected = parentDoc;
+ }
+ console.log(selected, docType, colType);
+ if (selected && docType && selected.type === docType) return false;
+ else if (selected && colType && selected.viewType === colType) return false;
+ else if (selected && !colType && !docType) return false;
+ else return true;
+});
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 5b092258a..cb0ee411c 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -144,9 +144,11 @@ export class DocumentManager {
originalTarget = originalTarget ?? targetDoc;
const getFirstDocView = LightboxView.LightboxDoc ? DocumentManager.Instance.getLightboxDocumentView : DocumentManager.Instance.getFirstDocumentView;
const docView = getFirstDocView(targetDoc, originatingDoc);
+ const wasHidden = targetDoc.hidden; //
+ if (wasHidden) runInAction(() => targetDoc.hidden = false); // if the target is hidden, un-hide it here.
const focusAndFinish = (didFocus: boolean) => {
if (originatingDoc?.isPushpin) {
- if (!didFocus || targetDoc.hidden) {
+ if (!didFocus && !wasHidden) { // don't toggle the hidden state if the doc was already un-hidden as part of this document traversal
targetDoc.hidden = !targetDoc.hidden;
}
} else {
@@ -161,13 +163,14 @@ export class DocumentManager {
const contextDocs = docContext ? await DocListCastAsync(docContext.data) : undefined;
const contextDoc = contextDocs?.find(doc => Doc.AreProtosEqual(doc, targetDoc) || Doc.AreProtosEqual(doc, annotatedDoc)) ? docContext : undefined;
const targetDocContext = contextDoc || annotatedDoc;
- const targetDocContextView = targetDocContext && getFirstDocView(targetDocContext);
+ const targetDocContextView = (targetDocContext && getFirstDocView(targetDocContext)) ||
+ (wasHidden && annoContainerView);// if we have an annotation container and the target was hidden, then try again because we just un-hid the document above
const focusView = !docView && targetDoc.type === DocumentType.MARKER && annoContainerView ? annoContainerView : docView;
- if (!docView && annoContainerView && !focusView) {
+ if (!docView && annoContainerView) {
annoContainerView.focus(targetDoc); // this allows something like a PDF view to remove its doc filters to expose the target so that it can be found in the retry code below
}
if (focusView) {
- focusView && Doc.linkFollowHighlight(focusView.rootDoc);
+ Doc.linkFollowHighlight(focusView.rootDoc);
focusView.focus(targetDoc, {
originalTarget, willZoom, afterFocus: (didFocus: boolean) =>
new Promise<ViewAdjustment>(res => {
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index c4842e88a..f7ef9ae6f 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -15,6 +15,15 @@ import { SnappingManager } from "./SnappingManager";
import { UndoManager } from "./UndoManager";
export type dropActionType = "alias" | "copy" | "move" | "same" | "proto" | "none" | undefined; // undefined = move, "same" = move but don't call removeDropProperties
+
+/**
+ * Initialize drag
+ * @param _reference: The HTMLElement that is being dragged
+ * @param docFunc: The Dash document being moved
+ * @param moveFunc: The function called when the document is moved
+ * @param dropAction: What to do with the document when it is dropped
+ * @param dragStarted: Method to call when the drag is started
+ */
export function SetupDrag(
_reference: React.RefObject<HTMLElement>,
docFunc: () => Doc | Promise<Doc> | undefined,
@@ -416,10 +425,10 @@ export namespace DragManager {
AbortDrag = () => {
options?.dragComplete?.(new DragCompleteEvent(true, dragData));
- endDrag();
+ cleanupDrag();
};
- const endDrag = action(() => {
+ const cleanupDrag = action(() => {
hideDragShowOriginalElements(false);
document.removeEventListener("pointermove", moveHandler, true);
document.removeEventListener("pointerup", upHandler);
@@ -509,15 +518,14 @@ export namespace DragManager {
`translate(${(xs[i] += moveVec.x) + (options?.offsetX || 0)}px, ${(ys[i] += moveVec.y) + (options?.offsetY || 0)}px) scale(${scaleXs[i]}, ${scaleYs[i]})`)
);
};
- const upHandler = async (e: PointerEvent) => {
- dispatchDrag(document.elementFromPoint(e.x, e.y) || document.body, e, new DragCompleteEvent(false, dragData), snapDrag(e, xFromLeft, yFromTop, xFromRight, yFromBottom), finishDrag, options);
- endDrag();
+ const upHandler = (e: PointerEvent) => {
+ dispatchDrag(document.elementFromPoint(e.x, e.y) || document.body, e, new DragCompleteEvent(false, dragData), snapDrag(e, xFromLeft, yFromTop, xFromRight, yFromBottom), finishDrag, options, cleanupDrag);
};
document.addEventListener("pointermove", moveHandler, true);
document.addEventListener("pointerup", upHandler);
}
- async function dispatchDrag(target: Element, e: PointerEvent, complete: DragCompleteEvent, pos: { x: number, y: number }, finishDrag?: (e: DragCompleteEvent) => void, options?: DragOptions) {
+ async function dispatchDrag(target: Element, e: PointerEvent, complete: DragCompleteEvent, pos: { x: number, y: number }, finishDrag?: (e: DragCompleteEvent) => void, options?: DragOptions, endDrag?: () => void) {
const dropArgs = {
bubbles: true,
detail: {
@@ -534,5 +542,6 @@ export namespace DragManager {
await finishDrag?.(complete);
target.dispatchEvent(new CustomEvent<DropEvent>("dashOnDrop", dropArgs));
options?.dragComplete?.(complete);
+ endDrag?.();
}
} \ No newline at end of file
diff --git a/src/client/util/GroupMemberView.tsx b/src/client/util/GroupMemberView.tsx
index 927200ed3..b7f89794d 100644
--- a/src/client/util/GroupMemberView.tsx
+++ b/src/client/util/GroupMemberView.tsx
@@ -50,7 +50,6 @@ export class GroupMemberView extends React.Component<GroupMemberViewProps> {
onChange={selectedOption => GroupManager.Instance.addMemberToGroup(this.props.group, (selectedOption as UserOptions).value)}
placeholder={"Add members"}
value={null}
- closeMenuOnSelect={true}
styles={{
dropdownIndicator: (base, state) => ({
...base,
diff --git a/src/client/util/HypothesisUtils.ts b/src/client/util/HypothesisUtils.ts
index 8ddfce772..635673025 100644
--- a/src/client/util/HypothesisUtils.ts
+++ b/src/client/util/HypothesisUtils.ts
@@ -126,7 +126,7 @@ export namespace Hypothesis {
});
const annotationId = StrCast(linkDoc.annotationId);
- const linkUrl = Utils.prepend("/doc/" + sourceDoc[Id]);
+ const linkUrl = Doc.globalServerPath(sourceDoc);
const interval = setInterval(() => {// keep trying to edit until annotations have loaded and editing is successful
!success && document.dispatchEvent(new CustomEvent<{ targetUrl: string, id: string }>("deleteLink", {
detail: { targetUrl: linkUrl, id: annotationId },
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index ba935e3bf..e009fb3a9 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -210,7 +210,8 @@ export namespace InteractionUtils {
style={{
// filter: drawHalo ? "url(#inkSelectionHalo)" : undefined,
fill: fill ? fill : "none",
- opacity: strokeWidth !== width ? 0.5 : undefined,
+ opacity: 1.0,
+ // opacity: strokeWidth !== width ? 0.5 : undefined,
pointerEvents: pevents as any,
stroke: color ?? "rgb(0, 0, 0)",
strokeWidth: strokeWidth,
@@ -299,8 +300,6 @@ export namespace InteractionUtils {
return points;
case "circle":
-
-
const centerX = (Math.max(left, right) + Math.min(left, right)) / 2;
const centerY = (Math.max(top, bottom) + Math.min(top, bottom)) / 2;
const radius = Math.max(centerX - Math.min(left, right), centerY - Math.min(top, bottom));
@@ -315,7 +314,6 @@ export namespace InteractionUtils {
points.push({ X: newX, Y: y });
}
points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(top, bottom) - centerY), 2))) + centerX, Y: Math.min(top, bottom) });
-
} else {
for (var x = Math.min(left, right); x < Math.max(left, right); x++) {
const y = Math.sqrt(Math.pow(radius, 2) - (Math.pow((x - centerX), 2))) + centerY;
@@ -327,7 +325,6 @@ export namespace InteractionUtils {
points.push({ X: x, Y: newY });
}
points.push({ X: Math.min(left, right), Y: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(left, right) - centerX), 2))) + centerY });
-
}
return points;
// case "arrow":
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 08f4ac9b7..f0c055049 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -1,8 +1,9 @@
-import { observable, observe, action } from "mobx";
+import { observable, observe, action, reaction, computed } from "mobx";
import { computedFn } from "mobx-utils";
import { DirectLinksSym, Doc, DocListCast, Field, Opt } from "../../fields/Doc";
import { List } from "../../fields/List";
import { ProxyField } from "../../fields/Proxy";
+import { listSpec } from "../../fields/Schema";
import { BoolCast, Cast, PromiseValue, StrCast } from "../../fields/Types";
import { LightboxView } from "../views/LightboxView";
import { DocumentViewSharedProps, ViewAdjustment } from "../views/nodes/DocumentView";
@@ -26,36 +27,44 @@ type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => vo
export class LinkManager {
@observable static _instance: LinkManager;
- @observable static userDocs: Doc[] = [];
+ @observable static userLinkDBs: Doc[] = [];
public static currentLink: Opt<Doc>;
public static get Instance() { return LinkManager._instance; }
+ public static addLinkDB = (linkDb: any) => LinkManager.userLinkDBs.push(linkDb);
+ static links: Doc[] = [];
constructor() {
LinkManager._instance = this;
+ this.createLinkrelationshipLists();
setTimeout(() => {
- LinkManager.userDocs = [Doc.LinkDBDoc().data as Doc, ...SharingManager.Instance.users.map(user => user.linkDatabase)];
- const addLinkToDoc = action((link: Doc): any => {
- const a1 = link?.anchor1;
- const a2 = link?.anchor2;
- if (a1 instanceof Promise || a2 instanceof Promise) return PromiseValue(a1).then(a1 => PromiseValue(a2).then(a2 => addLinkToDoc(link)));
- if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) {
- Doc.GetProto(a1)[DirectLinksSym].add(link);
- Doc.GetProto(a2)[DirectLinksSym].add(link);
- Doc.GetProto(link)[DirectLinksSym].add(link);
- }
- });
- const remLinkFromDoc = action((link: Doc): any => {
+ LinkManager.userLinkDBs = [];
+ const addLinkToDoc = (link: Doc) => {
+ const a1Prom = link?.anchor1;
+ const a2Prom = link?.anchor2;
+ Promise.all([a1Prom, a2Prom]).then(action((all) => {
+ const a1 = all[0];
+ const a2 = all[1];
+ if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) {
+ Doc.GetProto(a1)[DirectLinksSym].add(link);
+ Doc.GetProto(a2)[DirectLinksSym].add(link);
+ Doc.GetProto(link)[DirectLinksSym].add(link);
+ }
+ }));
+ };
+ const remLinkFromDoc = (link: Doc) => {
const a1 = link?.anchor1;
const a2 = link?.anchor2;
- if (a1 instanceof Promise || a2 instanceof Promise) return PromiseValue(a1).then(a1 => PromiseValue(a2).then(a2 => remLinkFromDoc(link)));
- if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) {
- Doc.GetProto(a1)[DirectLinksSym].delete(link);
- Doc.GetProto(a2)[DirectLinksSym].delete(link);
- Doc.GetProto(link)[DirectLinksSym].delete(link);
- }
- });
- const watchUserLinks = (userLinks: List<Doc>) => {
+ Promise.all([a1, a2]).then(action(() => {
+ if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) {
+ Doc.GetProto(a1)[DirectLinksSym].delete(link);
+ Doc.GetProto(a2)[DirectLinksSym].delete(link);
+ Doc.GetProto(link)[DirectLinksSym].delete(link);
+ }
+ }));
+ };
+ const watchUserLinkDB = (userLinkDBDoc: Doc) => {
+ LinkManager.links.push(...DocListCast(userLinkDBDoc.data));
const toRealField = (field: Field) => field instanceof ProxyField ? field.value() : field; // see List.ts. data structure is not a simple list of Docs, but a list of ProxyField/Fields
- observe(userLinks, change => {
+ observe(userLinkDBDoc.data as Doc, change => { // observe pushes/splices on a user link DB 'data' field (should only happen for local changes)
switch (change.type as any) {
case "splice":
(change as any).added.forEach((link: any) => addLinkToDoc(toRealField(link)));
@@ -64,16 +73,43 @@ export class LinkManager {
case "update": //let oldValue = change.oldValue;
}
}, true);
+ observe(userLinkDBDoc, "data", // obsever when a new array of links is assigned as the link DB 'data' field (should happen whenever a remote user adds/removes a link)
+ change => {
+ switch (change.type as any) {
+ case "update":
+ Promise.all([...(change.oldValue as any as Doc[] || []), ...(change.newValue as any as Doc[] || [])]).then(doclist => {
+ const oldDocs = doclist.slice(0, (change.oldValue as any as Doc[] || []).length);
+ const newDocs = doclist.slice((change.oldValue as any as Doc[] || []).length, doclist.length);
+
+ const added = newDocs?.filter(link => !(oldDocs || []).includes(link));
+ const removed = oldDocs?.filter(link => !(newDocs || []).includes(link));
+ added?.forEach((link: any) => addLinkToDoc(toRealField(link)));
+ removed?.forEach((link: any) => remLinkFromDoc(toRealField(link)));
+ });
+ }
+ }, true);
};
- observe(LinkManager.userDocs, change => {
+ observe(LinkManager.userLinkDBs, change => {
switch (change.type as any) {
- case "splice": (change as any).added.forEach(watchUserLinks); break;
+ case "splice": (change as any).added.forEach(watchUserLinkDB); break;
case "update": //let oldValue = change.oldValue;
}
}, true);
+ LinkManager.addLinkDB(Doc.LinkDBDoc());
});
}
+
+ public createLinkrelationshipLists = () => {
+ //create new lists for link relations and their associated colors if the lists don't already exist
+ if (!Doc.UserDoc().linkRelationshipList && !Doc.UserDoc().linkColorList) {
+ const linkRelationshipList = new List<string>();
+ const linkColorList = new List<string>();
+ Doc.UserDoc().linkRelationshipList = linkRelationshipList;
+ Doc.UserDoc().linkColorList = linkColorList;
+ }
+ }
+
public addLink(linkDoc: Doc, checkExists = false) {
if (!checkExists || !DocListCast(Doc.LinkDBDoc().data).includes(linkDoc)) {
Doc.AddDocToList(Doc.LinkDBDoc(), "data", linkDoc);
@@ -84,14 +120,20 @@ export class LinkManager {
public getAllRelatedLinks(anchor: Doc) { return this.relatedLinker(anchor); } // finds all links that contain the given anchor
public getAllDirectLinks(anchor: Doc): Doc[] {
- return Array.from(Doc.GetProto(anchor)[DirectLinksSym]);
+ // FIXME:glr Why is Doc undefined?
+ if (Doc.GetProto(anchor)[DirectLinksSym]){
+ return Array.from(Doc.GetProto(anchor)[DirectLinksSym]);
+ } else {
+ return [];
+ }
} // finds all links that contain the given anchor
relatedLinker = computedFn(function relatedLinker(this: any, anchor: Doc): Doc[] {
const lfield = Doc.LayoutFieldKey(anchor);
- return DocListCast(anchor[lfield + "-annotations"]).concat(DocListCast(anchor[lfield + "-annotations-timeline"])).reduce((list, anno) =>
+ const related = DocListCast(anchor[lfield + "-annotations"]).concat(DocListCast(anchor[lfield + "-annotations-timeline"])).reduce((list, anno) =>
[...list, ...LinkManager.Instance.relatedLinker(anno)],
- Array.from(Doc.GetProto(anchor)[DirectLinksSym]).slice());// LinkManager.Instance.directLinker(anchor).slice());
+ Array.from(Doc.GetProto(anchor)[DirectLinksSym]).slice());
+ return related;
}, true);
// returns map of group type to anchor's links in that group type
@@ -135,7 +177,8 @@ export class LinkManager {
const where = LightboxView.LightboxDoc ? "lightbox" : StrCast(sourceDoc.followLinkLocation, followLoc);
docViewProps.addDocTab(doc, where);
setTimeout(() => {
- const targDocView = DocumentManager.Instance.getFirstDocumentView(doc);
+ const getFirstDocView = LightboxView.LightboxDoc ? DocumentManager.Instance.getLightboxDocumentView : DocumentManager.Instance.getFirstDocumentView;
+ const targDocView = getFirstDocView(doc); // get first document view available within the lightbox if that's open, or anywhere otherwise.
if (targDocView) {
targDocView.props.focus(doc, {
willZoom: BoolCast(sourceDoc.followLinkZoom, false),
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index f981f84cd..efee37419 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -12,7 +12,7 @@ export { ts };
import * as typescriptlib from '!!raw-loader!./type_decls.d';
import { Doc, Field } from '../../fields/Doc';
-export interface ScriptSucccess {
+export interface ScriptSuccess {
success: true;
result: any;
}
@@ -23,7 +23,7 @@ export interface ScriptError {
result: any;
}
-export type ScriptResult = ScriptSucccess | ScriptError;
+export type ScriptResult = ScriptSuccess | ScriptError;
export type ScriptParam = { [name: string]: string };
@@ -171,10 +171,12 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an
if (!options.editable) {
batch = Doc.MakeReadOnly();
}
+
const result = compiledFunction.apply(thisParam, params).apply(thisParam, argsArray);
if (batch) {
batch.end();
}
+
return { success: true, result };
} catch (error) {
@@ -315,7 +317,7 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
}
const paramString = paramList.join(", ");
const funcScript = `(function(${paramString})${requiredType ? `: ${requiredType}` : ''} {
- ${addReturn ? `return ${script};` : script}
+ ${addReturn ? `return ${script};` : `return ${script};`}
})`;
host.writeFile("file.ts", funcScript);
diff --git a/src/client/util/SettingsManager.scss b/src/client/util/SettingsManager.scss
index c9db94419..b7199f433 100644
--- a/src/client/util/SettingsManager.scss
+++ b/src/client/util/SettingsManager.scss
@@ -360,17 +360,18 @@
flex-direction: row;
position: relative;
min-height: 250px;
+ height: 100%;
width: 100%;
.settings-content {
- background-color: #fdfdfd;
+ background-color: $off-white;
}
}
.settings-panel {
position: relative;
min-width: 150px;
- background-color: #e4e4e4;
+ background-color: $light-blue;
.settings-user {
position: absolute;
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index 3987497b8..bd91db779 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -268,7 +268,8 @@ export class SettingsManager extends React.Component<{}> {
<div className="tab-column-content">
<button onClick={() => GroupManager.Instance?.open()}>Manage groups</button>
<div className="default-acl">
- <input className="acl-check" type="checkbox" checked={BoolCast(Doc.UserDoc()?.defaultAclPrivate)} onChange={action(() => Doc.UserDoc().defaultAclPrivate = !Doc.UserDoc().defaultAclPrivate)} />
+ <input className="acl-check" type="checkbox" checked={BoolCast(Doc.UserDoc()?.defaultAclPrivate)}
+ onChange={action(() => Doc.UserDoc().defaultAclPrivate = !Doc.UserDoc().defaultAclPrivate)} />
<div className="acl-text">Default access private</div>
</div>
</div>
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index dc5f488b2..6d7f7e8df 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -5,7 +5,7 @@ import { observer } from "mobx-react";
import * as React from "react";
import Select from "react-select";
import * as RequestPromise from "request-promise";
-import { AclAddonly, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, DataSym, Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc";
+import { AclAugment, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, AclUnset, DataSym, Doc, DocListCast, DocListCastAsync, Opt, AclSelfEdit } from "../../fields/Doc";
import { List } from "../../fields/List";
import { Cast, NumCast, StrCast } from "../../fields/Types";
import { distributeAcls, GetEffectiveAcl, normalizeEmail, SharingPermissions, TraceMobx } from "../../fields/util";
@@ -17,11 +17,13 @@ import { MainViewModal } from "../views/MainViewModal";
import { DocumentView } from "../views/nodes/DocumentView";
import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox";
import { SearchBox } from "../views/search/SearchBox";
+import { CurrentUserUtils } from "./CurrentUserUtils";
import { DocumentManager } from "./DocumentManager";
import { GroupManager, UserOptions } from "./GroupManager";
import { GroupMemberView } from "./GroupMemberView";
import { SelectionManager } from "./SelectionManager";
import "./SharingManager.scss";
+import { LinkManager } from "./LinkManager";
export interface User {
email: string;
@@ -38,7 +40,7 @@ interface GroupedOptions {
}
// const SharingKey = "sharingPermissions";
-// const PublicKey = "publicLinkPermissions";
+// const PublicKey = "all";
// const DefaultColor = "black";
// used to differentiate between individuals and groups when sharing
@@ -84,13 +86,14 @@ export class SharingManager extends React.Component<{}> {
private AclMap = new Map<symbol, string>([
[AclPrivate, SharingPermissions.None],
[AclReadonly, SharingPermissions.View],
- [AclAddonly, SharingPermissions.Add],
+ [AclAugment, SharingPermissions.Augment],
+ [AclSelfEdit, SharingPermissions.SelfEdit],
[AclEdit, SharingPermissions.Edit],
[AclAdmin, SharingPermissions.Admin]
]);
// private get linkVisible() {
- // return this.sharingDoc ? this.sharingDoc[PublicKey] !== SharingPermissions.None : false;
+ // return this.targetDoc ? this.targetDoc["acl-" + PublicKey] !== SharingPermissions.None : false;
// }
public open = (target?: DocumentView, target_doc?: Doc) => {
@@ -100,7 +103,7 @@ export class SharingManager extends React.Component<{}> {
this.targetDoc = target_doc || target?.props.Document;
DictationOverlay.Instance.hasActiveModal = true;
this.isOpen = this.targetDoc !== undefined;
- this.permissions = SharingPermissions.Add;
+ this.permissions = SharingPermissions.Augment;
});
}
@@ -152,10 +155,11 @@ export class SharingManager extends React.Component<{}> {
}
});
return Promise.all(evaluating).then(() => {
- runInAction(() => {
+ runInAction(async () => {
for (const sharer of sharingDocs) {
if (!this.users.find(user => user.user.email === sharer.user.email)) {
this.users.push(sharer);
+ LinkManager.addLinkDB(sharer.linkDatabase);
}
}
});
@@ -172,10 +176,11 @@ export class SharingManager extends React.Component<{}> {
const target = targetDoc || this.targetDoc!;
const acl = `acl-${normalizeEmail(user.email)}`;
const myAcl = `acl-${Doc.CurrentUserEmailNormalized}`;
+ const isDashboard = DocListCast(CurrentUserUtils.MyDashboards.data).indexOf(target) !== -1;
const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document);
return !docs.map(doc => {
- doc.author === Doc.CurrentUserEmail && !doc[myAcl] && distributeAcls(myAcl, SharingPermissions.Admin, doc);
+ doc.author === Doc.CurrentUserEmail && !doc[myAcl] && distributeAcls(myAcl, SharingPermissions.Admin, doc, undefined, undefined, isDashboard);
if (permission === SharingPermissions.None) {
if (doc[acl] && doc[acl] !== SharingPermissions.None) doc.numUsersShared = NumCast(doc.numUsersShared, 1) - 1;
@@ -184,8 +189,9 @@ export class SharingManager extends React.Component<{}> {
if (!doc[acl] || doc[acl] === SharingPermissions.None) doc.numUsersShared = NumCast(doc.numUsersShared, 0) + 1;
}
- distributeAcls(acl, permission as SharingPermissions, doc);
+ distributeAcls(acl, permission as SharingPermissions, doc, undefined, undefined, isDashboard);
+ this.setDashboardBackground(doc, permission as SharingPermissions);
if (permission !== SharingPermissions.None) return Doc.AddDocToList(sharingDoc, storage, doc);
else return GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc));
}).some(success => !success);
@@ -201,12 +207,13 @@ export class SharingManager extends React.Component<{}> {
const target = targetDoc || this.targetDoc!;
const key = normalizeEmail(StrCast(group.title));
const acl = `acl-${key}`;
+ const isDashboard = DocListCast(CurrentUserUtils.MyDashboards.data).indexOf(target) !== -1;
const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document);
// ! ensures it returns true if document has been shared successfully, false otherwise
return !docs.map(doc => {
- doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmailNormalized}`] && distributeAcls(`acl-${Doc.CurrentUserEmailNormalized}`, SharingPermissions.Admin, doc);
+ doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmailNormalized}`] && distributeAcls(`acl-${Doc.CurrentUserEmailNormalized}`, SharingPermissions.Admin, doc, undefined, undefined, isDashboard);
if (permission === SharingPermissions.None) {
if (doc[acl] && doc[acl] !== SharingPermissions.None) doc.numGroupsShared = NumCast(doc.numGroupsShared, 1) - 1;
@@ -215,7 +222,8 @@ export class SharingManager extends React.Component<{}> {
if (!doc[acl] || doc[acl] === SharingPermissions.None) doc.numGroupsShared = NumCast(doc.numGroupsShared, 0) + 1;
}
- distributeAcls(acl, permission as SharingPermissions, doc);
+ distributeAcls(acl, permission as SharingPermissions, doc, undefined, undefined, isDashboard);
+ this.setDashboardBackground(doc, permission as SharingPermissions);
if (group instanceof Doc) {
const members: string[] = JSON.parse(StrCast(group.members));
@@ -264,13 +272,34 @@ export class SharingManager extends React.Component<{}> {
});
}
else {
+ const dashboards = DocListCast(CurrentUserUtils.MyDashboards.data);
docs.forEach(doc => {
- if (GetEffectiveAcl(doc) === AclAdmin) distributeAcls(`acl-${shareWith}`, permission, doc);
+ const isDashboard = dashboards.indexOf(doc) !== -1;
+ if (GetEffectiveAcl(doc) === AclAdmin) distributeAcls(`acl-${shareWith}`, permission, doc, undefined, undefined, isDashboard);
});
}
}
/**
+ * Sets the background of the Dashboard if it has been shared as a visual indicator
+ */
+ setDashboardBackground = async (doc: Doc, permission: SharingPermissions) => {
+ if (Doc.IndexOf(doc, DocListCast(CurrentUserUtils.MyDashboards.data)) !== -1) {
+ if (permission !== SharingPermissions.None) {
+ doc.isShared = true;
+ doc.backgroundColor = "green";
+ }
+ else {
+ const acls = doc[DataSym][AclSym];
+ if (Object.keys(acls).every(key => key === `acl-${Doc.CurrentUserEmailNormalized}` ? true : [AclUnset, AclPrivate].includes(acls[key]))) {
+ doc.isShared = undefined;
+ doc.backgroundColor = undefined;
+ }
+ }
+ }
+ }
+
+ /**
* Removes the documents shared with a user through a group when the user is removed from the group.
* @param group
* @param emailId
@@ -294,10 +323,11 @@ export class SharingManager extends React.Component<{}> {
*/
removeGroup = (group: Doc) => {
if (group.docsShared) {
+ const dashboards = DocListCast(CurrentUserUtils.MyDashboards.data);
DocListCast(group.docsShared).forEach(doc => {
const acl = `acl-${StrCast(group.title)}`;
-
- distributeAcls(acl, SharingPermissions.None, doc);
+ const isDashboard = dashboards.indexOf(doc) !== -1;
+ distributeAcls(acl, SharingPermissions.None, doc, undefined, undefined, isDashboard);
const members: string[] = JSON.parse(StrCast(group.members));
const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email));
@@ -310,11 +340,11 @@ export class SharingManager extends React.Component<{}> {
// private setExternalSharing = (permission: string) => {
- // const sharingDoc = this.sharingDoc;
- // if (!sharingDoc) {
+ // const targetDoc = this.targetDoc;
+ // if (!targetDoc) {
// return;
// }
- // sharingDoc[PublicKey] = permission;
+ // targetDoc["acl-" + PublicKey] = permission;
// }
// private get sharingUrl() {
@@ -339,10 +369,10 @@ export class SharingManager extends React.Component<{}> {
const dropdownValues: string[] = Object.values(SharingPermissions);
if (!uniform) dropdownValues.unshift("-multiple-");
if (override) dropdownValues.unshift("None");
- return dropdownValues.filter(permission => permission !== SharingPermissions.View).map(permission =>
+ return dropdownValues.filter(permission => !Doc.UserDoc().noviceMode || ![SharingPermissions.View, SharingPermissions.SelfEdit].includes(permission as any)).map(permission =>
(
<option key={permission} value={permission}>
- {permission === SharingPermissions.Add ? "Can Augment" : permission}
+ {permission}
</option>
)
);
@@ -423,16 +453,16 @@ export class SharingManager extends React.Component<{}> {
}
}
- distributeOverCollection = (targetDoc?: Doc) => {
- const target = targetDoc || this.targetDoc!;
+ // distributeOverCollection = (targetDoc?: Doc) => {
+ // const target = targetDoc || this.targetDoc!;
- const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document);
- docs.forEach(doc => {
- for (const [key, value] of Object.entries(doc[AclSym])) {
- distributeAcls(key, this.AclMap.get(value)! as SharingPermissions, target);
- }
- });
- }
+ // const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document);
+ // docs.forEach(doc => {
+ // for (const [key, value] of Object.entries(doc[AclSym])) {
+ // distributeAcls(key, this.AclMap.get(value)! as SharingPermissions, target);
+ // }
+ // });
+ // }
/**
* Sorting algorithm to sort users.
@@ -483,7 +513,7 @@ export class SharingManager extends React.Component<{}> {
if (this.myDocAcls) {
const newDocs: Doc[] = [];
- SearchBox.foreachRecursiveDoc(docs, doc => newDocs.push(doc));
+ SearchBox.foreachRecursiveDoc(docs, (depth, doc) => newDocs.push(doc));
docs = newDocs.filter(doc => GetEffectiveAcl(doc) === AclAdmin);
}
@@ -519,7 +549,7 @@ export class SharingManager extends React.Component<{}> {
</select>
) : (
<div className={"permissions-dropdown"}>
- {permissions === SharingPermissions.Add ? "Can Augment" : permissions}
+ {permissions}
</div>
)}
</div>
@@ -565,7 +595,7 @@ export class SharingManager extends React.Component<{}> {
// the list of groups shared with
const groupListMap: (Doc | { title: string })[] = groups.filter(({ title }) => docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(StrCast(title))}`) : true);
- groupListMap.unshift({ title: "Public" });//, { title: "Override" });
+ groupListMap.unshift({ title: "Public" });//, { title: "ALL" });
const groupListContents = groupListMap.map(group => {
const groupKey = `acl-${StrCast(group.title)}`;
const uniform = docs.every(doc => this.layoutDocAcls ? doc?.[AclSym]?.[groupKey] === docs[0]?.[AclSym]?.[groupKey] : doc?.[DataSym]?.[AclSym]?.[groupKey] === docs[0]?.[DataSym]?.[AclSym]?.[groupKey]);
@@ -614,6 +644,11 @@ export class SharingManager extends React.Component<{}> {
<div className={"close-button"} onClick={this.close}>
<FontAwesomeIcon icon={"times"} color={"black"} size={"lg"} />
</div>
+ {/* {this.linkVisible ?
+ <div>
+ {this.sharingUrl}
+ </div> :
+ (null)} */}
{<div className="share-container">
<div className="share-setup">
<Select
diff --git a/src/client/views/.DS_Store b/src/client/views/.DS_Store
index 33e624ef4..e4ac87aad 100644
--- a/src/client/views/.DS_Store
+++ b/src/client/views/.DS_Store
Binary files differ
diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss
index 795529780..41a6dc569 100644
--- a/src/client/views/ContextMenu.scss
+++ b/src/client/views/ContextMenu.scss
@@ -3,14 +3,12 @@
.contextMenu-cont {
position: absolute;
display: flex;
- z-index: $contextMenu-zindex;
- box-shadow: $medium-gray 0.2vw 0.2vw 0.4vw;
+ z-index: 100000;
+ box-shadow: 0px 3px 4px rgba(0,0,0,30%);
flex-direction: column;
background: whitesmoke;
- padding-top: 10px;
- padding-bottom: 10px;
- border-radius: 15px;
- border: solid #BBBBBBBB 1px;
+ border-radius: 3px;
+ border: solid $light-gray 1px;
}
// .contextMenu-item:first-child {
diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index 6fe2abd21..168fcb888 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -90,7 +90,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select
</span>
) : null}
<div className="contextMenu-description">
- {this.props.description}
+ {this.props.description.replace(":","")}
</div>
</div>
);
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 0b70ce68d..d9cc29bed 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -1,4 +1,4 @@
-import { Doc, Opt, DataSym, AclReadonly, AclAddonly, AclPrivate, AclEdit, AclSym, DocListCastAsync, DocListCast, AclAdmin } from '../../fields/Doc';
+import { Doc, Opt, DataSym, AclReadonly, AclAugment, AclPrivate, AclEdit, AclSym, DocListCast, AclAdmin, AclSelfEdit } from '../../fields/Doc';
import { Touchable } from './Touchable';
import { computed, action, observable } from 'mobx';
import { Cast, BoolCast, ScriptCast } from '../../fields/Types';
@@ -7,7 +7,7 @@ import { InteractionUtils } from '../util/InteractionUtils';
import { List } from '../../fields/List';
import { DateField } from '../../fields/DateField';
import { ScriptField } from '../../fields/ScriptField';
-import { GetEffectiveAcl, SharingPermissions, distributeAcls, denormalizeEmail } from '../../fields/util';
+import { GetEffectiveAcl, SharingPermissions, distributeAcls, denormalizeEmail, inheritParentAcls } from '../../fields/util';
import { CurrentUserUtils } from '../util/CurrentUserUtils';
import { DocUtils } from '../documents/Documents';
import { returnFalse } from '../../Utils';
@@ -90,9 +90,9 @@ export interface ViewBoxAnnotatableProps {
renderDepth: number;
isAnnotationOverlay?: boolean;
}
-export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T>(schemaCtor: (doc: Doc) => T, _annotationKey: string = "annotations") {
+export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T>(schemaCtor: (doc: Doc) => T) {
class Component extends Touchable<P> {
- @observable _annotationKey: string = _annotationKey;
+ @observable _annotationKeySuffix = () => "annotations";
@observable _isAnyChildContentActive = false;
//TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
@@ -107,13 +107,6 @@ 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],
- [AclAdmin, SharingPermissions.Admin]
- ]);
lookupField = (field: string) => ScriptCast((this.layoutDoc as any).lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field }).result;
@@ -132,13 +125,13 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
- @computed public get annotationKey() { return this.fieldKey + (this._annotationKey ? "-" + this._annotationKey : ""); }
+ @computed public get annotationKey() { return this.fieldKey + (this._annotationKeySuffix() ? "-" + this._annotationKeySuffix() : ""); }
@action.bound
removeDocument(doc: Doc | Doc[], annotationKey?: string, leavePushpin?: boolean): boolean {
const effectiveAcl = GetEffectiveAcl(this.dataDoc);
const indocs = doc instanceof Doc ? [doc] : doc;
- const docs = indocs.filter(doc => effectiveAcl === AclEdit || effectiveAcl === AclAdmin || GetEffectiveAcl(doc) === AclAdmin);
+ const docs = indocs.filter(doc => [AclEdit, AclAdmin].includes(effectiveAcl) || GetEffectiveAcl(doc) === AclAdmin);
if (docs.length) {
setTimeout(() => docs.map(doc => { // this allows 'addDocument' to see the annotationOn field in order to create a pushin
Doc.SetInPlace(doc, "isPushpin", undefined, true);
@@ -201,17 +194,16 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
if (this.props.Document[AclSym] && Object.keys(this.props.Document[AclSym]).length) {
added.forEach(d => {
for (const [key, value] of Object.entries(this.props.Document[AclSym])) {
- if (d.author === denormalizeEmail(key.substring(4)) && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d, true);
- //else if (this.props.Document[key] === SharingPermissions.Admin) distributeAcls(key, SharingPermissions.Add, d, true);
- // else distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true);
+ if (d.author === denormalizeEmail(key.substring(4)) && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d);
}
});
}
- if (effectiveAcl === AclAddonly) {
+ if (effectiveAcl === AclAugment) {
added.map(doc => {
+ if ([AclAdmin, AclEdit].includes(GetEffectiveAcl(doc))) inheritParentAcls(CurrentUserUtils.ActiveDashboard, doc);
doc.context = this.props.Document;
- if (annotationKey ?? this._annotationKey) Doc.GetProto(doc).annotationOn = this.props.Document;
+ if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.props.Document;
this.props.layerProvider?.(doc, true);
Doc.AddDocToList(targetDataDoc, annotationKey ?? this.annotationKey, doc);
});
@@ -222,10 +214,12 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
//DocUtils.LeavePushpin(doc);
doc._stayInCollection = undefined;
doc.context = this.props.Document;
- if (annotationKey ?? this._annotationKey) Doc.GetProto(doc).annotationOn = this.props.Document;
+ if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.props.Document;
+
+ inheritParentAcls(CurrentUserUtils.ActiveDashboard, doc);
});
const annoDocs = targetDataDoc[annotationKey ?? this.annotationKey] as List<Doc>;
- if (annoDocs) annoDocs.push(...added);
+ if (annoDocs instanceof List) annoDocs.push(...added);
else targetDataDoc[annotationKey ?? this.annotationKey] = new List<Doc>(added);
targetDataDoc[(annotationKey ?? this.annotationKey) + "-lastModified"] = new DateField(new Date(Date.now()));
}
diff --git a/src/client/views/DocumentButtonBar.scss b/src/client/views/DocumentButtonBar.scss
index 171e7134f..a112f4745 100644
--- a/src/client/views/DocumentButtonBar.scss
+++ b/src/client/views/DocumentButtonBar.scss
@@ -46,7 +46,6 @@ $linkGap : 3px;
.documentButtonBar {
display: flex;
flex-direction: row;
- gap: 3px;
}
.documentButtonBar-button {
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index df1e6899d..5640e5132 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -27,6 +27,7 @@ import React = require("react");
import { PresBox } from './nodes/trails/PresBox';
import { undoBatch } from '../util/UndoManager';
import { CollectionViewType } from './collections/CollectionView';
+import { Colors } from './global/globalEnums';
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -187,9 +188,9 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
get followLinkButton() {
const targetDoc = this.view0?.props.Document;
return !targetDoc ? (null) : <Tooltip title={
- <div className="dash-tooltip">{"follow primary link on click"}</div>}>
+ <div className="dash-tooltip">{"Set onClick to follow primary link"}</div>}>
<div className="documentButtonBar-icon"
- style={{ color: targetDoc.isLinkButton ? "black" : "white" }}
+ style={{ backgroundColor: targetDoc.isLinkButton ? Colors.LIGHT_BLUE : Colors.DARK_GRAY, color: targetDoc.isLinkButton ? Colors.BLACK : Colors.WHITE }}
onClick={undoBatch(e => this.props.views().map(view => view?.docView?.toggleFollowLink(undefined, false, false)))}>
<FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="hand-point-right" />
</div>
@@ -355,7 +356,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
<div className="documentButtonBar-button">
<DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={true} />
</div>
- {(DocumentLinksButton.StartLink || Doc.UserDoc()["documentLinksButton-fullMenu"]) && DocumentLinksButton.StartLink != doc ? <div className="documentButtonBar-button">
+ {(DocumentLinksButton.StartLink || Doc.UserDoc()["documentLinksButton-fullMenu"]) && DocumentLinksButton.StartLink !== doc ? <div className="documentButtonBar-button">
<DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={false} />
</div> : (null)}
{/*!Doc.UserDoc()["documentLinksButton-fullMenu"] ? (null) : <div className="documentButtonBar-button">
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 316f63240..d34efd01a 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -51,6 +51,7 @@ $linkGap : 3px;
pointer-events: auto;
background: $medium-gray;
opacity: 0.1;
+
&:hover {
opacity: 1;
}
@@ -94,6 +95,7 @@ $linkGap : 3px;
position: absolute;
}
}
+
.documentDecorations-rotation {
background: transparent;
right: -15;
@@ -189,6 +191,7 @@ $linkGap : 3px;
margin-left: 5px;
height: 22px;
position: absolute;
+
.documentDecorations-titleSpan {
width: 100%;
border-radius: 8px;
@@ -263,7 +266,7 @@ $linkGap : 3px;
}
.link-button-container {
- border-radius: 10px;
+ border-radius: 13px;
width: max-content;
height: auto;
display: flex;
@@ -338,6 +341,7 @@ $linkGap : 3px;
.documentdecorations-icon {
margin: 0px;
}
+
.templating-button,
.docDecs-tagButton {
width: 20px;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 0424b4e63..e454be618 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -27,6 +27,7 @@ import { LightboxView } from './LightboxView';
import { DocumentView } from "./nodes/DocumentView";
import React = require("react");
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
+import { DateField } from '../../fields/DateField';
@observer
export class DocumentDecorations extends React.Component<{ boundsLeft: number, boundsTop: number }, { value: string }> {
@@ -367,6 +368,7 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b
dW && (doc._width = actualdW);
dH && (doc._autoHeight = false);
}
+ doc._lastModified = new DateField();
}
const val = this._dragHeights.get(docView.layoutDoc);
if (val) this._dragHeights.set(docView.layoutDoc, { start: val.start, lowest: Math.min(val.lowest, NumCast(docView.layoutDoc._height)) });
diff --git a/src/client/views/EditableView.scss b/src/client/views/EditableView.scss
index 5dc0c1962..1aebedf2e 100644
--- a/src/client/views/EditableView.scss
+++ b/src/client/views/EditableView.scss
@@ -26,4 +26,10 @@
width: 100%;
background: inherit;
pointer-events: all;
-} \ No newline at end of file
+}
+
+.editableView-input:focus {
+ border: none;
+ outline: none;
+}
+ \ No newline at end of file
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index 03d9efff3..83336c180 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -155,7 +155,10 @@ export class EditableView extends React.Component<EditableProps> {
return wasFocused !== this._editing;
}
+
+
renderEditor() {
+ console.log("render editor", this.props.autosuggestProps);
return this.props.autosuggestProps
? <Autosuggest
{...this.props.autosuggestProps.autosuggestProps}
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 6a4f55bef..24f9501ce 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -14,6 +14,7 @@ import { DocUtils } from "../documents/Documents";
import { CurrentUserUtils } from "../util/CurrentUserUtils";
import { InteractionUtils } from "../util/InteractionUtils";
import { Scripting } from "../util/Scripting";
+import { SelectionManager } from "../util/SelectionManager";
import { Transform } from "../util/Transform";
import { CollectionFreeFormViewChrome } from "./collections/CollectionMenu";
import "./GestureOverlay.scss";
@@ -23,7 +24,6 @@ import { RadialMenu } from "./nodes/RadialMenu";
import HorizontalPalette from "./Palette";
import { Touchable } from "./Touchable";
import TouchScrollableMenu, { TouchScrollableMenuItem } from "./TouchScrollableMenu";
-import { SelectionManager } from "../util/SelectionManager";
@observer
export class GestureOverlay extends Touchable {
@@ -588,8 +588,9 @@ export class GestureOverlay extends Touchable {
this.makePolygon(this.InkShape, false);
this.dispatchGesture(GestureUtils.Gestures.Stroke);
this._points = [];
- if (!CollectionFreeFormViewChrome.Instance._keepPrimitiveMode) {
+ if (!CollectionFreeFormViewChrome.Instance?._keepPrimitiveMode) {
this.InkShape = "";
+ Doc.UserDoc().activeInkTool = InkTool.None;
}
}
// if we're not drawing in a toolglass try to recognize as gesture
@@ -726,39 +727,36 @@ export class GestureOverlay extends Touchable {
break;
case "circle":
-
+ // Approximation of a circle using 4 Bézier curves in which the constant "c" reduces the maximum radial drift to 0.019608%,
+ // making the curves indistinguishable from a circle.
+ // Source: https://spencermortensen.com/articles/bezier-circle/
+ const c = 0.551915024494;
const centerX = (Math.max(left, right) + Math.min(left, right)) / 2;
const centerY = (Math.max(top, bottom) + Math.min(top, bottom)) / 2;
const radius = Math.max(centerX - Math.min(left, right), centerY - Math.min(top, bottom));
- if (centerX - Math.min(left, right) < centerY - Math.min(top, bottom)) {
- for (var y = Math.min(top, bottom); y < Math.max(top, bottom); y++) {
- const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX;
- this._points.push({ X: x, Y: y });
- }
- for (var y = Math.max(top, bottom); y > Math.min(top, bottom); y--) {
- const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX;
- const newX = centerX - (x - centerX);
- this._points.push({ X: newX, Y: y });
- }
- this._points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(top, bottom) - centerY), 2))) + centerX, Y: Math.min(top, bottom) });
- this._points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(top, bottom) - centerY), 2))) + centerX, Y: Math.min(top, bottom) - 1 });
-
+ // Dividing the circle into four equal sections, and fitting each section to a cubic Bézier curve.
+ this._points.push({ X: centerX - radius, Y: centerY });
+ this._points.push({ X: centerX - radius, Y: centerY + (c * radius) });
+ this._points.push({ X: centerX - (c * radius), Y: centerY + radius });
+ this._points.push({ X: centerX, Y: centerY + radius });
+
+ this._points.push({ X: centerX, Y: centerY + radius });
+ this._points.push({ X: centerX + (c * radius), Y: centerY + radius });
+ this._points.push({ X: centerX + radius, Y: centerY + (c * radius) });
+ this._points.push({ X: centerX + radius, Y: centerY });
+
+ this._points.push({ X: centerX + radius, Y: centerY });
+ this._points.push({ X: centerX + radius, Y: centerY - (c * radius) });
+ this._points.push({ X: centerX + (c * radius), Y: centerY - radius });
+ this._points.push({ X: centerX, Y: centerY - radius });
+
+ this._points.push({ X: centerX, Y: centerY - radius });
+ this._points.push({ X: centerX - (c * radius), Y: centerY - radius });
+ this._points.push({ X: centerX - radius, Y: centerY - (c * radius) });
+ this._points.push({ X: centerX - radius, Y: centerY });
- } else {
- for (var x = Math.min(left, right); x < Math.max(left, right); x++) {
- const y = Math.sqrt(Math.pow(radius, 2) - (Math.pow((x - centerX), 2))) + centerY;
- this._points.push({ X: x, Y: y });
- }
- for (var x = Math.max(left, right); x > Math.min(left, right); x--) {
- const y = Math.sqrt(Math.pow(radius, 2) - (Math.pow((x - centerX), 2))) + centerY;
- const newY = centerY - (y - centerY);
- this._points.push({ X: x, Y: newY });
- }
- this._points.push({ X: Math.min(left, right), Y: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(left, right) - centerX), 2))) + centerY });
- this._points.push({ X: Math.min(left, right), Y: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(left, right) - centerX), 2))) + centerY - 1 });
-
- }
break;
+
case "line":
if (Math.abs(firstx - lastx) < 20) {
lastx = firstx;
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 76eb4c142..0127d3080 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -27,7 +27,6 @@ import { LightboxView } from "./LightboxView";
import { MainView } from "./MainView";
import { DocumentLinksButton } from "./nodes/DocumentLinksButton";
import { AnchorMenu } from "./pdf/AnchorMenu";
-import { SearchBox } from "./search/SearchBox";
import { CurrentUserUtils } from "../util/CurrentUserUtils";
import { SettingsManager } from "../util/SettingsManager";
@@ -223,8 +222,11 @@ export class KeyManager {
PromiseValue(Cast(Doc.UserDoc()["tabs-button-tools"], Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv }));
break;
case "f":
- SearchBox.Instance._searchFullDB = "My Stuff";
- SearchBox.Instance.enter(undefined);
+ const searchBtn = Doc.UserDoc().searchBtn as Doc;
+
+ if (searchBtn) {
+ MainView.Instance.selectMenu(searchBtn);
+ }
break;
case "o":
const target = SelectionManager.Views()[0];
diff --git a/src/client/views/InkControls.tsx b/src/client/views/InkControls.tsx
index 23f22c774..6213a4075 100644
--- a/src/client/views/InkControls.tsx
+++ b/src/client/views/InkControls.tsx
@@ -6,8 +6,13 @@ import { setupMoveUpEvents, emptyFunction } from "../../Utils";
import { UndoManager } from "../util/UndoManager";
import { ControlPoint, InkData, PointData } from "../../fields/InkField";
import { Transform } from "../util/Transform";
+import { Colors } from "./global/globalEnums";
+import { Doc } from "../../fields/Doc";
+import { listSpec } from "../../fields/Schema";
+import { Cast } from "../../fields/Types";
export interface InkControlProps {
+ inkDoc: Doc;
data: InkData;
addedPoints: PointData[];
format: number[];
@@ -30,19 +35,20 @@ export class InkControls extends React.Component<InkControlProps> {
const controlUndo = UndoManager.StartBatch("DocDecs set radius");
const screenScale = this.props.ScreenToLocalTransform().Scale;
const order = controlIndex % 4;
- const handleIndexA = order === 2 ? controlIndex - 1 : controlIndex - 2;
+ const handleIndexA = order === 2 ? controlIndex - 1 : controlIndex - 2;
const handleIndexB = order === 2 ? controlIndex + 2 : controlIndex + 1;
+ const brokenIndices = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number"));
setupMoveUpEvents(this, e,
(e: PointerEvent, down: number[], delta: number[]) => {
InkStrokeProperties.Instance?.moveControl(-delta[0] * screenScale, -delta[1] * screenScale, controlIndex);
return false;
},
- () => controlUndo?.end(),
- emptyFunction);
- // action((e: PointerEvent, doubleTap: boolean | undefined) =>
- // { if (doubleTap && InkStrokeProperties.Instance?._brokenIndices.includes(controlIndex)) {
- // InkStrokeProperties.Instance?.snapHandleTangent(controlIndex, handleIndexA, handleIndexB);
- // }}));
+ () => controlUndo?.end(),
+ action((e: PointerEvent, doubleTap: boolean | undefined) => {
+ if (doubleTap && brokenIndices && brokenIndices.includes(controlIndex)) {
+ InkStrokeProperties.Instance?.snapHandleTangent(controlIndex, handleIndexA, handleIndexB);
+ }
+ }));
}
}
@@ -75,7 +81,7 @@ export class InkControls extends React.Component<InkControlProps> {
@action onLeaveControl = () => { this._overControl = -1; };
@action onEnterAddPoint = (i: number) => { this._overAddPoint = i; };
@action onLeaveAddPoint = () => { this._overAddPoint = -1; };
-
+
render() {
const formatInstance = InkStrokeProperties.Instance;
if (!formatInstance) return (null);
@@ -90,41 +96,42 @@ export class InkControls extends React.Component<InkControlProps> {
}
}
const addedPoints = this.props.addedPoints;
- const [left, top, scaleX, scaleY, strokeWidth, dotsize] = this.props.format;
+ const [left, top, scaleX, scaleY, strokeWidth] = this.props.format;
return (
<>
{addedPoints.map((pts, i) =>
<svg height="10" width="10" key={`add${i}`}>
- <circle
- cx={(pts.X - left - strokeWidth / 2) * scaleX + strokeWidth / 2}
- cy={(pts.Y - top - strokeWidth / 2) * scaleY + strokeWidth / 2}
- r={strokeWidth / 2}
- stroke={this._overAddPoint === i ? "#1F85DE" : "transparent"}
- strokeWidth={dotsize / 4} fill={this._overAddPoint === i ? "#1F85DE" : "transparent"}
- onPointerDown={() => { formatInstance?.addPoints(pts.X, pts.Y, addedPoints, i, controlPoints); }}
- onMouseEnter={() => this.onEnterAddPoint(i)}
- onMouseLeave={this.onLeaveAddPoint}
- pointerEvents="all"
+ <circle
+ cx={(pts.X - left - strokeWidth / 2) * scaleX + strokeWidth / 2}
+ cy={(pts.Y - top - strokeWidth / 2) * scaleY + strokeWidth / 2}
+ r={strokeWidth / 1.5}
+ stroke={this._overAddPoint === i ? Colors.MEDIUM_BLUE : "transparent"}
+ strokeWidth={0} fill={this._overAddPoint === i ? Colors.MEDIUM_BLUE : "transparent"}
+ onPointerDown={() => { formatInstance?.addPoints(pts.X, pts.Y, addedPoints, i, controlPoints); }}
+ onMouseEnter={() => this.onEnterAddPoint(i)}
+ onMouseLeave={this.onLeaveAddPoint}
+ pointerEvents="all"
cursor="all-scroll"
/>
</svg>
)}
{controlPoints.map((control, i) =>
<svg height="10" width="10" key={`ctrl${i}`}>
- <rect
- x={(control.X - left - strokeWidth / 2) * scaleX}
- y={(control.Y - top - strokeWidth / 2) * scaleY}
- height={this._overControl === i ? strokeWidth * 1.5 : strokeWidth}
- width={this._overControl === i ? strokeWidth * 1.5 : strokeWidth}
- strokeWidth={strokeWidth / 6} stroke="#1F85DE"
- fill={formatInstance?._currentPoint === control.I ? "#1F85DE" : "white"}
- onPointerDown={(e) => {
- this.changeCurrPoint(control.I);
- this.onControlDown(e, control.I); }}
- onMouseEnter={() => this.onEnterControl(i)}
- onMouseLeave={this.onLeaveControl}
- pointerEvents="all"
+ <rect
+ x={(control.X - left - strokeWidth / 2) * scaleX}
+ y={(control.Y - top - strokeWidth / 2) * scaleY}
+ height={this._overControl === i ? strokeWidth * 1.5 : strokeWidth}
+ width={this._overControl === i ? strokeWidth * 1.5 : strokeWidth}
+ strokeWidth={strokeWidth / 6} stroke={Colors.MEDIUM_BLUE}
+ fill={formatInstance?._currentPoint === control.I ? Colors.MEDIUM_BLUE : Colors.WHITE}
+ onPointerDown={(e) => {
+ this.changeCurrPoint(control.I);
+ this.onControlDown(e, control.I);
+ }}
+ onMouseEnter={() => this.onEnterControl(i)}
+ onMouseLeave={this.onLeaveControl}
+ pointerEvents="all"
cursor="default"
/>
</svg>
diff --git a/src/client/views/InkHandles.tsx b/src/client/views/InkHandles.tsx
index 28b6dd820..0b24c3c32 100644
--- a/src/client/views/InkHandles.tsx
+++ b/src/client/views/InkHandles.tsx
@@ -10,10 +10,13 @@ import { Doc } from "../../fields/Doc";
import { listSpec } from "../../fields/Schema";
import { List } from "../../fields/List";
import { Cast } from "../../fields/Types";
+import { Colors } from "./global/globalEnums";
+import { GestureOverlay } from "./GestureOverlay";
export interface InkHandlesProps {
inkDoc: Doc;
data: InkData;
+ shape?: string;
format: number[];
ScreenToLocalTransform: () => Transform;
}
@@ -50,6 +53,7 @@ export class InkHandles extends React.Component<InkHandlesProps> {
onBreakTangent = (e: KeyboardEvent, controlIndex: number) => {
const doc: Doc = this.props.inkDoc;
if (["Alt"].includes(e.key)) {
+ e.stopPropagation();
if (doc) {
const brokenIndices = Cast(doc.brokenInkIndices, listSpec("number")) || new List;
if (brokenIndices && !brokenIndices.includes(controlIndex)) {
@@ -66,6 +70,7 @@ export class InkHandles extends React.Component<InkHandlesProps> {
// Accessing the current ink's data and extracting all handle points and handle lines.
const data = this.props.data;
+ const shape = this.props.shape;
const handlePoints: HandlePoint[] = [];
const handleLines: HandleLine[] = [];
if (data.length >= 4) {
@@ -80,7 +85,7 @@ export class InkHandles extends React.Component<InkHandlesProps> {
handleLines.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 });
}
}
- const [left, top, scaleX, scaleY, strokeWidth, dotsize] = this.props.format;
+ const [left, top, scaleX, scaleY, strokeWidth] = this.props.format;
return (
<>
@@ -91,7 +96,7 @@ export class InkHandles extends React.Component<InkHandlesProps> {
cy={(pts.Y - top - strokeWidth / 2) * scaleY + strokeWidth / 2}
r={strokeWidth / 2}
strokeWidth={0}
- fill="#1F85DE"
+ fill={Colors.MEDIUM_BLUE}
onPointerDown={(e) => this.onHandleDown(e, pts.I)}
pointerEvents="all"
cursor="default"
@@ -104,16 +109,16 @@ export class InkHandles extends React.Component<InkHandlesProps> {
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="#1F85DE"
- strokeWidth={dotsize / 8}
+ stroke={Colors.MEDIUM_BLUE}
+ strokeWidth={strokeWidth / 4}
display={(pts.dot1 === formatInstance._currentPoint || pts.dot2 === formatInstance._currentPoint) ? "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="#1F85DE"
- strokeWidth={dotsize / 8}
+ stroke={Colors.MEDIUM_BLUE}
+ strokeWidth={strokeWidth / 4}
display={(pts.dot1 === formatInstance._currentPoint || pts.dot2 === formatInstance._currentPoint) ? "inherit" : "none"} />
</svg>)}
</>
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 1a3585f3e..d527b2a05 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -35,7 +35,7 @@ export class InkStrokeProperties {
* @param func The inputted function.
* @param requireCurrPoint Indicates whether the current selected point is needed.
*/
- applyFunction = (func: (doc: Doc, ink: InkData, ptsXscale: number, ptsYscale: number) => { X: number, Y: number }[] | undefined, requireCurrPoint: boolean = false) => {
+ applyFunction = (func: (doc: Doc, ink: InkData, ptsXscale: number, ptsYscale: number) => { X: number, Y: number }[] | undefined, requireCurrPoint: boolean = false) => {
var appliedFunc = false;
this.selectedInk?.forEach(action(inkView => {
if (this.selectedInk?.length === 1 && (!requireCurrPoint || this._currentPoint !== -1)) {
@@ -91,16 +91,16 @@ export class InkStrokeProperties {
});
}
}
- if (end === 0) end = points.length-1;
+ if (end === 0) end = points.length - 1;
// Index of new control point with regards to the ink data.
const newIndex = Math.floor(counter / 2) * 4 + 2;
// Creating new ink data with the new control point and handle points inputted.
for (let i = 0; i < ink.length; i++) {
- if (i === newIndex) {
- const [handleA, handleB] = this.getNewHandlePoints(points.slice(start, index+1), points.slice(index, end), newControl);
+ if (i === newIndex) {
+ const [handleA, handleB] = this.getNewHandlePoints(points.slice(start, index + 1), points.slice(index, end), newControl);
newPoints.push(handleA, newControl, newControl, handleB);
// Adjusting the magnitude of the left handle line of the right neighboring control point.
- const [rightControl, rightHandle] = [points[end], ink[i]];
+ const [rightControl, rightHandle] = [points[end], ink[i]];
const scaledVector = this.getScaledHandlePoint(false, start, end, index, rightControl, rightHandle);
rightHandle && newPoints.push({ X: rightControl.X - scaledVector.X, Y: rightControl.Y - scaledVector.Y });
} else if (i === newIndex - 1) {
@@ -111,14 +111,14 @@ export class InkStrokeProperties {
} else {
ink[i] && newPoints.push({ X: ink[i].X, Y: ink[i].Y });
}
-
+
}
let brokenIndices = Cast(doc.brokenInkIndices, listSpec("number"));
// Updating the indices of the control points whose handle tangency has been broken.
if (brokenIndices) {
- brokenIndices = new List(brokenIndices.map((control) => {
+ brokenIndices = new List(brokenIndices.map((control) => {
if (control >= newIndex) {
- return control + 4;
+ return control + 4;
} else {
return control;
}
@@ -158,7 +158,7 @@ export class InkStrokeProperties {
getNewHandlePoints = (C: PointData[], D: PointData[], newControl: PointData) => {
const [m, n] = [C.length, D.length];
let handleSizeA = Math.sqrt((Math.pow(newControl.X - C[0].X, 2)) + (Math.pow(newControl.Y - C[0].Y, 2)));
- let handleSizeB = Math.sqrt((Math.pow(D[n-1].X - newControl.X, 2)) + (Math.pow(D[n-1].Y - newControl.Y, 2)));
+ let handleSizeB = Math.sqrt((Math.pow(D[n - 1].X - newControl.X, 2)) + (Math.pow(D[n - 1].Y - newControl.Y, 2)));
// Scaling adjustments to improve the ratio between the magnitudes of the two handle lines.
// (Ensures that the new point added doesn't augment the inital shape of the curve much).
if (handleSizeA < 75 && handleSizeB < 75) {
@@ -173,7 +173,7 @@ export class InkStrokeProperties {
handleSizeB *= 2;
}
// Finding the last leg of the derivative curve of C.
- const dC = { X: (handleSizeA / n) * (C[m-1].X - C[m-2].X), Y: (handleSizeA / n) * (C[m-1].Y - C[m-2].Y) };
+ const dC = { X: (handleSizeA / n) * (C[m - 1].X - C[m - 2].X), Y: (handleSizeA / n) * (C[m - 1].Y - C[m - 2].Y) };
// Finding the first leg of the derivative curve of D.
const dD = { X: (handleSizeB / m) * (D[1].X - D[0].X), Y: (handleSizeB / m) * (D[1].Y - D[0].Y) };
const handleA = { X: newControl.X - dC.X, Y: newControl.Y - dC.Y };
@@ -257,15 +257,29 @@ export class InkStrokeProperties {
return newPoints;
})
+ /**
+ * Snaps a control point with broken tangency back to synced rotation.
+ * @param handleIndexA The handle point that retains its current position.
+ * @param handleIndexB The handle point that is rotated to be 180 degrees from its opposite.
+ */
snapHandleTangent = (controlIndex: number, handleIndexA: number, handleIndexB: number) => {
this.applyFunction((doc: Doc, ink: InkData) => {
- // doc.brokenIndices.splice(this._brokenIndices.indexOf(controlIndex), 1);
- const [controlPoint, handleA, handleB] = [ink[controlIndex], ink[handleIndexA], ink[handleIndexB]];
- const oppositeHandleA = this.rotatePoint(handleA, controlPoint, Math.PI);
- const angleDifference = this.angleChange(handleB, oppositeHandleA, controlPoint);
- const newHandleB = this.rotatePoint(handleB, controlPoint, angleDifference);
- ink[handleIndexB] = newHandleB;
- return ink;
+ const brokenIndices = Cast(doc.brokenInkIndices, listSpec("number"));
+ if (brokenIndices) {
+ const newBrokenIndices = new List;
+ brokenIndices.forEach(brokenIndex => {
+ if (brokenIndex !== controlIndex) {
+ newBrokenIndices.push(brokenIndex);
+ }
+ });
+ doc.brokenInkIndices = newBrokenIndices;
+ const [controlPoint, handleA, handleB] = [ink[controlIndex], ink[handleIndexA], ink[handleIndexB]];
+ const oppositeHandleA = this.rotatePoint(handleA, controlPoint, Math.PI);
+ const angleDifference = this.angleChange(handleB, oppositeHandleA, controlPoint);
+ const newHandleB = this.rotatePoint(handleB, controlPoint, angleDifference);
+ ink[handleIndexB] = newHandleB;
+ return ink;
+ }
});
}
@@ -274,13 +288,12 @@ export class InkStrokeProperties {
*/
@action
rotatePoint = (target: PointData, origin: PointData, angle: number) => {
- target.X -= origin.X;
- target.Y -= origin.Y;
- const newX = Math.cos(angle) * target.X - Math.sin(angle) * target.Y;
- const newY = Math.sin(angle) * target.X + Math.cos(angle) * target.Y;
- target.X = newX + origin.X;
- target.Y = newY + origin.Y;
- return target;
+ const rotatedTarget = { X: target.X - origin.X, Y: target.Y - origin.Y };
+ const newX = Math.cos(angle) * rotatedTarget.X - Math.sin(angle) * rotatedTarget.Y;
+ const newY = Math.sin(angle) * rotatedTarget.X + Math.cos(angle) * rotatedTarget.Y;
+ rotatedTarget.X = newX + origin.X;
+ rotatedTarget.Y = newY + origin.Y;
+ return rotatedTarget;
}
/**
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index bd71aaf19..db09849fd 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -5,7 +5,7 @@ import { Doc } from "../../fields/Doc";
import { documentSchema } from "../../fields/documentSchemas";
import { InkData, InkField, InkTool } from "../../fields/InkField";
import { makeInterface } from "../../fields/Schema";
-import { Cast, StrCast } from "../../fields/Types";
+import { Cast, StrCast, NumCast } from "../../fields/Types";
import { TraceMobx } from "../../fields/util";
import { setupMoveUpEvents, emptyFunction, returnFalse } from "../../Utils";
import { CognitiveServices } from "../cognitive_services/CognitiveServices";
@@ -19,6 +19,8 @@ import { InkStrokeProperties } from "./InkStrokeProperties";
import { CurrentUserUtils } from "../util/CurrentUserUtils";
import { InkControls } from "./InkControls";
import { InkHandles } from "./InkHandles";
+import { Colors } from "./global/globalEnums";
+import { GestureOverlay } from "./GestureOverlay";
type InkDocument = makeInterface<[typeof documentSchema]>;
const InkDocument = makeInterface(documentSchema);
@@ -43,14 +45,13 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], [data]);
}
- @action
- public static toggleMask = (inkDoc: Doc) => {
+ public static toggleMask = action((inkDoc: Doc) => {
inkDoc.isInkMask = !inkDoc.isInkMask;
inkDoc._backgroundColor = inkDoc.isInkMask ? "rgba(0,0,0,0.7)" : undefined;
inkDoc.mixBlendMode = inkDoc.isInkMask ? "hard-light" : undefined;
inkDoc.color = "#9b9b9bff";
inkDoc._stayInCollection = inkDoc.isInkMask ? true : undefined;
- }
+ });
/**
* Handles the movement of the entire ink object when the user clicks and drags.
@@ -64,9 +65,22 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
}
}
+ /**
+ * Ensures the ink controls and handles aren't rendered when the current ink stroke is reselected.
+ */
+ @action
+ toggleControlButton = () => {
+ if (!this.props.isSelected() && this._properties) {
+ this._properties._controlButton = false;
+ }
+ }
+
render() {
TraceMobx();
+ this.toggleControlButton();
// Extracting the ink data and formatting information of the current ink stroke.
+ // console.log(InkingStroke.InkShape);
+ const InkShape = GestureOverlay.Instance.InkShape;
const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? [];
const inkDoc: Doc = this.layoutDoc;
const strokeWidth = Number(this.layoutDoc.strokeWidth);
@@ -86,32 +100,17 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
const dotsize = Math.max(width * scaleX, height * scaleY) / 40;
// Visually renders the polygonal line made by the user.
- const inkLine = InteractionUtils.CreatePolyline(data, left, top, strokeColor, strokeWidth, strokeWidth,
- StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"),
- StrCast(this.layoutDoc.strokeStartMarker), StrCast(this.layoutDoc.strokeEndMarker),
- StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none",
- this.props.isSelected() && strokeWidth <= 5 && lineBottom - lineTop > 1 && lineRight - lineLeft > 1,
- false);
+ const inkLine = InteractionUtils.CreatePolyline(data, left, top, strokeColor, strokeWidth, strokeWidth, StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"), StrCast(this.layoutDoc.strokeStartMarker),
+ StrCast(this.layoutDoc.strokeEndMarker), StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", this.props.isSelected() && strokeWidth <= 5 && lineBottom - lineTop > 1 && lineRight - lineLeft > 1, false);
// Thin blue line indicating that the current ink stroke is selected.
- const selectedLine = InteractionUtils.CreatePolyline(
- data, lineLeft - strokeWidth * 3, lineTop - strokeWidth * 3, "#1F85DE", strokeWidth / 6,
- strokeWidth / 6, StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"),
- StrCast(this.layoutDoc.strokeStartMarker), StrCast(this.layoutDoc.strokeEndMarker),
- StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none",
- this.props.isSelected() && strokeWidth <= 5 && lineBottom - lineTop > 1 && lineRight - lineLeft > 1,
- false);
+ const selectedLine = InteractionUtils.CreatePolyline(data, left, top, Colors.MEDIUM_BLUE, strokeWidth, strokeWidth / 6, StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"),
+ StrCast(this.layoutDoc.strokeStartMarker), StrCast(this.layoutDoc.strokeEndMarker), StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", this.props.isSelected() && strokeWidth <= 5 && lineBottom - lineTop > 1 && lineRight - lineLeft > 1, false);
// Invisible polygonal line that enables the ink to be selected by the user.
- const clickableLine = InteractionUtils.CreatePolyline(data, left, top,
- this.props.isSelected() && strokeWidth > 5 ? strokeColor : "transparent", strokeWidth,
- strokeWidth + 15, StrCast(this.layoutDoc.strokeBezier),
- StrCast(this.layoutDoc.fillColor, "none"), "none", "none", undefined, scaleX, scaleY, "",
- this.props.layerProvider?.(this.props.Document) === false ? "none" : "visiblepainted", false, true);
+ const clickableLine = InteractionUtils.CreatePolyline(data, left, top, "transparent", strokeWidth, strokeWidth + 15, StrCast(this.layoutDoc.strokeBezier),
+ StrCast(this.layoutDoc.fillColor, "none"), "none", "none", undefined, scaleX, scaleY, "", this.props.layerProvider?.(this.props.Document) === false ? "none" : "visiblepainted", false, true);
// Set of points rendered upon the ink that can be added if a user clicks on one.
- const addedPoints = InteractionUtils.CreatePoints(data, left, top, strokeColor, strokeWidth, strokeWidth,
- StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"),
- StrCast(this.layoutDoc.strokeStartMarker), StrCast(this.layoutDoc.strokeEndMarker),
- StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none",
- this.props.isSelected() && strokeWidth <= 5, false);
+ const addedPoints = InteractionUtils.CreatePoints(data, left, top, strokeColor, strokeWidth, strokeWidth, StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"), StrCast(this.layoutDoc.strokeStartMarker),
+ StrCast(this.layoutDoc.strokeEndMarker), StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", this.props.isSelected() && strokeWidth <= 5, false);
return (
<svg className="inkStroke"
@@ -138,14 +137,16 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
{this.props.isSelected() && this._properties?._controlButton ?
<>
<InkControls
+ inkDoc={inkDoc}
data={data}
addedPoints={addedPoints}
- format={[left, top, scaleX, scaleY, strokeWidth, dotsize]}
+ format={[left, top, scaleX, scaleY, strokeWidth]}
ScreenToLocalTransform={this.props.ScreenToLocalTransform} />
<InkHandles
inkDoc={inkDoc}
data={data}
- format={[left, top, scaleX, scaleY, strokeWidth, dotsize]}
+ shape={InkShape}
+ format={[left, top, scaleX, scaleY, strokeWidth]}
ScreenToLocalTransform={this.props.ScreenToLocalTransform} />
</> : ""}
</svg>
@@ -167,7 +168,7 @@ export function ActiveFillColor(): string { return StrCast(ActiveInkPen()?.activ
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 ActiveInkWidth(): number { return NumCast(ActiveInkPen()?.activeInkWidth); }
export function ActiveInkBezierApprox(): string { return StrCast(ActiveInkPen()?.activeInkBezier); }
Scripting.addGlobal(function activateBrush(pen: any, width: any, color: any, fill: any, arrowStart: any, arrowEnd: any, dash: any) {
CurrentUserUtils.SelectedTool = pen ? InkTool.Highlighter : InkTool.None;
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 60327f1bf..7553c8118 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -12,6 +12,7 @@ import { LinkManager } from "../util/LinkManager";
AssignAllExtensions();
(async () => {
+ MainView.Live = window.location.search.includes("live");
window.location.search.includes("safe") && CollectionView.SetSafeMode(true);
const info = await CurrentUserUtils.loadCurrentUser();
if (info.id !== "__guest__") {
diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss
index d913f2069..817e45699 100644
--- a/src/client/views/MainView.scss
+++ b/src/client/views/MainView.scss
@@ -265,11 +265,6 @@
height: 35px;
padding: 5px;
}
-
- svg {
- width: 95% !important;
- height: 95%;
- }
}
.mainView-searchPanel {
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 49f4f7a6e..dcdf6b9c3 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -13,7 +13,7 @@ import { List } from '../../fields/List';
import { PrefetchProxy } from '../../fields/Proxy';
import { BoolCast, PromiseValue, StrCast } from '../../fields/Types';
import { TraceMobx } from '../../fields/util';
-import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick, Utils } from '../../Utils';
+import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick, Utils } from '../../Utils';
import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
import { DocServer } from '../DocServer';
import { Docs, DocUtils } from '../documents/Documents';
@@ -29,28 +29,27 @@ import { SettingsManager } from '../util/SettingsManager';
import { SharingManager } from '../util/SharingManager';
import { SnappingManager } from '../util/SnappingManager';
import { Transform } from '../util/Transform';
-import { undoBatch, UndoManager } from '../util/UndoManager';
import { TimelineMenu } from './animationtimeline/TimelineMenu';
import { CollectionDockingView } from './collections/CollectionDockingView';
import { MarqueeOptionsMenu } from './collections/collectionFreeForm/MarqueeOptionsMenu';
import { CollectionLinearView } from './collections/CollectionLinearView';
import { CollectionMenu } from './collections/CollectionMenu';
import { CollectionViewType } from './collections/CollectionView';
+import "./collections/TreeView.scss";
import { ContextMenu } from './ContextMenu';
import { DictationOverlay } from './DictationOverlay';
import { DocumentDecorations } from './DocumentDecorations';
import { GestureOverlay } from './GestureOverlay';
import { MENU_PANEL_WIDTH, SEARCH_PANEL_HEIGHT } from './global/globalCssVariables.scss';
+import { Colors } from './global/globalEnums';
import { KeyManager } from './GlobalKeyHandler';
import { InkStrokeProperties } from './InkStrokeProperties';
import { LightboxView } from './LightboxView';
import { LinkMenu } from './linking/LinkMenu';
import "./MainView.scss";
-import "./collections/TreeView.scss";
import { AudioBox } from './nodes/AudioBox';
import { DocumentLinksButton } from './nodes/DocumentLinksButton';
-import { DocumentView, DocumentViewProps, DocAfterFocusFunc } from './nodes/DocumentView';
-import { FieldViewProps } from './nodes/FieldView';
+import { DocumentView } from './nodes/DocumentView';
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup';
import { LinkDocPreview } from './nodes/LinkDocPreview';
@@ -61,15 +60,14 @@ import { OverlayView } from './OverlayView';
import { AnchorMenu } from './pdf/AnchorMenu';
import { PreviewCursor } from './PreviewCursor';
import { PropertiesView } from './PropertiesView';
-import { SearchBox } from './search/SearchBox';
-import { DefaultStyleProvider, DashboardStyleProvider, StyleProp } from './StyleProvider';
+import { DashboardStyleProvider, DefaultStyleProvider } from './StyleProvider';
import { TopBar } from './topbar/TopBar';
-import { Colors } from './global/globalEnums';
const _global = (window /* browser */ || global /* node */) as any;
@observer
export class MainView extends React.Component {
public static Instance: MainView;
+ public static Live: boolean = false;
private _docBtnRef = React.createRef<HTMLDivElement>();
@observable public LastButton: Opt<Doc>;
@observable private _windowWidth: number = 0;
@@ -105,8 +103,11 @@ export class MainView extends React.Component {
}
new InkStrokeProperties();
this._sidebarContent.proto = undefined;
- DocServer.setPlaygroundFields(["x", "y", "dataTransition", "_autoHeight", "_showSidebar", "_sidebarWidthPercent", "_width", "_height", "_viewTransition", "_panX", "_panY", "_viewScale", "_scrollTop", "hidden", "_curPage", "_viewType", "_chromeHidden"]); // can play with these fields on someone else's
-
+ if (!MainView.Live) {
+ DocServer.setPlaygroundFields(["dataTransition", "treeViewOpen", "autoHeight", "showSidebar", "sidebarWidthPercent", "viewTransition",
+ "panX", "panY", "width", "height", "nativeWidth", "nativeHeight", "text-scrollHeight", "text-height", "hideMinimap",
+ "viewScale", "scrollTop", "hidden", "curPage", "viewType", "chromeHidden", "nativeWidth"]); // can play with these fields on someone else's
+ }
DocServer.GetRefField("rtfProto").then(proto => (proto instanceof Doc) && reaction(() => StrCast(proto.BROADCAST_MESSAGE), msg => msg && alert(msg)));
const tag = document.createElement('script');
@@ -180,12 +181,6 @@ export class MainView extends React.Component {
const targets = document.elementsFromPoint(e.x, e.y);
if (targets.length) {
const targClass = targets[0].className.toString();
- // if (SearchBox.Instance._searchbarOpen || SearchBox.Instance.open) {
- // const check = targets.some((thing) =>
- // (thing.className === "collectionSchemaView-searchContainer" || (thing as any)?.dataset.icon === "filter" ||
- // thing.className === "collectionSchema-header-menuOptions"));
- // !check && SearchBox.Instance.resetSearch(true);
- // }
!targClass.includes("contextMenu") && ContextMenu.Instance.closeMenu();
!["timeline-menu-desc", "timeline-menu-item", "timeline-menu-input"].includes(targClass) && TimelineMenu.Instance.closeMenu();
}
@@ -336,7 +331,7 @@ export class MainView extends React.Component {
PanelHeight={this.getContentsHeight}
renderDepth={0}
isContentActive={returnTrue}
- scriptContext={CollectionDockingView.Instance.props.Document}
+ scriptContext={CollectionDockingView.Instance?.props.Document}
focus={DocUtils.DefaultFocus}
whenChildContentsActiveChanged={emptyFunction}
bringToFront={emptyFunction}
@@ -392,10 +387,6 @@ export class MainView extends React.Component {
case "Settings":
SettingsManager.Instance.open();
break;
- case "Catalog":
- SearchBox.Instance._searchFullDB = "My Stuff";
- SearchBox.Instance.enter(undefined);
- break;
case "Help":
break;
default:
@@ -448,6 +439,7 @@ export class MainView extends React.Component {
this._flyoutWidth = (this._flyoutWidth || 250);
this._sidebarContent.proto = button.target as any;
this.LastButton = button;
+ console.log(button.title);
});
closeFlyout = action(() => {
@@ -535,7 +527,7 @@ export class MainView extends React.Component {
</svg>;
}
- @computed get search() {
+ @computed get topbar() {
TraceMobx();
return <div className="mainView-topbar">
<TopBar />
@@ -589,7 +581,7 @@ export class MainView extends React.Component {
<GroupManager />
<GoogleAuthenticationManager />
<DocumentDecorations boundsLeft={this.leftOffset} boundsTop={this.topOffset} />
- {this.search}
+ {this.topbar}
{LinkDescriptionPopup.descriptionPopup ? <LinkDescriptionPopup /> : null}
{DocumentLinksButton.LinkEditorDocView ? <LinkMenu docView={DocumentLinksButton.LinkEditorDocView} changeFlyout={emptyFunction} /> : (null)}
{LinkDocPreview.LinkInfo ? <LinkDocPreview {...LinkDocPreview.LinkInfo} /> : (null)}
@@ -607,7 +599,7 @@ export class MainView extends React.Component {
{this.snapLines}
<div className="mainView-webRef" ref={this.makeWebRef} />
<LightboxView PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} />
- </div>);
+ </div >);
}
makeWebRef = (ele: HTMLDivElement) => {
diff --git a/src/client/views/MainViewModal.scss b/src/client/views/MainViewModal.scss
index 5f19590b4..03cb5cc84 100644
--- a/src/client/views/MainViewModal.scss
+++ b/src/client/views/MainViewModal.scss
@@ -4,6 +4,7 @@
z-index: 10000;
width: 100%;
height: 100%;
+
.dialogue-box {
position: absolute;
z-index: 1000;
@@ -11,11 +12,8 @@
justify-content: center;
align-self: center;
align-content: center;
- padding: 20px;
- // background: gainsboro;
background: white;
border-radius: 10px;
- border: 0.5px solid black;
box-shadow: #00000044 5px 5px 10px;
transform: translate(-50%, -50%);
top: 50%;
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index 805cda95c..1897572f8 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -1,6 +1,6 @@
import { action, observable, ObservableMap, runInAction } from "mobx";
import { observer } from "mobx-react";
-import { AclAddonly, AclAdmin, AclEdit, DataSym, Doc, Opt } from "../../fields/Doc";
+import { AclAugment, AclAdmin, AclEdit, DataSym, Doc, Opt, AclSelfEdit } from "../../fields/Doc";
import { Id } from "../../fields/FieldSymbols";
import { List } from "../../fields/List";
import { NumCast } from "../../fields/Types";
@@ -43,15 +43,13 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
@observable private _width: number = 0;
@observable private _height: number = 0;
- constructor(props: any) {
- super(props);
- runInAction(() => {
- AnchorMenu.Instance.Status = "marquee";
- AnchorMenu.Instance.fadeOut(true);
- // clear out old marquees and initialize menu for new selection
- Array.from(this.props.savedAnnotations.values()).forEach(v => v.forEach(a => a.remove()));
- this.props.savedAnnotations.clear();
- });
+ @action
+ static clearAnnotations(savedAnnotations: ObservableMap<number, HTMLDivElement[]>) {
+ AnchorMenu.Instance.Status = "marquee";
+ AnchorMenu.Instance.fadeOut(true);
+ // clear out old marquees and initialize menu for new selection
+ Array.from(savedAnnotations.values()).forEach(v => v.forEach(a => a.remove()));
+ savedAnnotations.clear();
}
@action componentDidMount() {
@@ -68,6 +66,8 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
AnchorMenu.Instance.OnClick = (e: PointerEvent) => this.props.anchorMenuClick?.()?.(this.highlight("rgba(173, 216, 230, 0.75)", true));
AnchorMenu.Instance.Highlight = this.highlight;
AnchorMenu.Instance.GetAnchor = (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => this.highlight("rgba(173, 216, 230, 0.75)", true, savedAnnotations);
+ AnchorMenu.Instance.onMakeAnchor = AnchorMenu.Instance.GetAnchor;
+
/**
* This function is used by the AnchorMenu to create an anchor highlight and a new linked text annotation.
* It also initiates a Drag/Drop interaction to place the text annotation.
@@ -156,7 +156,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
highlight = (color: string, isLinkButton: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => {
// creates annotation documents for current highlights
const effectiveAcl = GetEffectiveAcl(this.props.rootDoc[DataSym]);
- const annotationDoc = [AclAddonly, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeAnnotationDocument(color, isLinkButton, savedAnnotations);
+ const annotationDoc = [AclAugment, AclSelfEdit, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeAnnotationDocument(color, isLinkButton, savedAnnotations);
!savedAnnotations && annotationDoc && this.props.addDocument(annotationDoc);
return annotationDoc as Doc ?? undefined;
}
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index 679a4b81e..2b82ef475 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -158,7 +158,7 @@ export class PreviewCursor extends React.Component<{}> {
}
render() {
return (!PreviewCursor._clickPoint || !PreviewCursor.Visible) ? (null) :
- <div className="previewCursor" onBlur={this.onBlur} tabIndex={0} ref={e => e && e.focus()}
+ <div className="previewCursor" onBlur={this.onBlur} tabIndex={0} ref={e => e?.focus()}
style={{ transform: `translate(${PreviewCursor._clickPoint[0]}px, ${PreviewCursor._clickPoint[1]}px)` }}>
I
</div >;
diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss
index fa45a065d..321b83f52 100644
--- a/src/client/views/PropertiesView.scss
+++ b/src/client/views/PropertiesView.scss
@@ -1,6 +1,8 @@
+@import "./global/globalCssVariables.scss";
+
.propertiesView {
height: 100%;
- font-family: "Noto Sans";
+ font-family: "Roboto";
cursor: auto;
overflow-x: hidden;
@@ -28,9 +30,7 @@
color: grey;
cursor: pointer;
}
-
}
-
}
.propertiesView-name {
@@ -80,7 +80,6 @@
padding-bottom: 10px;
padding-top: 8px;
}
-
}
.propertiesView-sharing {
@@ -140,8 +139,6 @@
}
}
-
-
.change-buttons {
display: flex;
@@ -216,7 +213,6 @@
}
}
-
.propertiesView-appearance {
//border-bottom: 1px solid black;
//padding: 8.5px;
@@ -305,7 +301,7 @@
.notify-button-icon {
width: 6px;
height: 6.5px;
- margin-left: .5px;
+ margin-left: 0.5px;
}
&:hover {
@@ -331,7 +327,6 @@
}
.propertiesView-sharingTable {
-
// whatever's commented out - add it back in when adding the buttons
// border: 1.5px solid black;
@@ -347,7 +342,6 @@
width: 92%;
.propertiesView-sharingTable-item {
-
display: flex;
// padding: 5px;
padding: 3px;
@@ -421,7 +415,6 @@
cursor: pointer;
}
}
-
}
.propertiesView-fields-checkbox {
@@ -468,7 +461,6 @@
}
.propertiesView-contexts {
-
.propertiesView-contexts-title {
font-weight: bold;
font-size: 12.5px;
@@ -499,11 +491,9 @@
overflow: hidden;
padding: 10px;
}
-
}
.propertiesView-layout {
-
.propertiesView-layout-title {
font-weight: bold;
font-size: 12.5px;
@@ -534,7 +524,6 @@
overflow: hidden;
padding: 10px;
}
-
}
.propertiesView-presTrails {
@@ -576,7 +565,6 @@
}
.inking-button {
-
display: flex;
.inking-button-points {
@@ -635,7 +623,6 @@
}
.inputBox {
-
margin-top: 10px;
display: flex;
height: 19px;
@@ -658,7 +645,6 @@
}
.inputBox-button {
-
.inputBox-button-up {
background-color: #333333;
height: 9px;
@@ -690,7 +676,6 @@
cursor: pointer;
}
}
-
}
}
@@ -767,7 +752,6 @@
}
.widthAndDash {
-
.width {
.width-top {
display: flex;
@@ -792,13 +776,11 @@
}
.arrows {
-
display: flex;
margin-bottom: 3px;
margin-left: 4px;
.arrows-head {
-
display: flex;
margin-right: 35px;
@@ -827,7 +809,6 @@
}
.dashed {
-
display: flex;
margin-left: 64px;
margin-bottom: 6px;
@@ -844,19 +825,15 @@
}
.editable-title {
- border: none;
padding: 6px;
padding-bottom: 2px;
- background: #eeeeee;
- border-top: 1px solid;
- border-left: 1px solid;
+ border: solid 1px $dark-gray;
&:hover {
- border: 0.75px solid rgb(122, 28, 28);
+ border: 0.75px solid $medium-blue;
}
}
-
.properties-flyout {
grid-column: 2/4;
-} \ No newline at end of file
+}
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 8136edf04..17b137c31 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -5,7 +5,7 @@ import { intersection } from "lodash";
import { action, autorun, computed, Lambda, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
import { ColorState, SketchPicker } from "react-color";
-import { AclAddonly, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, AclUnset, DataSym, Doc, Field, HeightSym, Opt, WidthSym } from "../../fields/Doc";
+import { AclAugment, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, AclUnset, DataSym, Doc, Field, HeightSym, Opt, WidthSym, AclSelfEdit } from "../../fields/Doc";
import { Id } from "../../fields/FieldSymbols";
import { InkField } from "../../fields/InkField";
import { ComputedField } from "../../fields/ScriptField";
@@ -342,12 +342,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
return <select className="permissions-select"
value={permission}
onChange={e => this.changePermissions(e, user)}>
- {dropdownValues.filter(permission => permission !== SharingPermissions.View).map(permission => {
- return (
- <option key={permission} value={permission}>
- {permission === SharingPermissions.Add ? "Can Augment" : permission}
- </option>);
- })}
+ {dropdownValues.filter(permission =>
+ !Doc.UserDoc().noviceMode || ![SharingPermissions.View, SharingPermissions.SelfEdit].includes(permission as any)).map(permission =>
+ <option key={permission} value={permission}> {permission} </option>)}
</select>;
}
@@ -402,7 +399,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
[AclUnset, "None"],
[AclPrivate, SharingPermissions.None],
[AclReadonly, SharingPermissions.View],
- [AclAddonly, SharingPermissions.Add],
+ [AclAugment, SharingPermissions.Augment],
+ [AclSelfEdit, SharingPermissions.SelfEdit],
[AclEdit, SharingPermissions.Edit],
[AclAdmin, SharingPermissions.Admin]
]);
@@ -905,7 +903,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
* If it doesn't exist, it creates it.
*/
checkFilterDoc() {
- if (this.selectedDoc.type === DocumentType.COL && !this.selectedDoc.currentFilter) CurrentUserUtils.setupFilterDocs(this.selectedDoc);
+ if (!this.selectedDoc.currentFilter) CurrentUserUtils.setupFilterDocs(this.selectedDoc);
}
/**
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index c5ae82c61..1f9763d18 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -23,6 +23,8 @@ interface ExtraProps {
layoutDoc: Doc;
rootDoc: Doc;
dataDoc: Doc;
+ showSidebar: boolean;
+ nativeWidth: number;
whenChildContentsActiveChanged: (isActive: boolean) => void;
ScreenToLocalTransform: () => Transform;
sidebarAddDocument: (doc: (Doc | Doc[]), suffix: string) => boolean;
@@ -31,18 +33,22 @@ interface ExtraProps {
}
@observer
export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
+ constructor(props: Readonly<FieldViewProps & ExtraProps>) {
+ super(props);
+ // this.props.dataDoc[this.sidebarKey] = new List<Doc>(); // bcz: can't do this here. it blows away existing things and isn't a robust solution for making sure the field exists -- instead this should happen when the document is created and/or shared
+ }
_stackRef = React.createRef<CollectionStackingView>();
@computed get allHashtags() {
const keys = new Set<string>();
- DocListCast(this.props.rootDoc[this.sidebarKey()]).forEach(doc => SearchBox.documentKeys(doc).forEach(key => keys.add(key)));
+ DocListCast(this.props.rootDoc[this.sidebarKey]).forEach(doc => SearchBox.documentKeys(doc).forEach(key => keys.add(key)));
return Array.from(keys.keys()).filter(key => key[0]).filter(key => !key.startsWith("_") && (key[0] === "#" || key[0] === key[0].toUpperCase())).sort();
}
@computed get allUsers() {
const keys = new Set<string>();
- DocListCast(this.props.rootDoc[this.sidebarKey()]).forEach(doc => keys.add(StrCast(doc.author)));
+ DocListCast(this.props.rootDoc[this.sidebarKey]).forEach(doc => keys.add(StrCast(doc.author)));
return Array.from(keys.keys()).sort();
}
- get filtersKey() { return "_" + this.sidebarKey() + "-docFilters"; }
+ get filtersKey() { return "_" + this.sidebarKey + "-docFilters"; }
anchorMenuClick = (anchor: Doc) => {
const startup = StrListCast(this.props.rootDoc.docFilters).map(filter => filter.split(":")[0]).join(" ");
@@ -59,7 +65,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
this._stackRef.current?.focusDocument(target);
}
makeDocUnfiltered = (doc: Doc) => {
- if (DocListCast(this.props.rootDoc[this.sidebarKey()]).includes(doc)) {
+ if (DocListCast(this.props.rootDoc[this.sidebarKey]).includes(doc)) {
if (this.props.layoutDoc[this.filtersKey]) {
this.props.layoutDoc[this.filtersKey] = new List<string>();
return true;
@@ -67,14 +73,15 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
}
return false;
}
- sidebarKey = () => this.props.fieldKey + "-sidebar";
+
+ get sidebarKey() { return this.props.fieldKey + "-sidebar"; }
filtersHeight = () => 38;
screenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(Doc.NativeWidth(this.props.dataDoc), 0).scale(this.props.scaling?.() || 1);
- panelWidth = () => !this.props.layoutDoc._showSidebar ? 0 : this.props.layoutDoc.type === DocumentType.RTF ? this.props.PanelWidth() : (NumCast(this.props.layoutDoc.nativeWidth) - Doc.NativeWidth(this.props.dataDoc)) * this.props.PanelWidth() / NumCast(this.props.layoutDoc.nativeWidth);
+ panelWidth = () => !this.props.showSidebar ? 0 : this.props.layoutDoc.type === DocumentType.RTF ? this.props.PanelWidth() : (NumCast(this.props.nativeWidth) - Doc.NativeWidth(this.props.dataDoc)) * this.props.PanelWidth() / NumCast(this.props.nativeWidth);
panelHeight = () => this.props.PanelHeight() - this.filtersHeight();
- addDocument = (doc: Doc | Doc[]) => this.props.sidebarAddDocument(doc, this.sidebarKey());
- moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.props.moveDocument(doc, targetCollection, addDocument, this.sidebarKey());
- removeDocument = (doc: Doc | Doc[]) => this.props.removeDocument(doc, this.sidebarKey());
+ addDocument = (doc: Doc | Doc[]) => this.props.sidebarAddDocument(doc, this.sidebarKey);
+ moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.props.moveDocument(doc, targetCollection, addDocument, this.sidebarKey);
+ removeDocument = (doc: Doc | Doc[]) => this.props.removeDocument(doc, this.sidebarKey);
docFilters = () => [...StrListCast(this.props.layoutDoc._docFilters), ...StrListCast(this.props.layoutDoc[this.filtersKey])];
sidebarStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps | DocumentViewProps>, property: string) => {
@@ -87,18 +94,18 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
const renderTag = (tag: string) => {
const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`${tag}:${tag}:check`);
return <div key={tag} className={`sidebarAnnos-filterTag${active ? "-active" : ""}`}
- onClick={e => Doc.setDocFilter(this.props.rootDoc, tag, tag, "check", true, this.sidebarKey(), e.shiftKey)}>
+ onClick={e => Doc.setDocFilter(this.props.rootDoc, tag, tag, "check", true, this.sidebarKey, e.shiftKey)}>
{tag}
</div>;
};
const renderUsers = (user: string) => {
const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`author:${user}:check`);
return <div key={user} className={`sidebarAnnos-filterUser${active ? "-active" : ""}`}
- onClick={e => Doc.setDocFilter(this.props.rootDoc, "author", user, "check", true, this.sidebarKey(), e.shiftKey)}>
+ onClick={e => Doc.setDocFilter(this.props.rootDoc, "author", user, "check", true, this.sidebarKey, e.shiftKey)}>
{user}
</div>;
};
- return !this.props.layoutDoc._showSidebar ? (null) :
+ return !this.props.showSidebar ? (null) :
<div style={{
position: "absolute", pointerEvents: this.props.isContentActive() ? "all" : undefined, top: 0, right: 0,
background: this.props.styleProvider?.(this.props.rootDoc, this.props, StyleProp.WidgetColor),
@@ -118,7 +125,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
PanelWidth={this.panelWidth}
styleProvider={this.sidebarStyleProvider}
docFilters={this.docFilters}
- scaleField={this.sidebarKey() + "-scale"}
+ scaleField={this.sidebarKey + "-scale"}
setHeight={(height) => this.props.setHeight(height + this.filtersHeight())}
isAnnotationOverlay={false}
select={emptyFunction}
@@ -132,7 +139,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
ScreenToLocalTransform={this.screenToLocalTransform}
renderDepth={this.props.renderDepth + 1}
viewType={CollectionViewType.Stacking}
- fieldKey={this.sidebarKey()}
+ fieldKey={this.sidebarKey}
pointerEvents={"all"}
/>
</div>
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index c9e532745..470ae7c77 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -21,6 +21,7 @@ import "./nodes/FilterBox.scss";
import "./StyleProvider.scss";
import React = require("react");
import Color = require('color');
+import { lightOrDark } from '../../Utils';
export enum StyleLayers {
Background = "background"
@@ -96,16 +97,12 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
!Doc.IsSystem(doc) && doc.type === DocumentType.RTF ?
(doc.author === Doc.CurrentUserEmail ? StrCast(Doc.UserDoc().showTitle) : "author;creationDate") : "") || "";
case StyleProp.Color:
+ if (MainView.Instance.LastButton === doc) return Colors.DARK_GRAY;
const docColor: Opt<string> = StrCast(doc?.[fieldKey + "color"], StrCast(doc?._color));
if (docColor) return docColor;
const backColor = backgroundCol();
if (!backColor) return undefined;
- const nonAlphaColor = backColor.startsWith("#") ? (backColor as string).substring(0, 7) :
- backColor.startsWith("rgba") ? backColor.replace(/,.[^,]*\)/, ")").replace("rgba", "rgb") : backColor;
- const col = Color(nonAlphaColor).rgb();
- const colsum = (col.red() + col.green() + col.blue());
- if (colsum / col.alpha() > 400 || col.alpha() < 0.25) return Colors.DARK_GRAY;
- return Colors.WHITE;
+ return lightOrDark(backColor)
case StyleProp.Hidden: return BoolCast(doc?._hidden);
case StyleProp.BorderRounding: return StrCast(doc?.[fieldKey + "borderRounding"], doc?._viewType === CollectionViewType.Pile ? "50%" : "");
case StyleProp.TitleHeight: return 15;
@@ -114,8 +111,8 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
case StyleProp.HeaderMargin: return ([CollectionViewType.Stacking, CollectionViewType.Masonry].includes(doc?._viewType as any) ||
doc?.type === DocumentType.RTF) && showTitle() && !StrCast(doc?.showTitle).includes(":hover") ? 15 : 0;
case StyleProp.BackgroundColor: {
+ if (MainView.Instance.LastButton === doc) return Colors.LIGHT_GRAY;
let docColor: Opt<string> = StrCast(doc?.[fieldKey + "backgroundColor"], StrCast(doc?._backgroundColor, isCaption ? "rgba(0,0,0,0.4)" : ""));
- if (MainView.Instance.LastButton === doc) return darkScheme() ? Colors.MEDIUM_GRAY : Colors.LIGHT_GRAY;
switch (doc?.type) {
case DocumentType.PRESELEMENT: docColor = docColor || (darkScheme() ? "" : ""); break;
case DocumentType.PRES: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.WHITE); break;
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index a8471f8e2..cae08e1f4 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -4,7 +4,7 @@ import { action, IReactionDisposer, observable, reaction, runInAction } from "mo
import { observer } from "mobx-react";
import * as ReactDOM from 'react-dom';
import * as GoldenLayout from "../../../client/goldenLayout";
-import { Doc, DocListCast, Opt, DocListCastAsync } from "../../../fields/Doc";
+import { Doc, DocListCast, Opt, DocListCastAsync, DataSym } from "../../../fields/Doc";
import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
import { List } from '../../../fields/List';
@@ -24,6 +24,7 @@ import React = require("react");
import { DocumentType } from '../../documents/DocumentTypes';
import { listSpec } from '../../../fields/Schema';
import { LightboxView } from '../LightboxView';
+import { inheritParentAcls } from '../../../fields/util';
const _global = (window /* browser */ || global /* node */) as any;
@observer
@@ -160,6 +161,18 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) {
}
const instance = CollectionDockingView.Instance;
if (!instance) return false;
+ else {
+ const docList = DocListCast(instance.props.Document[DataSym]["data-all"]);
+ // adds the doc of the newly created tab to the data-all field if it doesn't already include that doc or one of its aliases
+ !docList.includes(document) && !docList.includes(document.aliasOf as Doc) && Doc.AddDocToList(instance.props.Document[DataSym], "data-all", document);
+ // adds an alias of the doc to the data-all field of the layoutdocs of the aliases
+ DocListCast(instance.props.Document[DataSym].aliases).forEach(alias => {
+ const aliasDocList = DocListCast(alias["data-all"]);
+ // if aliasDocList contains the alias, don't do anything
+ // otherwise add the original or an alias depending on whether the doc you're looking at is the current doc or a different alias
+ !DocListCast(document.aliases).some(a => aliasDocList.includes(a)) && Doc.AddDocToList(alias, "data-all", document);//alias !== instance.props.Document ? Doc.MakeAlias(document) : document);
+ });
+ }
const docContentConfig = CollectionDockingView.makeDocumentConfig(document, panelName);
if (!pullSide && stack) {
@@ -381,15 +394,22 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) {
setTimeout(async () => {
const sublists = await DocListCastAsync(this.props.Document[this.props.fieldKey]);
const tabs = sublists && Cast(sublists[0], Doc, null);
- const other = sublists && Cast(sublists[1], Doc, null);
+ // const other = sublists && Cast(sublists[1], Doc, null);
const tabdocs = await DocListCastAsync(tabs?.data);
- const otherdocs = await DocListCastAsync(other?.data);
- tabs && (Doc.GetProto(tabs).data = new List<Doc>(docs));
- const otherSet = new Set<Doc>();
- otherdocs?.filter(doc => !docs.includes(doc)).forEach(doc => otherSet.add(doc));
- tabdocs?.filter(doc => !docs.includes(doc) && doc.type !== DocumentType.KVP).forEach(doc => otherSet.add(doc));
- const vals = Array.from(otherSet.values()).filter(val => val instanceof Doc).map(d => d).filter(d => d.type !== DocumentType.KVP);
- other && (Doc.GetProto(other).data = new List<Doc>(vals));
+ // const otherdocs = await DocListCastAsync(other?.data);
+ if (tabs) {
+ tabs.data = new List<Doc>(docs);
+ // DocListCast(tabs.aliases).forEach(tab => tab !== tabs && (tab.data = new List<Doc>(docs)));
+ }
+ // const otherSet = new Set<Doc>();
+ // otherdocs?.filter(doc => !docs.includes(doc)).forEach(doc => otherSet.add(doc));
+ // tabdocs?.filter(doc => !docs.includes(doc) && doc.type !== DocumentType.KVP).forEach(doc => otherSet.add(doc));
+ // const vals = Array.from(otherSet.values()).filter(val => val instanceof Doc).map(d => d).filter(d => d.type !== DocumentType.KVP);
+ // this.props.Document[DataSym][this.props.fieldKey + "-all"] = new List<Doc>([...docs, ...vals]);
+ // if (other) {
+ // other.data = new List<Doc>(vals);
+ // // DocListCast(other.aliases).forEach(tab => tab !== other && (tab.data = new List<Doc>(vals)));
+ // }
}, 0);
}
@@ -399,7 +419,7 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) {
tab.reactComponents?.forEach((ele: any) => ReactDOM.unmountComponentAtNode(ele));
}
tabCreated = (tab: any) => {
- tab.contentItem.element[0]?.firstChild?.firstChild?.InitTab?.(tab); // have to explicitly initialize tabs that reuse contents from previous abs (ie, when dragging a tab around a new tab is created for the old content)
+ tab.contentItem.element[0]?.firstChild?.firstChild?.InitTab?.(tab); // have to explicitly initialize tabs that reuse contents from previous tabs (ie, when dragging a tab around a new tab is created for the old content)
}
stackCreated = (stack: any) => {
@@ -407,9 +427,11 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) {
if (e.target === stack.header?.element[0] && e.button === 2) {
const emptyPane = CurrentUserUtils.EmptyPane;
emptyPane["dragFactory-count"] = NumCast(emptyPane["dragFactory-count"]) + 1;
- CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([], {
- _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), _fitWidth: true, title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}`
- }), "", stack);
+ const docToAdd = Docs.Create.FreeformDocument([], {
+ _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), _fitWidth: true, title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}`,
+ });
+ this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd);
+ CollectionDockingView.AddSplit(docToAdd, "", stack);
}
});
@@ -430,9 +452,11 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) {
// stack.config.fixed = !stack.config.fixed; // force the stack to have a fixed size
const emptyPane = CurrentUserUtils.EmptyPane;
emptyPane["dragFactory-count"] = NumCast(emptyPane["dragFactory-count"]) + 1;
- CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([], {
+ const docToAdd = Docs.Create.FreeformDocument([], {
_width: this.props.PanelWidth(), _height: this.props.PanelHeight(), _fitWidth: true, title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}`
- }), "", stack);
+ });
+ this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd);
+ CollectionDockingView.AddSplit(docToAdd, "", stack);
}));
}
diff --git a/src/client/views/collections/CollectionMenu.scss b/src/client/views/collections/CollectionMenu.scss
index f04b19ef7..c35f088a6 100644
--- a/src/client/views/collections/CollectionMenu.scss
+++ b/src/client/views/collections/CollectionMenu.scss
@@ -1,628 +1,659 @@
@import "../global/globalCssVariables";
-.collectionMenu-cont {
- position: relative;
- display: inline-flex;
- width: 100%;
- opacity: 0.9;
- z-index: 901;
- transition: top .5s;
- background: $dark-gray;
- color: $white;
- transform-origin: top left;
- top: 0;
- width: 100%;
-
- .recordButtonOutline {
- border-radius: 100%;
- width: 18px;
- height: 18px;
- border: solid 1px $white;
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- .recordButtonInner {
- border-radius: 100%;
- width: 70%;
- height: 70%;
- background: $white;
- }
-
- .collectionMenu {
- display: flex;
- height: 100%;
- overflow: visible;
- z-index: 901;
- border: unset;
-
- .collectionMenu-divider {
- height: 100%;
- margin-left: 3px;
- margin-right: 3px;
- width: 2px;
- background-color: $medium-gray;
- }
-
- .collectionViewBaseChrome {
- display: flex;
- align-items: center;
-
- .collectionViewBaseChrome-viewPicker {
- font-size: $small-text;
- outline-color: $black;
- color: $white;
- border: none;
- background: $dark-gray;
- }
-
- .collectionViewBaseChrome-viewPicker:focus {
- outline: none;
- border: none;
- }
-
- .collectionViewBaseChrome-viewPicker:active {
- outline-color: $black;
- }
-
- .collectionViewBaseChrome-button {
- font-size: $small-text;
- text-transform: uppercase;
- letter-spacing: 2px;
- background: $white;
- color: $pink;
- outline-color: $black;
- border: none;
- padding: 12px 10px 11px 10px;
- margin-left: 10px;
- }
-
- .collectionViewBaseChrome-cmdPicker {
- margin-left: 3px;
- margin-right: 0px;
- font-size: $small-text;
- text-transform: capitalize;
- color: $white;
- border: none;
- background: $dark-gray;
- }
-
- .collectionViewBaseChrome-cmdPicker:focus {
- border: none;
- outline: none;
- }
-
- .commandEntry-outerDiv {
- pointer-events: all;
- background-color: transparent;
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: center;
- height: 100%;
- overflow: hidden;
-
- .commandEntry-drop {
- color: $white;
- width: 30px;
- margin-top: auto;
- margin-bottom: auto;
- }
- }
-
- .commandEntry-outerDiv:hover{
- background-color: $drop-shadow;
-
- .collectionViewBaseChrome-viewPicker,
- .collectionViewBaseChrome-cmdPicker{
- background: $dark-gray;
- }
- }
-
- .collectionViewBaseChrome-collapse {
- transition: all .5s, opacity 0.3s;
- position: absolute;
- 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 {
- align-items: center;
- height: 100%;
- display: flex;
- background: transparent;
- color: $medium-gray;
- justify-content: center;
- }
-
- .collectionViewBaseChrome-viewSpecs {
- margin-left: 5px;
- display: grid;
- border: none;
- border-right: solid $medium-gray 1px;
-
- .collectionViewBaseChrome-filterIcon {
- position: relative;
- display: flex;
- margin: auto;
- background: $dark-gray;
- color: $white;
- width: 30px;
- height: 30px;
- align-items: center;
- justify-content: center;
- border: none;
- border-right: solid $medium-gray 1px;
- }
-
- .collectionViewBaseChrome-viewSpecsInput {
- padding: 12px 10px 11px 10px;
- border: 0px;
- color: $medium-gray;
- text-align: center;
- letter-spacing: 2px;
- outline-color: $black;
- font-size: $small-text;
- background: $white;
- height: 100%;
- width: 75px;
- }
-
- .collectionViewBaseChrome-viewSpecsMenu {
- overflow: hidden;
- transition: height .5s, display .5s;
- position: absolute;
- top: 60px;
- z-index: 100;
- display: flex;
- flex-direction: column;
- background: $white;
- box-shadow: $medium-gray 2px 2px 4px;
-
- .qs-datepicker {
- left: unset;
- right: 0;
- }
-
- .collectionViewBaseChrome-viewSpecsMenu-row {
- display: grid;
- grid-template-columns: 150px 200px 150px;
- margin-top: 10px;
- margin-right: 10px;
-
- .collectionViewBaseChrome-viewSpecsMenu-rowLeft,
- .collectionViewBaseChrome-viewSpecsMenu-rowMiddle,
- .collectionViewBaseChrome-viewSpecsMenu-rowRight {
- font-size: $small-text;
- letter-spacing: 2px;
- color: $medium-gray;
- margin-left: 10px;
- padding: 5px;
- border: none;
- outline-color: $black;
- }
- }
-
- .collectionViewBaseChrome-viewSpecsMenu-lastRow {
- display: grid;
- grid-template-columns: 1fr 1fr 1fr;
- grid-gap: 10px;
- margin: 10px;
- }
- }
- }
- }
-
- .collectionStackingViewChrome-cont,
- .collectionTreeViewChrome-cont,
- .collection3DCarouselViewChrome-cont {
- display: flex;
- justify-content: space-between;
- }
-
- .collectionGridViewChrome-cont {
- display: flex;
- margin-left: 10;
-
- .collectionGridViewChrome-viewPicker {
- font-size: $small-text;
- //text-transform: uppercase;
- //letter-spacing: 2px;
- background: $dark-gray;
- color: $white;
- outline-color: $black;
- color: $white;
- border: none;
- border-right: solid $medium-gray 1px;
- }
-
- .collectionGridViewChrome-viewPicker:active {
- outline-color: $black;
- }
-
- .grid-control {
- align-self: center;
- display: flex;
- flex-direction: row;
- margin-right: 5px;
-
- .grid-icon {
- margin-right: 5px;
- align-self: center;
- }
-
- .flexLabel {
- margin-bottom: 0;
- }
-
- .collectionGridViewChrome-entryBox {
- width: 50%;
- color: $black;
- }
-
- .collectionGridViewChrome-columnButton {
- color: $black;
- }
- }
- }
-
- .collectionStackingViewChrome-sort,
- .collectionTreeViewChrome-sort {
- display: flex;
- align-items: center;
- justify-content: space-between;
-
- .collectionStackingViewChrome-sortIcon,
- .collectionTreeViewChrome-sortIcon {
- transition: transform .5s;
- margin-left: 10px;
- }
- }
-
- button:hover {
- transform: scale(1);
- }
-
-
- .collectionStackingViewChrome-pivotField-cont,
- .collectionTreeViewChrome-pivotField-cont,
- .collection3DCarouselViewChrome-scrollSpeed-cont {
- justify-self: right;
- align-items: center;
- display: flex;
- grid-auto-columns: auto;
- font-size: $small-text;
- letter-spacing: 2px;
-
- .collectionStackingViewChrome-pivotField-label,
- .collectionTreeViewChrome-pivotField-label,
- .collection3DCarouselViewChrome-scrollSpeed-label {
- grid-column: 1;
- margin-right: 7px;
- user-select: none;
- font-family: $sans-serif;
- letter-spacing: normal;
- }
-
- .collectionStackingViewChrome-sortIcon {
- transition: transform .5s;
- grid-column: 3;
- text-align: center;
- display: flex;
- justify-content: center;
- align-items: center;
- cursor: pointer;
- width: 25px;
- height: 25px;
- border-radius: 100%;
- }
-
- .collectionStackingViewChrome-sortIcon:hover {
- background-color: $drop-shadow;
- }
-
- .collectionStackingViewChrome-pivotField,
- .collectionTreeViewChrome-pivotField,
- .collection3DCarouselViewChrome-scrollSpeed {
- color: $white;
- grid-column: 2;
- grid-row: 1;
- width: 90%;
- min-width: 100px;
- display: flex;
- height: 80%;
- border-radius: 7px;
- align-items: center;
- background: $white;
-
- .editable-view-input,
- input,
- .editableView-container-editing-oneLine,
- .editableView-container-editing {
- margin: auto;
- border: 0px;
- color: $light-gray !important;
- text-align: center;
- letter-spacing: 2px;
- outline-color: $black;
- height: 100%;
- }
-
- .react-autosuggest__container {
- margin: 0;
- color: $medium-gray;
- padding: 0px;
- }
- }
- }
-
- .collectionStackingViewChrome-pivotField:hover,
- .collectionTreeViewChrome-pivotField:hover,
- .collection3DCarouselViewChrome-scrollSpeed:hover {
- cursor: text;
- }
-
- }
-}
-
-.collectionMenu-webUrlButtons {
- margin-left: 44;
- background: lightGray;
+.collectionMenu-container {
display: flex;
-}
-
-.webBox-urlEditor {
- position: relative;
- opacity: 0.9;
- z-index: 901;
- transition: top .5s;
-
- .urlEditor {
- display: grid;
- grid-template-columns: 1fr auto;
- padding-bottom: 10px;
- overflow: hidden;
- margin-top: 5px;
- height: 35px;
-
- .editorBase {
- display: flex;
-
- .editor-collapse {
- transition: all .5s, opacity 0.3s;
- position: absolute;
- width: 40px;
- transform-origin: top left;
- }
-
- .switchToText {
- color: $medium-gray;
- }
-
- .switchToText:hover {
- color: $dark-gray;
- }
- }
-
- button:hover {
- transform: scale(1);
- }
- }
-}
-
-.collectionMenu-urlInput {
- padding: 12px 10px 11px 10px;
- border: 0px;
- color: $black;
- font-size: $small-text;
- letter-spacing: 2px;
- outline-color: $black;
- background: $white;
- width: 100%;
- min-width: 350px;
- margin-right: 10px;
- height: 100%;
-}
-
-.collectionFreeFormMenu-cont {
- display: inline-flex;
position: relative;
+ align-content: center;
+ justify-content: space-between;
+ background-color: $dark-gray;
+ height: 35px;
+ border-bottom: $standard-border;
+ padding-right: 5px;
align-items: center;
- height: 100%;
-
- .color-previewI {
- width: 60%;
- top: 80%;
- position: absolute;
- height: 4px;
- }
-
- .color-previewII {
- width: 80%;
- height: 80%;
- margin-left: 10%;
- position: absolute;
- bottom: 5;
- }
-
- .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 {
+ .collectionMenu-hardCodedButton {
cursor: pointer;
- position: relative;
- width: 20;
- height: 30;
- bottom: 0;
- background: $dark-gray;
- display: inline-flex;
- align-items: center;
color: $white;
- }
-
- .backKeyframe {
- svg {
- display: block;
- margin: auto;
- }
- }
-
-
- .numKeyframe {
- flex-direction: column;
- padding-top: 5px;
- }
-
- .fwdKeyframe {
- svg {
- display: block;
- margin: auto;
- }
-
- border-right: solid $medium-gray 1px;
- }
-}
-
-.collectionSchemaViewChrome-cont {
- display: flex;
- font-size: $small-text;
-
- .collectionSchemaViewChrome-toggle {
- display: flex;
- margin-left: 10px;
- }
-
- .collectionSchemaViewChrome-label {
- text-transform: uppercase;
- letter-spacing: 2px;
- margin-right: 5px;
+ width: 25px;
+ height: 25px;
+ padding: 5;
+ text-align: center;
display: flex;
- flex-direction: column;
justify-content: center;
- }
-
- .collectionSchemaViewChrome-toggler {
- width: 100px;
- height: 35px;
- background-color: $black;
+ align-items: center;
position: relative;
- }
-
- .collectionSchemaViewChrome-togglerButton {
- width: 47px;
- height: 30px;
- background-color: $light-gray;
- // position: absolute;
- transition: all 0.5s ease;
- // top: 3px;
- margin-top: 3px;
- color: $medium-gray;
- letter-spacing: 2px;
- text-transform: uppercase;
- display: flex;
- flex-direction: column;
- justify-content: center;
- text-align: center;
+ transition: 0.2s;
+ border-radius: 3px;
- &.on {
- margin-left: 3px;
- }
-
- &.off {
- margin-left: 50px;
+ &:hover {
+ background-color: rgba(0, 0, 0, 0.2);
}
}
}
-
-.commandEntry-outerDiv {
- display: flex;
- flex-direction: column;
- height: 40px;
-}
-
-.commandEntry-inputArea {
- display: flex;
- flex-direction: row;
- width: 150px;
- margin: auto auto auto auto;
-}
-
-.react-autosuggest__container {
- position: relative;
- width: 100%;
- margin-left: 5px;
- margin-right: 5px;
-}
-
-.react-autosuggest__input {
- border: 1px solid $light-gray;
- border-radius: 4px;
- width: 100%;
-}
-
-.react-autosuggest__input--focused {
- outline: none;
-}
-
-.react-autosuggest__input--open {
- border-bottom-left-radius: 0;
- border-bottom-right-radius: 0;
-}
-
-.react-autosuggest__suggestions-container {
- display: none;
-}
-
-.react-autosuggest__suggestions-container--open {
- display: block;
- position: fixed;
- overflow-y: auto;
- max-height: 400px;
- width: 180px;
- border: 1px solid $light-gray;
- background-color: $white;
- font-family: $sans-serif;
- font-weight: 300;
- font-size: $large-header;
- border-bottom-left-radius: 4px;
- border-bottom-right-radius: 4px;
- z-index: 2;
-}
-
-.react-autosuggest__suggestions-list {
- margin: 0;
- padding: 0;
- list-style-type: none;
-}
-
-.react-autosuggest__suggestion {
- cursor: pointer;
- padding: 10px 20px;
-}
-
-.react-autosuggest__suggestion--highlighted {
- background-color: $light-gray;
-} \ No newline at end of file
+// .collectionMenu-cont {
+// position: relative;
+// display: inline-flex;
+// width: 100%;
+// opacity: 0.9;
+// z-index: 901;
+// transition: top .5s;
+// background: $dark-gray;
+// color: $white;
+// transform-origin: top left;
+// top: 0;
+// width: 100%;
+
+// .recordButtonOutline {
+// border-radius: 100%;
+// width: 18px;
+// height: 18px;
+// border: solid 1px $white;
+// display: flex;
+// align-items: center;
+// justify-content: center;
+// }
+
+// .recordButtonInner {
+// border-radius: 100%;
+// width: 70%;
+// height: 70%;
+// background: $white;
+// }
+
+// .collectionMenu {
+// display: flex;
+// height: 100%;
+// overflow: visible;
+// z-index: 901;
+// border: unset;
+
+// .collectionMenu-divider {
+// height: 100%;
+// margin-left: 3px;
+// margin-right: 3px;
+// width: 2px;
+// background-color: $medium-gray;
+// }
+
+// .collectionViewBaseChrome {
+// display: flex;
+// align-items: center;
+
+// .collectionViewBaseChrome-viewPicker {
+// font-size: $small-text;
+// outline-color: $black;
+// color: $white;
+// border: none;
+// background: $dark-gray;
+// }
+
+// .collectionViewBaseChrome-viewPicker:focus {
+// outline: none;
+// border: none;
+// }
+
+// .collectionViewBaseChrome-viewPicker:active {
+// outline-color: $black;
+// }
+
+// .collectionViewBaseChrome-button {
+// font-size: $small-text;
+// text-transform: uppercase;
+// letter-spacing: 2px;
+// background: $white;
+// color: $pink;
+// outline-color: $black;
+// border: none;
+// padding: 12px 10px 11px 10px;
+// margin-left: 10px;
+// }
+
+// .collectionViewBaseChrome-cmdPicker {
+// margin-left: 3px;
+// margin-right: 0px;
+// font-size: $small-text;
+// text-transform: capitalize;
+// color: $white;
+// border: none;
+// background: $dark-gray;
+// }
+
+// .collectionViewBaseChrome-cmdPicker:focus {
+// border: none;
+// outline: none;
+// }
+
+// .commandEntry-outerDiv {
+// pointer-events: all;
+// background-color: transparent;
+// display: flex;
+// flex-direction: row;
+// align-items: center;
+// justify-content: center;
+// height: 100%;
+// overflow: hidden;
+
+// .commandEntry-drop {
+// color: $white;
+// width: 30px;
+// margin-top: auto;
+// margin-bottom: auto;
+// }
+// }
+
+// .commandEntry-outerDiv:hover{
+// background-color: $drop-shadow;
+
+// .collectionViewBaseChrome-viewPicker,
+// .collectionViewBaseChrome-cmdPicker{
+// background: $dark-gray;
+// }
+// }
+
+// .collectionViewBaseChrome-collapse {
+// transition: all .5s, opacity 0.3s;
+// position: absolute;
+// 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 {
+// align-items: center;
+// height: 100%;
+// display: flex;
+// background: transparent;
+// color: $medium-gray;
+// justify-content: center;
+// }
+
+// .collectionViewBaseChrome-viewSpecs {
+// margin-left: 5px;
+// display: grid;
+// border: none;
+// border-right: solid $medium-gray 1px;
+
+// .collectionViewBaseChrome-filterIcon {
+// position: relative;
+// display: flex;
+// margin: auto;
+// background: $dark-gray;
+// color: $white;
+// width: 30px;
+// height: 30px;
+// align-items: center;
+// justify-content: center;
+// border: none;
+// border-right: solid $medium-gray 1px;
+// }
+
+// .collectionViewBaseChrome-viewSpecsInput {
+// padding: 12px 10px 11px 10px;
+// border: 0px;
+// color: $medium-gray;
+// text-align: center;
+// letter-spacing: 2px;
+// outline-color: $black;
+// font-size: $small-text;
+// background: $white;
+// height: 100%;
+// width: 75px;
+// }
+
+// .collectionViewBaseChrome-viewSpecsMenu {
+// overflow: hidden;
+// transition: height .5s, display .5s;
+// position: absolute;
+// top: 60px;
+// z-index: 100;
+// display: flex;
+// flex-direction: column;
+// background: $white;
+// box-shadow: $medium-gray 2px 2px 4px;
+
+// .qs-datepicker {
+// left: unset;
+// right: 0;
+// }
+
+// .collectionViewBaseChrome-viewSpecsMenu-row {
+// display: grid;
+// grid-template-columns: 150px 200px 150px;
+// margin-top: 10px;
+// margin-right: 10px;
+
+// .collectionViewBaseChrome-viewSpecsMenu-rowLeft,
+// .collectionViewBaseChrome-viewSpecsMenu-rowMiddle,
+// .collectionViewBaseChrome-viewSpecsMenu-rowRight {
+// font-size: $small-text;
+// letter-spacing: 2px;
+// color: $medium-gray;
+// margin-left: 10px;
+// padding: 5px;
+// border: none;
+// outline-color: $black;
+// }
+// }
+
+// .collectionViewBaseChrome-viewSpecsMenu-lastRow {
+// display: grid;
+// grid-template-columns: 1fr 1fr 1fr;
+// grid-gap: 10px;
+// margin: 10px;
+// }
+// }
+// }
+// }
+
+// .collectionStackingViewChrome-cont,
+// .collectionTreeViewChrome-cont,
+// .collection3DCarouselViewChrome-cont {
+// display: flex;
+// justify-content: space-between;
+// }
+
+// .collectionGridViewChrome-cont {
+// display: flex;
+// margin-left: 10;
+
+// .collectionGridViewChrome-viewPicker {
+// font-size: $small-text;
+// //text-transform: uppercase;
+// //letter-spacing: 2px;
+// background: $dark-gray;
+// color: $white;
+// outline-color: $black;
+// color: $white;
+// border: none;
+// border-right: solid $medium-gray 1px;
+// }
+
+// .collectionGridViewChrome-viewPicker:active {
+// outline-color: $black;
+// }
+
+// .grid-control {
+// align-self: center;
+// display: flex;
+// flex-direction: row;
+// margin-right: 5px;
+
+// .grid-icon {
+// margin-right: 5px;
+// align-self: center;
+// }
+
+// .flexLabel {
+// margin-bottom: 0;
+// }
+
+// .collectionGridViewChrome-entryBox {
+// width: 50%;
+// color: $black;
+// }
+
+// .collectionGridViewChrome-columnButton {
+// color: $black;
+// }
+// }
+// }
+
+// .collectionStackingViewChrome-sort,
+// .collectionTreeViewChrome-sort {
+// display: flex;
+// align-items: center;
+// justify-content: space-between;
+
+// .collectionStackingViewChrome-sortIcon,
+// .collectionTreeViewChrome-sortIcon {
+// transition: transform .5s;
+// margin-left: 10px;
+// }
+// }
+
+// button:hover {
+// transform: scale(1);
+// }
+
+
+// .collectionStackingViewChrome-pivotField-cont,
+// .collectionTreeViewChrome-pivotField-cont,
+// .collection3DCarouselViewChrome-scrollSpeed-cont {
+// justify-self: right;
+// align-items: center;
+// display: flex;
+// grid-auto-columns: auto;
+// font-size: $small-text;
+// letter-spacing: 2px;
+
+// .collectionStackingViewChrome-pivotField-label,
+// .collectionTreeViewChrome-pivotField-label,
+// .collection3DCarouselViewChrome-scrollSpeed-label {
+// grid-column: 1;
+// margin-right: 7px;
+// user-select: none;
+// font-family: $sans-serif;
+// letter-spacing: normal;
+// }
+
+// .collectionStackingViewChrome-sortIcon {
+// transition: transform .5s;
+// grid-column: 3;
+// text-align: center;
+// display: flex;
+// justify-content: center;
+// align-items: center;
+// cursor: pointer;
+// width: 25px;
+// height: 25px;
+// border-radius: 100%;
+// }
+
+// .collectionStackingViewChrome-sortIcon:hover {
+// background-color: $drop-shadow;
+// }
+
+// .collectionStackingViewChrome-pivotField,
+// .collectionTreeViewChrome-pivotField,
+// .collection3DCarouselViewChrome-scrollSpeed {
+// color: $white;
+// grid-column: 2;
+// grid-row: 1;
+// width: 90%;
+// min-width: 100px;
+// display: flex;
+// height: 80%;
+// border-radius: 7px;
+// align-items: center;
+// background: $white;
+
+// .editable-view-input,
+// input,
+// .editableView-container-editing-oneLine,
+// .editableView-container-editing {
+// margin: auto;
+// border: 0px;
+// color: $light-gray !important;
+// text-align: center;
+// letter-spacing: 2px;
+// outline-color: $black;
+// height: 100%;
+// }
+
+// .react-autosuggest__container {
+// margin: 0;
+// color: $medium-gray;
+// padding: 0px;
+// }
+// }
+// }
+
+// .collectionStackingViewChrome-pivotField:hover,
+// .collectionTreeViewChrome-pivotField:hover,
+// .collection3DCarouselViewChrome-scrollSpeed:hover {
+// cursor: text;
+// }
+
+// }
+// }
+
+// .collectionMenu-webUrlButtons {
+// margin-left: 44;
+// background: lightGray;
+// display: flex;
+// }
+
+// .webBox-urlEditor {
+// position: relative;
+// opacity: 0.9;
+// z-index: 901;
+// transition: top .5s;
+
+// .urlEditor {
+// display: grid;
+// grid-template-columns: 1fr auto;
+// padding-bottom: 10px;
+// overflow: hidden;
+// margin-top: 5px;
+// height: 35px;
+
+// .editorBase {
+// display: flex;
+
+// .editor-collapse {
+// transition: all .5s, opacity 0.3s;
+// position: absolute;
+// width: 40px;
+// transform-origin: top left;
+// }
+
+// .switchToText {
+// color: $medium-gray;
+// }
+
+// .switchToText:hover {
+// color: $dark-gray;
+// }
+// }
+
+// button:hover {
+// transform: scale(1);
+// }
+// }
+// }
+
+// .collectionMenu-urlInput {
+// padding: 12px 10px 11px 10px;
+// border: 0px;
+// color: $black;
+// font-size: $small-text;
+// letter-spacing: 2px;
+// outline-color: $black;
+// background: $white;
+// width: 100%;
+// min-width: 350px;
+// margin-right: 10px;
+// height: 100%;
+// }
+
+// .collectionFreeFormMenu-cont {
+// display: inline-flex;
+// position: relative;
+// align-items: center;
+// height: 100%;
+
+// .color-previewI {
+// width: 60%;
+// top: 80%;
+// position: absolute;
+// height: 4px;
+// }
+
+// .color-previewII {
+// width: 80%;
+// height: 80%;
+// margin-left: 10%;
+// position: absolute;
+// bottom: 5;
+// }
+
+// .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: relative;
+// width: 20;
+// height: 30;
+// bottom: 0;
+// background: $dark-gray;
+// display: inline-flex;
+// align-items: center;
+// color: $white;
+// }
+
+// .backKeyframe {
+// svg {
+// display: block;
+// margin: auto;
+// }
+// }
+
+
+// .numKeyframe {
+// flex-direction: column;
+// padding-top: 5px;
+// }
+
+// .fwdKeyframe {
+// svg {
+// display: block;
+// margin: auto;
+// }
+
+// border-right: solid $medium-gray 1px;
+// }
+// }
+
+// .collectionSchemaViewChrome-cont {
+// display: flex;
+// font-size: $small-text;
+
+// .collectionSchemaViewChrome-toggle {
+// display: flex;
+// margin-left: 10px;
+// }
+
+// .collectionSchemaViewChrome-label {
+// text-transform: uppercase;
+// letter-spacing: 2px;
+// margin-right: 5px;
+// display: flex;
+// flex-direction: column;
+// justify-content: center;
+// }
+
+// .collectionSchemaViewChrome-toggler {
+// width: 100px;
+// height: 35px;
+// background-color: $black;
+// position: relative;
+// }
+
+// .collectionSchemaViewChrome-togglerButton {
+// width: 47px;
+// height: 30px;
+// background-color: $light-gray;
+// // position: absolute;
+// transition: all 0.5s ease;
+// // top: 3px;
+// margin-top: 3px;
+// color: $medium-gray;
+// letter-spacing: 2px;
+// text-transform: uppercase;
+// display: flex;
+// flex-direction: column;
+// justify-content: center;
+// text-align: center;
+
+// &.on {
+// margin-left: 3px;
+// }
+
+// &.off {
+// margin-left: 50px;
+// }
+// }
+// }
+
+
+// .commandEntry-outerDiv {
+// display: flex;
+// flex-direction: column;
+// height: 40px;
+// }
+
+// .commandEntry-inputArea {
+// display: flex;
+// flex-direction: row;
+// width: 150px;
+// margin: auto auto auto auto;
+// }
+
+// .react-autosuggest__container {
+// position: relative;
+// width: 100%;
+// margin-left: 5px;
+// margin-right: 5px;
+// }
+
+// .react-autosuggest__input {
+// border: 1px solid $light-gray;
+// border-radius: 4px;
+// width: 100%;
+// }
+
+// .react-autosuggest__input--focused {
+// outline: none;
+// }
+
+// .react-autosuggest__input--open {
+// border-bottom-left-radius: 0;
+// border-bottom-right-radius: 0;
+// }
+
+// .react-autosuggest__suggestions-container {
+// display: none;
+// }
+
+// .react-autosuggest__suggestions-container--open {
+// display: block;
+// position: fixed;
+// overflow-y: auto;
+// max-height: 400px;
+// width: 180px;
+// border: 1px solid $light-gray;
+// background-color: $white;
+// font-family: $sans-serif;
+// font-weight: 300;
+// font-size: $large-header;
+// border-bottom-left-radius: 4px;
+// border-bottom-right-radius: 4px;
+// z-index: 2;
+// }
+
+// .react-autosuggest__suggestions-list {
+// margin: 0;
+// padding: 0;
+// list-style-type: none;
+// }
+
+// .react-autosuggest__suggestion {
+// cursor: pointer;
+// padding: 10px 20px;
+// }
+
+// .react-autosuggest__suggestion--highlighted {
+// background-color: $light-gray;
+// } \ No newline at end of file
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index a9b978c4e..77e5132fc 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -15,29 +15,32 @@ import { RichTextField } from "../../../fields/RichTextField";
import { listSpec } from "../../../fields/Schema";
import { ScriptField } from "../../../fields/ScriptField";
import { BoolCast, Cast, NumCast, StrCast } from "../../../fields/Types";
-import { emptyFunction, setupMoveUpEvents, Utils } from "../../../Utils";
+import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents, Utils } from "../../../Utils";
+import { Docs } from "../../documents/Documents";
import { DocumentType } from "../../documents/DocumentTypes";
import { CurrentUserUtils } from "../../util/CurrentUserUtils";
import { DragManager } from "../../util/DragManager";
import { Scripting } from "../../util/Scripting";
import { SelectionManager } from "../../util/SelectionManager";
+import { Transform } from "../../util/Transform";
import { undoBatch } from "../../util/UndoManager";
import { AntimodeMenu, AntimodeMenuProps } from "../AntimodeMenu";
import { EditableView } from "../EditableView";
import { GestureOverlay } from "../GestureOverlay";
-import { ActiveFillColor, ActiveInkColor, SetActiveArrowEnd, SetActiveArrowStart, SetActiveBezierApprox, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth, ActiveArrowStart, ActiveArrowEnd } from "../InkingStroke";
+import { ActiveFillColor, ActiveInkColor, SetActiveArrowEnd, SetActiveArrowStart, SetActiveBezierApprox, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth } from "../InkingStroke";
+import { LightboxView } from "../LightboxView";
import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView";
import { DocumentView } from "../nodes/DocumentView";
+import { FormattedTextBox } from "../nodes/formattedText/FormattedTextBox";
import { RichTextMenu } from "../nodes/formattedText/RichTextMenu";
import { PresBox } from "../nodes/trails/PresBox";
+import { DefaultStyleProvider } from "../StyleProvider";
+import { CollectionDockingView } from "./CollectionDockingView";
+import { CollectionLinearView } from "./CollectionLinearView";
import "./CollectionMenu.scss";
import { CollectionViewType, COLLECTION_BORDER_WIDTH } from "./CollectionView";
import { TabDocView } from "./TabDocView";
-import { LightboxView } from "../LightboxView";
-import { Docs } from "../../documents/Documents";
-import { DocumentManager } from "../../util/DocumentManager";
-import { CollectionDockingView } from "./CollectionDockingView";
-import { FormattedTextBox } from "../nodes/formattedText/FormattedTextBox";
+import { Colors } from "../global/globalEnums";
@observer
export class CollectionMenu extends AntimodeMenu<AntimodeMenuProps> {
@@ -46,6 +49,8 @@ export class CollectionMenu extends AntimodeMenu<AntimodeMenuProps> {
@observable SelectedCollection: DocumentView | undefined;
@observable FieldKey: string;
+ private _docBtnRef = React.createRef<HTMLDivElement>();
+
constructor(props: any) {
super(props);
this.FieldKey = "";
@@ -82,30 +87,87 @@ export class CollectionMenu extends AntimodeMenu<AntimodeMenuProps> {
}
}
+ buttonBarXf = () => {
+ if (!this._docBtnRef.current) return Transform.Identity();
+ const { scale, translateX, translateY } = Utils.GetScreenTransform(this._docBtnRef.current);
+ return new Transform(-translateX, -translateY, 1 / scale);
+ }
+
+ @computed get contMenuButtons() {
+ const selDoc = Doc.UserDoc().contextMenuBtns;
+ return !(selDoc instanceof Doc) ? (null) : <div className="collectionMenu-contMenuButtons" ref={this._docBtnRef} style={{ height: "35px" }} >
+ <CollectionLinearView
+ Document={selDoc}
+ DataDoc={undefined}
+ fieldKey={"data"}
+ dropAction={"alias"}
+ setHeight={returnFalse}
+ styleProvider={DefaultStyleProvider}
+ layerProvider={undefined}
+ rootSelected={returnTrue}
+ bringToFront={emptyFunction}
+ select={emptyFunction}
+ isContentActive={returnFalse}
+ isSelected={returnFalse}
+ docViewPath={returnEmptyDoclist}
+ moveDocument={returnFalse}
+ CollectionView={undefined}
+ addDocument={returnFalse}
+ addDocTab={returnFalse}
+ pinToPres={emptyFunction}
+ removeDocument={returnFalse}
+ ScreenToLocalTransform={this.buttonBarXf}
+ PanelWidth={() => 100}
+ PanelHeight={() => 35}
+ renderDepth={0}
+ focus={() => undefined}
+ whenChildContentsActiveChanged={emptyFunction}
+ docFilters={returnEmptyFilter}
+ docRangeFilters={returnEmptyFilter}
+ searchFilterDocs={returnEmptyDoclist}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined} />
+ </div>;
+ }
+
render() {
- const button = <Tooltip title={<div className="dash-tooltip">Pin Menu</div>} key="pin menu" placement="bottom">
- <button className="antimodeMenu-button" 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>
- </Tooltip>;
const propIcon = CurrentUserUtils.propertiesWidth > 0 ? "angle-double-right" : "angle-double-left";
const propTitle = CurrentUserUtils.propertiesWidth > 0 ? "Close Properties Panel" : "Open Properties Panel";
const prop = <Tooltip title={<div className="dash-tooltip">{propTitle}</div>} key="properties" placement="bottom">
- <button className="antimodeMenu-button" key="properties" style={{ backgroundColor: "#424242" }}
+ <div className="collectionMenu-hardCodedButton"
+ style={{ backgroundColor: CurrentUserUtils.propertiesWidth > 0 ? Colors.MEDIUM_BLUE : undefined }}
+ key="properties"
onPointerDown={this.toggleProperties}>
<FontAwesomeIcon icon={propIcon} size="lg" />
- </button>
+ </div>
</Tooltip>;
- return this.getElement(!this.SelectedCollection ? [/*button*/] :
- [<CollectionViewBaseChrome key="chrome"
- docView={this.SelectedCollection}
- fieldKey={this.SelectedCollection.LayoutFieldKey}
- type={StrCast(this.SelectedCollection?.props.Document._viewType, CollectionViewType.Invalid) as CollectionViewType} />,
- prop,
- /*button*/]);
+ // NEW BUTTONS
+ //dash col linear view buttons
+ const contMenuButtons =
+ <div className="collectionMenu-container">
+ {this.contMenuButtons}
+ {prop}
+ </div>;
+
+ return contMenuButtons;
+
+ // const button = <Tooltip title={<div className="dash-tooltip">Pin Menu</div>} key="pin menu" placement="bottom">
+ // <button className="antimodeMenu-button" 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>
+ // </Tooltip>;
+
+ // OLD BUTTONS
+ // return this.getElement(!this.SelectedCollection ? [/*button*/] :
+ // [<CollectionViewBaseChrome key="chrome"
+ // docView={this.SelectedCollection}
+ // fieldKey={this.SelectedCollection.LayoutFieldKey}
+ // type={StrCast(this.SelectedCollection?.props.Document._viewType, CollectionViewType.Invalid) as CollectionViewType} />,
+ // prop,
+ // /*button*/]);
}
}
@@ -665,7 +727,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu
}
@computed get drawButtons() {
- const func = action((i: number, keep: boolean) => {
+ const func = action((e: React.MouseEvent | React.PointerEvent, i: number, keep: boolean) => {
this._keepPrimitiveMode = keep;
if (this._selectedPrimitive !== i) {
this._selectedPrimitive = i;
@@ -683,13 +745,14 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu
GestureOverlay.Instance.InkShape = "";
SetActiveBezierApprox("0");
}
+ e.stopPropagation();
});
return <div className="btn-draw" key="draw">
{this._draw.map((icon, i) =>
<Tooltip key={icon} title={<div className="dash-tooltip">{this._title[i]}</div>} placement="bottom">
<button className="antimodeMenu-button"
- onPointerDown={() => func(i, false)}
- onDoubleClick={() => func(i, true)}
+ onPointerDown={e => func(e, i, false)}
+ onDoubleClick={e => func(e, i, true)}
style={{ backgroundColor: i === this._selectedPrimitive ? "525252" : "", fontSize: "20" }}>
<FontAwesomeIcon icon={this._faName[i] as IconProp} size="sm" />
</button>
@@ -719,7 +782,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu
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>
+ </button>
</Tooltip>)}
</div>;
}
@@ -990,7 +1053,7 @@ export class CollectionTreeViewChrome extends React.Component<CollectionMenuProp
<button className="collectionTreeViewChrome-sort" onClick={this.toggleSort}>
<div className="collectionTreeViewChrome-sortLabel">
Sort
- </div>
+ </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>
@@ -1224,3 +1287,4 @@ Scripting.addGlobal(function gotoFrame(doc: any, newFrame: any) {
CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0);
doc._currentFrame = newFrame === undefined ? 0 : Math.max(0, newFrame);
});
+
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index e1e04915a..1efea96be 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -413,8 +413,8 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
isContentActive={returnTrue}
isDocumentActive={returnFalse}
ScreenToLocalTransform={this.getPreviewTransform}
- docFilters={this.docFilters}
- docRangeFilters={this.docRangeFilters}
+ docFilters={this.childDocFilters}
+ docRangeFilters={this.childDocRangeFilters}
searchFilterDocs={this.searchFilterDocs}
styleProvider={DefaultStyleProvider}
layerProvider={undefined}
@@ -556,7 +556,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
style={{
overflow: this.props.scrollOverflow === true ? "scroll" : undefined, backgroundColor: "white",
pointerEvents: this.props.Document._searchDoc !== undefined && !this.props.isContentActive() && !SnappingManager.GetIsDragging() ? "none" : undefined,
- width: name === "collectionSchemaView-searchContainer" ? "auto" : this.props.PanelWidth() || "100%", height: this.props.PanelHeight() || "100%", position: "relative",
+ width: this.props.PanelWidth() || "100%", height: this.props.PanelHeight() || "100%", position: "relative",
}} >
<div className="collectionSchemaView-tableContainer"
style={{ width: `calc(100% - ${this.previewWidth()}px)` }}
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 7aa8dfd56..b9bc83d49 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -239,10 +239,10 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument,
onDoubleClick={this.onChildDoubleClickHandler}
ScreenToLocalTransform={stackedDocTransform}
focus={this.focusDocument}
- docFilters={this.docFilters}
+ docFilters={this.childDocFilters}
hideDecorationTitle={this.props.childHideDecorationTitle?.()}
hideTitle={this.props.childHideTitle?.()}
- docRangeFilters={this.docRangeFilters}
+ docRangeFilters={this.childDocRangeFilters}
searchFilterDocs={this.searchFilterDocs}
ContainingCollectionDoc={this.props.CollectionView?.props.Document}
ContainingCollectionView={this.props.CollectionView}
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index a5d27f038..1f4c35daa 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -1,6 +1,6 @@
import { action, computed, IReactionDisposer, reaction, observable, runInAction } from "mobx";
import CursorField from "../../../fields/CursorField";
-import { Doc, Opt, Field, DocListCast, AclPrivate } from "../../../fields/Doc";
+import { Doc, Opt, Field, DocListCast, AclPrivate, StrListCast } from "../../../fields/Doc";
import { Id, ToString } from "../../../fields/FieldSymbols";
import { List } from "../../../fields/List";
import { listSpec } from "../../../fields/Schema";
@@ -8,7 +8,7 @@ import { ScriptField } from "../../../fields/ScriptField";
import { WebField } from "../../../fields/URLField";
import { Cast, ScriptCast, NumCast, StrCast } from "../../../fields/Types";
import { GestureUtils } from "../../../pen-gestures/GestureUtils";
-import { Utils, returnFalse } from "../../../Utils";
+import { Utils, returnFalse, returnEmptyFilter } from "../../../Utils";
import { DocServer } from "../../DocServer";
import { Networking } from "../../Network";
import { ImageUtils } from "../../util/Import & Export/ImageUtils";
@@ -22,6 +22,7 @@ import ReactLoading from 'react-loading';
export interface SubCollectionViewProps extends CollectionViewProps {
CollectionView: Opt<CollectionView>;
+ SetSubView?: (subView: any) => void;
}
export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: X) {
@@ -30,6 +31,8 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
private gestureDisposer?: GestureUtils.GestureEventDisposer;
protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
protected _mainCont?: HTMLDivElement;
+ @observable _focusFilters: Opt<string[]>; // docFilters that are overridden when previewing a link to an anchor which has docFilters set on it
+ @observable _focusRangeFilters: Opt<string[]>; // docRangeFilters that are overridden when previewing a link to an anchor which has docRangeFilters set on it
protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view
this.dropDisposer?.();
this.gestureDisposer?.();
@@ -45,6 +48,10 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
this.createDashEventsTarget(ele);
}
+ componentDidMount() {
+ this.props.SetSubView?.(this);
+ }
+
componentWillUnmount() {
this.gestureDisposer?.();
this._multiTouchDisposer?.();
@@ -73,23 +80,22 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
get childLayoutPairs(): { layout: Doc; data: Doc; }[] {
const { Document, DataDoc } = this.props;
const validPairs = this.childDocs.map(doc => Doc.GetLayoutDataDocPair(Document, !this.props.isAnnotationOverlay ? DataDoc : undefined, doc)).
- filter(pair => { // filter out any documents that have a proto that we don't have permissions to (which we determine by not having any keys
- return pair.layout && /*!pair.layout.hidden &&*/ (!pair.layout.proto || (pair.layout.proto instanceof Doc && GetEffectiveAcl(pair.layout.proto) !== AclPrivate));// Object.keys(pair.layout.proto).length));
+ filter(pair => { // filter out any documents that have a proto that we don't have permissions to
+ return pair.layout && (!pair.layout.proto || (pair.layout.proto instanceof Doc && GetEffectiveAcl(pair.layout.proto) !== AclPrivate));
});
return validPairs.map(({ data, layout }) => ({ data: data as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types
}
get childDocList() {
return Cast(this.dataField, listSpec(Doc));
}
- docFilters = () => {
- return [...this.props.docFilters(), ...Cast(this.props.Document._docFilters, listSpec("string"), [])];
- }
- docRangeFilters = () => {
- return [...this.props.docRangeFilters(), ...Cast(this.props.Document._docRangeFilters, listSpec("string"), [])];
- }
- searchFilterDocs = () => {
- return [...this.props.searchFilterDocs(), ...DocListCast(this.props.Document._searchFilterDocs)];
- }
+ collectionFilters = () => this._focusFilters ?? StrListCast(this.props.Document._docFilters);
+ collectionRangeDocFilters = () => this._focusRangeFilters ?? Cast(this.props.Document._docRangeFilters, listSpec("string"), []);
+ childDocFilters = () => [...(this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f)) || []), ...this.collectionFilters()];
+ unrecursiveDocFilters = () => [...(this.props.docFilters?.().filter(f => !Utils.IsRecursiveFilter(f)) || [])];
+ childDocRangeFilters = () => [...(this.props.docRangeFilters?.() || []), ...this.collectionRangeDocFilters()];
+ IsFiltered = () => this.collectionFilters().length || this.collectionRangeDocFilters().length ? "hasFilter" :
+ this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f)).length || this.props.docRangeFilters().length ? "inheritsFilter" : undefined
+ searchFilterDocs = () => this.props.searchFilterDocs?.() ?? DocListCast(this.props.Document._searchFilterDocs);
@computed.struct get childDocs() {
TraceMobx();
let rawdocs: (Doc | Promise<Doc>)[] = [];
@@ -108,10 +114,10 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField);
const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;
- const docFilters = this.docFilters();
- const docRangeFilters = this.docRangeFilters();
+ const childDocFilters = this.childDocFilters();
+ const docRangeFilters = this.childDocRangeFilters();
const searchDocs = this.searchFilterDocs();
- if (this.props.Document.dontRegisterView || (!docFilters.length && !docRangeFilters.length && !searchDocs.length)) {
+ if (this.props.Document.dontRegisterView || (!childDocFilters.length && !this.unrecursiveDocFilters().length && !docRangeFilters.length && !searchDocs.length)) {
return childDocs.filter(cd => !cd.cookies); // remove any documents that require a cookie if there are no filters to provide one
}
@@ -122,24 +128,27 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
const docsforFilter: Doc[] = [];
childDocs.forEach((d) => {
// if (DocUtils.Excluded(d, docFilters)) return;
- let notFiltered = d.z || Doc.IsSystem(d) || ((!searchDocs.length || searchDocs.includes(d)) && (DocUtils.FilterDocs([d], docFilters, docRangeFilters, viewSpecScript, this.props.Document).length > 0));
- const fieldKey = Doc.LayoutFieldKey(d);
- const annos = !Field.toString(Doc.LayoutField(d) as Field).includes("CollectionView");
- const data = d[annos ? fieldKey + "-annotations" : fieldKey];
- if (data !== undefined) {
- let subDocs = DocListCast(data);
- if (subDocs.length > 0) {
- let newarray: Doc[] = [];
- notFiltered = notFiltered || (!searchDocs.length && DocUtils.FilterDocs(subDocs, docFilters, docRangeFilters, viewSpecScript, d).length);
- while (subDocs.length > 0 && !notFiltered) {
- newarray = [];
- subDocs.forEach((t) => {
- const fieldKey = Doc.LayoutFieldKey(t);
- const annos = !Field.toString(Doc.LayoutField(t) as Field).includes("CollectionView");
- notFiltered = notFiltered || ((!searchDocs.length || searchDocs.includes(t)) && ((!docFilters.length && !docRangeFilters.length) || DocUtils.FilterDocs([t], docFilters, docRangeFilters, viewSpecScript, d).length));
- DocListCast(t[annos ? fieldKey + "-annotations" : fieldKey]).forEach((newdoc) => newarray.push(newdoc));
- });
- subDocs = newarray;
+ let notFiltered = d.z || Doc.IsSystem(d) || (DocUtils.FilterDocs([d], this.unrecursiveDocFilters(), docRangeFilters, viewSpecScript, this.props.Document).length > 0);
+ if (notFiltered) {
+ notFiltered = ((!searchDocs.length || searchDocs.includes(d)) && (DocUtils.FilterDocs([d], childDocFilters, docRangeFilters, viewSpecScript, this.props.Document).length > 0));
+ const fieldKey = Doc.LayoutFieldKey(d);
+ const annos = !Field.toString(Doc.LayoutField(d) as Field).includes("CollectionView");
+ const data = d[annos ? fieldKey + "-annotations" : fieldKey];
+ if (data !== undefined) {
+ let subDocs = DocListCast(data);
+ if (subDocs.length > 0) {
+ let newarray: Doc[] = [];
+ notFiltered = notFiltered || (!searchDocs.length && DocUtils.FilterDocs(subDocs, childDocFilters, docRangeFilters, viewSpecScript, d).length);
+ while (subDocs.length > 0 && !notFiltered) {
+ newarray = [];
+ subDocs.forEach((t) => {
+ const fieldKey = Doc.LayoutFieldKey(t);
+ const annos = !Field.toString(Doc.LayoutField(t) as Field).includes("CollectionView");
+ notFiltered = notFiltered || ((!searchDocs.length || searchDocs.includes(t)) && ((!childDocFilters.length && !docRangeFilters.length) || DocUtils.FilterDocs([t], childDocFilters, docRangeFilters, viewSpecScript, d).length));
+ DocListCast(t[annos ? fieldKey + "-annotations" : fieldKey]).forEach((newdoc) => newarray.push(newdoc));
+ });
+ subDocs = newarray;
+ }
}
}
}
@@ -303,7 +312,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
} else {
const path = window.location.origin + "/doc/";
if (text.startsWith(path)) {
- const docid = text.replace(Utils.prepend("/doc/"), "").split("?")[0];
+ const docid = text.replace(Doc.globalServerPath(), "").split("?")[0];
DocServer.GetRefField(docid).then(f => {
if (f instanceof Doc) {
if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index 339163510..292dfd77c 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -32,12 +32,10 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
@observable _collapsed: boolean = false;
@observable _childClickedScript: Opt<ScriptField>;
@observable _viewDefDivClick: Opt<ScriptField>;
- @observable _focusDocFilters: Opt<string[]>; // fields that get overridden by a focus anchor
@observable _focusPivotField: Opt<string>;
- @observable _focusRangeFilters: Opt<string[]>;
getAnchor = () => {
- const anchor = Docs.Create.HTMLAnchorDocument({
+ const anchor = Docs.Create.HTMLAnchorDocument([], {
title: ComputedField.MakeFunction(`"${this.pivotField}"])`) as any,
annotationOn: this.rootDoc
});
@@ -72,9 +70,9 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
@action
setViewSpec = (anchor: Doc, preview: boolean) => {
if (preview) { // if in preview, then override document's fields with view spec
+ this._focusFilters = StrListCast(Doc.GetProto(anchor).docFilters);
+ this._focusRangeFilters = StrListCast(Doc.GetProto(anchor).docRangeFilters);
this._focusPivotField = StrCast(anchor.pivotField);
- this._focusDocFilters = StrListCast(anchor.docFilters);
- this._focusRangeFilters = StrListCast(anchor.docRangeFilters);
} else if (anchor.pivotField !== undefined) { // otherwise set document's fields based on anchor view spec
this.layoutDoc._prevFilterIndex = 1;
this.layoutDoc._pivotField = StrCast(anchor.pivotField);
@@ -84,8 +82,6 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
return 0;
}
- pivotDocFilters = () => this._focusDocFilters || this.props.docFilters();
- pivotDocRangeFilters = () => this._focusRangeFilters || this.props.docRangeFilters();
layoutEngine = () => this._layoutEngine;
toggleVisibility = action(() => this._collapsed = !this._collapsed);
@@ -139,10 +135,8 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
return <div className="collectionTimeView-innards" key="timeline" style={{ pointerEvents: this.props.isContentActive() ? undefined : "none" }}
onClick={this.contentsDown}>
<CollectionFreeFormView {...this.props}
- engineProps={{ pivotField: this.pivotField, docFilters: this.docFilters, docRangeFilters: this.docRangeFilters }}
+ engineProps={{ pivotField: this.pivotField, docFilters: this.childDocFilters, docRangeFilters: this.childDocRangeFilters }}
fitContentsToDoc={returnTrue}
- docFilters={this.pivotDocFilters}
- docRangeFilters={this.pivotDocRangeFilters}
childClickScript={this._childClickedScript}
viewDefDivClick={this._viewDefDivClick}
childFreezeDimensions={true}
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 3eece0086..a78034dca 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -1,10 +1,10 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, reaction, IReactionDisposer, observable } from "mobx";
import { observer } from "mobx-react";
-import { DataSym, Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
+import { DataSym, Doc, DocListCast, HeightSym, Opt, WidthSym, StrListCast } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
-import { Document } from '../../../fields/Schema';
+import { Document, listSpec } from '../../../fields/Schema';
import { ScriptField } from '../../../fields/ScriptField';
import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
@@ -27,6 +27,7 @@ import { TreeView } from "./TreeView";
import React = require("react");
import { InkTool } from '../../../fields/InkField';
import { CurrentUserUtils } from '../../util/CurrentUserUtils';
+import { CollectionView, CollectionViewType } from './CollectionView';
const _global = (window /* browser */ || global /* node */) as any;
export type collectionTreeViewProps = {
@@ -52,6 +53,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
@computed get treeChildren() { TraceMobx(); return this.props.childDocuments || this.childDocs; }
@computed get outlineMode() { return this.doc.treeViewType === "outline"; }
@computed get fileSysMode() { return this.doc.treeViewType === "fileSystem"; }
+ @computed get dashboardMode() { return this.doc === Doc.UserDoc().myDashboards; }
// these should stay in synch with counterparts in DocComponent.ts ViewBoxAnnotatableComponent
@observable _isAnyChildContentActive = false;
@@ -215,6 +217,11 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
/>
</div>;
}
+ childContextMenuItems = () => {
+ const customScripts = Cast(this.doc.childContextMenuScripts, listSpec(ScriptField), []);
+ const filterScripts = Cast(this.doc.childContextMenuFilters, listSpec(ScriptField), []);
+ return StrListCast(this.doc.childContextMenuLabels).map((label, i) => ({ script: customScripts[i], filter: filterScripts[i], label }));
+ }
@computed get treeViewElements() {
TraceMobx();
const dropAction = StrCast(this.doc.childDropAction) as dropActionType;
@@ -247,7 +254,9 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
this.whenChildContentsActiveChanged,
this.props.dontRegisterView || Cast(this.props.Document.childDontRegisterViews, "boolean", null),
this.observerHeight,
- this.unobserveHeight);
+ this.unobserveHeight,
+ this.childContextMenuItems()
+ );
}
@computed get titleBar() {
const hideTitle = this.props.treeViewHideTitle || this.doc.treeViewHideTitle;
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index e225c4a11..a821aeeea 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -1,4 +1,4 @@
-import { computed, observable, runInAction } from 'mobx';
+import { computed, observable, runInAction, action } from 'mobx';
import { observer } from "mobx-react";
import * as React from 'react';
import 'react-image-lightbox-with-rotate/style.css'; // This only needs to be imported once in your app
@@ -36,6 +36,7 @@ import { CollectionTimeView } from './CollectionTimeView';
import { CollectionTreeView } from "./CollectionTreeView";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import './CollectionView.scss';
+import { returnEmptyString } from '../../../Utils';
export const COLLECTION_BORDER_WIDTH = 2;
const path = require('path');
@@ -62,7 +63,7 @@ export enum CollectionViewType {
export interface CollectionViewProps extends FieldViewProps {
isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc)
layoutEngine?: () => string;
- setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void;
+ setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean) => void) => void;
// property overrides for child documents
children?: never | (() => JSX.Element[]) | React.ReactNode;
@@ -70,6 +71,7 @@ export interface CollectionViewProps extends FieldViewProps {
childDocumentsActive?: () => boolean;// whether child documents can be dragged if collection can be dragged (eg., in a when a Pile document is in startburst mode)
childFitWidth?: () => boolean;
childOpacity?: () => number;
+ childContextMenuItems?: () => { script: ScriptField, label: string }[];
childHideTitle?: () => boolean; // whether to hide the documentdecorations title for children
childHideDecorationTitle?: () => boolean;
childLayoutTemplate?: () => (Doc | undefined);// specify a layout Doc template to use for children of the collection
@@ -83,7 +85,7 @@ export interface CollectionViewProps extends FieldViewProps {
type CollectionDocument = makeInterface<[typeof documentSchema]>;
const CollectionDocument = makeInterface(documentSchema);
@observer
-export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & CollectionViewProps, CollectionDocument>(CollectionDocument, "") {
+export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & CollectionViewProps, CollectionDocument>(CollectionDocument) {
public static LayoutString(fieldStr: string) { return FieldView.LayoutString(CollectionView, fieldStr); }
@observable private static _safeMode = false;
@@ -91,6 +93,11 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
+ constructor(props: any) {
+ super(props);
+ runInAction(() => this._annotationKeySuffix = returnEmptyString);
+ }
+
get collectionViewType(): CollectionViewType | undefined {
const viewField = StrCast(this.layoutDoc._viewType);
if (CollectionView._safeMode) {
@@ -186,15 +193,20 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
}
!Doc.UserDoc().noviceMode && optionItems.push({ description: `${this.rootDoc.isInPlaceContainer ? "Unset" : "Set"} inPlace Container`, event: () => this.rootDoc.isInPlaceContainer = !this.rootDoc.isInPlaceContainer, icon: "project-diagram" });
- optionItems.push({
- description: "Create Branch", event: async () => this.props.addDocTab(await BranchCreate(this.rootDoc), "add:right"), icon: "project-diagram"
- });
- optionItems.push({
- description: "Pull Master", event: () => BranchTask(this.rootDoc, "pull"), icon: "project-diagram"
- });
- optionItems.push({
- description: "Merge Branches", event: () => BranchTask(this.rootDoc, "merge"), icon: "project-diagram"
- });
+ if (!Doc.UserDoc().noviceMode) {
+ optionItems.push({
+ description: "Create Branch", event: async () => this.props.addDocTab(await BranchCreate(this.rootDoc), "add:right"), icon: "project-diagram"
+ });
+ optionItems.push({
+ description: "Pull Master", event: () => BranchTask(this.rootDoc, "pull"), icon: "project-diagram"
+ });
+ optionItems.push({
+ description: "Merge Branches", event: () => BranchTask(this.rootDoc, "merge"), icon: "project-diagram"
+ });
+ }
+ if (this.Document._viewType === CollectionViewType.Docking) {
+ optionItems.push({ description: "Create Dashboard", event: () => CurrentUserUtils.createNewDashboard(Doc.UserDoc()), icon: "project-diagram" });
+ }
!options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "hand-point-right" });
@@ -236,13 +248,16 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
* Shows the filter icon if it's a user-created collection which isn't a dashboard and has some docFilters applied on it or on the current dashboard.
*/
@computed get showFilterIcon() {
- return this.props.Document.viewType !== CollectionViewType.Docking && !Doc.IsSystem(this.props.Document) && ((StrListCast(this.props.Document._docFilters).length || StrListCast(this.props.Document._docRangeFilters).length || StrListCast(CurrentUserUtils.ActiveDashboard._docFilters).length || StrListCast(CurrentUserUtils.ActiveDashboard._docRangeFilters).length));
+ return this.props.Document.viewType !== CollectionViewType.Docking && !Doc.IsSystem(this.props.Document) && this._subView?.IsFiltered();
}
+ @observable _subView: any = undefined;
+
render() {
TraceMobx();
const props: SubCollectionViewProps = {
...this.props,
+ SetSubView: action((subView: any) => this._subView = subView),
addDocument: this.addDocument,
moveDocument: this.moveDocument,
removeDocument: this.removeDocument,
@@ -260,8 +275,8 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
{this.collectionViewType !== undefined ? this.SubView(this.collectionViewType, props) : (null)}
{this.showFilterIcon ?
<FontAwesomeIcon icon={"filter"} size="lg"
- style={{ position: 'absolute', top: '1%', right: '1%', cursor: "pointer", padding: 1, color: '#18c718bd', zIndex: 1 }}
- onPointerDown={e => { runInAction(() => CurrentUserUtils.propertiesWidth = 250); e.stopPropagation(); }}
+ style={{ position: 'absolute', top: '1%', right: '1%', cursor: "pointer", padding: 1, color: this.showFilterIcon === "hasFilter" ? '#18c718bd' : "orange", zIndex: 1 }}
+ onPointerDown={action(e => { this.props.select(false); CurrentUserUtils.propertiesWidth = 250; e.stopPropagation(); })}
/>
: (null)}
</div>);
diff --git a/src/client/views/collections/TabDocView.scss b/src/client/views/collections/TabDocView.scss
index a963f1cb9..7f62ecaa0 100644
--- a/src/client/views/collections/TabDocView.scss
+++ b/src/client/views/collections/TabDocView.scss
@@ -1,3 +1,6 @@
+@import "../global/globalCssVariables.scss";
+
+
input.lm_title:focus,
input.lm_title {
max-width: unset !important;
@@ -57,12 +60,11 @@ input.lm_title {
}
}
-.miniMap-hidden,
.miniMap {
position: absolute;
overflow: hidden;
- right: 10;
- bottom: 10;
+ right: 15;
+ bottom: 15;
border: solid 1px;
box-shadow: black 0.4vw 0.4vw 0.8vw;
width: 100%;
@@ -82,17 +84,21 @@ input.lm_title {
}
.miniMap-hidden {
+ cursor: pointer;
position: absolute;
- bottom: 0;
- right: 0;
- width: 45px;
- height: 45px;
- transform: translate(20px, 20px) rotate(45deg);
- border-radius: 30px;
+ bottom: 5;
+ display: flex;
+ right: 5;
+ width: 25px;
+ height: 25px;
+ border-radius: 3px;
padding: 2px;
+ justify-content: center;
+ align-items: center;
+ align-content: center;
+ background-color: $light-gray;
- >svg {
- margin-top: 3px;
- transform: translate(0px, 7px);
+ &:hover {
+ box-shadow: none;
}
} \ No newline at end of file
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index a24f1eb7a..f80a2d8b5 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -11,7 +11,6 @@ import { DataSym, Doc, DocListCast, DocListCastAsync, HeightSym, Opt, WidthSym }
import { Id } from '../../../fields/FieldSymbols';
import { FieldId } from "../../../fields/RefField";
import { BoolCast, Cast, NumCast, StrCast } from "../../../fields/Types";
-import { TraceMobx } from '../../../fields/util';
import { emptyFunction, lightOrDark, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents, Utils } from "../../../Utils";
import { DocServer } from "../../DocServer";
import { DocUtils } from '../../documents/Documents';
@@ -34,6 +33,7 @@ import { CollectionView, CollectionViewType } from './CollectionView';
import "./TabDocView.scss";
import React = require("react");
import Color = require('color');
+import { Colors, Shadows } from '../global/globalEnums';
const _global = (window /* browser */ || global /* node */) as any;
interface TabDocViewProps {
@@ -124,6 +124,8 @@ export class TabDocView extends React.Component<TabDocViewProps> {
tab.element[0].prepend(iconWrap);
tab._disposers.layerDisposer = reaction(() => ({ layer: tab.DashDoc.activeLayer, color: this.tabColor }),
({ layer, color }) => {
+ // console.log("TabDocView: " + this.tabColor);
+ // console.log("lightOrDark: " + lightOrDark(this.tabColor));
const textColor = lightOrDark(this.tabColor); //not working with StyleProp.Color
titleEle.style.color = textColor;
titleEle.style.backgroundColor = "transparent";
@@ -171,10 +173,9 @@ export class TabDocView extends React.Component<TabDocViewProps> {
//attach the selection doc buttons menu to the drag handle
const stack: HTMLDivElement = tab.contentItem.parent;
const header: HTMLDivElement = tab;
- console.log("Stack: " + stack.id, stack.className)
stack.onscroll = action((e: any) => {
- console.log('scrolling...')
- })
+ console.log('scrolling...');
+ });
const moreInfoDrag = document.createElement("div");
moreInfoDrag.className = "lm_iconWrap";
tab._disposers.buttonDisposer = reaction(() => this.view, view =>
@@ -314,6 +315,14 @@ export class TabDocView extends React.Component<TabDocViewProps> {
return CollectionDockingView.AddSplit(doc, locationParams, this.stack);
}
}
+ remDocTab = (doc: Doc | Doc[]) => {
+ if (doc === this._document) {
+ SelectionManager.DeselectAll();
+ CollectionDockingView.CloseSplit(this._document);
+ return true;
+ }
+ return false;
+ }
getCurrentFrame = () => {
return NumCast(Cast(PresBox.Instance.childDocs[PresBox.Instance.itemIndex].presentationTargetDoc, Doc, null)._currentFrame);
@@ -350,7 +359,6 @@ export class TabDocView extends React.Component<TabDocViewProps> {
@computed get layerProvider() { return this._document && DefaultLayerProvider(this._document); }
@computed get docView() {
- TraceMobx();
return !this._activated || !this._document || this._document._viewType === CollectionViewType.Docking ? (null) :
<><DocumentView key={this._document[Id]} ref={action((r: DocumentView) => this._view = r)}
renderDepth={0}
@@ -363,11 +371,11 @@ export class TabDocView extends React.Component<TabDocViewProps> {
PanelHeight={this.PanelHeight}
layerProvider={this.layerProvider}
styleProvider={DefaultStyleProvider}
- docFilters={CollectionDockingView.Instance.docFilters}
- docRangeFilters={CollectionDockingView.Instance.docRangeFilters}
+ docFilters={CollectionDockingView.Instance.childDocFilters}
+ docRangeFilters={CollectionDockingView.Instance.childDocRangeFilters}
searchFilterDocs={CollectionDockingView.Instance.searchFilterDocs}
addDocument={undefined}
- removeDocument={undefined}
+ removeDocument={this.remDocTab}
addDocTab={this.addDocTab}
ScreenToLocalTransform={this.ScreenToLocalTransform}
dontCenter={"y"}
@@ -385,8 +393,15 @@ export class TabDocView extends React.Component<TabDocViewProps> {
background={this.miniMapColor}
document={this._document}
tabView={this.tabView} />
- <Tooltip style={{ display: this.disableMinimap() ? "none" : undefined }} key="ttip" title={<div className="dash-tooltip">{"toggle minimap"}</div>}>
- <div className="miniMap-hidden" onPointerDown={e => e.stopPropagation()} onClick={action(e => { e.stopPropagation(); this._document!.hideMinimap = !this._document!.hideMinimap; })} >
+ <Tooltip style={{ display: this.disableMinimap() ? "none" : undefined }} key="ttip" title={<div className="dash-tooltip">{this._document.hideMinimap ? "Open minimap" : "Close minimap"}</div>}>
+ <div className="miniMap-hidden"
+ style={{
+ color: this._document.hideMinimap ? Colors.BLACK : Colors.WHITE,
+ backgroundColor: this._document.hideMinimap ? Colors.LIGHT_GRAY : Colors.MEDIUM_BLUE,
+ boxShadow: this._document.hideMinimap ? Shadows.STANDARD_SHADOW : undefined
+ }}
+ onPointerDown={e => e.stopPropagation()}
+ onClick={action(e => { e.stopPropagation(); this._document!.hideMinimap = !this._document!.hideMinimap; })} >
<FontAwesomeIcon icon={"globe-asia"} size="lg" />
</div>
</Tooltip>
@@ -394,7 +409,6 @@ export class TabDocView extends React.Component<TabDocViewProps> {
}
render() {
- this.tab && CollectionDockingView.Instance.tabMap.delete(this.tab);
return (
<div className="collectionDockingView-content" style={{
fontFamily: Doc.UserDoc().renderStyle === "comic" ? "Comic Sans MS" : undefined,
@@ -468,6 +482,7 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> {
<div className="miniMap" style={{ width: miniSize, height: miniSize, background: this.props.background() }}>
<CollectionFreeFormView
Document={this.props.document}
+ SetSubView={() => this}
CollectionView={undefined}
ContainingCollectionView={undefined}
ContainingCollectionDoc={undefined}
@@ -496,8 +511,8 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> {
layerProvider={undefined}
addDocTab={this.props.addDocTab}
pinToPres={TabDocView.PinDoc}
- docFilters={CollectionDockingView.Instance.docFilters}
- docRangeFilters={CollectionDockingView.Instance.docRangeFilters}
+ docFilters={CollectionDockingView.Instance.childDocFilters}
+ docRangeFilters={CollectionDockingView.Instance.childDocRangeFilters}
searchFilterDocs={CollectionDockingView.Instance.searchFilterDocs}
fitContentsToDoc={returnTrue}
/>
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 401bdcb02..3ee9dbf59 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -1,7 +1,7 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, IReactionDisposer, observable, reaction } from "mobx";
import { observer } from "mobx-react";
-import { DataSym, Doc, DocListCast, DocListCastOrNull, Field, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
+import { DataSym, Doc, DocListCast, DocListCastOrNull, Field, HeightSym, Opt, WidthSym, StrListCast } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { RichTextField } from '../../../fields/RichTextField';
@@ -54,6 +54,7 @@ export interface TreeViewProps {
indentDocument?: (editTitle: boolean) => void;
outdentDocument?: (editTitle: boolean) => void;
ScreenToLocalTransform: () => Transform;
+ contextMenuItems: { script: ScriptField, filter: ScriptField, label: string }[];
dontRegisterView?: boolean;
styleProvider?: StyleProviderFunc | undefined;
treeViewHideHeaderFields: () => boolean;
@@ -99,13 +100,14 @@ export class TreeView extends React.Component<TreeViewProps> {
@observable _dref: DocumentView | undefined | null;
get displayName() { return "TreeView(" + this.props.document.title + ")"; } // this makes mobx trace() statements more descriptive
get defaultExpandedView() {
- return this.props.treeView.fileSysMode ? (this.doc.isFolder ? this.fieldKey : "aliases") :
- this.props.treeView.outlineMode || this.childDocs ? this.fieldKey : Doc.UserDoc().noviceMode ? "layout" : StrCast(this.props.treeView.doc.treeViewExpandedView, "fields");
+ return this.doc.viewType === CollectionViewType.Docking ? this.fieldKey :
+ this.props.treeView.fileSysMode ? (this.doc.isFolder ? this.fieldKey : "layout") :
+ this.props.treeView.outlineMode || this.childDocs ? this.fieldKey : Doc.UserDoc().noviceMode ? "layout" : StrCast(this.props.treeView.doc.treeViewExpandedView, "fields");
}
@computed get doc() { return this.props.document; }
@computed get treeViewOpen() { return (!this.treeViewOpenIsTransient && Doc.GetT(this.doc, "treeViewOpen", "boolean", true)) || this._transientOpenState; }
- @computed get treeViewExpandedView() { return StrCast(this.doc.treeViewExpandedView, this.defaultExpandedView); }
+ @computed get treeViewExpandedView() { return this.validExpandViewTypes.includes(StrCast(this.doc.treeViewExpandedView)) ? StrCast(this.doc.treeViewExpandedView) : this.defaultExpandedView; }
@computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.containerCollection.maxEmbedHeight, 200); }
@computed get dataDoc() { return this.doc[DataSym]; }
@computed get layoutDoc() { return Doc.Layout(this.doc); }
@@ -151,7 +153,10 @@ export class TreeView extends React.Component<TreeViewProps> {
this.treeViewOpen = !this.treeViewOpen;
} else {
// choose an appropriate alias or make one. --- choose the first alias that (1) user owns, (2) has no context field ... otherwise make a new alias
+ // this.props.addDocTab(CurrentUserUtils.ActiveDashboard.isShared ? Doc.MakeAlias(this.props.document) : this.props.document, "add:right");
+ // choose an appropriate alias or make one -- -- choose the first alias that (1) the user owns, (2) has no context field - if I own it and someone else does not have it open,, otherwise create an alias
this.props.addDocTab(this.props.document, "add:right");
+
}
}
constructor(props: any) {
@@ -333,7 +338,7 @@ export class TreeView extends React.Component<TreeViewProps> {
this.props.dropAction, this.props.addDocTab, this.titleStyleProvider, this.props.ScreenToLocalTransform, this.props.isContentActive,
this.props.panelWidth, this.props.renderDepth, this.props.treeViewHideHeaderFields,
[...this.props.renderedIds, doc[Id]], this.props.onCheckedClick, this.props.onChildClick, this.props.skipFields, false, this.props.whenChildContentsActiveChanged,
- this.props.dontRegisterView, emptyFunction, emptyFunction);
+ this.props.dontRegisterView, emptyFunction, emptyFunction, this.childContextMenuItems());
} else {
contentElement = <EditableView key="editableView"
contents={contents !== undefined ? Field.toString(contents as Field) : "null"}
@@ -415,7 +420,7 @@ export class TreeView extends React.Component<TreeViewProps> {
StrCast(this.doc.childDropAction, this.props.dropAction) as dropActionType, this.props.addDocTab, this.titleStyleProvider, this.props.ScreenToLocalTransform,
this.props.isContentActive, this.props.panelWidth, this.props.renderDepth, this.props.treeViewHideHeaderFields,
[...this.props.renderedIds, this.doc[Id]], this.props.onCheckedClick, this.props.onChildClick, this.props.skipFields, false, this.props.whenChildContentsActiveChanged,
- this.props.dontRegisterView, emptyFunction, emptyFunction)}
+ this.props.dontRegisterView, emptyFunction, emptyFunction, this.childContextMenuItems())}
</ul >;
} else if (this.treeViewExpandedView === "fields") {
return <ul key={this.doc[Id] + this.doc.title}>
@@ -472,16 +477,20 @@ export class TreeView extends React.Component<TreeViewProps> {
</div>;
}
+ @computed get validExpandViewTypes() {
+ if (this.doc.viewType === CollectionViewType.Docking) return [this.fieldKey];
+ const annos = () => DocListCast(this.doc[this.fieldKey + "-annotations"]).length ? "annotations" : "";
+ const links = () => DocListCast(this.doc.links).length ? "links" : "";
+ const data = () => this.childDocs && !this.props.treeView.dashboardMode ? this.fieldKey : "";
+ const aliases = () => this.props.treeView.dashboardMode ? "" : "aliases";
+ const fields = () => Doc.UserDoc().noviceMode ? "" : "fields";
+ return [data(), "layout", ...(this.props.treeView.fileSysMode ? [aliases(), links(), annos()] : []), fields()].filter(m => m);
+ }
@action
expandNextviewType = () => {
if (this.treeViewOpen && !this.doc.isFolder && !this.props.treeView.outlineMode && !this.doc.treeViewExpandedViewLock) {
- const next = (modes: any[]) => modes[(modes.indexOf(StrCast(this.doc.treeViewExpandedView)) + 1) % modes.length];
- const annos = () => DocListCast(this.doc[this.fieldKey + "-annotations"]).length ? "annotations" : "";
- const links = () => DocListCast(this.doc.links).length ? "links" : "";
- const children = () => this.childDocs ? this.fieldKey : "";
- this.doc.treeViewExpandedView = next(this.props.treeView.fileSysMode ?
- (Doc.UserDoc().noviceMode ? ["layout", "aliases"] : ["layout", "aliases", "fields"]) :
- (Doc.UserDoc().noviceMode ? [children(), "layout"] : [children(), "fields", "layout", links(), annos()]).filter(mode => mode));
+ const next = (modes: any[]) => modes[(modes.indexOf(StrCast(this.treeViewExpandedView)) + 1) % modes.length];
+ this.doc.treeViewExpandedView = next(this.validExpandViewTypes);
}
this.treeViewOpen = true;
}
@@ -504,13 +513,22 @@ export class TreeView extends React.Component<TreeViewProps> {
}
contextMenuItems = () => {
const makeFolder = { script: ScriptField.MakeFunction(`scriptContext.makeFolder()`, { scriptContext: "any" })!, label: "New Folder" };
- return this.doc.isFolder ? [makeFolder] :
+ const openAlias = { script: ScriptField.MakeFunction(`openOnRight(getAlias(self))`)!, label: "Open Alias" };
+ const focusDoc = { script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, label: "Focus or Open" };
+ return [...this.props.contextMenuItems.filter(mi => !mi.filter ? true : mi.filter.script.run({ doc: this.doc })?.result), ... (this.doc.isFolder ? [makeFolder] :
Doc.IsSystem(this.doc) ? [] :
this.props.treeView.fileSysMode && this.doc === Doc.GetProto(this.doc) ?
- [{ script: ScriptField.MakeFunction(`openOnRight(getAlias(self))`)!, label: "Open Alias" }, makeFolder] :
- [{ script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, label: "Focus or Open" }];
+ [openAlias, makeFolder] :
+ this.doc.viewType === CollectionViewType.Docking ? [] :
+ [openAlias, focusDoc])];
+ }
+ childContextMenuItems = () => {
+ const customScripts = Cast(this.doc.childContextMenuScripts, listSpec(ScriptField), []);
+ const customFilters = Cast(this.doc.childContextMenuFilters, listSpec(ScriptField), []);
+ return StrListCast(this.doc.childContextMenuLabels).map((label, i) => ({ script: customScripts[i], filter: customFilters[i], label }));
}
onChildClick = () => this.props.onChildClick?.() ?? (this._editTitleScript?.() || ScriptCast(this.doc.treeChildClick));
+
onChildDoubleClick = () => (!this.props.treeView.outlineMode && this._openScript?.()) || ScriptCast(this.doc.treeChildDoubleClick);
refocus = () => this.props.treeView.props.focus(this.props.treeView.props.Document);
@@ -813,7 +831,8 @@ export class TreeView extends React.Component<TreeViewProps> {
whenChildContentsActiveChanged: (isActive: boolean) => void,
dontRegisterView: boolean | undefined,
observerHeight: (ref: any) => void,
- unobserveHeight: (ref: any) => void
+ unobserveHeight: (ref: any) => void,
+ contextMenuItems: ({ script: ScriptField, filter: ScriptField, label: string }[])
) {
const viewSpecScript = Cast(conainerCollection.viewSpecScript, ScriptField);
if (viewSpecScript) {
@@ -878,6 +897,7 @@ export class TreeView extends React.Component<TreeViewProps> {
parentTreeView={parentTreeView}
observeHeight={observerHeight}
unobserveHeight={unobserveHeight}
+ contextMenuItems={contextMenuItems}
/>;
});
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index a8f5e6dd2..59891b7a1 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -9,6 +9,9 @@ import { SnappingManager } from "../../../util/SnappingManager";
import { DocumentView } from "../../nodes/DocumentView";
import "./CollectionFreeFormLinkView.scss";
import React = require("react");
+import { LinkManager } from "../../../util/LinkManager";
+import { List } from "../../../fields/List";
+
export interface CollectionFreeFormLinkViewProps {
A: DocumentView;
@@ -173,8 +176,14 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
render() {
if (!this.renderData) return (null);
const { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1, pt2 } = this.renderData;
+ LinkManager.currentLink = this.props.LinkDocs[0];
+ const linkRelationship = LinkManager.currentLink?.linkRelationship; //get string representing relationship
+ const linkRelationshipList = Doc.UserDoc().linkRelationshipList as List<string>;
+ const linkColorList = Doc.UserDoc().linkColorList as List<string>;
+ //access stroke color using index of the relationship in the color list (default black)
+ const strokeColor = linkRelationshipList.indexOf(linkRelationship) == -1 ? "black" : linkColorList[linkRelationshipList.indexOf(linkRelationship)];
return !a.width || !b.width || ((!this.props.LinkDocs[0].linkDisplay) && !aActive && !bActive) ? (null) : (<>
- <path className="collectionfreeformlinkview-linkLine" style={{ opacity: this._opacity, strokeDasharray: "2 2" }}
+ <path className="collectionfreeformlinkview-linkLine" style={{ opacity: this._opacity, strokeDasharray: "2 2", stroke: strokeColor }}
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]}`} />
{textX === undefined ? (null) : <text className="collectionfreeformlinkview-linkText" x={textX} y={textY} onPointerDown={this.pointerDown} >
{StrCast(this.props.LinkDocs[0].description)}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index 5e0b31754..e812064b7 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -9,6 +9,7 @@ import "./CollectionFreeFormLinksView.scss";
import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView";
import React = require("react");
import { DocumentType } from "../../../documents/DocumentTypes";
+import { LinkManager } from "../../../util/LinkManager";
@observer
export class CollectionFreeFormLinksView extends React.Component {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 143d8e070..985d162f9 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -49,6 +49,7 @@ import "./CollectionFreeFormView.scss";
import { MarqueeView } from "./MarqueeView";
import React = require("react");
import { DocumentType } from "../../../documents/DocumentTypes";
+import { DateField } from "../../../../fields/DateField";
export const panZoomSchema = createSchema({
_panX: "number",
@@ -73,6 +74,8 @@ export type collectionFreeformViewProps = {
scaleField?: string;
noOverlay?: boolean; // used to suppress docs in the overlay (z) layer (ie, for minimap since overlay doesn't scale)
engineProps?: any;
+ dontRenderDocuments?: boolean; // used for annotation overlays which need to distribute documents into different freeformviews with different mixBlendModes depending on whether they are trnasparent or not.
+ // However, this screws up interactions since only the top layer gets events. so we render the freeformview a 3rd time with all documents in order to get interaction events (eg., marquee) but we don't actually want to display the documents.
};
@observer
@@ -110,8 +113,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
@observable _timelineRef = React.createRef<Timeline>();
@observable _marqueeRef = React.createRef<HTMLDivElement>();
@observable _keyframeEditing = false;
- @observable _focusFilters: Opt<string[]>; // docFilters that are overridden when previewing a link to an anchor which has docFilters set on it
- @observable _focusRangeFilters: Opt<string[]>; // docRangeFilters that are overridden when previewing a link to an anchor which has docRangeFilters set on it
@observable ChildDrag: DocumentView | undefined; // child document view being dragged. needed to update drop areas of groups when a group item is dragged.
@computed get views() { return this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); }
@@ -159,8 +160,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
this.layoutDoc._viewScale = vals.scale;
}
freeformData = (force?: boolean) => this.fitToContent || force ? this.fitToContentVals : undefined;
- freeformDocFilters = () => this._focusFilters || this.docFilters();
- freeformRangeDocFilters = () => this._focusRangeFilters || this.docRangeFilters();
reverseNativeScaling = () => this.fitToContent ? true : false;
panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document._panX);
panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document._panY);
@@ -261,6 +260,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
d.x = x + NumCast(d.x) - dropPos[0];
d.y = y + NumCast(d.y) - dropPos[1];
}
+ d._lastModified = new DateField();
const nd = [Doc.NativeWidth(layoutDoc), Doc.NativeHeight(layoutDoc)];
layoutDoc._width = NumCast(layoutDoc._width, 300);
layoutDoc._height = NumCast(layoutDoc._height, nd[0] && nd[1] ? nd[1] / nd[0] * NumCast(layoutDoc._width) : 300);
@@ -508,7 +508,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
const points = ge.points;
const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height);
const inkDoc = Docs.Create.InkDocument(ActiveInkColor(), CurrentUserUtils.SelectedTool, ActiveInkWidth(), ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), points,
- { title: "ink stroke", x: B.x - Number(ActiveInkWidth()) / 2, y: B.y - Number(ActiveInkWidth()) / 2, _width: B.width + Number(ActiveInkWidth()), _height: B.height + Number(ActiveInkWidth()) });
+ { title: "ink stroke", x: B.x - ActiveInkWidth() / 2, y: B.y - ActiveInkWidth() / 2, _width: B.width + ActiveInkWidth(), _height: B.height + ActiveInkWidth() });
this.addDocument(inkDoc);
e.stopPropagation();
break;
@@ -1030,8 +1030,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
ScreenToLocalTransform={childLayout.z ? this.getTransformOverlay : this.getTransform}
PanelWidth={childLayout[WidthSym]}
PanelHeight={childLayout[HeightSym]}
- docFilters={this.freeformDocFilters}
- docRangeFilters={this.freeformRangeDocFilters}
+ docFilters={this.childDocFilters}
+ docRangeFilters={this.childDocRangeFilters}
searchFilterDocs={this.searchFilterDocs}
isContentActive={this.isAnnotationOverlay ? this.props.isContentActive : returnFalse}
isDocumentActive={this.props.childDocumentsActive ? this.props.isDocumentActive : this.isContentActive}
@@ -1197,15 +1197,22 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
if (preview) {
this._focusFilters = StrListCast(Doc.GetProto(anchor).docFilters);
this._focusRangeFilters = StrListCast(Doc.GetProto(anchor).docRangeFilters);
- } else if (anchor.pivotField !== undefined) {
- this.layoutDoc._docFilters = new List<string>(StrListCast(anchor.docFilters));
- this.layoutDoc._docRangeFilters = new List<string>(StrListCast(anchor.docRangeFilters));
+ } else {
+ if (anchor.docFilters) {
+ this.layoutDoc._docFilters = new List<string>(StrListCast(anchor.docFilters));
+ }
+ if (anchor.docRangeFilters) {
+ this.layoutDoc._docRangeFilters = new List<string>(StrListCast(anchor.docRangeFilters));
+ }
}
return 0;
}
getAnchor = () => {
- const anchor = Docs.Create.TextanchorDocument({ title: StrCast(this.layoutDoc._viewType), annotationOn: this.rootDoc });
+ if (this.props.Document.annotationOn) {
+ return this.rootDoc;
+ }
+ const anchor = Docs.Create.TextanchorDocument({ title: "ViewSpec - " + StrCast(this.layoutDoc._viewType), annotationOn: this.rootDoc });
const proto = Doc.GetProto(anchor);
proto[ViewSpecPrefix + "_viewType"] = this.layoutDoc._viewType;
proto.docFilters = ObjectField.MakeCopy(this.layoutDoc.docFilters as ObjectField) || new List<string>([]);
@@ -1447,7 +1454,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
getContainerTransform={this.getContainerTransform}
getTransform={this.getTransform}
isAnnotationOverlay={this.isAnnotationOverlay}>
- <div ref={this._marqueeRef}>
+ <div ref={this._marqueeRef} style={{ display: this.props.dontRenderDocuments ? "none" : undefined }}>
{this.layoutDoc["_backgroundGrid-show"] ? this.backgroundGrid : (null)}
<CollectionFreeFormViewPannableContents
isAnnotationOverlay={this.isAnnotationOverlay}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 846d28214..81f6307d1 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -1,6 +1,6 @@
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
-import { AclAddonly, AclAdmin, AclEdit, DataSym, Doc, Opt } from "../../../../fields/Doc";
+import { AclAugment, AclAdmin, AclEdit, DataSym, Doc, Opt } from "../../../../fields/Doc";
import { Id } from "../../../../fields/FieldSymbols";
import { InkData, InkField, InkTool } from "../../../../fields/InkField";
import { List } from "../../../../fields/List";
@@ -41,7 +41,7 @@ interface MarqueeViewProps {
trySelectCluster: (addToSel: boolean) => boolean;
nudge?: (x: number, y: number, nudgeTime?: number) => boolean;
ungroup?: () => void;
- setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void;
+ setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean) => void) => void;
}
@observer
export class MarqueeView extends React.Component<SubCollectionViewProps & MarqueeViewProps>
@@ -92,7 +92,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
cm.setDefaultItem("?", (str: string) => this.props.addDocTab(
Docs.Create.WebDocument(`https://bing.com/search?q=${str}`, { _width: 400, x, y, _height: 512, _nativeWidth: 850, title: "bing", useCors: true }), "add:right"));
- cm.displayMenu(this._downX, this._downY);
+ cm.displayMenu(this._downX, this._downY, undefined, true);
e.stopPropagation();
} else
if (e.key === "u" && this.props.ungroup) {
@@ -211,7 +211,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
// allow marquee if right click OR alt+left click
if (e.button === 2 || (e.button === 0 && e.altKey)) {
// if (e.altKey || (MarqueeView.DragMarquee && this.props.active(true))) {
- this.setPreviewCursor(e.clientX, e.clientY, true);
+ this.setPreviewCursor(e.clientX, e.clientY, true, false);
// (!e.altKey) && e.stopPropagation(); // bcz: removed so that you can alt-click on button in a collection to switch link following behaviors.
e.preventDefault();
// }
@@ -284,8 +284,13 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
else if (document.getSelection()) { document.getSelection()?.empty(); }
}
- setPreviewCursor = action((x: number, y: number, drag: boolean) => {
- if (drag) {
+ setPreviewCursor = action((x: number, y: number, drag: boolean, hide: boolean) => {
+ if (hide) {
+ this._downX = this._lastX = x;
+ this._downY = this._lastY = y;
+ this._commandExecuted = false;
+ PreviewCursor.Visible = false;
+ } else if (drag) {
this._downX = this._lastX = x;
this._downY = this._lastY = y;
this._commandExecuted = false;
@@ -298,7 +303,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
this._downX = x;
this._downY = y;
const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]);
- if ([AclAdmin, AclEdit, AclAddonly].includes(effectiveAcl)) {
+ if ([AclAdmin, AclEdit, AclAugment].includes(effectiveAcl)) {
PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge);
}
this.clearSelection();
@@ -313,7 +318,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
if (!(e.nativeEvent as any).marqueeHit) {
(e.nativeEvent as any).marqueeHit = true;
if (!this.props.trySelectCluster(e.shiftKey)) {
- this.setPreviewCursor(e.clientX, e.clientY, false);
+ this.setPreviewCursor(e.clientX, e.clientY, false, false);
} else e.stopPropagation();
}
}
@@ -443,8 +448,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
syntaxHighlight = (e: KeyboardEvent | React.PointerEvent | undefined) => {
const selected = this.marqueeSelect(false);
if (e instanceof KeyboardEvent ? e.key === "i" : true) {
- const inks = selected.filter(s => s.proto?.type === DocumentType.INK);
- const setDocs = selected.filter(s => s.proto?.type === DocumentType.RTF && s.color);
+ const inks = selected.filter(s => s.type === DocumentType.INK);
+ const setDocs = selected.filter(s => s.type === DocumentType.RTF && s.color);
const sets = setDocs.map((sd) => Cast(sd.data, RichTextField)?.Text as string);
const colors = setDocs.map(sd => FieldValue(sd.color) as string);
const wordToColor = new Map<string, string>();
diff --git a/src/client/views/collections/collectionFreeForm/index.ts b/src/client/views/collections/collectionFreeForm/index.ts
new file mode 100644
index 000000000..702dc8d42
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/index.ts
@@ -0,0 +1,7 @@
+export * from "./CollectionFreeFormLayoutEngines";
+export * from "./CollectionFreeFormLinkView";
+export * from "./CollectionFreeFormLinksView";
+export * from "./CollectionFreeFormRemoteCursors";
+export * from "./CollectionFreeFormView";
+export * from "./MarqueeOptionsMenu";
+export * from "./MarqueeView"; \ No newline at end of file
diff --git a/src/client/views/collections/collectionGrid/index.ts b/src/client/views/collections/collectionGrid/index.ts
new file mode 100644
index 000000000..be5d5667a
--- /dev/null
+++ b/src/client/views/collections/collectionGrid/index.ts
@@ -0,0 +1,2 @@
+export * from "./Grid";
+export * from "./CollectionGridView"; \ No newline at end of file
diff --git a/src/client/views/collections/CollectionLinearView.scss b/src/client/views/collections/collectionLinearView/CollectionLinearView.scss
index 913a65774..24c9bc9cb 100644
--- a/src/client/views/collections/CollectionLinearView.scss
+++ b/src/client/views/collections/collectionLinearView/CollectionLinearView.scss
@@ -1,15 +1,31 @@
-@import "../global/globalCssVariables";
-@import "../_nodeModuleOverrides";
+@import "../../global/globalCssVariables";
+@import "../../_nodeModuleOverrides";
.collectionLinearView-outer {
overflow: visible;
height: 100%;
pointer-events: none;
+ &.true {
+ padding-left: 5px;
+ padding-right: 5px;
+ border-left: $standard-border;
+ background-color: $medium-blue-alt;
+ }
+
+ >input:not(:checked)~&.true {
+ background-color: transparent;
+ }
+
.collectionLinearView {
display: flex;
height: 100%;
align-items: center;
+ gap: 5px;
+
+ .collectionView {
+ overflow: visible !important;
+ }
>span {
background: $dark-gray;
@@ -22,6 +38,7 @@
.bottomPopup-background {
background: $medium-blue;
display: flex;
+ border-radius: 10px;
height: 35;
transform: translate3d(6px, 0px, 0px);
align-content: center;
@@ -40,6 +57,7 @@
}
.bottomPopup-descriptions {
+ cursor: pointer;
display: inline;
white-space: nowrap;
padding-left: 8px;
@@ -52,6 +70,7 @@
}
.bottomPopup-exit {
+ cursor: pointer;
display: inline;
white-space: nowrap;
margin-right: 10px;
@@ -64,29 +83,26 @@
}
>label {
- margin-top: "auto";
- margin-bottom: "auto";
- background: $dark-gray;
- color: $white;
- display: inline-block;
- border-radius: 18px;
- font-size: 12.5px;
- width: 18px;
- height: 18px;
- margin-top: auto;
- margin-bottom: auto;
- margin-right: 3px;
+ pointer-events: all;
cursor: pointer;
+ background-color: $medium-blue;
+ padding: 5;
+ border-radius: 2px;
+ height: 25;
+ min-width: 25;
+ margin: 0;
+ color: $white;
+ display: flex;
+ font-weight: 100;
+ width: fit-content;
transition: transform 0.2s;
- }
-
- label p {
- padding-left: 5px;
- }
+ align-items: center;
+ justify-content: center;
+ transition: 0.1s;
- label:hover {
- background: $medium-gray;
- transform: scale(1.15);
+ &:hover {
+ transform: scale(1.05);
+ }
}
>input {
@@ -107,13 +123,11 @@
display: flex;
opacity: 1;
position: relative;
- margin-top: auto;
.collectionLinearView-docBtn,
.collectionLinearView-docBtn-scalable {
position: relative;
margin: auto;
- margin-left: 3px;
transform-origin: center 80%;
}
diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/collectionLinearView/CollectionLinearView.tsx
index 52c836556..74d581946 100644
--- a/src/client/views/collections/CollectionLinearView.tsx
+++ b/src/client/views/collections/collectionLinearView/CollectionLinearView.tsx
@@ -2,21 +2,22 @@ import { Tooltip } from '@material-ui/core';
import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { Doc, HeightSym, WidthSym } from '../../../fields/Doc';
-import { documentSchema } from '../../../fields/documentSchemas';
-import { Id } from '../../../fields/FieldSymbols';
-import { makeInterface } from '../../../fields/Schema';
-import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
-import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, Utils } from '../../../Utils';
-import { DragManager } from '../../util/DragManager';
-import { Transform } from '../../util/Transform';
-import { DocumentLinksButton } from '../nodes/DocumentLinksButton';
-import { DocumentView } from '../nodes/DocumentView';
-import { LinkDescriptionPopup } from '../nodes/LinkDescriptionPopup';
-import { StyleProp } from '../StyleProvider';
+import { Doc, HeightSym, WidthSym } from '../../../../fields/Doc';
+import { documentSchema } from '../../../../fields/documentSchemas';
+import { Id } from '../../../../fields/FieldSymbols';
+import { makeInterface } from '../../../../fields/Schema';
+import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
+import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, Utils } from '../../../../Utils';
+import { DragManager } from '../../../util/DragManager';
+import { Transform } from '../../../util/Transform';
+import { DocumentLinksButton } from '../../nodes/DocumentLinksButton';
+import { DocumentView } from '../../nodes/DocumentView';
+import { LinkDescriptionPopup } from '../../nodes/LinkDescriptionPopup';
+import { StyleProp } from '../../StyleProvider';
import "./CollectionLinearView.scss";
-import { CollectionSubView } from './CollectionSubView';
-import { CollectionViewType } from './CollectionView';
+import { CollectionSubView } from '.././CollectionSubView';
+import { CollectionViewType } from '.././CollectionView';
+import { Colors, Shadows } from '../../global/globalEnums';
type LinearDocument = makeInterface<[typeof documentSchema,]>;
@@ -107,35 +108,56 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
}
render() {
- const guid = Utils.GenerateGuid();
- const flexDir: any = StrCast(this.Document.flexDirection);
+ const guid = Utils.GenerateGuid(); // Generate a unique ID to use as the label
+ const flexDir: any = StrCast(this.Document.flexDirection); // Specify direction of linear view content
+ const flexGap: number = NumCast(this.Document.flexGap); // Specify the gap between linear view content
+ const expandable: boolean = BoolCast(this.props.Document.linearViewExpandable); // Specify whether it is expandable or not
+ const floating: boolean = BoolCast(this.props.Document.linearViewFloating); // Specify whether it is expandable or not
+
const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
-
- const menuOpener = <label htmlFor={`${guid}`} style={{ pointerEvents: "all", cursor: "pointer", background: backgroundColor === color ? "black" : backgroundColor, }}
+ const icon: string = StrCast(this.props.Document.icon); // Menu opener toggle
+ const menuOpener = <label htmlFor={`${guid}`}
+ style={{
+ color: BoolCast(this.layoutDoc.linearViewIsExpanded) ? undefined : Colors.BLACK,
+ backgroundColor: backgroundColor === color ? "black" : BoolCast(this.layoutDoc.linearViewIsExpanded) ? undefined : Colors.LIGHT_GRAY
+ }}
onPointerDown={e => e.stopPropagation()} >
- <p>{BoolCast(this.layoutDoc.linearViewIsExpanded) ? "–" : "+"}</p>
+ <div className="collectionLinearView-menuOpener"
+ style={{ boxShadow: floating ? Shadows.STANDARD_SHADOW : undefined }}
+ >
+ {BoolCast(this.layoutDoc.linearViewIsExpanded) ? icon ? icon : "–" : icon ? icon : "+"}
+ </div>
</label>;
- return <div className="collectionLinearView-outer">
+ return <div className={`collectionLinearView-outer ${this.layoutDoc.linearViewSubMenu}`} style={{ backgroundColor: BoolCast(this.layoutDoc.linearViewIsExpanded) ? undefined : "transparent" }}>
<div className="collectionLinearView" ref={this.createDashEventsTarget} >
- <Tooltip title={<><div className="dash-tooltip">{BoolCast(this.layoutDoc.linearViewIsExpanded) ? "Close menu" : "Open menu"}</div></>} placement="top">
+ {!expandable ? (null) : <Tooltip title={<><div className="dash-tooltip">{BoolCast(this.props.Document.linearViewIsExpanded) ? "Close" : "Open"}</div></>} placement="top">
{menuOpener}
- </Tooltip>
- <input id={`${guid}`} type="checkbox" checked={BoolCast(this.layoutDoc.linearViewIsExpanded)} ref={this.addMenuToggle}
- onChange={action(() => this.layoutDoc.linearViewIsExpanded = this.addMenuToggle.current!.checked)} />
-
- <div className="collectionLinearView-content" style={{ height: this.dimension(), flexDirection: flexDir }}>
+ </Tooltip>}
+ <input id={`${guid}`} type="checkbox" checked={BoolCast(this.props.Document.linearViewIsExpanded)} ref={this.addMenuToggle}
+ onChange={action(() => this.props.Document.linearViewIsExpanded = this.addMenuToggle.current!.checked)} />
+
+ <div className="collectionLinearView-content"
+ style={{
+ height: this.dimension(),
+ flexDirection: flexDir,
+ gap: flexGap
+ }}>
{this.childLayoutPairs.map((pair, ind) => {
const nested = pair.layout._viewType === CollectionViewType.Linear;
const dref = React.createRef<HTMLDivElement>();
- const scalable = pair.layout.onClick || pair.layout.onDragStart;
- return <div className={`collectionLinearView-docBtn` + (scalable ? "-scalable" : "")} key={pair.layout[Id]} ref={dref}
+ const hidden = pair.layout.hidden === true;
+ // const scalable = pair.layout.onClick || pair.layout.onDragStart;
+ return hidden ? (null) : <div className={`collectionLinearView-docBtn`} key={pair.layout[Id]} ref={dref}
style={{
pointerEvents: "all",
- minWidth: 30,
- width: nested ? pair.layout[WidthSym]() : this.dimension(),
- height: nested && pair.layout.linearViewIsExpanded ? pair.layout[HeightSym]() : this.dimension(),
+ width: nested ? undefined : NumCast(pair.layout._width),
+ height: nested ? undefined : NumCast(pair.layout._height),
+ marginLeft: !nested ? 2.5 : 0,
+ marginRight: !nested ? 2.5 : 0,
+ // width: NumCast(pair.layout._width),
+ // height: NumCast(pair.layout._height),
}} >
<DocumentView
Document={pair.layout}
@@ -166,7 +188,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
</div>;
})}
</div>
- {DocumentLinksButton.StartLink ? <span className="bottomPopup-background" style={{
+ {DocumentLinksButton.StartLink && StrCast(this.layoutDoc.title) === "docked buttons" ? <span className="bottomPopup-background" style={{
pointerEvents: "all"
}}
onPointerDown={e => e.stopPropagation()} >
diff --git a/src/client/views/collections/collectionLinearView/index.ts b/src/client/views/collections/collectionLinearView/index.ts
new file mode 100644
index 000000000..ff73e14ae
--- /dev/null
+++ b/src/client/views/collections/collectionLinearView/index.ts
@@ -0,0 +1 @@
+export * from "./CollectionLinearView"; \ No newline at end of file
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
index f3a39a262..65c345547 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -237,8 +237,8 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
onDoubleClick={this.onChildDoubleClickHandler}
ScreenToLocalTransform={dxf}
focus={this.props.focus}
- docFilters={this.docFilters}
- docRangeFilters={this.docRangeFilters}
+ docFilters={this.childDocFilters}
+ docRangeFilters={this.childDocRangeFilters}
searchFilterDocs={this.searchFilterDocs}
ContainingCollectionDoc={this.props.CollectionView?.props.Document}
ContainingCollectionView={this.props.CollectionView}
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
index 29cb3511a..30836854a 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
@@ -236,9 +236,9 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
onDoubleClick={this.onChildDoubleClickHandler}
ScreenToLocalTransform={dxf}
focus={this.props.focus}
- docFilters={this.docFilters}
+ docFilters={this.childDocFilters}
isContentActive={returnFalse}
- docRangeFilters={this.docRangeFilters}
+ docRangeFilters={this.childDocRangeFilters}
searchFilterDocs={this.searchFilterDocs}
ContainingCollectionDoc={this.props.CollectionView?.props.Document}
ContainingCollectionView={this.props.CollectionView}
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx
index b2115b22e..b28c32e0e 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx
@@ -413,7 +413,7 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> {
bool = fields ? fields[1] === "check" : false;
}
return <div key={key} className="key-option" style={{
- border: "1px solid lightgray", paddingLeft: 5, textAlign: "left",
+ paddingLeft: 5, textAlign: "left",
width: this.props.width, maxWidth: this.props.width, overflowX: "hidden", background: "white", backgroundColor: "white",
}}
>
@@ -424,7 +424,7 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> {
e.target.checked === true ? Doc.setDocFilter(this.props.Document, this._key, key, "check") : Doc.setDocFilter(this.props.Document, this._key, key, "remove");
e.target.checked === true ? this.closeResultsVisibility = "contents" : console.log("");
e.target.checked === true ? this.props.col.setColor("green") : this.updateFilter();
- e.target.checked === true && SearchBox.Instance.filter === true ? Doc.setDocFilter(docs[0], this._key, key, "check") : Doc.setDocFilter(docs[0], this._key, key, "remove");
+ e.target.checked === true ? Doc.setDocFilter(docs[0], this._key, key, "check") : Doc.setDocFilter(docs[0], this._key, key, "remove");
}}
checked={bool}
/>
@@ -489,8 +489,10 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> {
}
render() {
return (
- <div style={{ display: "flex" }} ref={this.setNode}>
- <FontAwesomeIcon onClick={e => { this.props.openHeader(this.props.col, e.clientX, e.clientY); e.stopPropagation(); }} icon={this.props.icon} size="lg" style={{ display: "inline", paddingBottom: "1px", paddingTop: "4px", cursor: "hand" }} />
+ <div style={{ display: "flex", width: '100%', alignContent: 'center', alignItems: 'center' }} ref={this.setNode}>
+ <div className="schema-icon" onClick={e => { this.props.openHeader(this.props.col, e.clientX, e.clientY); e.stopPropagation(); }}>
+ <FontAwesomeIcon icon={this.props.icon} size="lg" style={{ display: "inline" }} />
+ </div>
{/* <FontAwesomeIcon icon={fa.faSearchMinus} size="lg" style={{ display: "inline", paddingBottom: "1px", paddingTop: "4px", cursor: "hand" }} onClick={e => {
runInAction(() => { this._isOpen === undefined ? this._isOpen = true : this._isOpen = !this._isOpen })
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx
index f48906ba5..0e19ef3d9 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx
@@ -134,9 +134,9 @@ export class MovableRow extends React.Component<MovableRowProps> {
<div className="collectionSchema-row-wrapper" onKeyPress={this.onKeyDown} ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
<ReactTableDefaults.TrComponent onKeyPress={this.onKeyDown} >
<div className="row-dragger">
- <div className="row-option" style={{ left: 5 }} onClick={undoBatch(() => this.props.removeDoc(this.props.rowInfo.original))}><FontAwesomeIcon icon="trash" size="sm" /></div>
- <div className="row-option" style={{ cursor: "grab", left: 25 }} ref={reference} onPointerDown={onItemDown}><FontAwesomeIcon icon="grip-vertical" size="sm" /></div>
- <div className="row-option" style={{ left: 40 }} onClick={() => this.props.addDocTab(this.props.rowInfo.original, "add:right")}><FontAwesomeIcon icon="external-link-alt" size="sm" /></div>
+ <div className="row-option" onClick={undoBatch(() => this.props.removeDoc(this.props.rowInfo.original))}><FontAwesomeIcon icon="trash" size="sm" /></div>
+ <div className="row-option" style={{ cursor: "grab" }} ref={reference} onPointerDown={onItemDown}><FontAwesomeIcon icon="grip-vertical" size="sm" /></div>
+ <div className="row-option" onClick={() => this.props.addDocTab(this.props.rowInfo.original, "add:right")}><FontAwesomeIcon icon="external-link-alt" size="sm" /></div>
</div>
{children}
</ReactTableDefaults.TrComponent>
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
index 40cdcd14b..3074ce66e 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
@@ -108,9 +108,7 @@
}
.rt-th {
padding: 0;
- border: solid lightgray;
- border-width: 0 1px;
- border-bottom: 2px solid lightgray;
+ border-left: solid 1px $light-gray;
}
}
.rt-th {
@@ -213,6 +211,8 @@
}
}
+
+
.collectionSchemaView-header {
height: 100%;
color: gray;
@@ -227,6 +227,15 @@ button.add-column {
width: 28px;
}
+.collectionSchemaView-menuOptions-wrapper {
+ background: rgb(241, 239, 235);
+ display: flex;
+ cursor: default;
+ height: 100%;
+ align-content: center;
+ align-items: center;
+}
+
.collectionSchema-header-menuOptions {
color: black;
width: 180px;
@@ -272,6 +281,9 @@ button.add-column {
width: 10px;
}
}
+
+
+
.keys-dropdown {
position: relative;
//width: 100%;
@@ -287,26 +299,7 @@ button.add-column {
font-weight: normal;
}
}
- .keys-options-wrapper {
- width: 100%;
- max-height: 150px;
- overflow-y: scroll;
- position: absolute;
- top: 28px;
- box-shadow: 0 10px 16px rgba(0, 0, 0, 0.1);
- background-color: white;
- .key-option {
- background-color: white;
- border: 1px solid lightgray;
- padding: 2px 3px;
- &:not(:first-child) {
- border-top: 0;
- }
- &:hover {
- background-color: $light-gray;
- }
- }
- }
+
}
.columnMenu-colors {
display: flex;
@@ -325,11 +318,53 @@ button.add-column {
}
}
+.schema-icon {
+ cursor: pointer;
+ width: 25px;
+ height: 25px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ align-content: center;
+ background-color: $medium-blue;
+ color: white;
+ margin-right: 5px;
+ font-size: 10px;
+ border-radius: 3px;
+
+}
+
+.keys-options-wrapper {
+ position: absolute;
+ text-align: left;
+ height: fit-content;
+ top: 100%;
+ z-index: 21;
+ background-color: #ffffff;
+ box-shadow: 0px 3px 4px rgba(0,0,0,30%);
+ padding: 1px;
+ .key-option {
+ cursor: pointer;
+ color: #000000;
+ width: 100%;
+ height: 25px;
+ font-weight: 400;
+ display: flex;
+ justify-content: left;
+ align-items: center;
+ padding-left: 5px;
+ &:hover {
+ background-color: $light-gray;
+ }
+ }
+}
+
.collectionSchema-row {
height: 100%;
background-color: white;
&.row-focused .rt-td {
- background-color: #bfffc0; //$light-gray;
+ background-color: $light-blue; //$light-gray;
+ overflow: visible;
}
&.row-wrapped {
.rt-td {
@@ -338,39 +373,40 @@ button.add-column {
}
.row-dragger {
display: flex;
- justify-content: space-around;
- //flex: 50 0 auto;
- width: 0;
- max-width: 50px;
- //height: 100%;
+ justify-content: space-evenly;
+ width: 58px;
+ position: absolute;
+ /* max-width: 50px; */
min-height: 30px;
align-items: center;
color: lightgray;
background-color: white;
transition: color 0.1s ease;
.row-option {
- // padding: 5px;
+ color: black;
cursor: pointer;
- position: absolute;
+ position: relative;
transition: color 0.1s ease;
display: flex;
flex-direction: column;
justify-content: center;
z-index: 2;
+ border-radius: 3px;
+ padding: 3px;
&:hover {
- color: gray;
+ background-color: $light-gray;
}
}
}
.collectionSchema-row-wrapper {
&.row-above {
- border-top: 1px solid red;
+ border-top: 1px solid $medium-blue;
}
&.row-below {
- border-bottom: 1px solid red;
+ border-bottom: 1px solid $medium-blue;
}
&.row-inside {
- border: 1px solid red;
+ border: 2px dashed $medium-blue;
}
.row-dragging {
background-color: blue;
@@ -383,24 +419,32 @@ button.add-column {
height: unset;
}
+.collectionSchemaView-cellContents {
+ width: 100%;
+}
+
.collectionSchemaView-cellWrapper {
+ display: flex;
height: 100%;
- padding: 4px;
text-align: left;
padding-left: 19px;
position: relative;
+ align-items: center;
+ align-content: center;
&:focus {
outline: none;
}
&.editing {
padding: 0;
+ box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3);
+ transform: scale(1.1);
+ z-index: 40;
input {
outline: 0;
border: none;
- background-color: rgb(255, 217, 217);
+ background-color: $white;
width: 100%;
height: 100%;
- padding: 2px 3px;
min-height: 26px;
}
}
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index 585cda729..fed64b620 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -413,8 +413,8 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
isContentActive={returnTrue}
isDocumentActive={returnFalse}
ScreenToLocalTransform={this.getPreviewTransform}
- docFilters={this.docFilters}
- docRangeFilters={this.docRangeFilters}
+ docFilters={this.childDocFilters}
+ docRangeFilters={this.childDocRangeFilters}
searchFilterDocs={this.searchFilterDocs}
styleProvider={DefaultStyleProvider}
layerProvider={undefined}
diff --git a/src/client/views/collections/collectionSchema/SchemaTable.tsx b/src/client/views/collections/collectionSchema/SchemaTable.tsx
index de08c327a..1cf466cba 100644
--- a/src/client/views/collections/collectionSchema/SchemaTable.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTable.tsx
@@ -194,9 +194,10 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
const sortIcon = col.desc === undefined ? "caret-right" : col.desc === true ? "caret-down" : "caret-up";
const header = <div className="collectionSchemaView-menuOptions-wrapper" style={{ background: col.color, padding: "2px", display: "flex", cursor: "default", height: "100%", }}>
{keysDropdown}
- <div onClick={e => this.changeSorting(col)} style={{ width: 21, padding: 1, display: "inline", zIndex: 1, background: "inherit", cursor: "hand" }}>
+ <div onClick={e => this.changeSorting(col)} style={{ width: 21, padding: 1, display: "inline", zIndex: 1, background: "inherit", cursor: "pointer" }}>
<FontAwesomeIcon icon={sortIcon} size="lg" />
</div>
+ {/* {this.props.Document._chromeHidden || this.props.addDocument == returnFalse ? undefined : <div className="collectionSchemaView-addRow" onClick={this.createRow}>+ new</div>} */}
</div>;
return {
@@ -561,7 +562,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
onPointerDown={this.props.onPointerDown} onClick={this.props.onClick} onWheel={e => this.props.active(true) && e.stopPropagation()}
onDrop={e => this.props.onDrop(e, {})} onContextMenu={this.onContextMenu} >
{this.reactTable}
- {this.props.Document._chromeHidden ? undefined : <div className="collectionSchemaView-addRow" onClick={this.createRow}>+ new</div>}
+ {this.props.Document._chromeHidden || this.props.addDocument == returnFalse ? undefined : <div className="collectionSchemaView-addRow" onClick={this.createRow}>+ new</div>}
{!this._showDoc ? (null) :
<div className="collectionSchemaView-documentPreview" ref="overlay"
style={{
diff --git a/src/client/views/global/globalCssVariables.scss b/src/client/views/global/globalCssVariables.scss
index 1b881ba43..caa9f4fe5 100644
--- a/src/client/views/global/globalCssVariables.scss
+++ b/src/client/views/global/globalCssVariables.scss
@@ -1,20 +1,23 @@
-@import url("https://fonts.googleapis.com/css?family=Noto+Sans:400,700|Crimson+Text:400,400i,700");
+@import url("https://fonts.googleapis.com/css2?family=Roboto&display=swap");
// colors
$white: #ffffff;
-$light-gray:#dfdfdf;
-$medium-gray: #9F9F9F;
+$off-white: #fdfdfd;
+$light-gray: #dfdfdf;
+$medium-gray: #9f9f9f;
$dark-gray: #323232;
$black: #000000;
-$light-blue: #BDDDF5;
-$medium-blue: #4476F7;
-$pink: #E0217D;
-$yellow: #F5D747;
+$light-blue: #bdddf5;
+$medium-blue: #4476f7;
+$medium-blue-alt: #4476f73d;
+$pink: #e0217d;
+$yellow: #f5d747;
$close-red: #e48282;
$drop-shadow: "#32323215";
+
//padding
$minimum-padding: 4px;
$medium-padding: 16px;
@@ -24,7 +27,8 @@ $large-padding: 32px;
$icon-size: 28px;
// fonts
-$sans-serif: "Noto Sans", sans-serif;
+$sans-serif: "Roboto",
+sans-serif;
$large-header: 16px;
$body-text: 12px;
$small-text: 9px;
@@ -41,7 +45,14 @@ $contextMenu-zindex: 100000; // context menu shows up over everything
$radialMenu-zindex: 100000; // context menu shows up over everything
// borders
-$standard-border: solid 1px #9F9F9F;
+$standard-border: solid 1px #9f9f9f;
+
+// border radius
+$standard-border-radius: 3px;
+
+// shadow
+$standard-box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3);
+
$searchpanel-height: 32px;
$mainTextInput-zindex: 999; // then text input overlay so that it's context menu will appear over decorations, etc
@@ -49,7 +60,7 @@ $docDecorations-zindex: 998; // then doc decorations appear over everything else
$remoteCursors-zindex: 997; // ... not sure what level the remote cursors should go -- is this right?
$COLLECTION_BORDER_WIDTH: 0;
$SCHEMA_DIVIDER_WIDTH: 4;
-$MINIMIZED_ICON_SIZE:24;
+$MINIMIZED_ICON_SIZE: 24;
$MAX_ROW_HEIGHT: 44px;
$DFLT_IMAGE_NATIVE_DIM: 900px;
$MENU_PANEL_WIDTH: 60px;
diff --git a/src/client/views/global/globalEnums.tsx b/src/client/views/global/globalEnums.tsx
index 2aeb8e338..56779c37c 100644
--- a/src/client/views/global/globalEnums.tsx
+++ b/src/client/views/global/globalEnums.tsx
@@ -5,6 +5,7 @@ export enum Colors {
LIGHT_GRAY = "#DFDFDF",
WHITE = "#FFFFFF",
MEDIUM_BLUE = "#4476F7",
+ MEDIUM_BLUE_ALT = "#4476f73d", // REDUCED OPACITY
LIGHT_BLUE = "#BDDDF5",
PINK = "#E0217D",
YELLOW = "#F5D747",
@@ -35,4 +36,8 @@ export enum IconSizes {
export enum Borders {
STANDARD = "solid 1px #9F9F9F"
+}
+
+export enum Shadows {
+ STANDARD_SHADOW = "0px 3px 4px rgba(0, 0, 0, 0.3)"
} \ No newline at end of file
diff --git a/src/client/views/linking/LinkEditor.scss b/src/client/views/linking/LinkEditor.scss
index e45a91d57..abd413f57 100644
--- a/src/client/views/linking/LinkEditor.scss
+++ b/src/client/views/linking/LinkEditor.scss
@@ -106,6 +106,24 @@
}
}
+.linkEditor-relationship-dropdown {
+ position: absolute;
+ width: 154px;
+ max-height: 90px;
+ overflow: auto;
+ background: white;
+
+ p {
+ padding: 3px;
+ cursor: pointer;
+ border: 1px solid $medium-gray;
+ }
+
+ p:hover {
+ background: $light-blue;
+ }
+}
+
.linkEditor-followingDropdown {
padding-left: 26px;
padding-right: 6.5px;
diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx
index f74b422d3..fba95cad2 100644
--- a/src/client/views/linking/LinkEditor.tsx
+++ b/src/client/views/linking/LinkEditor.tsx
@@ -2,11 +2,12 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Tooltip } from "@material-ui/core";
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
-import { Doc } from "../../../fields/Doc";
+import { Doc, StrListCast } from "../../../fields/Doc";
import { DateCast, StrCast } from "../../../fields/Types";
import { LinkManager } from "../../util/LinkManager";
import { undoBatch } from "../../util/UndoManager";
import './LinkEditor.scss';
+import { LinkRelationshipSearch } from "./LinkRelationshipSearch";
import React = require("react");
@@ -26,6 +27,8 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
@computed get infoIcon() { if (this.showInfo) { return "chevron-up"; } return "chevron-down"; }
@observable private buttonColor: string = "";
@observable private relationshipButtonColor: string = "";
+ @observable private relationshipSearchVisibility: string = "none";
+ @observable private searchIsActive: boolean = false;
//@observable description = this.props.linkDoc.description ? StrCast(this.props.linkDoc.description) : "DESCRIPTION";
@@ -39,12 +42,40 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
setRelationshipValue = action((value: string) => {
if (LinkManager.currentLink) {
LinkManager.currentLink.linkRelationship = value;
+ const linkRelationshipList = StrListCast(Doc.UserDoc().linkRelationshipList);
+ const linkColorList = StrListCast(Doc.UserDoc().linkColorList);
+ // if the relationship does not exist in the list, add it and a corresponding unique randomly generated color
+ if (linkRelationshipList && !linkRelationshipList.includes(value)) {
+ linkRelationshipList.push(value);
+ const randColor = "rgb(" + Math.floor(Math.random() * 255) + "," + Math.floor(Math.random() * 255) + "," + Math.floor(Math.random() * 255) + ")";
+ linkColorList.push(randColor)
+ }
this.relationshipButtonColor = "rgb(62, 133, 55)";
setTimeout(action(() => this.relationshipButtonColor = ""), 750);
return true;
}
});
+ /**
+ * returns list of strings with possible existing relationships that contain what is currently in the input field
+ */
+ @action
+ getRelationshipResults = () => {
+ const query = this.relationship; //current content in input box
+ const linkRelationshipList = StrListCast(Doc.UserDoc().linkRelationshipList)
+ if (linkRelationshipList) {
+ return linkRelationshipList.filter(rel => rel.includes(query));
+ }
+ }
+
+ /**
+ * toggles visibility of the relationship search results when the input field is focused on
+ */
+ @action
+ toggleRelationshipResults = () => {
+ this.relationshipSearchVisibility = this.relationshipSearchVisibility == "none" ? "block" : "none";
+ }
+
@undoBatch
setDescripValue = action((value: string) => {
if (LinkManager.currentLink) {
@@ -55,7 +86,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
}
});
- onKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
+ onDescriptionKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
this.setDescripValue(this.description);
document.getElementById('input')?.blur();
@@ -69,16 +100,38 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
}
}
- onDown = () => this.setDescripValue(this.description);
- onRelationshipDown = () => this.setRelationshipValue(this.description);
+ onDescriptionDown = () => this.setDescripValue(this.description);
+ onRelationshipDown = () => this.setRelationshipValue(this.relationship);
+
+ onBlur = () => {
+ //only hide the search results if the user clicks out of the input AND not on any of the search results
+ // i.e. if search is not active
+ if (!this.searchIsActive) {
+ this.toggleRelationshipResults();
+ }
+ }
+ onFocus = () => {
+ this.toggleRelationshipResults();
+ }
+ toggleSearchIsActive = () => {
+ this.searchIsActive = !this.searchIsActive;
+ }
@action
- handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.description = e.target.value; }
+ handleDescriptionChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.description = e.target.value; }
@action
- handleRelationshipChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.relationship = e.target.value; }
-
+ handleRelationshipChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this.relationship = e.target.value;
+ }
+ @action
+ handleRelationshipSearchChange = (result: string) => {
+ this.setRelationshipValue(result);
+ this.toggleRelationshipResults();
+ this.relationship = result;
+ }
@computed
get editRelationship() {
+ //NOTE: confusingly, the classnames for the following relationship JSX elements are the same as the for the description elements for shared CSS
return <div className="linkEditor-description">
<div className="linkEditor-description-label">Link Relationship:</div>
<div className="linkEditor-description-input">
@@ -87,11 +140,18 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
style={{ width: "100%" }}
id="input"
value={this.relationship}
- placeholder={"enter link label"}
- // color={"rgb(88, 88, 88)"}
+ placeholder={"Enter link relationship"}
onKeyDown={this.onRelationshipKey}
onChange={this.handleRelationshipChange}
+ onFocus={this.onFocus}
+ onBlur={this.onBlur}
></input>
+ <LinkRelationshipSearch
+ results={this.getRelationshipResults()}
+ display={this.relationshipSearchVisibility}
+ handleRelationshipSearchChange={this.handleRelationshipSearchChange}
+ toggleSearch={this.toggleSearchIsActive}
+ />
</div>
<div className="linkEditor-description-add-button"
style={{ background: this.relationshipButtonColor }}
@@ -110,15 +170,14 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
style={{ width: "100%" }}
id="input"
value={this.description}
- placeholder={"enter link label"}
- // color={"rgb(88, 88, 88)"}
- onKeyDown={this.onKey}
- onChange={this.handleChange}
+ placeholder={"Enter link description"}
+ onKeyDown={this.onDescriptionKey}
+ onChange={this.handleDescriptionChange}
></input>
</div>
<div className="linkEditor-description-add-button"
style={{ background: this.buttonColor }}
- onPointerDown={this.onDown}>Set</div>
+ onPointerDown={this.onDescriptionDown}>Set</div>
</div>
</div>;
}
@@ -149,35 +208,35 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
<div className="linkEditor-followingDropdown-option"
onPointerDown={() => this.changeFollowBehavior("default")}>
Default
- </div>
+ </div>
<div className="linkEditor-followingDropdown-option"
onPointerDown={() => this.changeFollowBehavior("add:left")}>
Always open in new left pane
- </div>
+ </div>
<div className="linkEditor-followingDropdown-option"
onPointerDown={() => this.changeFollowBehavior("add:right")}>
Always open in new right pane
- </div>
+ </div>
<div className="linkEditor-followingDropdown-option"
onPointerDown={() => this.changeFollowBehavior("replace:right")}>
Always replace right tab
- </div>
+ </div>
<div className="linkEditor-followingDropdown-option"
onPointerDown={() => this.changeFollowBehavior("replace:left")}>
Always replace left tab
- </div>
+ </div>
<div className="linkEditor-followingDropdown-option"
onPointerDown={() => this.changeFollowBehavior("fullScreen")}>
Always open full screen
- </div>
+ </div>
<div className="linkEditor-followingDropdown-option"
onPointerDown={() => this.changeFollowBehavior("add")}>
Always open in a new tab
- </div>
+ </div>
<div className="linkEditor-followingDropdown-option"
onPointerDown={() => this.changeFollowBehavior("replace")}>
Replace Tab
- </div>
+ </div>
{this.props.linkDoc.linksToAnnotation ?
<div className="linkEditor-followingDropdown-option"
onPointerDown={() => this.changeFollowBehavior("openExternal")}>
diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx
index 6fc860447..53fe3f682 100644
--- a/src/client/views/linking/LinkMenu.tsx
+++ b/src/client/views/linking/LinkMenu.tsx
@@ -41,7 +41,7 @@ export class LinkMenu extends React.Component<Props> {
/**
* maps each link to a JSX element to be rendered
- * @param groups LinkManager containing info of all of the links
+ * @param groups containing info of all of the links
* @returns list of link JSX elements if there at least one linked element
*/
renderAllGroups = (groups: Map<string, Array<Doc>>): Array<JSX.Element> => {
diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx
index c7586a467..cb6571f92 100644
--- a/src/client/views/linking/LinkMenuGroup.tsx
+++ b/src/client/views/linking/LinkMenuGroup.tsx
@@ -1,5 +1,5 @@
import { observer } from "mobx-react";
-import { Doc } from "../../../fields/Doc";
+import { Doc, StrListCast } from "../../../fields/Doc";
import { Id } from "../../../fields/FieldSymbols";
import { Cast } from "../../../fields/Types";
import { LinkManager } from "../../util/LinkManager";
@@ -20,6 +20,23 @@ interface LinkMenuGroupProps {
export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
private _menuRef = React.createRef<HTMLDivElement>();
+ getBackgroundColor = (): string => {
+ const linkRelationshipList = StrListCast(Doc.UserDoc().linkRelationshipList);
+ const linkColorList = StrListCast(Doc.UserDoc().linkColorList);
+ let color = "white";
+ // if this link's relationship property is not default "link", set its color
+ if (linkRelationshipList) {
+ const relationshipIndex = linkRelationshipList.indexOf(this.props.groupType);
+ const RGBcolor: string = linkColorList[relationshipIndex];
+ if (RGBcolor) {
+ //set opacity to 0.25 by modifiying the rgb string
+ color = RGBcolor.slice(0, RGBcolor.length - 1) + ", 0.25)";
+ console.log(color);
+ }
+ }
+ return color;
+ }
+
render() {
const set = new Set<Doc>(this.props.group);
const groupItems = Array.from(set.keys()).map(linkDoc => {
@@ -39,7 +56,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
return (
<div className="linkMenu-group" ref={this._menuRef}>
- <div className="linkMenu-group-name">
+ <div className="linkMenu-group-name" style={{ background: this.getBackgroundColor() }}>
<p className={this.props.groupType === "*" || this.props.groupType === "" ? "" : "expand-one"}> {this.props.groupType}:</p>
</div>
<div className="linkMenu-group-wrapper">
diff --git a/src/client/views/linking/LinkPopup.scss b/src/client/views/linking/LinkPopup.scss
index 8ae65158d..60c9ebfcd 100644
--- a/src/client/views/linking/LinkPopup.scss
+++ b/src/client/views/linking/LinkPopup.scss
@@ -5,7 +5,7 @@
height: 200px;
width: 200px;
position: absolute;
- padding: 15px;
+ // padding: 15px;
border-radius: 3px;
input {
diff --git a/src/client/views/linking/LinkPopup.tsx b/src/client/views/linking/LinkPopup.tsx
index 2c4b718f4..c8be9069c 100644
--- a/src/client/views/linking/LinkPopup.tsx
+++ b/src/client/views/linking/LinkPopup.tsx
@@ -1,33 +1,21 @@
-import { IconProp } from '@fortawesome/fontawesome-svg-core';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Tooltip } from '@material-ui/core';
-import { action, observable, runInAction } from 'mobx';
+import { action, observable } from 'mobx';
import { observer } from "mobx-react";
-import { Doc, DocListCast } from '../../../fields/Doc';
-import { Cast, StrCast } from '../../../fields/Types';
-import { WebField } from '../../../fields/URLField';
-import { emptyFunction, setupMoveUpEvents, returnFalse, returnTrue, returnEmptyDoclist, returnEmptyFilter } from '../../../Utils';
-import { DocumentType } from '../../documents/DocumentTypes';
-import { DocumentManager } from '../../util/DocumentManager';
-import { DragManager } from '../../util/DragManager';
-import { Hypothesis } from '../../util/HypothesisUtils';
-import { LinkManager } from '../../util/LinkManager';
-import { undoBatch } from '../../util/UndoManager';
-import { DocumentLinksButton } from '../nodes/DocumentLinksButton';
-import { DocumentView, DocumentViewSharedProps } from '../nodes/DocumentView';
-import { LinkDocPreview } from '../nodes/LinkDocPreview';
-import './LinkPopup.scss';
-import React = require("react");
+import { EditorView } from 'prosemirror-view';
+import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../../Utils';
+import { DocUtils } from '../../documents/Documents';
import { CurrentUserUtils } from '../../util/CurrentUserUtils';
-import { DefaultStyleProvider } from '../StyleProvider';
import { Transform } from '../../util/Transform';
-import { DocUtils } from '../../documents/Documents';
-import { SearchBox } from '../search/SearchBox';
-import { EditorView } from 'prosemirror-view';
+import { undoBatch } from '../../util/UndoManager';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
+import { SearchBox } from '../search/SearchBox';
+import { DefaultStyleProvider } from '../StyleProvider';
+import './LinkPopup.scss';
+import React = require("react");
+import { Doc, Opt } from '../../../fields/Doc';
interface LinkPopupProps {
showPopup: boolean;
+ linkFrom?: () => Doc | undefined;
// groupType: string;
// linkDoc: Doc;
// docView: DocumentView;
@@ -54,7 +42,6 @@ export class LinkPopup extends React.Component<LinkPopupProps> {
@action
onLinkChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.linkURL = e.target.value;
- console.log(this.linkURL)
}
@@ -63,9 +50,10 @@ export class LinkPopup extends React.Component<LinkPopupProps> {
render() {
const popupVisibility = this.props.showPopup ? "block" : "none";
+ const linkDoc = this.props.linkFrom ? this.props.linkFrom : undefined;
return (
<div className="linkPopup-container" style={{ display: popupVisibility }}>
- <div className="linkPopup-url-container">
+ {/* <div className="linkPopup-url-container">
<input autoComplete="off" type="text" value={this.linkURL} placeholder="Enter URL..." onChange={this.onLinkChange} />
<button onPointerDown={e => this.makeLinkToURL(this.linkURL, "add:right")}
style={{ display: "block", margin: "10px auto", }}>Apply hyperlink</button>
@@ -73,14 +61,17 @@ export class LinkPopup extends React.Component<LinkPopupProps> {
<div className="divider">
<div className="line"></div>
<p className="divider-text">or</p>
- </div>
+ </div> */}
<div className="linkPopup-document-search-container">
{/* <i></i>
<input defaultValue={""} autoComplete="off" type="text" placeholder="Search for Document..." id="search-input"
className="linkPopup-searchBox searchBox-input" /> */}
- <SearchBox Document={CurrentUserUtils.MySearchPanelDoc}
+ <SearchBox
+ Document={CurrentUserUtils.MySearchPanelDoc}
DataDoc={CurrentUserUtils.MySearchPanelDoc}
+ linkFrom={linkDoc}
+ linkSearch={true}
fieldKey="data"
dropAction="move"
isSelected={returnTrue}
diff --git a/src/client/views/linking/LinkRelationshipSearch.tsx b/src/client/views/linking/LinkRelationshipSearch.tsx
new file mode 100644
index 000000000..8f83f0e6e
--- /dev/null
+++ b/src/client/views/linking/LinkRelationshipSearch.tsx
@@ -0,0 +1,63 @@
+import { observer } from "mobx-react";
+import './LinkEditor.scss';
+import React = require("react");
+
+interface LinkRelationshipSearchProps {
+ results: string[] | undefined;
+ display: string;
+ //callback fn to set rel + hide dropdown upon setting
+ handleRelationshipSearchChange: (result: string) => void;
+ toggleSearch: () => void;
+}
+@observer
+export class LinkRelationshipSearch extends React.Component<LinkRelationshipSearchProps> {
+
+ handleResultClick = (e: React.MouseEvent) => {
+ const relationship = (e.target as HTMLParagraphElement).textContent;
+ if (relationship) {
+ this.props.handleRelationshipSearchChange(relationship)
+ }
+ }
+
+ handleMouseEnter = () => {
+ this.props.toggleSearch();
+ }
+
+ handleMouseLeave = () => {
+ this.props.toggleSearch();
+ }
+
+ /**
+ * Render an empty div to increase the height of LinkEditor to accommodate 2+ results
+ */
+ emptyDiv = () => {
+ if (this.props.results && this.props.results.length > 2 && this.props.display == "block") {
+ return <div style={{ height: "50px" }}></div>
+ }
+ }
+
+ render() {
+ return (
+ <div className="linkEditor-relationship-dropdown-container">
+ <div className="linkEditor-relationship-dropdown"
+ style={{ display: this.props.display }}
+ onMouseEnter={this.handleMouseEnter}
+ onMouseLeave={this.handleMouseLeave}
+ >
+ { // return a dropdown of relationship results if there exist results
+ this.props.results
+ ? this.props.results.map(result => {
+ return (<p key={result} onClick={this.handleResultClick}>
+ {result}
+ </p>)
+ })
+ : <p>No matching relationships</p>
+ }
+ </div>
+
+ {/*Render an empty div to increase the height of LinkEditor to accommodate 2+ results */}
+ {this.emptyDiv()}
+ </div>
+ )
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index bcc8e042a..a3f03fc4b 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -16,7 +16,7 @@ import { makeInterface } from "../../../fields/Schema";
import { ComputedField } from "../../../fields/ScriptField";
import { Cast, NumCast } from "../../../fields/Types";
import { AudioField, nullAudio } from "../../../fields/URLField";
-import { emptyFunction, formatTime, Utils } from "../../../Utils";
+import { emptyFunction, formatTime } from "../../../Utils";
import { DocUtils } from "../../documents/Documents";
import { Networking } from "../../Network";
import { CurrentUserUtils } from "../../util/CurrentUserUtils";
@@ -312,9 +312,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<
this._recorder.ondataavailable = async (e: any) => {
const [{ result }] = await Networking.UploadFilesToServer(e.data);
if (!(result instanceof Error)) {
- this.props.Document[this.props.fieldKey] = new AudioField(
- Utils.prepend(result.accessPaths.agnostic.client)
- );
+ this.props.Document[this.props.fieldKey] = new AudioField(result.accessPaths.agnostic.client);
}
};
this._recordStart = new Date().getTime();
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 3d2cdf5a4..544125ede 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -23,7 +23,7 @@ import "./DocumentView.scss";
import { EquationBox } from "./EquationBox";
import { FieldView, FieldViewProps } from "./FieldView";
import { FilterBox } from "./FilterBox";
-import { FontIconBox } from "./FontIconBox";
+import { FontIconBox } from "./button/FontIconBox";
import { FormattedTextBox, FormattedTextBoxProps } from "./formattedText/FormattedTextBox";
import { FunctionPlotBox } from "./FunctionPlotBox";
import { ImageBox } from "./ImageBox";
diff --git a/src/client/views/nodes/DocumentLinksButton.scss b/src/client/views/nodes/DocumentLinksButton.scss
index b37b68249..228e1bdcb 100644
--- a/src/client/views/nodes/DocumentLinksButton.scss
+++ b/src/client/views/nodes/DocumentLinksButton.scss
@@ -50,6 +50,7 @@
width: 80%;
height: 80%;
font-size: 100%;
+ font-family: 'Roboto';
transition: 0.2s ease all;
&:hover {
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index 482f551b5..93cd02d93 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -2,25 +2,24 @@ 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, Opt, WidthSym, DocListCastAsync } from "../../../fields/Doc";
-import { emptyFunction, setupMoveUpEvents, returnFalse, Utils, emptyPath } from "../../../Utils";
+import { Doc, DocListCast, DocListCastAsync, Opt, WidthSym } from "../../../fields/Doc";
+import { Id } from "../../../fields/FieldSymbols";
+import { Cast, StrCast } from "../../../fields/Types";
import { TraceMobx } from "../../../fields/util";
-import { DocUtils, Docs } from "../../documents/Documents";
+import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../../Utils";
+import { DocServer } from "../../DocServer";
+import { Docs, DocUtils } from "../../documents/Documents";
import { DragManager } from "../../util/DragManager";
+import { Hypothesis } from "../../util/HypothesisUtils";
import { LinkManager } from "../../util/LinkManager";
import { undoBatch, UndoManager } from "../../util/UndoManager";
+import { Colors } from "../global/globalEnums";
+import { LightboxView } from "../LightboxView";
+import './DocumentLinksButton.scss';
import { DocumentView } from "./DocumentView";
-import { StrCast, Cast } from "../../../fields/Types";
import { LinkDescriptionPopup } from "./LinkDescriptionPopup";
-import { Hypothesis } from "../../util/HypothesisUtils";
-import { Id } from "../../../fields/FieldSymbols";
import { TaskCompletionBox } from "./TaskCompletedBox";
import React = require("react");
-import './DocumentLinksButton.scss';
-import { DocServer } from "../../DocServer";
-import { LightboxView } from "../LightboxView";
-import { cat } from "shelljs";
-import { Colors } from "../global/globalEnums";
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
@@ -114,7 +113,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
DocumentLinksButton.StartLink = this.props.View.props.Document;
DocumentLinksButton.StartLinkView = this.props.View;
}
- };
+ }
}));
}
@@ -198,7 +197,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
Doc.GetProto(linkDoc as Doc).linksToAnnotation = true;
Doc.GetProto(linkDoc as Doc).annotationId = DocumentLinksButton.AnnotationId;
Doc.GetProto(linkDoc as Doc).annotationUri = DocumentLinksButton.AnnotationUri;
- const dashHyperlink = Utils.prepend("/doc/" + (startIsAnnotation ? endLink[Id] : startLink[Id]));
+ const dashHyperlink = Doc.globalServerPath(startIsAnnotation ? endLink : startLink);
Hypothesis.makeLink(StrCast(startIsAnnotation ? endLink.title : startLink.title), dashHyperlink, DocumentLinksButton.AnnotationId,
(startIsAnnotation ? startLink : endLink)); // edit annotation to add a Dash hyperlink to the linked doc
}
@@ -266,6 +265,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
style={{
backgroundColor: Colors.LIGHT_BLUE,
color: Colors.BLACK,
+ fontSize: "20px",
width: btnDim,
height: btnDim,
}}>
@@ -291,7 +291,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
(null)
}
</div>
- )
+ );
}
render() {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 23166335e..2bd79df64 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -13,7 +13,7 @@ import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from "../../../fields/Ty
import { AudioField } from "../../../fields/URLField";
import { GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util';
import { MobileInterface } from '../../../mobile/MobileInterface';
-import { emptyFunction, hasDescendantTarget, OmitKeys, returnVal, Utils, returnTrue } from "../../../Utils";
+import { emptyFunction, hasDescendantTarget, OmitKeys, returnTrue, returnVal, Utils } from "../../../Utils";
import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
import { Docs, DocUtils } from "../../documents/Documents";
import { DocumentType } from '../../documents/DocumentTypes';
@@ -25,6 +25,7 @@ import { InteractionUtils } from '../../util/InteractionUtils';
import { LinkManager } from '../../util/LinkManager';
import { Scripting } from '../../util/Scripting';
import { SelectionManager } from "../../util/SelectionManager";
+import { ColorScheme } from "../../util/SettingsManager";
import { SharingManager } from '../../util/SharingManager';
import { SnappingManager } from '../../util/SnappingManager';
import { Transform } from "../../util/Transform";
@@ -41,13 +42,13 @@ import { CollectionFreeFormDocumentView } from "./CollectionFreeFormDocumentView
import { DocumentContentsView } from "./DocumentContentsView";
import { DocumentLinksButton } from './DocumentLinksButton';
import "./DocumentView.scss";
+import { FormattedTextBox } from "./formattedText/FormattedTextBox";
import { LinkAnchorBox } from './LinkAnchorBox';
import { LinkDocPreview } from "./LinkDocPreview";
-import { PresBox } from './trails/PresBox';
import { RadialMenu } from './RadialMenu';
-import React = require("react");
import { ScriptingBox } from "./ScriptingBox";
-import { FormattedTextBox } from "./formattedText/FormattedTextBox";
+import { PresBox } from './trails/PresBox';
+import React = require("react");
const { Howl } = require('howler');
interface Window {
@@ -64,7 +65,7 @@ export enum ViewAdjustment {
doNothing = 0
}
-export const ViewSpecPrefix = "_VIEW"; // field prefix for anchor fields that are immediately copied over to the target document when link is followed. Other anchor properties will be copied over in the specific setViewSpec() method on their view (which allows for seting preview values instead of writing to the document)
+export const ViewSpecPrefix = "viewSpec"; // field prefix for anchor fields that are immediately copied over to the target document when link is followed. Other anchor properties will be copied over in the specific setViewSpec() method on their view (which allows for seting preview values instead of writing to the document)
export interface DocFocusOptions {
originalTarget?: Doc; // set in JumpToDocument, used by TabDocView to determine whether to fit contents to tab
@@ -88,6 +89,8 @@ export interface DocComponentView {
setKeyFrameEditing?: (set: boolean) => void; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown)
playFrom?: (time: number, endTime?: number) => void;
setFocus?: () => void;
+ fieldKey?: string;
+ annotationKey?: string;
getTitle?: () => string;
}
export interface DocumentViewSharedProps {
@@ -133,7 +136,7 @@ export interface DocumentViewSharedProps {
export interface DocumentViewProps extends DocumentViewSharedProps {
// properties specific to DocumentViews but not to FieldView
freezeDimensions?: boolean;
- hideResizeHandles?: boolean; // whether to suppress DocumentDecorations when this document is selected
+ hideResizeHandles?: boolean; // whether to suppress DocumentDecorations when this document is selected
hideTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings
hideDecorationTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings
treeViewDoc?: Doc;
@@ -183,7 +186,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
_componentView: Opt<DocComponentView>; // needs to be accessed from DocumentView wrapper class
- private get topMost() { return this.props.renderDepth === 0; }
+ private get topMost() { return this.props.renderDepth === 0 && !LightboxView.LightboxDoc; }
public get displayName() { return "DocumentView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive
public get ContentDiv() { return this._mainCont.current; }
public get LayoutFieldKey() { return Doc.LayoutFieldKey(this.layoutDoc); }
@@ -421,11 +424,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
focus = (anchor: Doc, options?: DocFocusOptions) => {
LightboxView.SetCookie(StrCast(anchor["cookies-set"]));
- // copying over _VIEW fields immediately allows the view type to switch to create the right _componentView
- Array.from(Object.keys(Doc.GetProto(anchor))).filter(key => key.startsWith(ViewSpecPrefix)).forEach(spec => this.layoutDoc[spec.replace(ViewSpecPrefix, "")] = ((field) => field instanceof ObjectField ? ObjectField.MakeCopy(field) : field)(anchor[spec]));
+ // copying over VIEW fields immediately allows the view type to switch to create the right _componentView
+ Array.from(Object.keys(Doc.GetProto(anchor))).filter(key => key.startsWith(ViewSpecPrefix)).forEach(spec => {
+ this.layoutDoc[spec.replace(ViewSpecPrefix, "")] = ((field) => field instanceof ObjectField ? ObjectField.MakeCopy(field) : field)(anchor[spec]);
+ });
// after a timeout, the right _componentView should have been created, so call it to update its view spec values
setTimeout(() => this._componentView?.setViewSpec?.(anchor, LinkDocPreview.LinkInfo ? true : false));
- const focusSpeed = this._componentView?.scrollFocus?.(anchor, !LinkDocPreview.LinkInfo); // bcz: smooth parameter should really be passed into focus() instead of inferred here
+ const focusSpeed = this._componentView?.scrollFocus?.(anchor, !LinkDocPreview.LinkInfo); // bcz: smooth parameter should really be passed into focus() instead of inferred here
const endFocus = focusSpeed === undefined ? options?.afterFocus : async (moved: boolean) => options?.afterFocus ? options?.afterFocus(true) : ViewAdjustment.doNothing;
this.props.focus(options?.docTransform ? anchor : this.rootDoc, {
...options, afterFocus: (didFocus: boolean) =>
@@ -663,7 +668,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
if (!cm || (e as any)?.nativeEvent?.SchemaHandled) return;
const customScripts = Cast(this.props.Document.contextMenuScripts, listSpec(ScriptField), []);
- Cast(this.props.Document.contextMenuLabels, listSpec("string"), []).forEach((label, i) =>
+ StrListCast(this.Document.contextMenuLabels).forEach((label, i) =>
cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ documentView: this, this: this.layoutDoc, scriptContext: this.props.scriptContext, self: this.rootDoc }), icon: "sticky-note" }));
this.props.contextMenuItems?.().forEach(item =>
item.label && cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.layoutDoc, scriptContext: this.props.scriptContext, self: this.rootDoc }), icon: "sticky-note" }));
@@ -747,7 +752,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
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: "Copy ID", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "fingerprint" });
+ moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Doc.globalServerPath(this.props.Document)), icon: "fingerprint" });
}
}
@@ -755,16 +760,15 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
moreItems.push({ description: "Close", event: this.deleteClicked, icon: "times" });
}
- !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 : [];
!Doc.UserDoc().novice && helpItems.push({ description: "Show Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "add:right"), icon: "layer-group" });
- helpItems.push({ description: "Text Shortcuts Ctrl+/", event: () => this.props.addDocTab(Docs.Create.PdfDocument(Utils.prepend("/assets/cheat-sheet.pdf"), { _width: 300, _height: 300 }), "add:right"), icon: "keyboard" });
+ helpItems.push({ description: "Text Shortcuts Ctrl+/", event: () => this.props.addDocTab(Docs.Create.PdfDocument("/assets/cheat-sheet.pdf", { _width: 300, _height: 300 }), "add:right"), icon: "keyboard" });
!Doc.UserDoc().novice && helpItems.push({ description: "Print Document in Console", event: () => console.log(this.props.Document), icon: "hand-point-right" });
+ !Doc.UserDoc().novice && helpItems.push({ description: "Print DataDoc in Console", event: () => console.log(this.props.Document[DataSym]), icon: "hand-point-right" });
cm.addItem({ description: "Help...", noexpand: true, subitems: helpItems, icon: "question" });
}
+
if (!this.topMost) e?.stopPropagation(); // DocumentViews should stop propagation of this event
cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15);
DocumentViewInternal.SelectAfterContextMenu && !this.props.isSelected(true) && setTimeout(() => SelectionManager.SelectView(this.props.DocumentView(), false), 300); // on a mac, the context menu is triggered on mouse down, but a YouTube video becaomes interactive when selected which means that the context menu won't show up. by delaying the selection until hopefully after the pointer up, the context menu will appear.
@@ -777,7 +781,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
onClickFunc = () => this.onClickHandler;
setHeight = (height: number) => this.layoutDoc._height = height;
setContentView = (view: { getAnchor?: () => Doc, forward?: () => boolean, back?: () => boolean }) => this._componentView = view;
- isContentActive = (outsideReaction?: boolean) => this.props.isContentActive() ? true : false;
+ isContentActive = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || this.props.Document.forceActive || this.props.isContentActive() ? true : false;
@computed get contents() {
TraceMobx();
const audioView = !this.layoutDoc._showAudio ? (null) :
@@ -792,7 +796,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
</div>;
return <div className="documentView-contentsView"
style={{
- pointerEvents: this.props.contentPointerEvents as any,
+ pointerEvents: (this.props.contentPointerEvents as any),// || (CurrentUserUtils.SelectedTool !== InkTool.None || SnappingManager.GetIsDragging() || this.isContentActive() ? "all" : "none"),
height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
}}>
<DocumentContentsView key={1} {...this.props}
@@ -809,7 +813,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
layoutKey={this.finalLayoutKey} />
{this.layoutDoc.hideAllLinks ? (null) : this.allLinkEndpoints}
{this.hideLinkButton ? (null) :
- <DocumentLinksButton View={this.props.DocumentView()} Offset={[this.topMost ? 0 : -15, undefined, undefined, this.topMost ? 10 : -20]} />}
+ <div style={{ transformOrigin: "top left", transform: `scale(${Math.min(1, this.props.ScreenToLocalTransform().scale(this.props.ContentScaling?.() || 1).Scale)})` }}>
+ <DocumentLinksButton View={this.props.DocumentView()} Offset={[this.topMost ? 0 : -15, undefined, undefined, this.topMost ? 10 : -20]} />
+ </div>}
{audioView}
</div>;
@@ -839,7 +845,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
if (this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return null;
if (this.layoutDoc.presBox || this.rootDoc.type === DocumentType.LINK || this.props.dontRegisterView) return (null);
// need to use allLinks for RTF since embedded linked text anchors are not rendered with DocumentViews. All other documents render their anchors with nested DocumentViews so we just need to render the directLinks here
- const filtered = DocUtils.FilterDocs(this.rootDoc.type === DocumentType.RTF ? this.allLinks : this.directLinks, this.props.docFilters(), []).filter(d => !d.hidden);
+ const filtered = DocUtils.FilterDocs(this.rootDoc.type === DocumentType.RTF ? this.allLinks : this.directLinks, this.props.docFilters?.() ?? [], []).filter(d => !d.hidden);
return filtered.map((link, i) =>
<div className="documentView-anchorCont" key={i + 1}>
<DocumentView {...this.props}
@@ -886,7 +892,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
recorder.ondataavailable = async (e: any) => {
const [{ result }] = await Networking.UploadFilesToServer(e.data);
if (!(result instanceof Error)) {
- const audioDoc = Docs.Create.AudioDocument(Utils.prepend(result.accessPaths.agnostic.client), { title: "audio test", _width: 200, _height: 32 });
+ const audioDoc = Docs.Create.AudioDocument(result.accessPaths.agnostic.client, { title: "audio test", _width: 200, _height: 32 });
audioDoc.treeViewExpandedView = "layout";
const audioAnnos = Cast(self.dataDoc[self.LayoutFieldKey + "-audioAnnotations"], listSpec(Doc));
if (audioAnnos === undefined) {
@@ -949,12 +955,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
}
@computed get renderDoc() {
TraceMobx();
+ const isButton: boolean = this.props.Document.type === DocumentType.FONTICON;
if (!(this.props.Document instanceof Doc) || GetEffectiveAcl(this.props.Document[DataSym]) === AclPrivate || this.hidden) return null;
return this.docContents ??
<div className={`documentView-node${this.topMost ? "-topmost" : ""}`}
id={this.props.Document[Id]}
style={{
- background: this.backgroundColor,
+ background: isButton ? undefined : this.backgroundColor,
opacity: this.opacity,
color: StrCast(this.layoutDoc.color, "inherit"),
fontFamily: StrCast(this.Document._fontFamily, "inherit"),
@@ -970,9 +977,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
}
render() {
const highlightIndex = this.props.LayoutTemplateString ? (Doc.IsHighlighted(this.props.Document) ? 6 : 0) : Doc.isBrushedHighlightedDegree(this.props.Document); // bcz: Argh!! need to identify a tree view doc better than a LayoutTemlatString
- const highlightColor = (CurrentUserUtils.ActiveDashboard?.darkScheme ?
+ const highlightColor = (Doc.UserDoc().colorScheme === ColorScheme.Dark ?
["transparent", "#65350c", "#65350c", "yellow", "magenta", "cyan", "orange"] :
- ["transparent", "maroon", "maroon", "yellow", "magenta", "cyan", "orange"])[highlightIndex];
+ ["transparent", "#4476F7", "#4476F7", "yellow", "magenta", "cyan", "orange"])[highlightIndex];
const highlightStyle = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid"][highlightIndex];
const excludeTypes = !this.props.treeViewDoc ? [DocumentType.FONTICON, DocumentType.INK] : [DocumentType.FONTICON];
let highlighting = !this.props.disableDocBrushing && highlightIndex && !excludeTypes.includes(this.layoutDoc.type as any) && this.layoutDoc._viewType !== CollectionViewType.Linear;
@@ -980,8 +987,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
const borderPath = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.BorderPath) || { path: undefined };
const internal = PresBox.EffectsProvider(this.layoutDoc, this.renderDoc) || this.renderDoc;
- const boxShadow = highlighting && this.borderRounding && highlightStyle !== "dashed" ? `0 0 0 ${highlightIndex}px ${highlightColor}` :
+ const boxShadow = this.props.treeViewDoc ? null : highlighting && this.borderRounding && highlightStyle !== "dashed" ? `0 0 0 ${highlightIndex}px ${highlightColor}` :
this.boxShadow || (this.props.Document.isTemplateForField ? "black 0.2vw 0.2vw 0.8vw" : undefined);
+
+ // Return surrounding highlight
return <div className={DocumentView.ROOT_DIV} ref={this._mainCont}
onContextMenu={this.onContextMenu}
onKeyDown={this.onKeyDown}
@@ -1142,14 +1151,15 @@ export class DocumentView extends React.Component<DocumentViewProps> {
TraceMobx();
const xshift = () => (this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined);
const yshift = () => (this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined);
+ const isButton: boolean = this.props.Document.type === DocumentType.FONTICON || this.props.Document._viewType === CollectionViewType.Linear;
return (<div className="contentFittingDocumentView">
{!this.props.Document || !this.props.PanelWidth() ? (null) : (
<div className="contentFittingDocumentView-previewDoc" ref={this.ContentRef}
style={{
position: this.props.Document.isInkMask ? "absolute" : undefined,
- transform: `translate(${this.centeringX}px, ${this.centeringY}px)`,
- width: xshift() ?? `${100 * (this.props.PanelWidth() - this.Xshift * 2) / this.props.PanelWidth()}%`,
- height: yshift() ?? (this.fitWidth ? `${this.panelHeight}px` :
+ transform: isButton ? undefined : `translate(${this.centeringX}px, ${this.centeringY}px)`,
+ width: isButton ? "100%" : xshift() ?? `${100 * (this.props.PanelWidth() - this.Xshift * 2) / this.props.PanelWidth()}%`,
+ height: isButton ? undefined : yshift() ?? (this.fitWidth ? `${this.panelHeight}px` :
`${100 * this.effectiveNativeHeight / this.effectiveNativeWidth * this.props.PanelWidth() / this.props.PanelHeight()}%`),
}}>
<DocumentViewInternal {...this.props}
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 86250c9d1..ee81e106a 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -2,11 +2,10 @@ import React = require("react");
import { computed } from "mobx";
import { observer } from "mobx-react";
import { DateField } from "../../../fields/DateField";
-import { Doc, Field, FieldResult, Opt } from "../../../fields/Doc";
+import { Doc, Field, FieldResult } from "../../../fields/Doc";
import { List } from "../../../fields/List";
-import { VideoField, WebField } from "../../../fields/URLField";
+import { WebField } from "../../../fields/URLField";
import { DocumentViewSharedProps } from "./DocumentView";
-import { VideoBox } from "./VideoBox";
//
// these properties get assigned through the render() method of the DocumentView when it creates this node.
@@ -64,9 +63,9 @@ export class FieldView extends React.Component<FieldViewProps> {
// else if (field instaceof PresBox) {
// return <PresBox {...this.props} />;
// }
- else if (field instanceof VideoField) {
- return <VideoBox {...this.props} />;
- }
+ // else if (field instanceof VideoField) {
+ // return <VideoBox {...this.props} />;
+ // }
// else if (field instanceof AudioField) {
// return <AudioBox {...this.props} />;
//}
diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx
index c892a9f6c..1e13d1b5a 100644
--- a/src/client/views/nodes/FilterBox.tsx
+++ b/src/client/views/nodes/FilterBox.tsx
@@ -79,10 +79,19 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc
* @returns the relevant doc according to the value of FilterBox._filterScope i.e. either the Current Dashboard or the Current Collection
*/
@computed static get targetDoc() {
+ return SelectionManager.Views().length ? SelectionManager.Views()[0].Document : CurrentUserUtils.ActiveDashboard;
+ }
+ @computed static get targetDocChildKey() {
+ if (SelectionManager.Views().length) {
+ return SelectionManager.Views()[0].ComponentView?.annotationKey || SelectionManager.Views()[0].ComponentView?.fieldKey || "data";
+ }
+ return "data";
+ }
+ @computed static get targetDocChildren() {
if (SelectionManager.Views().length) {
- return SelectionManager.Views()[0]?.Document.type === DocumentType.COL ? SelectionManager.Views()[0].Document : SelectionManager.Views()[0]?.props.ContainingCollectionDoc!;
+ return DocListCast(FilterBox.targetDoc[FilterBox.targetDocChildKey]);
}
- return CurrentUserUtils.ActiveDashboard;
+ return DocListCast(CurrentUserUtils.ActiveDashboard.data);
}
@observable _loaded = false;
@@ -100,8 +109,8 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc
const targetDoc = FilterBox.targetDoc;
if (this._loaded && targetDoc) {
const allDocs = new Set<Doc>();
- const activeTabs = DocListCast(targetDoc.data);
- SearchBox.foreachRecursiveDoc(activeTabs, (doc: Doc) => allDocs.add(doc));
+ const activeTabs = FilterBox.targetDocChildren;
+ SearchBox.foreachRecursiveDoc(activeTabs, (depth, doc) => allDocs.add(doc));
setTimeout(action(() => this._allDocs = Array.from(allDocs)));
}
return this._allDocs;
@@ -133,8 +142,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc
return this.activeAttributes.map(attribute => StrCast(attribute.title));
}
- gatherFieldValues(dashboard: Doc, facetKey: string) {
- const childDocs = DocListCast(dashboard.data);
+ gatherFieldValues(childDocs: Doc[], facetKey: string) {
const valueSet = new Set<string>();
let rtFields = 0;
childDocs.forEach((d) => {
@@ -194,13 +202,13 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc
* Responds to clicking the check box in the flyout menu
*/
facetClick = (facetHeader: string) => {
- const { targetDoc } = FilterBox;
+ const { targetDoc, targetDocChildren } = FilterBox;
const found = this.activeAttributes.findIndex(doc => doc.title === facetHeader);
if (found !== -1) {
this.removeFilter(facetHeader);
} else {
- const allCollectionDocs = DocListCast((targetDoc.data as any)?.[0].data);
- const facetValues = this.gatherFieldValues(targetDoc, facetHeader);
+ const allCollectionDocs = targetDocChildren;
+ const facetValues = this.gatherFieldValues(targetDocChildren, facetHeader);
let nonNumbers = 0;
let minVal = Number.MAX_VALUE, maxVal = -Number.MAX_VALUE;
@@ -248,7 +256,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc
newFacet.layoutKey = "layout";
newFacet.type = DocumentType.COL;
newFacet.target = targetDoc;
- newFacet.data = ComputedField.MakeFunction(`readFacetData(self.target, "${facetHeader}")`);
+ newFacet.data = ComputedField.MakeFunction(`readFacetData(self.target, "${FilterBox.targetDocChildKey}", "${facetHeader}")`);
}
newFacet.hideContextMenu = true;
Doc.AddDocToList(this.dataDoc, this.props.fieldKey, newFacet);
@@ -479,10 +487,10 @@ Scripting.addGlobal(function determineCheckedState(layoutDoc: Doc, facetHeader:
}
return undefined;
});
-Scripting.addGlobal(function readFacetData(layoutDoc: Doc, facetHeader: string) {
+Scripting.addGlobal(function readFacetData(layoutDoc: Doc, childKey: string, facetHeader: string) {
const allCollectionDocs = new Set<Doc>();
- const activeTabs = DocListCast(layoutDoc.data);
- SearchBox.foreachRecursiveDoc(activeTabs, (doc: Doc) => allCollectionDocs.add(doc));
+ const activeTabs = DocListCast(layoutDoc[childKey]);
+ SearchBox.foreachRecursiveDoc(activeTabs, (depth: number, doc: Doc) => allCollectionDocs.add(doc));
const set = new Set<string>();
if (facetHeader === "tags") allCollectionDocs.forEach(child => Field.toString(child[facetHeader] as Field).split(":").forEach(key => set.add(key)));
else allCollectionDocs.forEach(child => set.add(Field.toString(child[facetHeader] as Field)));
diff --git a/src/client/views/nodes/FontIconBox.scss b/src/client/views/nodes/FontIconBox.scss
deleted file mode 100644
index 718af2c16..000000000
--- a/src/client/views/nodes/FontIconBox.scss
+++ /dev/null
@@ -1,103 +0,0 @@
-@import "../global/globalCssVariables";
-
-.fontIconBox-label {
- color: $white;
- margin-right: 4px;
- margin-top: 1px;
- position: relative;
- text-align: center;
- font-size: 7px;
- letter-spacing: normal;
- background-color: inherit;
- border-radius: 8px;
- margin-top: -8px;
- padding: 0;
- width: 100%;
-}
-
-.fontIconBadge-container {
- position:absolute;
- z-index: 1000;
- top: 12px;
-
- .fontIconBadge {
- position: absolute;
- top: -10px;
- right: -10px;
- color: $white;
- background: $pink;
- font-weight: 300;
- border-radius: 100%;
- width: 25px;
- height: 25px;
- text-align: center;
- padding-top: 4px;
- font-size: 12px;
- }
-}
-
-.menuButton-circle,
-.menuButton-round {
- border-radius: 100%;
- background-color: $dark-gray;
- padding: 0;
-
- .fontIconBox-label {
- //margin-left: -10px; // button padding is 10px;
- bottom: 0;
- position: absolute;
- }
-
- &:hover {
- background-color: $light-gray;
- }
-}
-
-.menuButton-square {
- padding-top: 3px;
- padding-bottom: 3px;
- background-color: $dark-gray;
-
- .fontIconBox-label {
- border-radius: 0px;
- margin-top: 0px;
- border-radius: "inherit";
- }
-}
-
-.menuButton,
-.menuButton-circle,
-.menuButton-round,
-.menuButton-square {
- margin-left: -5%;
- width: 110%;
- height: 100%;
- pointer-events: all;
- touch-action: none;
-
- .menuButton-wrap {
- touch-action: none;
- border-radius: 8px;
- width: 100%;
- }
-
- .menuButton-icon-square {
- width: auto;
- height: 29px;
- padding: 4px;
- }
-
- svg {
- width: 95% !important;
- height: 95%;
- }
-}
-.menuButton-round {
- width: 100%;
- svg {
- width: 50% !important;
- height: 50%;
- position: relative;
- bottom: 2px;
- }
-} \ No newline at end of file
diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx
deleted file mode 100644
index 0d415e238..000000000
--- a/src/client/views/nodes/FontIconBox.tsx
+++ /dev/null
@@ -1,96 +0,0 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Tooltip } from '@material-ui/core';
-import { observer } from 'mobx-react';
-import * as React from 'react';
-import { AclPrivate, Doc, DocListCast } from '../../../fields/Doc';
-import { createSchema, makeInterface } from '../../../fields/Schema';
-import { ScriptField } from '../../../fields/ScriptField';
-import { Cast, StrCast } from '../../../fields/Types';
-import { GetEffectiveAcl } from '../../../fields/util';
-import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../../Utils";
-import { DragManager } from '../../util/DragManager';
-import { ContextMenu } from '../ContextMenu';
-import { DocComponent } from '../DocComponent';
-import { StyleProp } from '../StyleProvider';
-import { FieldView, FieldViewProps } from './FieldView';
-import './FontIconBox.scss';
-import { Colors } from '../global/globalEnums';
-const FontIconSchema = createSchema({
- icon: "string",
-});
-
-type FontIconDocument = makeInterface<[typeof FontIconSchema]>;
-const FontIconDocument = makeInterface(FontIconSchema);
-@observer
-export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>(FontIconDocument) {
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(FontIconBox, fieldKey); }
- showTemplate = (): void => {
- const dragFactory = Cast(this.layoutDoc.dragFactory, Doc, null);
- dragFactory && this.props.addDocTab(dragFactory, "add:right");
- }
- dragAsTemplate = (): void => { this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)'); };
- useAsPrototype = (): void => { this.layoutDoc.onDragStart = ScriptField.MakeFunction('makeDelegate(this.dragFactory, true)'); };
-
- specificContextMenu = (): void => {
- if (!Doc.UserDoc().noviceMode) {
- const cm = ContextMenu.Instance;
- cm.addItem({ description: "Show Template", event: this.showTemplate, icon: "tag" });
- cm.addItem({ description: "Use as Render Template", event: this.dragAsTemplate, icon: "tag" });
- cm.addItem({ description: "Use as Prototype", event: this.useAsPrototype, icon: "tag" });
- }
- }
-
- render() {
- const label = StrCast(this.rootDoc.label, StrCast(this.rootDoc.title));
- const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
- const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
- const shape = StrCast(this.layoutDoc.iconShape, label ? "round" : "circle");
- const icon = StrCast(this.dataDoc.icon, "user") as any;
- const presSize = shape === 'round' ? 25 : 30;
- const presTrailsIcon = <img src={`/assets/${"presTrails.png"}`}
- style={{ width: presSize, height: presSize, filter: `invert(${color === Colors.DARK_GRAY ? "0%" : "100%"})`, marginBottom: "5px" }} />;
- const button = <button className={`menuButton-${shape}`} onContextMenu={this.specificContextMenu}
- style={{ backgroundColor: backgroundColor, }}>
- <div className="menuButton-wrap">
- {icon === 'pres-trail' ? presTrailsIcon : <FontAwesomeIcon className={`menuButton-icon-${shape}`} icon={icon} color={color}
- size={this.layoutDoc.iconShape === "square" ? "sm" : "sm"} />}
- {!label ? (null) : <div className="fontIconBox-label" style={{ color, backgroundColor }}> {label} </div>}
- <FontIconBadge collection={Cast(this.rootDoc.watchedDocuments, Doc, null)} />
- </div>
- </button>;
- return !this.layoutDoc.toolTip ? button :
- <Tooltip title={<div className="dash-tooltip">{StrCast(this.layoutDoc.toolTip)}</div>}>
- {button}
- </Tooltip>;
- }
-}
-
-interface FontIconBadgeProps {
- collection: Doc | undefined;
-}
-
-@observer
-export class FontIconBadge extends React.Component<FontIconBadgeProps> {
- _notifsRef = React.createRef<HTMLDivElement>();
-
- onPointerDown = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e,
- (e: PointerEvent) => {
- const dragData = new DragManager.DocumentDragData([this.props.collection!]);
- DragManager.StartDocumentDrag([this._notifsRef.current!], dragData, e.x, e.y);
- return true;
- },
- returnFalse, emptyFunction, false);
- }
-
- render() {
- if (!(this.props.collection instanceof Doc)) return (null);
- const length = DocListCast(this.props.collection.data).filter(d => GetEffectiveAcl(d) !== AclPrivate).length; // Object.keys(d).length).length; // filter out any documents that we can't read
- return <div className="fontIconBadge-container" style={{ width: 15, height: 15, top: 12 }} ref={this._notifsRef}>
- <div className="fontIconBadge" style={length > 0 ? { "display": "initial" } : { "display": "none" }}
- onPointerDown={this.onPointerDown} >
- {length}
- </div>
- </div>;
- }
-} \ No newline at end of file
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index cfd43bb62..c4e74ebd2 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,19 +1,21 @@
-import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx';
+import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, trace } from 'mobx';
import { observer } from "mobx-react";
import { DataSym, Doc, DocListCast, WidthSym } from '../../../fields/Doc';
import { documentSchema } from '../../../fields/documentSchemas';
import { Id } from '../../../fields/FieldSymbols';
+import { InkTool } from '../../../fields/InkField';
import { List } from '../../../fields/List';
import { ObjectField } from '../../../fields/ObjectField';
import { createSchema, makeInterface } from '../../../fields/Schema';
import { ComputedField } from '../../../fields/ScriptField';
-import { Cast, NumCast, StrCast } from '../../../fields/Types';
+import { Cast, NumCast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, OmitKeys, returnOne, Utils, returnFalse } from '../../../Utils';
+import { emptyFunction, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils';
import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices';
import { Networking } from '../../Network';
+import { CurrentUserUtils } from '../../util/CurrentUserUtils';
import { DragManager } from '../../util/DragManager';
import { undoBatch } from '../../util/UndoManager';
import { ContextMenu } from "../../views/ContextMenu";
@@ -21,15 +23,13 @@ import { CollectionFreeFormView } from '../collections/collectionFreeForm/Collec
import { ContextMenuProps } from '../ContextMenuItem';
import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
import { MarqueeAnnotator } from '../MarqueeAnnotator';
+import { AnchorMenu } from '../pdf/AnchorMenu';
import { StyleProp } from '../StyleProvider';
import { FaceRectangles } from './FaceRectangles';
import { FieldView, FieldViewProps } from './FieldView';
import "./ImageBox.scss";
import React = require("react");
-import { InkTool } from '../../../fields/InkField';
-import { CurrentUserUtils } from '../../util/CurrentUserUtils';
-import { AnchorMenu } from '../pdf/AnchorMenu';
-import { Docs } from '../../documents/Documents';
+import { SnappingManager } from '../../util/SnappingManager';
const path = require('path');
export const pageSchema = createSchema({
@@ -238,7 +238,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
let succeeded = true;
let data: ImageField | undefined;
try {
- data = new ImageField(Utils.prepend(accessPaths.agnostic.client));
+ data = new ImageField(accessPaths.agnostic.client);
} catch {
succeeded = false;
}
@@ -294,7 +294,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
<div className="imageBox-fader" >
<img key="paths" ref={this._imgRef}
src={srcpath}
- style={{ transform, transformOrigin }} draggable={false}
+ style={{ transform, transformOrigin }}
+ draggable={false}
width={nativeWidth} />
{fadepath === srcpath ? (null) : <div className="imageBox-fadeBlocker">
<img className="imageBox-fadeaway" key={"fadeaway"} ref={this._imgRef}
@@ -330,11 +331,13 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
TraceMobx();
return <div className="imageBox-annotationLayer" style={{ height: this.props.PanelHeight() }} ref={this._annotationLayer} />;
}
- @action
marqueeDown = (e: React.PointerEvent) => {
if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool)) {
- this._marqueeing = [e.clientX, e.clientY];
- e.stopPropagation();
+ setupMoveUpEvents(this, e, action(e => {
+ MarqueeAnnotator.clearAnnotations(this._savedAnnotations)
+ this._marqueeing = [e.clientX, e.clientY];
+ return true;
+ }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false);
}
}
@action
diff --git a/src/client/views/nodes/LinkDescriptionPopup.scss b/src/client/views/nodes/LinkDescriptionPopup.scss
index d92823ccc..a8db5d360 100644
--- a/src/client/views/nodes/LinkDescriptionPopup.scss
+++ b/src/client/views/nodes/LinkDescriptionPopup.scss
@@ -1,9 +1,13 @@
+@import "../global/globalCssVariables.scss";
+
.linkDescriptionPopup {
display: flex;
-
- border: 1px solid rgb(170, 26, 26);
-
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ border: 2px solid $medium-blue;
+ background-color: $white;
width: auto;
position: absolute;
@@ -11,17 +15,11 @@
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;
+ gap: 5px;
+ padding: 9px;
.linkDescriptionPopup-input {
float: left;
- background-color: rgba(250, 250, 250, 0.95);
color: rgb(100, 100, 100);
border: none;
min-width: 160px;
@@ -30,46 +28,29 @@
.linkDescriptionPopup-btn {
float: right;
-
justify-content: center;
vertical-align: middle;
-
.linkDescriptionPopup-btn-dismiss {
- background-color: white;
- color: black;
+ cursor: pointer;
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;
- }
+ white-space: nowrap;
+ padding: 5px;
+ vertical-align: middle;
+ background-color: $close-red;
+ border-radius: 3px;
+ color: black;
}
.linkDescriptionPopup-btn-add {
- background-color: black;
- color: white;
+ cursor: pointer;
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;
- }
+ white-space: nowrap;
+ padding: 5px;
+ vertical-align: middle;
+ background-color: $light-blue;
+ border-radius: 3px;
+ color: black;
}
}
diff --git a/src/client/views/nodes/LinkDescriptionPopup.tsx b/src/client/views/nodes/LinkDescriptionPopup.tsx
index 30b272a9a..b62a4dd56 100644
--- a/src/client/views/nodes/LinkDescriptionPopup.tsx
+++ b/src/client/views/nodes/LinkDescriptionPopup.tsx
@@ -54,7 +54,7 @@ export class LinkDescriptionPopup extends React.Component<{}> {
}}>
<input className="linkDescriptionPopup-input"
onKeyPress={e => e.key === "Enter" && this.onDismiss(true)}
- placeholder={"(optional) enter link label..."}
+ placeholder={"(Optional) Enter link description..."}
onChange={(e) => this.descriptionChanged(e)}>
</input>
<div className="linkDescriptionPopup-btn">
@@ -65,4 +65,4 @@ export class LinkDescriptionPopup extends React.Component<{}> {
</div>
</div>;
}
-} \ No newline at end of file
+} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index b73fb10df..126a37eb8 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -72,14 +72,14 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
@computed get href() {
if (this.props.hrefs?.length) {
const href = this.props.hrefs[this._hrefInd];
- if (href.indexOf(Utils.prepend("/doc/")) !== 0) { // link to a web page URL -- try to show a preview
+ if (href.indexOf(Doc.localServerPath()) !== 0) { // link to a web page URL -- try to show a preview
if (href.startsWith("https://en.wikipedia.org/wiki/")) {
wiki().page(href.replace("https://en.wikipedia.org/wiki/", "")).then(page => page.summary().then(action(summary => this._toolTipText = summary.substring(0, 500))));
} else {
setTimeout(action(() => this._toolTipText = "url => " + href));
}
} else { // hyperlink to a document .. decode doc id and retrieve from the server. this will trigger vals() being invalidated
- const anchorDoc = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+ const anchorDoc = href.replace(Doc.localServerPath(), "").split("?")[0];
anchorDoc && DocServer.GetRefField(anchorDoc).then(action(anchor => {
if (anchor instanceof Doc && DocListCast(anchor.links).length) {
this._linkDoc = DocListCast(anchor.links)[0];
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 8f61e252b..23236cf20 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -3,13 +3,13 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction
import { observer } from "mobx-react";
import * as Pdfjs from "pdfjs-dist";
import "pdfjs-dist/web/pdf_viewer.css";
-import { Doc, Opt, WidthSym } from "../../../fields/Doc";
+import { Doc, Opt, WidthSym, DocListCast } from "../../../fields/Doc";
import { documentSchema } from '../../../fields/documentSchemas';
import { makeInterface } from "../../../fields/Schema";
-import { Cast, NumCast, StrCast } from '../../../fields/Types';
+import { Cast, NumCast, StrCast, BoolCast } from '../../../fields/Types';
import { PdfField } from "../../../fields/URLField";
import { TraceMobx } from '../../../fields/util';
-import { Utils, setupMoveUpEvents, emptyFunction } from '../../../Utils';
+import { Utils, setupMoveUpEvents, emptyFunction, returnOne } from '../../../Utils';
import { Docs } from '../../documents/Documents';
import { KeyCodes } from '../../util/KeyCodes';
import { undoBatch } from '../../util/UndoManager';
@@ -31,6 +31,7 @@ const PdfDocument = makeInterface(documentSchema, panZoomSchema, pageSchema);
@observer
export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps, PdfDocument>(PdfDocument) {
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PDFBox, fieldKey); }
+ public static openSidebarWidth = 250;
private _searchString: string = "";
private _initialScrollTarget: Opt<Doc>;
private _pdfViewer: PDFViewer | undefined;
@@ -53,30 +54,6 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
if (PDFBox.pdfcache.get(this.pdfUrl.url.href)) runInAction(() => this._pdf = PDFBox.pdfcache.get(this.pdfUrl!.url.href));
else if (PDFBox.pdfpromise.get(this.pdfUrl.url.href)) PDFBox.pdfpromise.get(this.pdfUrl.url.href)?.then(action(pdf => this._pdf = pdf));
}
-
- const backup = "oldPath";
- const href = this.pdfUrl?.url.href;
- if (href) {
- const pathCorrectionTest = /upload\_[a-z0-9]{32}.(.*)/g;
- const matches = pathCorrectionTest.exec(href);
- // console.log("\nHere's the { url } being fed into the outer regex:");
- // console.log(href);
- // console.log("And here's the 'properPath' build from the captured filename:\n");
- if (matches !== null && href.startsWith(window.location.origin)) {
- const properPath = Utils.prepend(`/files/pdfs/${matches[0]}`);
- //console.log(properPath);
- if (!properPath.includes(href)) {
- console.log(`The two (url and proper path) were not equal`);
- const proto = Doc.GetProto(this.props.Document);
- proto[this.props.fieldKey] = new PdfField(properPath);
- proto[backup] = href;
- } else {
- //console.log(`The two (url and proper path) were equal`);
- }
- } else {
- console.log("Outer matches was null!");
- }
- }
}
componentWillUnmount() { this._selectReactionDisposer?.(); }
@@ -90,6 +67,9 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
scrollFocus = (doc: Doc, smooth: boolean) => {
+ if (DocListCast(this.props.Document[this.fieldKey + "-sidebar"]).includes(doc) && !this.SidebarShown) {
+ this.toggleSidebar(!smooth);
+ }
if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1;
this._initialScrollTarget = doc;
return this._pdfViewer?.scrollFocus(doc, smooth);
@@ -165,15 +145,24 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth;
}
return false;
- }, emptyFunction, this.toggleSidebar);
+ }, emptyFunction, () => this.toggleSidebar());
}
- toggleSidebar = action(() => {
+ @observable _previewNativeWidth: Opt<number> = undefined;
+ @observable _previewWidth: Opt<number> = undefined;
+ toggleSidebar = action((preview: boolean = false) => {
const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]);
- const ratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? 250 : 0) + nativeWidth) / nativeWidth;
+ const ratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? PDFBox.openSidebarWidth : 0) + nativeWidth) / nativeWidth;
const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth);
- this.layoutDoc.nativeWidth = nativeWidth * ratio;
- this.layoutDoc._width = this.layoutDoc[WidthSym]() * nativeWidth * ratio / curNativeWidth;
- this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth;
+ if (preview) {
+ this._previewNativeWidth = nativeWidth * ratio;
+ this._previewWidth = this.layoutDoc[WidthSym]() * nativeWidth * ratio / curNativeWidth;
+ this._showSidebar = true;
+ }
+ else {
+ this.layoutDoc.nativeWidth = nativeWidth * ratio;
+ this.layoutDoc._width = this.layoutDoc[WidthSym]() * nativeWidth * ratio / curNativeWidth;
+ this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth;
+ }
});
settingsPanel() {
const pageBtns = <>
@@ -226,7 +215,9 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
</button>
</div>;
}
- sidebarWidth = () => !this.layoutDoc._showSidebar ? 0 : (NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth() / NumCast(this.layoutDoc.nativeWidth);
+ sidebarWidth = () => !this.SidebarShown ? 0 :
+ this._previewWidth ? PDFBox.openSidebarWidth :
+ (NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth() / NumCast(this.layoutDoc.nativeWidth)
specificContextMenu = (e: React.MouseEvent): void => {
const funcs: ContextMenuProps[] = [];
@@ -247,38 +238,55 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
+ @observable _showSidebar = false;
+ @computed get SidebarShown() { return this._showSidebar || this.layoutDoc._showSidebar ? true : false; }
+ contentScaling = () => {
+ return 1;
+ }
@computed get renderPdfView() {
TraceMobx();
+ const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1;
+ const scale = previewScale * (this.props.scaling?.() || 1);
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
}}>
<div className="pdfBox-background" />
- <PDFViewer {...this.props}
- rootDoc={this.rootDoc}
- layoutDoc={this.layoutDoc}
- dataDoc={this.dataDoc}
- pdf={this._pdf!}
- url={this.pdfUrl!.url.pathname}
- isContentActive={this.isContentActive}
- anchorMenuClick={this.anchorMenuClick}
- loaded={!Doc.NativeAspect(this.dataDoc) ? this.loaded : undefined}
- setPdfViewer={this.setPdfViewer}
- addDocument={this.addDocument}
- moveDocument={this.moveDocument}
- removeDocument={this.removeDocument}
- whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- startupLive={true}
- ContentScaling={this.props.scaling}
- sidebarWidth={this.sidebarWidth}
- />
+ <div style={{
+ width: `calc(${100 / scale}% - ${this.sidebarWidth() / scale * (this._previewWidth ? scale : 1)}px)`,
+ height: `${100 / scale}%`,
+ transform: `scale(${scale})`,
+ position: "absolute",
+ transformOrigin: "top left",
+ top: 0
+ }}>
+ <PDFViewer {...this.props}
+ rootDoc={this.rootDoc}
+ layoutDoc={this.layoutDoc}
+ dataDoc={this.dataDoc}
+ pdf={this._pdf!}
+ url={this.pdfUrl!.url.pathname}
+ isContentActive={this.isContentActive}
+ anchorMenuClick={this.anchorMenuClick}
+ loaded={!Doc.NativeAspect(this.dataDoc) ? this.loaded : undefined}
+ setPdfViewer={this.setPdfViewer}
+ addDocument={this.addDocument}
+ moveDocument={this.moveDocument}
+ removeDocument={this.removeDocument}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ startupLive={true}
+ ContentScaling={returnOne}
+ />
+ </div>
<SidebarAnnos ref={this._sidebarRef}
{...this.props}
rootDoc={this.rootDoc}
layoutDoc={this.layoutDoc}
dataDoc={this.dataDoc}
+ nativeWidth={this._previewNativeWidth ?? NumCast(this.layoutDoc._nativeWidth)}
+ showSidebar={this.SidebarShown}
whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
sidebarAddDocument={this.sidebarAddDocument}
moveDocument={this.moveDocument}
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index 700f8a7d3..7ad96bf05 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -1,11 +1,11 @@
import React = require("react");
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
// import { Canvas } from '@react-three/fiber';
-import { action, computed, observable, reaction } from "mobx";
+import { action, computed, observable, reaction, trace, runInAction } from "mobx";
import { observer } from "mobx-react";
// import { BufferAttribute, Camera, Vector2, Vector3 } from 'three';
import { DateField } from "../../../fields/DateField";
-import { Doc, WidthSym } from "../../../fields/Doc";
+import { Doc, WidthSym, HeightSym } from "../../../fields/Doc";
import { documentSchema } from "../../../fields/documentSchemas";
import { Id } from "../../../fields/FieldSymbols";
import { InkTool } from "../../../fields/InkField";
@@ -175,8 +175,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
@computed get content() {
if (this.rootDoc.videoWall) return (null);
- const interactive = CurrentUserUtils.SelectedTool !== InkTool.None || !this.props.isSelected() ? "" : "-interactive";
- return <video className={"videoBox-content" + interactive} key="video"
+ return <video className={"videoBox-content"} key="video"
ref={r => {
this._videoRef = r;
setTimeout(() => {
@@ -218,16 +217,15 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
// }
return (null);
}
- toggleRecording = action(async () => {
- this._screenCapture = !this._screenCapture;
- if (this._screenCapture) {
+ toggleRecording = async () => {
+ if (!this._screenCapture) {
this._audioRec = new MediaRecorder(await navigator.mediaDevices.getUserMedia({ audio: true }));
const aud_chunks: any = [];
this._audioRec.ondataavailable = (e: any) => aud_chunks.push(e.data);
this._audioRec.onstop = async (e: any) => {
const [{ result }] = await Networking.UploadFilesToServer(aud_chunks);
if (!(result instanceof Error)) {
- this.dataDoc[this.props.fieldKey + "-audio"] = new AudioField(Utils.prepend(result.accessPaths.agnostic.client));
+ this.dataDoc[this.props.fieldKey + "-audio"] = new AudioField(result.accessPaths.agnostic.client);
}
};
this._videoRef!.srcObject = await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
@@ -244,23 +242,29 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
this.layoutDoc.layout = VideoBox.LayoutString(this.fieldKey);
this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = undefined;
this.layoutDoc._fitWidth = undefined;
- this.dataDoc[this.props.fieldKey] = new VideoField(Utils.prepend(result.accessPaths.agnostic.client));
+ this.dataDoc[this.props.fieldKey] = new VideoField(result.accessPaths.agnostic.client);
} else alert("video conversion failed");
};
this._audioRec.start();
this._videoRec.start();
- this.dataDoc.mediaState = "recording";
+ runInAction(() => {
+ this._screenCapture = true;
+ this.dataDoc.mediaState = "recording";
+ });
DocUtils.ActiveRecordings.push(this);
} else {
this._audioRec?.stop();
this._videoRec?.stop();
- this.dataDoc.mediaState = "paused";
+ runInAction(() => {
+ this._screenCapture = false;
+ this.dataDoc.mediaState = "paused";
+ });
const ind = DocUtils.ActiveRecordings.indexOf(this);
ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1));
CaptureManager.Instance.open(this.rootDoc);
}
- });
+ }
setupDictation = () => {
if (this.dataDoc[this.fieldKey + "-dictation"]) return;
@@ -275,7 +279,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
this.dataDoc[this.fieldKey + "-dictation"] = dictationText;
}
contentFunc = () => [this.threed, this.content];
- videoPanelHeight = () => NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"], 1) / NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"], 1) * this.props.PanelWidth();
+ videoPanelHeight = () => NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"], this.layoutDoc[HeightSym]()) / NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"], this.layoutDoc[WidthSym]()) * this.props.PanelWidth();
formattedPanelHeight = () => Math.max(0, this.props.PanelHeight() - this.videoPanelHeight());
render() {
TraceMobx();
diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss
index f593f74fb..cdd36eb3b 100644
--- a/src/client/views/nodes/VideoBox.scss
+++ b/src/client/views/nodes/VideoBox.scss
@@ -49,63 +49,28 @@
// pointer-events: all;
// }
-.videoBox-time{
- color : white;
- top :25px;
- left : 25px;
+.videoBox-ui {
position: absolute;
- background-color: rgba(50, 50, 50, 0.2);
- transform-origin: left top;
- pointer-events:all;
+ flex-direction: row;
+ right: 5px;
+ top: 5px;
+ display: none;
+ background-color: rgba(0, 0, 0, 0.6);
}
-.videoBox-snapshot{
+.videoBox-time, .videoBox-snapshot, .videoBox-timelineButton, .videoBox-play, .videoBox-full {
color : white;
- top :25px;
- right : 25px;
- position: absolute;
- background-color:rgba(50, 50, 50, 0.2);
+ position: relative;
transform-origin: left top;
pointer-events:all;
- cursor: default;
-}
-
-.videoBox-timelineButton {
- position: absolute;
- display: flex;
- align-items: center;
- z-index: 1010;
- bottom: 5px;
- right: 5px;
- color: white;
+ padding-right: 5px;
cursor: pointer;
- background: dimgrey;
- width: 20px;
- height: 20px;
-}
-.videoBox-play {
- width: 25px;
- height: 20px;
- bottom: 25px;
- left : 25px;
- position: absolute;
- color : white;
- background-color: rgba(50, 50, 50, 0.2);
- border-radius: 4px;
- text-align: center;
- transform-origin: left bottom;
- pointer-events:all;
+ &:hover {
+ background-color: gray;
+ }
}
-.videoBox-full {
- width: 25px;
- height: 20px;
- bottom: 25px;
- right : 25px;
- position: absolute;
- color : white;
- background-color: rgba(50, 50, 50, 0.2);
- border-radius: 4px;
- text-align: center;
- transform-origin: right bottom;
- pointer-events:all;
+.videoBox:hover {
+ .videoBox-ui {
+ display: flex;
+ }
} \ No newline at end of file
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 7e7ebcad3..d16b28f32 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -9,7 +9,7 @@ import { InkTool } from "../../../fields/InkField";
import { makeInterface } from "../../../fields/Schema";
import { Cast, NumCast, StrCast } from "../../../fields/Types";
import { AudioField, nullAudio, VideoField } from "../../../fields/URLField";
-import { emptyFunction, formatTime, OmitKeys, returnOne, setupMoveUpEvents, Utils } from "../../../Utils";
+import { emptyFunction, formatTime, OmitKeys, returnOne, setupMoveUpEvents, Utils, returnFalse } from "../../../Utils";
import { Docs, DocUtils } from "../../documents/Documents";
import { Networking } from "../../Network";
import { CurrentUserUtils } from "../../util/CurrentUserUtils";
@@ -28,6 +28,8 @@ import { LinkDocPreview } from "./LinkDocPreview";
import "./VideoBox.scss";
import { DragManager } from "../../util/DragManager";
import { DocumentManager } from "../../util/DocumentManager";
+import { DocumentType } from "../../documents/DocumentTypes";
+import { Tooltip } from "@material-ui/core";
const path = require('path');
type VideoDocument = makeInterface<[typeof documentSchema]>;
@@ -49,7 +51,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
private _playRegionTimer: any = null;
private _playRegionDuration = 0;
- @observable static _showControls: boolean;
+ @observable static _nativeControls: boolean;
@observable _marqueeing: number[] | undefined;
@observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
@observable _screenCapture = false;
@@ -75,10 +77,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow"/* videoStart */, "_timecodeToHide" /* videoEnd */, timecode ? timecode : undefined) || this.rootDoc;
}
- choosePath(url: string) {
- return url.indexOf(window.location.origin) === -1 ? Utils.CorsProxy(url) : url;
- }
-
videoLoad = () => {
const aspect = this.player!.videoWidth / this.player!.videoHeight;
Doc.SetNativeWidth(this.dataDoc, this.player!.videoWidth);
@@ -129,7 +127,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this.updateTimecode();
}
- @action public FullScreen() {
+ @action public FullScreen = () => {
this._fullScreen = true;
this.player && this.player.requestFullscreen();
try {
@@ -141,7 +139,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@action public Snapshot(downX?: number, downY?: number) {
const width = (this.layoutDoc._width || 0);
- const height = (this.layoutDoc._height || 0);
const canvas = document.createElement('canvas');
canvas.width = 640;
canvas.height = 640 * Doc.NativeHeight(this.layoutDoc) / (Doc.NativeWidth(this.layoutDoc) || 1);
@@ -182,8 +179,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
}
- private createRealSummaryLink = (relative: string, downX?: number, downY?: number) => {
- const url = this.choosePath(Utils.prepend(relative));
+ private createRealSummaryLink = (imagePath: string, downX?: number, downY?: number) => {
+ const url = !imagePath.startsWith("/") ? Utils.CorsProxy(imagePath) : imagePath;
const width = this.layoutDoc._width || 1;
const height = this.layoutDoc._height || 0;
const imageSummary = Docs.Create.ImageDocument(url, {
@@ -212,11 +209,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
componentDidMount() {
this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link.
- this._disposers.selection = reaction(() => this.props.isSelected(),
- selected => !selected && setTimeout(() => {
- Array.from(this._savedAnnotations.values()).forEach(v => v.forEach(a => a.remove()));
- this._savedAnnotations.clear();
- }));
this._disposers.triggerVideo = reaction(
() => !LinkDocPreview.LinkInfo && this.props.renderDepth !== -1 ? NumCast(this.Document._triggerVideo, null) : undefined,
time => time !== undefined && setTimeout(() => {
@@ -285,10 +277,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
if (field) {
const url = field.url.href;
const subitems: ContextMenuProps[] = [];
- subitems.push({ description: "Copy path", event: () => { Utils.CopyText(url); }, icon: "expand-arrows-alt" });
- subitems.push({ description: "Toggle Show Controls", event: action(() => VideoBox._showControls = !VideoBox._showControls), icon: "expand-arrows-alt" });
- subitems.push({ description: "Take Snapshot", event: () => this.Snapshot(), icon: "expand-arrows-alt" });
- subitems.push({
+ subitems.push({ description: "Full Screen", event: this.FullScreen, icon: "expand" });
+ subitems.push({ description: "Take Snapshot", event: this.Snapshot, icon: "expand-arrows-alt" });
+ this.rootDoc.type === DocumentType.SCREENSHOT && subitems.push({
description: "Screen Capture", event: (async () => {
runInAction(() => this._screenCapture = !this._screenCapture);
this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
@@ -296,6 +287,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
});
subitems.push({ description: (this.layoutDoc.dontAutoPlayFollowedLinks ? "" : "Don't") + " play when link is selected", event: () => this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks, icon: "expand-arrows-alt" });
subitems.push({ description: (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : "Auto play") + " anchors onClick", event: () => this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors, icon: "expand-arrows-alt" });
+ subitems.push({ description: "Toggle Native Controls", event: action(() => VideoBox._nativeControls = !VideoBox._nativeControls), icon: "expand-arrows-alt" });
+ subitems.push({ description: "Copy path", event: () => { Utils.CopyText(url); }, icon: "expand-arrows-alt" });
ContextMenu.Instance.addItem({ description: "Options...", subitems: subitems, icon: "video" });
}
}
@@ -319,7 +312,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
<video key="video" autoPlay={this._screenCapture} ref={this.setVideoRef}
style={{ height: "100%", width: "auto", display: "flex", margin: "auto" }}
onCanPlay={this.videoLoad}
- controls={VideoBox._showControls}
+ controls={VideoBox._nativeControls}
onPlay={() => this.Play()}
onSeeked={this.updateTimecode}
onPause={() => this.Pause()}
@@ -384,26 +377,36 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
private get uIButtons() {
const curTime = (this.layoutDoc._currentTimecode || 0);
- return ([<div className="videoBox-time" key="time" onPointerDown={this.onResetDown} >
- <span>{"" + formatTime(curTime)}</span>
- <span style={{ fontSize: 8 }}>{" " + Math.round((curTime - Math.trunc(curTime)) * 100)}</span>
- </div>,
- <div className="videoBox-snapshot" key="snap" onPointerDown={this.onSnapshotDown} >
- <FontAwesomeIcon icon="camera" size="lg" />
- </div>,
- <div className="videoBox-timelineButton" key="timeline" onPointerDown={this.onTimelineHdlDown} style={{ bottom: `${100 - this.heightPercent}%` }}>
- <FontAwesomeIcon icon={"eye"} size="lg" />
- </div>,
- VideoBox._showControls ? (null) : [
- // <div className="control-background">
- <div className="videoBox-play" key="play" onPointerDown={this.onPlayDown} >
- <FontAwesomeIcon icon={this._playing ? "pause" : "play"} size="lg" />
- </div>,
- <div className="videoBox-full" key="full" onPointerDown={this.onFullDown} >
- F
- {/* </div> */}
- </div>
- ]]);
+ const nonNativeControls = [
+ <Tooltip title={<div className="dash-tooltip">{"playback"}</div>} placement="bottom">
+ <div className="videoBox-play" key="play" onPointerDown={this.onPlayDown} >
+ <FontAwesomeIcon icon={this._playing ? "pause" : "play"} size="lg" />
+ </div>
+ </Tooltip>,
+ <Tooltip title={<div className="dash-tooltip">{"timecode"}</div>} placement="bottom">
+ <div className="videoBox-time" onPointerDown={this.onResetDown} >
+ <span>{formatTime(curTime)}</span>
+ <span style={{ fontSize: 8 }}>{" " + Math.floor((curTime - Math.trunc(curTime)) * 100).toString().padStart(2, "0")}</span>
+ </div>
+ </Tooltip>,
+ <Tooltip title={<div className="dash-tooltip">{"view full screen"}</div>} placement="bottom">
+ <div className="videoBox-full" onPointerDown={this.FullScreen}>
+ <FontAwesomeIcon icon="expand" size="lg" />
+ </div>
+ </Tooltip>];
+ return <div className="videoBox-ui">
+ {[...(VideoBox._nativeControls ? [] : nonNativeControls),
+ <Tooltip title={<div className="dash-tooltip">{"snapshot current frame"}</div>} placement="bottom">
+ <div className="videoBox-snapshot" onPointerDown={this.onSnapshotDown} >
+ <FontAwesomeIcon icon="camera" size="lg" />
+ </div>
+ </Tooltip>,
+ <Tooltip title={<div className="dash-tooltip">{"show annotation timeline"}</div>} placement="bottom">
+ <div className="videoBox-timelineButton" onPointerDown={this.onTimelineHdlDown}>
+ <FontAwesomeIcon icon="eye" size="lg" />
+ </div>
+ </Tooltip>,]}
+ </div>;
}
onPlayDown = () => this._playing ? this.Pause() : this.Play();
@@ -457,7 +460,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`}
onPointerLeave={this.updateTimecode}
onLoad={this.youtubeIframeLoaded} className={`${style}`} width={Doc.NativeWidth(this.layoutDoc) || 640} height={Doc.NativeHeight(this.layoutDoc) || 390}
- src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=0&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._showControls ? 1 : 0}`} />;
+ src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=0&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._nativeControls ? 1 : 0}`} />;
}
@action.bound
@@ -548,9 +551,15 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
return <div className="imageBox-annotationLayer" style={{ transition: this.transition, height: `${this.heightPercent}%` }} ref={this._annotationLayer} />;
}
- marqueeDown = action((e: React.PointerEvent) => {
- if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool)) this._marqueeing = [e.clientX, e.clientY];
- });
+ marqueeDown = (e: React.PointerEvent) => {
+ if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool)) {
+ setupMoveUpEvents(this, e, action(e => {
+ MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
+ this._marqueeing = [e.clientX, e.clientY];
+ return true;
+ }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false);
+ }
+ }
finishMarquee = action(() => {
this._marqueeing = undefined;
@@ -620,4 +629,4 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
}
-VideoBox._showControls = true; \ No newline at end of file
+VideoBox._nativeControls = false; \ No newline at end of file
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index f5b1f96f2..ae6b90e41 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -13,7 +13,7 @@ import { ComputedField } from "../../../fields/ScriptField";
import { Cast, NumCast, StrCast } from "../../../fields/Types";
import { WebField } from "../../../fields/URLField";
import { TraceMobx } from "../../../fields/util";
-import { emptyFunction, getWordAtPoint, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, Utils } from "../../../Utils";
+import { emptyFunction, getWordAtPoint, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, Utils, returnEmptyString, returnEmptyFilter } from "../../../Utils";
import { Docs } from "../../documents/Documents";
import { DocumentType } from '../../documents/DocumentTypes';
import { CurrentUserUtils } from "../../util/CurrentUserUtils";
@@ -43,7 +43,8 @@ const WebDocument = makeInterface(documentSchema);
@observer
export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps, WebDocument>(WebDocument) {
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(WebBox, fieldKey); }
- private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void);
+ public static openSidebarWidth = 250;
+ private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void);
private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
private _outerRef: React.RefObject<HTMLDivElement> = React.createRef();
private _disposers: { [name: string]: IReactionDisposer } = {};
@@ -54,12 +55,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@observable private _scrollTimer: any;
@observable private _overlayAnnoInfo: Opt<Doc>;
@observable private _marqueeing: number[] | undefined;
- @observable private _url: string = "hello";
@observable private _isAnnotating = false;
@observable private _iframeClick: HTMLIFrameElement | undefined = undefined;
@observable private _iframe: HTMLIFrameElement | null = null;
@observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
@observable private _scrollHeight = 1500;
+ @computed get _url() { return this.webField?.toString() || ""; }
+ @computed get _urlHash() { return this._url ? WebBox.urlHash(this._url) + "" : ""; }
@computed get scrollHeight() { return this._scrollHeight; }
@computed get allAnnotations() { return DocListCast(this.dataDoc[this.annotationKey]); }
@computed get inlineTextAnnotations() { return this.allAnnotations.filter(a => a.textInlineAnnotations); }
@@ -67,7 +69,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
constructor(props: any) {
super(props);
- if (this.webField) {
+ if (true) {// his.webField) {
Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(this.dataDoc) || 850);
Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(this.dataDoc) || this.Document[HeightSym]() / this.Document[WidthSym]() * 850);
}
@@ -80,10 +82,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link.
runInAction(() => {
- this._url = this.webField?.toString() || "";
- this._annotationKey = "annotations-" + WebBox.urlHash(this._url);
+ this._annotationKeySuffix = () => this._urlHash + "-annotations";
// bcz: need to make sure that doc.data-annotations points to the currently active web page's annotations (this could/should be when the doc is created)
- this.dataDoc[this.fieldKey + "-annotations"] = ComputedField.MakeFunction(`copyField(this["${this.fieldKey}-annotations-"+urlHash(this["${this.fieldKey}"]?.url?.toString()))`);
+ this.dataDoc[this.fieldKey + "-annotations"] = ComputedField.MakeFunction(`copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-annotations"`);
+ this.dataDoc[this.fieldKey + "-sidebar"] = ComputedField.MakeFunction(`copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-sidebar"`);
});
this._disposers.selection = reaction(() => this.props.isSelected(),
@@ -160,6 +162,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
menuControls = () => this.urlEditor; // controls to be added to the top bar when a document of this type is selected
scrollFocus = (doc: Doc, smooth: boolean) => {
+ if (StrCast(doc.webUrl) !== this._url) this.submitURL(StrCast(doc.webUrl), !smooth);
+ if (DocListCast(this.props.Document[this.fieldKey + "-sidebar"]).includes(doc) && !this.SidebarShown) {
+ this.toggleSidebar(!smooth);
+ }
if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1;
if (doc !== this.rootDoc && this._outerRef.current) {
const windowHeight = this.props.PanelHeight() / (this.props.scaling?.() || 1);
@@ -171,19 +177,19 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
return focusSpeed;
}
}
- this._initialScroll = NumCast(doc.y);
+ this._initialScroll = NumCast(this.layoutDoc._scrollTop);
return 0;
}
getAnchor = () => {
const anchor =
AnchorMenu.Instance?.GetAnchor(this._savedAnnotations) ??
- Docs.Create.TextanchorDocument({
+ Docs.Create.WebanchorDocument(this._url, {
title: StrCast(this.rootDoc.title + " " + this.layoutDoc._scrollTop),
annotationOn: this.rootDoc,
- y: NumCast(this.layoutDoc._scrollTop),
+ y: NumCast(this.layoutDoc._scrollTop)
});
- this.addDocument(anchor);
+ this.addDocumentWrapper(anchor);
return anchor;
}
@@ -206,6 +212,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const mainContBounds = Utils.GetScreenTransform(this._mainCont.current!);
const scale = (this.props.scaling?.() || 1) * mainContBounds.scale;
const word = getWordAtPoint(e.target, e.clientX, e.clientY);
+ this._setPreviewCursor?.(e.clientX, e.clientY, false, true);
+ MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
this._marqueeing = [e.clientX * scale + mainContBounds.translateX,
e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._scrollTop) * scale];
if (word) {
@@ -233,8 +241,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
iframe?.contentDocument.addEventListener("pointerdown", this.iframeDown);
this._scrollHeight = Math.max(this.scrollHeight, iframe?.contentDocument.body.scrollHeight);
setTimeout(action(() => this._scrollHeight = Math.max(this.scrollHeight, iframe?.contentDocument?.body.scrollHeight || 0)), 5000);
- if (this._initialScroll !== undefined && this._outerRef.current) {
- this._outerRef.current.scrollTop = this._initialScroll;
+ const initialScroll = this._initialScroll;
+ if (initialScroll !== undefined && this._outerRef.current) {
+ // bcz: not sure why this happens, but if the webpage isn't ready yet, it's scroll height seems to be limited. So we need to wait tp set scroll location to what we want.
+ setTimeout(() => this._outerRef.current!.scrollTop = initialScroll);
this._initialScroll = undefined;
}
iframe.setAttribute("enable-annotation", "true");
@@ -295,9 +305,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string"), []);
const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), []);
if (future.length) {
- history.push(this._url);
- this.dataDoc[this.fieldKey] = new WebField(new URL(this._url = future.pop()!));
- this._annotationKey = "annotations-" + WebBox.urlHash(this._url);
+ this.dataDoc[this.fieldKey + "-history"] = new List<string>([...history, this._url]);
+ this.dataDoc[this.fieldKey] = new WebField(new URL(future.pop()!));
return true;
}
return false;
@@ -309,9 +318,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), []);
if (history.length) {
if (future === undefined) this.dataDoc[this.fieldKey + "-future"] = new List<string>([this._url]);
- else future.push(this._url);
- this.dataDoc[this.fieldKey] = new WebField(new URL(this._url = history.pop()!));
- this._annotationKey = "annotations-" + WebBox.urlHash(this._url);
+ else this.dataDoc[this.fieldKey + "-future"] = new List<string>([...future, this._url]);
+ this.dataDoc[this.fieldKey] = new WebField(new URL(history.pop()!));
return true;
}
return false;
@@ -322,25 +330,19 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
@action
- submitURL = (newUrl?: string) => {
+ submitURL = (newUrl?: string, preview?: boolean) => {
if (!newUrl) return;
if (!newUrl.startsWith("http")) newUrl = "http://" + newUrl;
try {
const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string"));
const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"));
const url = this.webField?.toString();
- if (url) {
- if (history === undefined) {
- this.dataDoc[this.fieldKey + "-history"] = new List<string>([url]);
- } else {
- history.push(url);
- }
+ if (url && !preview) {
+ this.dataDoc[this.fieldKey + "-history"] = new List<string>([...(history || []), url]);
this.layoutDoc._scrollTop = 0;
future && (future.length = 0);
}
- this._url = newUrl;
- this._annotationKey = "annotations-" + WebBox.urlHash(this._url);
- this.dataDoc[this.fieldKey] = new WebField(new URL(newUrl));
+ if (!preview) this.dataDoc[this.fieldKey] = new WebField(new URL(newUrl));
} catch (e) {
console.log("WebBox URL error:" + this._url);
}
@@ -400,15 +402,18 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@action
onMarqueeDown = (e: React.PointerEvent) => {
if (!e.altKey && e.button === 0 && this.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool)) {
- this._marqueeing = [e.clientX, e.clientY];
- this.props.select(false);
+ setupMoveUpEvents(this, e, action(e => {
+ MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
+ this._marqueeing = [e.clientX, e.clientY];
+ return true;
+ }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false);
}
}
@action finishMarquee = (x?: number, y?: number) => {
this._marqueeing = undefined;
this._isAnnotating = false;
this._iframeClick = undefined;
- x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false);
+ x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false, false);
}
@computed
@@ -418,7 +423,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
if (field instanceof HtmlField) {
view = <span className="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />;
} else if (field instanceof WebField) {
- const url = this.layoutDoc.useCors ? Utils.CorsProxy(field.url.href) : field.url.href;
+ const url = this.layoutDoc.useCors ? Utils.CorsProxy(this._url) : this._url;
view = <iframe className="webBox-iframe" enable-annotation={"true"}
style={{ pointerEvents: this._scrollTimer ? "none" : undefined }}
ref={action((r: HTMLIFrameElement | null) => this._iframe = r)} src={url} onLoad={this.iframeLoaded}
@@ -433,9 +438,14 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
return view;
}
+ addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => {
+ (doc instanceof Doc ? [doc] : doc).forEach(doc => doc.webUrl = this._url);
+ return this.addDocument(doc, annotationKey);
+ }
+
sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
if (!this.layoutDoc._showSidebar) this.toggleSidebar();
- return this.addDocument(doc, sidebarKey);
+ return this.addDocumentWrapper(doc, sidebarKey);
}
sidebarBtnDown = (e: React.PointerEvent) => {
setupMoveUpEvents(this, e, (e, down, delta) => {
@@ -449,17 +459,29 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth;
}
return false;
- }, emptyFunction, this.toggleSidebar);
+ }, emptyFunction, () => this.toggleSidebar());
}
- toggleSidebar = action(() => {
+ @observable _previewNativeWidth: Opt<number> = undefined;
+ @observable _previewWidth: Opt<number> = undefined;
+ toggleSidebar = action((preview: boolean = false) => {
const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]);
- const ratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? 250 : 0) + nativeWidth) / nativeWidth;
+ const ratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? WebBox.openSidebarWidth : 0) + nativeWidth) / nativeWidth;
const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth);
- this.layoutDoc.nativeWidth = nativeWidth * ratio;
- this.layoutDoc._width = this.layoutDoc[WidthSym]() * nativeWidth * ratio / curNativeWidth;
- this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth;
+ if (preview) {
+ this._previewNativeWidth = nativeWidth * ratio;
+ this._previewWidth = this.layoutDoc[WidthSym]() * nativeWidth * ratio / curNativeWidth;
+ this._showSidebar = true;
+ }
+ else {
+ this.layoutDoc.nativeWidth = nativeWidth * ratio;
+ this.layoutDoc._width = this.layoutDoc[WidthSym]() * nativeWidth * ratio / curNativeWidth;
+ this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth;
+ }
});
- sidebarWidth = () => !this.layoutDoc._showSidebar ? 0 : (NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth() / NumCast(this.layoutDoc.nativeWidth);
+ sidebarWidth = () => !this.SidebarShown ? 0 :
+ this._previewWidth ? WebBox.openSidebarWidth :
+ (NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth() /
+ NumCast(this.layoutDoc.nativeWidth)
@computed get content() {
return <div className={"webBox-cont" + (!this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && this.isContentActive() && CurrentUserUtils.SelectedTool === InkTool.None && !DocumentDecorations.Instance?.Interacting ? "-interactive" : "")}
@@ -475,60 +497,69 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
<Annotation {...this.props} fieldKey={this.annotationKey} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} />)
}
</div>;
+
}
+ @observable _showSidebar = false;
+ @computed get SidebarShown() { return this._showSidebar || this.layoutDoc._showSidebar ? true : false; }
showInfo = action((anno: Opt<Doc>) => this._overlayAnnoInfo = anno);
- setPreviewCursor = (func?: (x: number, y: number, drag: boolean) => void) => this._setPreviewCursor = func;
+ setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => this._setPreviewCursor = func;
panelWidth = () => this.props.PanelWidth() / (this.props.scaling?.() || 1) - this.sidebarWidth(); // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0);
panelHeight = () => this.props.PanelHeight() / (this.props.scaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document);
scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop));
anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
+ transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()];
+ opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()];
render() {
- const inactiveLayer = this.props.layerProvider?.(this.layoutDoc) === false;
- const scale = this.props.scaling?.() || 1;
+ const pointerEvents = this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined;
+ const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1;
+ const scale = previewScale * (this.props.scaling?.() || 1);
+ const renderAnnotations = (docFilters?: () => string[]) =>
+ <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
+ renderDepth={this.props.renderDepth + 1}
+ isAnnotationOverlay={true}
+ fieldKey={this.annotationKey}
+ CollectionView={undefined}
+ setPreviewCursor={this.setPreviewCursor}
+ PanelWidth={this.panelWidth}
+ PanelHeight={this.panelHeight}
+ ScreenToLocalTransform={this.scrollXf}
+ scaling={returnOne}
+ dropAction={"alias"}
+ docFilters={docFilters || this.props.docFilters}
+ dontRenderDocuments={docFilters ? false : true}
+ select={emptyFunction}
+ isContentActive={returnFalse}
+ ContentScaling={returnOne}
+ bringToFront={emptyFunction}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ removeDocument={this.removeDocument}
+ moveDocument={this.moveDocument}
+ addDocument={this.sidebarAddDocument}
+ childPointerEvents={true}
+ pointerEvents={CurrentUserUtils.SelectedTool !== InkTool.None || this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} />;
return (
<div className="webBox" ref={this._mainCont} style={{ pointerEvents: this.isContentActive() ? "all" : this.isContentActive() || SnappingManager.GetIsDragging() ? undefined : "none" }} >
- <div className={`webBox-container`}
- style={{ pointerEvents: inactiveLayer ? "none" : undefined }}
- onContextMenu={this.specificContextMenu}>
+ <div className={`webBox-container`} style={{ pointerEvents }} onContextMenu={this.specificContextMenu}>
<base target="_blank" />
<div className={"webBox-outerContent"} ref={this._outerRef}
style={{
- width: `calc(${100 / scale}% - ${this.sidebarWidth() / scale}px)`,
+ width: `calc(${100 / scale}% - ${this.sidebarWidth() / scale * (this._previewWidth ? scale : 1)}px)`,
height: `${100 / scale}%`,
transform: `scale(${scale})`,
- pointerEvents: inactiveLayer ? "none" : undefined
+ pointerEvents
}}
onWheel={e => { e.stopPropagation(); e.preventDefault(); }} // block wheel events from propagating since they're handled by the iframe
onScroll={e => this.setDashScrollTop(this._outerRef.current?.scrollTop || 0)}
onPointerDown={this.onMarqueeDown}
>
- <div className={"webBox-innerContent"} style={{
- height: NumCast(this.scrollHeight, 50),
- pointerEvents: inactiveLayer ? "none" : undefined
- }}>
+ <div className={"webBox-innerContent"} style={{ height: NumCast(this.scrollHeight, 50), pointerEvents }}>
{this.content}
- <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
- renderDepth={this.props.renderDepth + 1}
- isAnnotationOverlay={true}
- fieldKey={this.annotationKey}
- CollectionView={undefined}
- setPreviewCursor={this.setPreviewCursor}
- PanelWidth={this.panelWidth}
- PanelHeight={this.panelHeight}
- ScreenToLocalTransform={this.scrollXf}
- scaling={returnOne}
- dropAction={"alias"}
- select={emptyFunction}
- isContentActive={returnFalse}
- ContentScaling={returnOne}
- bringToFront={emptyFunction}
- whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- removeDocument={this.removeDocument}
- moveDocument={this.moveDocument}
- addDocument={this.sidebarAddDocument}
- childPointerEvents={true}
- pointerEvents={this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} />
+ <div style={{ mixBlendMode: "multiply" }}>
+ {renderAnnotations(this.transparentFilter)}
+ </div>
+ {renderAnnotations(this.opaqueFilter)}
+ {renderAnnotations()}
{this.annotationLayer}
</div>
</div>
@@ -539,7 +570,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
anchorMenuClick={this.anchorMenuClick}
scrollTop={0}
down={this._marqueeing} scaling={returnOne}
- addDocument={this.addDocument}
+ addDocument={this.addDocumentWrapper}
docView={this.props.docViewPath().lastElement()}
finishMarquee={this.finishMarquee}
savedAnnotations={this._savedAnnotations}
@@ -548,10 +579,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
</div >
<SidebarAnnos ref={this._sidebarRef}
{...this.props}
- fieldKey={this.annotationKey}
+ fieldKey={this.fieldKey + "-" + this._urlHash}
rootDoc={this.rootDoc}
layoutDoc={this.layoutDoc}
dataDoc={this.dataDoc}
+ nativeWidth={this._previewNativeWidth ?? NumCast(this.layoutDoc._nativeWidth)}
+ showSidebar={this.SidebarShown}
sidebarAddDocument={this.sidebarAddDocument}
moveDocument={this.moveDocument}
removeDocument={this.removeDocument}
diff --git a/src/client/views/nodes/button/ButtonScripts.ts b/src/client/views/nodes/button/ButtonScripts.ts
new file mode 100644
index 000000000..bb4dd8bc9
--- /dev/null
+++ b/src/client/views/nodes/button/ButtonScripts.ts
@@ -0,0 +1,14 @@
+import { Scripting } from "../../../util/Scripting";
+import { SelectionManager } from "../../../util/SelectionManager";
+
+// toggle: Set overlay status of selected document
+Scripting.addGlobal(function changeView(view: string) {
+ const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined;
+ selected ? selected.Document._viewType = view : console.log("[FontIconBox.tsx] changeView failed");
+});
+
+// toggle: Set overlay status of selected document
+Scripting.addGlobal(function toggleOverlay() {
+ const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined;
+ selected ? selected.props.CollectionFreeFormDocumentView?.().float() : console.log("failed");
+}); \ No newline at end of file
diff --git a/src/client/views/nodes/button/FontIconBadge.tsx b/src/client/views/nodes/button/FontIconBadge.tsx
new file mode 100644
index 000000000..3e451eea6
--- /dev/null
+++ b/src/client/views/nodes/button/FontIconBadge.tsx
@@ -0,0 +1,36 @@
+import { observer } from "mobx-react";
+import React from "react";
+import { AclPrivate, Doc, DocListCast } from "../../../../fields/Doc";
+import { GetEffectiveAcl } from "../../../../fields/util";
+import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../../../Utils";
+import { DragManager } from "../../../util/DragManager";
+
+interface FontIconBadgeProps {
+ collection: Doc | undefined;
+}
+
+@observer
+export class FontIconBadge extends React.Component<FontIconBadgeProps> {
+ _notifsRef = React.createRef<HTMLDivElement>();
+
+ onPointerDown = (e: React.PointerEvent) => {
+ setupMoveUpEvents(this, e,
+ (e: PointerEvent) => {
+ const dragData = new DragManager.DocumentDragData([this.props.collection!]);
+ DragManager.StartDocumentDrag([this._notifsRef.current!], dragData, e.x, e.y);
+ return true;
+ },
+ returnFalse, emptyFunction, false);
+ }
+
+ render() {
+ if (!(this.props.collection instanceof Doc)) return (null);
+ const length = DocListCast(this.props.collection.data).filter(d => GetEffectiveAcl(d) !== AclPrivate).length; // Object.keys(d).length).length; // filter out any documents that we can't read
+ return <div className="fontIconBadge-container" style={{ width: 15, height: 15, top: 12 }} ref={this._notifsRef}>
+ <div className="fontIconBadge" style={length > 0 ? { "display": "initial" } : { "display": "none" }}
+ onPointerDown={this.onPointerDown} >
+ {length}
+ </div>
+ </div>;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/button/FontIconBox.scss b/src/client/views/nodes/button/FontIconBox.scss
new file mode 100644
index 000000000..8de97ea89
--- /dev/null
+++ b/src/client/views/nodes/button/FontIconBox.scss
@@ -0,0 +1,383 @@
+@import "../../global/globalCssVariables";
+
+.menuButton {
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-size: 80%;
+ border-radius: $standard-border-radius;
+
+ .menuButton-wrap {
+ grid-column: 1;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+ }
+
+ .fontIconBox-label {
+ color: $white;
+ position: relative;
+ text-align: center;
+ font-size: 7px;
+ letter-spacing: normal;
+ background-color: inherit;
+ margin-top: 5px;
+ border-radius: 8px;
+ padding: 0;
+ width: 100%;
+ font-family: 'ROBOTO';
+ text-transform: uppercase;
+ font-weight: bold;
+ }
+
+ .fontIconBox-icon {
+ width: 80%;
+ height: 80%;
+ }
+
+ &.clickBtn {
+ cursor: pointer;
+
+ &:hover {
+ background-color: rgba(0, 0, 0, 0.3) !important;
+ }
+
+ svg {
+ width: 50% !important;
+ height: 50%;
+ }
+ }
+
+ &.tglBtn {
+ cursor: pointer;
+
+ &.switch {
+ //TOGGLE
+
+ .switch {
+ position: relative;
+ display: inline-block;
+ width: 100%;
+ height: 25px;
+ margin: 0;
+ }
+
+ .switch input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+ }
+
+ .slider {
+ position: absolute;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: lightgrey;
+ -webkit-transition: .4s;
+ transition: .4s;
+ }
+
+ .slider:before {
+ position: absolute;
+ content: "";
+ height: 21px;
+ width: 21px;
+ left: 2px;
+ bottom: 2px;
+ background-color: $white;
+ -webkit-transition: .4s;
+ transition: .4s;
+ }
+
+ input:checked+.slider {
+ background-color: $medium-blue;
+ }
+
+ input:focus+.slider {
+ box-shadow: 0 0 1px $medium-blue;
+ }
+
+ input:checked+.slider:before {
+ -webkit-transform: translateX(26px);
+ -ms-transform: translateX(26px);
+ transform: translateX(26px);
+ }
+
+ /* Rounded sliders */
+ .slider.round {
+ border-radius: $standard-border-radius;
+ }
+
+ .slider.round:before {
+ border-radius: $standard-border-radius;
+ }
+ }
+
+ svg {
+ width: 50% !important;
+ height: 50%;
+ }
+
+ &:hover {
+ background-color: rgba(0, 0, 0, 0.3) !important;
+ }
+ }
+
+ &.toolBtn {
+ cursor: pointer;
+ width: 40px;
+ border-radius: 100%;
+
+ svg {
+ width: 60% !important;
+ height: 60%;
+ }
+ }
+
+ &.menuBtn {
+ cursor: pointer;
+ border-radius: 0px;
+ flex-direction: column;
+
+ svg {
+ width: 45% !important;
+ height: 45%;
+ }
+ }
+
+
+
+ &.colorBtn {
+ color: black;
+ cursor: pointer;
+ flex-direction: column;
+ background: transparent;
+
+ .colorButton-color {
+ margin-top: 3px;
+ width: 90%;
+ height: 6px;
+ }
+
+ .menuButton-dropdownBox {
+ position: absolute;
+ width: fit-content;
+ height: fit-content;
+ color: black;
+ top: 100%;
+ z-index: 21;
+ background-color: #e3e3e3;
+ box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3);
+ border-radius: 3px;
+ }
+
+ &:hover {
+ background-color: rgba(0, 0, 0, 0.3) !important;
+ }
+ }
+
+ &.drpdownList {
+ width: 100%;
+ display: grid;
+ grid-auto-columns: 80px 20px;
+ justify-items: center;
+ font-family: 'Roboto';
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ font-size: 13;
+ font-weight: 600;
+ overflow: hidden;
+ cursor: pointer;
+ background: transparent;
+ align-content: center;
+ align-items: center;
+
+ &:hover {
+ background-color: rgba(0, 0, 0, 0.3) !important;
+ }
+
+ .menuButton-dropdownList {
+ position: absolute;
+ width: 150px;
+ height: fit-content;
+ top: 100%;
+ z-index: 21;
+ background-color: $white;
+ box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3);
+ padding: 1px;
+
+ .list-item {
+ color: $black;
+ width: 100%;
+ height: 25px;
+ font-weight: 400;
+ display: flex;
+ justify-content: left;
+ align-items: center;
+ padding-left: 5px;
+ }
+
+ .list-item:hover {
+ background-color: lightgrey;
+ }
+ }
+ }
+
+ &.numBtn {
+ cursor: pointer;
+ background: transparent;
+
+ &:hover {
+ background-color: rgba(0, 0, 0, 0.3) !important;
+ }
+
+ &.slider {
+ color: $white;
+ cursor: pointer;
+ flex-direction: column;
+ background: transparent;
+
+ .menu-slider {
+ width: 100px;
+ }
+
+ .menuButton-dropdownBox {
+ position: absolute;
+ width: fit-content;
+ height: fit-content;
+ top: 100%;
+ z-index: 21;
+ background-color: #e3e3e3;
+ box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3);
+ border-radius: $standard-border-radius;
+ }
+ }
+
+ .button {
+ width: 25%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &.number {
+ width: 50%;
+
+ .button-input {
+ background: none;
+ border: none;
+ text-align: right;
+ width: 100%;
+ color: $white;
+ height: 100%;
+ text-align: center;
+ }
+
+ .button-input:focus {
+ outline: none;
+ }
+ }
+ }
+
+ &.list {
+ width: 100%;
+ justify-content: space-around;
+ border: $standard-border;
+
+ .menuButton-dropdownList {
+ position: absolute;
+ width: fit-content;
+ height: fit-content;
+ min-width: 50%;
+ max-height: 50vh;
+ overflow-y: scroll;
+ top: 100%;
+ z-index: 21;
+ background-color: $white;
+ box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3);
+ padding: 1px;
+
+ .list-item {
+ color: $black;
+ width: 100%;
+ height: 25px;
+ font-weight: 400;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+ .list-item:hover {
+ background-color: lightgrey;
+ }
+ }
+ }
+ }
+
+ &.editableText {
+ cursor: pointer;
+ background: transparent;
+ width: 100%;
+ height: 100%;
+
+ &:hover {
+ background-color: $close-red;
+ }
+ }
+
+ &.drpDownBtn {
+ cursor: pointer;
+ background: transparent;
+ border: solid 0.5px grey;
+
+ &.true {
+ background: rgba(0, 0, 0, 0.3);
+ }
+
+ .menuButton-dropdownBox {
+ position: absolute;
+ width: 150px;
+ height: 250px;
+ top: 100%;
+ background-color: #e3e3e3;
+ box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3);
+ border-radius: $standard-border-radius;
+ }
+ }
+
+ .menuButton-dropdown {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-size: 15px;
+ grid-column: 2;
+ border-radius: 0px 7px 7px 0px;
+ width: 13px;
+ height: 100%;
+ right: 0;
+ }
+
+ .menuButton-dropdown-header {
+ width: 100%;
+ font-weight: 300;
+ padding: 5px;
+ overflow: hidden;
+ font-size: 12px;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+ .dropbox-background {
+ width: 100vw;
+ height: 100vh;
+ top: 0;
+ z-index: 20;
+ left: 0;
+ background: transparent;
+ position: fixed;
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
new file mode 100644
index 000000000..53cd017b0
--- /dev/null
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -0,0 +1,761 @@
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Tooltip } from '@material-ui/core';
+import { action, computed, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import { StringifyOptions } from 'querystring';
+import * as React from 'react';
+import { ColorState, SketchPicker } from 'react-color';
+import { Doc, StrListCast } from '../../../../fields/Doc';
+import { InkTool } from '../../../../fields/InkField';
+import { createSchema, makeInterface } from '../../../../fields/Schema';
+import { ScriptField } from '../../../../fields/ScriptField';
+import { BoolCast, Cast, NumCast, StrCast } from '../../../../fields/Types';
+import { DocumentType } from '../../../documents/DocumentTypes';
+import { Scripting } from "../../../util/Scripting";
+import { SelectionManager } from '../../../util/SelectionManager';
+import { ColorScheme } from '../../../util/SettingsManager';
+import { UndoManager } from '../../../util/UndoManager';
+import { CollectionViewType } from '../../collections/CollectionView';
+import { ContextMenu } from '../../ContextMenu';
+import { DocComponent } from '../../DocComponent';
+import { EditableView } from '../../EditableView';
+import { Colors } from '../../global/globalEnums';
+import { StyleProp } from '../../StyleProvider';
+import { FieldView, FieldViewProps } from '.././FieldView';
+import { RichTextMenu } from '../formattedText/RichTextMenu';
+import './FontIconBox.scss';
+import { SetActiveInkWidth, SetActiveInkColor } from '../../InkingStroke';
+import { GestureOverlay } from '../../GestureOverlay';
+import { WebField } from '../../../../fields/URLField';
+const FontIconSchema = createSchema({
+ icon: "string",
+});
+
+export enum ButtonType {
+ MenuButton = "menuBtn",
+ DropdownList = "drpdownList",
+ DropdownButton = "drpdownBtn",
+ ClickButton = "clickBtn",
+ DoubleButton = "dblBtn",
+ ToggleButton = "tglBtn",
+ ColorButton = "colorBtn",
+ ToolButton = "toolBtn",
+ NumberButton = "numBtn",
+ EditableText = "editableText"
+}
+
+export enum NumButtonType {
+ Slider = "slider",
+ DropdownOptions = "list",
+ Inline = "inline"
+}
+
+export interface ButtonProps extends FieldViewProps {
+ type?: ButtonType;
+}
+
+type FontIconDocument = makeInterface<[typeof FontIconSchema]>;
+const FontIconDocument = makeInterface(FontIconSchema);
+@observer
+export class FontIconBox extends DocComponent<ButtonProps, FontIconDocument>(FontIconDocument) {
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(FontIconBox, fieldKey); }
+ showTemplate = (): void => {
+ const dragFactory = Cast(this.layoutDoc.dragFactory, Doc, null);
+ dragFactory && this.props.addDocTab(dragFactory, "add:right");
+ }
+ dragAsTemplate = (): void => { this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)'); };
+ useAsPrototype = (): void => { this.layoutDoc.onDragStart = ScriptField.MakeFunction('makeDelegate(this.dragFactory, true)'); };
+
+ specificContextMenu = (): void => {
+ if (!Doc.UserDoc().noviceMode) {
+ const cm = ContextMenu.Instance;
+ cm.addItem({ description: "Show Template", event: this.showTemplate, icon: "tag" });
+ cm.addItem({ description: "Use as Render Template", event: this.dragAsTemplate, icon: "tag" });
+ cm.addItem({ description: "Use as Prototype", event: this.useAsPrototype, icon: "tag" });
+ }
+ }
+
+ // Determining UI Specs
+ @observable private label = StrCast(this.rootDoc.label, StrCast(this.rootDoc.title));
+ @observable private icon = StrCast(this.dataDoc.icon, "user") as any;
+ @observable private dropdown: boolean = BoolCast(this.rootDoc.dropDownOpen);
+ @observable private buttonList: string[] = StrListCast(this.rootDoc.btnList);
+ @observable private type = StrCast(this.rootDoc.btnType);
+
+ /**
+ * Types of buttons in dash:
+ * - Main menu button (LHS)
+ * - Tool button
+ * - Expandable button (CollectionLinearView)
+ * - Button inside of CollectionLinearView vs. outside of CollectionLinearView
+ * - Action button
+ * - Dropdown button
+ * - Color button
+ * - Dropdown list
+ * - Number button
+ **/
+
+ _batch: UndoManager.Batch | undefined = undefined;
+ /**
+ * Number button
+ */
+ @computed get numberButton() {
+ const numBtnType: string = StrCast(this.rootDoc.numBtnType);
+ const setValue = (value: number) => {
+ // Script for running the toggle
+ const script: string = StrCast(this.rootDoc.script) + "(" + value + ")";
+ ScriptField.MakeScript(script)?.script.run();
+ };
+
+ // Script for checking the outcome of the toggle
+ const checkScript: string = StrCast(this.rootDoc.script) + "(0, true)";
+ const checkResult: number = ScriptField.MakeScript(checkScript)?.script.run().result;
+
+
+ if (numBtnType === NumButtonType.Slider) {
+ const dropdown =
+ <div
+ className="menuButton-dropdownBox"
+ onPointerDown={e => e.stopPropagation()}
+ style={{ left: 0 }}
+ >
+ <input type="range" step="1" min="0" max="100" value={checkResult}
+ className={"menu-slider"} id="slider"
+ onPointerDown={() => { this._batch = UndoManager.StartBatch("presDuration"); }}
+ onPointerUp={() => { if (this._batch) this._batch.end(); }}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => { e.stopPropagation(); setValue(Number(e.target.value)); }}
+ />
+ </div>;
+ return (
+ <div
+ className={`menuButton ${this.type} ${numBtnType}`}
+ onClick={action(() => this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen)}
+ >
+ {checkResult}
+ {this.rootDoc.dropDownOpen ? dropdown : null}
+ </div>
+ );
+ } else if (numBtnType === NumButtonType.DropdownOptions) {
+ const items: number[] = [];
+ for (let i = 0; i < 100; i++) {
+ if (i % 2 === 0) {
+ items.push(i);
+ }
+ }
+ const list = items.map((value) => {
+ return <div className="list-item" key={`${value}`}
+ style={{
+ backgroundColor: value === checkResult ? Colors.LIGHT_BLUE : undefined
+ }}
+ onClick={() => setValue(value)}>
+ {value}
+ </div>;
+ });
+ return (
+ <div
+ className={`menuButton ${this.type} ${numBtnType}`}
+ >
+ <div className={`button`} onClick={action((e) => setValue(checkResult - 1))}>
+ <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={"minus"} />
+ </div>
+ <div
+ className={`button ${'number'}`}
+ onPointerDown={(e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ }}
+ onDoubleClick={action(() => this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen)}
+ >
+ <input
+ style={{ width: 30 }}
+ className="button-input"
+ type="number"
+ value={checkResult}
+ onChange={action((e) => setValue(Number(e.target.value)))}
+ />
+ </div>
+ <div className={`button`} onClick={action((e) => setValue(checkResult + 1))}>
+ <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={"plus"} />
+ </div>
+ {this.rootDoc.dropDownOpen ?
+ <div>
+ <div className="menuButton-dropdownList"
+ style={{ left: "25%" }}>
+ {list}
+ </div>
+ <div className="dropbox-background" onClick={(e) => { e.stopPropagation(); this.rootDoc.dropDownOpen = false; }} />
+ </div> : null}
+
+ </div>
+ );
+ } else {
+ return (
+ <div>
+
+ </div>
+ );
+ }
+
+
+ }
+
+ /**
+ * Dropdown button
+ */
+ @computed get dropdownButton() {
+ const active: string = StrCast(this.rootDoc.dropDownOpen);
+ const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
+ const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
+ return (
+ <div className={`menuButton ${this.type} ${active}`}
+ style={{ color: color, backgroundColor: backgroundColor, borderBottomLeftRadius: this.dropdown ? 0 : undefined }}
+ onClick={action(() => this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen)}>
+ <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
+ {!this.label || !Doc.UserDoc()._showLabel ? (null) : <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}> {this.label} </div>}
+ <div
+ className="menuButton-dropdown"
+ style={{ borderBottomRightRadius: this.dropdown ? 0 : undefined }}>
+ <FontAwesomeIcon icon={'caret-down'} color={color} size="sm" />
+ </div>
+ {this.rootDoc.dropDownOpen ?
+ <div className="menuButton-dropdownBox"
+ style={{ left: 0 }}>
+ {/* DROPDOWN BOX CONTENTS */}
+ </div> : null}
+ </div>
+ );
+ }
+
+ /**
+ * Dropdown list
+ */
+ @computed get dropdownListButton() {
+ const active: string = StrCast(this.rootDoc.dropDownOpen);
+ const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
+ const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
+
+ const script: string = StrCast(this.rootDoc.script);
+
+ let noviceList: string[] = [];
+ let text: string | undefined;
+ let dropdown = true;
+ let icon: IconProp = "caret-down";
+
+
+ if (script === 'setView') {
+ const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined;
+ if (selected && StrCast(selected.Document.type) === DocumentType.COL) {
+ text = StrCast(selected.Document._viewType);
+ } else if (selected) {
+ dropdown = false;
+ text = StrCast(selected.Document.type);
+ icon = Doc.toIcon(selected.Document);
+ } else {
+ dropdown = false;
+ text = "None selected";
+ }
+ noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Stacking];
+ } else if (script === 'setFont') {
+ const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined;
+ if (selected && StrCast(selected.Document.type) === DocumentType.RTF) {
+ text = StrCast(selected.Document._fontFamily);
+ } else {
+ const fontFamily = StrCast(Doc.UserDoc()._fontFamily);
+ text = fontFamily;
+ }
+ noviceList = ["Roboto", "Times New Roman", "Arial", "Georgia",
+ "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"];
+ }
+
+ // Get items to place into the list
+ const list = this.buttonList.map((value) => {
+ if (Doc.UserDoc().noviceMode && !noviceList.includes(value)) {
+ return;
+ }
+ const click = () => {
+ const s = ScriptField.MakeScript(script + '("' + value + '")');
+ if (s) {
+ s.script.run().result;
+ }
+ };
+ return <div className="list-item" key={`${value}`}
+ style={{
+ fontFamily: script === 'setFont' ? value : undefined,
+ backgroundColor: value === text ? Colors.LIGHT_BLUE : undefined
+ }}
+ onClick={click}>
+ {value[0].toUpperCase() + value.slice(1)}
+ </div>;
+ });
+
+ const label = !this.label || !Doc.UserDoc()._showLabel ? (null) :
+ <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor, position: "absolute" }}>
+ {this.label}
+ </div>;
+
+ return (
+ <div className={`menuButton ${this.type} ${active}`}
+ style={{ backgroundColor: this.rootDoc.dropDownOpen ? Colors.MEDIUM_BLUE : backgroundColor, color: color, display: dropdown ? undefined : "flex" }}
+ onClick={dropdown ? () => this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen : undefined}>
+ {dropdown ? (null) : <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={icon} color={color} />}
+ <div className="menuButton-dropdown-header">
+ {text && text[0].toUpperCase() + text.slice(1)}
+ </div>
+ {label}
+ {!dropdown ? (null) : <div className="menuButton-dropDown">
+ <FontAwesomeIcon icon={icon} color={color} size="sm" />
+ </div>}
+ {this.rootDoc.dropDownOpen ?
+ <div>
+ <div className="menuButton-dropdownList"
+ style={{ left: 0 }}>
+ {list}
+ </div>
+ <div className="dropbox-background" onClick={(e) => { e.stopPropagation(); this.rootDoc.dropDownOpen = false; }} />
+ </div>
+ : null}
+ </div>
+ );
+ }
+
+ /**
+ * Color button
+ */
+ @computed get colorButton() {
+ const active: string = StrCast(this.rootDoc.dropDownOpen);
+ const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
+ const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
+
+ const script: string = StrCast(this.rootDoc.script);
+ const scriptCheck: string = script + "(undefined, true)";
+ const boolResult = ScriptField.MakeScript(scriptCheck)?.script.run().result;
+
+ let stroke: boolean = false;
+ let strokeIcon: any;
+ if (script === "setStrokeColor"){
+ stroke = true;
+ const checkWidth = ScriptField.MakeScript("setStrokeWidth(0, true)")?.script.run().result;
+ strokeIcon = (<div style={{borderRadius: "100%", width: Number(checkWidth)+'%', height: Number(checkWidth)+'%', backgroundColor: boolResult }}/>)
+ }
+
+ const colorOptions: string[] = ['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505',
+ '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B',
+ '#FFFFFF', '#f1efeb'];
+
+ const colorBox = (func: (color: ColorState) => void) => <SketchPicker
+ disableAlpha={!stroke}
+ onChange={func} color={boolResult ? boolResult : "#FFFFFF"}
+ presetColors={colorOptions} />;
+ const label = !this.label || !Doc.UserDoc()._showLabel ? (null) :
+ <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor, position: "absolute" }}>
+ {this.label}
+ </div>;
+
+ const dropdownCaret = <div
+ className="menuButton-dropDown"
+ style={{ borderBottomRightRadius: this.dropdown ? 0 : undefined }}>
+ <FontAwesomeIcon icon={'caret-down'} color={color} size="sm" />
+ </div>;
+
+ const click = (value: ColorState) => {
+ const hex: string = value.hex;
+ const s = ScriptField.MakeScript(script + '("' + hex + '", false)');
+ if (s) {
+ s.script.run().result;
+ }
+ };
+ return (
+ <div className={`menuButton ${this.type} ${active}`}
+ style={{ color: color, borderBottomLeftRadius: this.dropdown ? 0 : undefined }}
+ onClick={() => this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen}
+ onPointerDown={e => e.stopPropagation()}>
+ {stroke ? strokeIcon : <><FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
+ <div className="colorButton-color"
+ style={{ backgroundColor: boolResult ? boolResult : "#FFFFFF" }}
+ ></div></>}
+ {label}
+ {/* {dropdownCaret} */}
+ {this.rootDoc.dropDownOpen ?
+ <div>
+ <div className="menuButton-dropdownBox"
+ onPointerDown={e => e.stopPropagation()}
+ onClick={e => e.stopPropagation()}
+ style={{ left: 0 }}>
+ {colorBox(click)}
+ </div>
+ <div className="dropbox-background" onClick={(e) => { e.stopPropagation(); this.rootDoc.dropDownOpen = false; }} />
+ </div>
+ : null}
+ </div>
+ );
+ }
+
+ @computed get toggleButton() {
+ // Determine the type of toggle button
+ const switchToggle: boolean = BoolCast(this.rootDoc.switchToggle);
+
+ // Script for running the toggle
+ const script: string = StrCast(this.rootDoc.script);
+
+ // Script for checking the outcome of the toggle
+ let checkScript:string;
+ if (StrCast(this.rootDoc.script).includes("(")){
+ checkScript = StrCast(this.rootDoc.script)
+
+ } else {
+ checkScript = StrCast(this.rootDoc.script) + "(true)";
+ }
+
+ // Function to run the script
+ const runScript = () => ScriptField.MakeScript(script + "()")?.script.run();
+ const checkResult = ScriptField.MakeScript(checkScript)?.script.run().result;
+
+ // Colors
+ const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
+ const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
+
+ // Button label
+ const label = !this.label || !Doc.UserDoc()._showLabel ? (null) :
+ <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor, position: "absolute" }}>
+ {this.label}
+ </div>;
+
+ if (switchToggle) {
+ return (
+ <div className={`menuButton ${this.type} ${'switch'}`}
+ onClick={runScript}
+ >
+ <label className="switch">
+ <input type="checkbox"
+ checked={checkResult}
+ onChange={runScript}
+ />
+ <span className="slider round"></span>
+ </label>
+ </div>
+ );
+ } else {
+ return (
+ <div className={`menuButton ${this.type}`}
+ style={{ opacity: 1, backgroundColor: checkResult ? Colors.MEDIUM_BLUE : "transparent" }}
+ onClick={this.layoutDoc.ignoreClick ? runScript : undefined}
+ >
+ <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={checkResult ? Colors.LIGHT_GRAY : Colors.LIGHT_GRAY} />
+ {label}
+ </div>
+ );
+ }
+ }
+
+
+
+ /**
+ * Default
+ */
+ @computed get defaultButton() {
+ const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
+ const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
+ const active: string = StrCast(this.rootDoc.dropDownOpen);
+ return (
+ <div className={`menuButton ${this.type}`} onContextMenu={this.specificContextMenu}
+ style={{ backgroundColor: "transparent", borderBottomLeftRadius: this.dropdown ? 0 : undefined }}>
+ <div className="menuButton-wrap">
+ <FontAwesomeIcon className={`menuButton-icon-${this.type}`} icon={this.icon} color={"black"} size={"sm"} />
+ {!this.label || !Doc.UserDoc()._showLabel ? (null) : <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}> {this.label} </div>}
+ </div>
+ </div>
+ );
+ }
+
+ @computed get editableText() {
+ // Script for running the toggle
+ const script: string = StrCast(this.rootDoc.script);
+
+ // Script for checking the outcome of the toggle
+ let checkScript:string = StrCast(this.rootDoc.script) + "('', true)";
+
+ // Function to run the script
+ const checkResult = ScriptField.MakeScript(checkScript)?.script.run().result;
+
+ const setValue = (value: string, shiftDown?: boolean): boolean => {
+ ScriptField.MakeScript(script + "('"+value+"')")?.script.run();
+ return true;
+ };
+ return (
+ <div className="menuButton editableText">
+ <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={"lock"} />
+ <EditableView GetValue={() => checkResult} SetValue={setValue} contents="...">
+ </EditableView>
+ </div>
+ );
+ }
+
+
+ render() {
+ const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
+ const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
+ // Variables called through eval (from button)
+ const numSelected = SelectionManager.Views().length;
+
+ const dark: boolean = Doc.UserDoc().colorScheme === ColorScheme.Dark;
+
+ const active: string = StrCast(this.rootDoc.dropDownOpen);
+ const label = !this.label || !Doc.UserDoc()._showLabel ? (null) :
+ <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor, position: "absolute" }}>
+ {this.label}
+ </div>;
+ const menuLabel = !this.label || !Doc.UserDoc()._showMenuLabel ? (null) :
+ <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}>
+ {this.label}
+ </div>;
+
+ // TODO:glr Add label of button type
+ let button = this.defaultButton;
+
+ switch (this.type) {
+ case ButtonType.EditableText:
+ console.log("Editable text");
+ button = this.editableText;
+ break;
+ case ButtonType.NumberButton:
+ button = this.numberButton;
+ break;
+ case ButtonType.DropdownButton:
+ button = this.dropdownButton;
+ break;
+ case ButtonType.DropdownList:
+ button = this.dropdownListButton;
+ break;
+ case ButtonType.ColorButton:
+ button = this.colorButton;
+ break;
+ case ButtonType.ToolButton:
+ button = (
+ <div className={`menuButton ${this.type}`} style={{ opacity: 1, backgroundColor: backgroundColor, color: color }}>
+ <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
+ {label}
+ </div>
+ );
+ break;
+ case ButtonType.ToggleButton:
+ button = this.toggleButton;
+ break;
+ case ButtonType.ClickButton:
+ button = (
+ <div className={`menuButton ${this.type}`} style={{ color: color, backgroundColor: backgroundColor, opacity: 1 }}>
+ <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
+ {label}
+ </div>
+ );
+ break;
+ case ButtonType.MenuButton:
+ button = (
+ <div className={`menuButton ${this.type}`} style={{ color: color, backgroundColor: backgroundColor }}>
+ <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
+ {menuLabel}
+ </div >
+ );
+ break;
+ default:
+ break;
+ }
+
+ return !this.layoutDoc.toolTip || this.type === ButtonType.DropdownList || this.type === ButtonType.ColorButton || this.type === ButtonType.NumberButton || this.type === ButtonType.EditableText ? button :
+ <Tooltip title={<div className="dash-tooltip">{StrCast(this.layoutDoc.toolTip)}</div>}>
+ {button}
+ </Tooltip>;
+ }
+}
+
+
+// toggle: Set overlay status of selected document
+Scripting.addGlobal(function setView(view: string) {
+ const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined;
+ selected ? selected.Document._viewType = view : console.log("[FontIconBox.tsx] changeView failed");
+});
+
+// toggle: Set overlay status of selected document
+Scripting.addGlobal(function setBackgroundColor(color?: string, checkResult?: boolean) {
+ const selected = SelectionManager.Views().length ? SelectionManager.Views()[0].rootDoc : undefined;
+ if (checkResult) {
+ if (selected) {
+ console.log("[Background] (selected): " + StrCast(selected._backgroundColor));
+ return selected._backgroundColor;
+ } else {
+ return "#FFFFFF";
+ }
+ }
+ if (selected && selected.type === DocumentType.INK) selected.fillColor = color;
+ if (selected) selected._backgroundColor = color;
+ Doc.UserDoc()._fontColor = color;
+});
+
+// toggle: Set overlay status of selected document
+Scripting.addGlobal(function toggleOverlay(checkResult?: boolean) {
+ const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined;
+ if (checkResult && selected) {
+ return NumCast(selected.Document.z) === 1;
+ }
+ selected ? selected.props.CollectionFreeFormDocumentView?.().float() : console.log("[FontIconBox.tsx] toggleOverlay failed");
+});
+
+/** TEXT
+ * setFont
+ * setFontSize
+ * toggleBold
+ * toggleUnderline
+ * toggleItalic
+ **/
+
+// toggle: Set overlay status of selected document
+Scripting.addGlobal(function setFont(font: string) {
+ SelectionManager.Views().map(dv => dv.props.Document._fontFamily = font);
+ Doc.UserDoc()._fontFamily = font;
+ return Doc.UserDoc()._fontFamily;
+});
+
+// toggle: Set overlay status of selected document
+Scripting.addGlobal(function setFontColor(color?: string, checkResult?: boolean) {
+ const selected = SelectionManager.Views().length ? SelectionManager.Views()[0].rootDoc : undefined;
+ if (checkResult) {
+ if (selected) {
+ console.log("[Font color] (selected): " + StrCast(selected._fontColor));
+ return selected._fontColor;
+ } else {
+ console.log("[Font color] (global): " + StrCast(Doc.UserDoc()._fontColor));
+ return Doc.UserDoc()._fontColor;
+ }
+ }
+ if (selected) selected._fontColor = color;
+ Doc.UserDoc()._fontColor = color;
+});
+
+// // toggle: Set overlay status of selected document
+// Scripting.addGlobal(function setFontHighlight(color?: string, checkResult?: boolean) {
+// const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined;
+// if (checkResult) {
+// if (selected) {
+// return selected.Document._fontColor;
+// } else {
+// return Doc.UserDoc()._fontColor;
+// }
+// }
+// Doc.UserDoc()._fontColor = color;
+// });
+
+
+
+// toggle: Set overlay status of selected document
+Scripting.addGlobal(function setFontSize(size: string, checkResult?: boolean) {
+ if (checkResult) {
+ const size: number = parseInt(StrCast(Doc.UserDoc()._fontSize), 10);
+ return size;
+ }
+ Doc.UserDoc()._fontSize = size + "px";
+});
+
+Scripting.addGlobal(function toggleBold(checkResult?: boolean) {
+ if (checkResult) {
+ return Doc.UserDoc().bold;
+ }
+ SelectionManager.Views().filter(i => StrCast(i.rootDoc.type) === DocumentType.RTF).map(dv => dv.props.Document.italic = !dv.props.Document.italic);
+ Doc.UserDoc().bold = !Doc.UserDoc().bold;
+ return Doc.UserDoc().bold;
+});
+
+Scripting.addGlobal(function toggleUnderline(checkResult?: boolean) {
+ if (checkResult) {
+ return Doc.UserDoc().underline;
+ }
+ SelectionManager.Views().filter(i => StrCast(i.rootDoc.type) === DocumentType.RTF).map(dv => dv.props.Document.italic = !dv.props.Document.italic);
+ Doc.UserDoc().underline = !Doc.UserDoc().underline;
+ return Doc.UserDoc().underline;
+});
+
+Scripting.addGlobal(function toggleItalic(checkResult?: boolean) {
+ if (checkResult) {
+ return Doc.UserDoc().italic;
+ }
+ SelectionManager.Views().filter(i => StrCast(i.rootDoc.type) === DocumentType.RTF).map(dv => dv.props.Document.italic = !dv.props.Document.italic);
+ Doc.UserDoc().italic = !Doc.UserDoc().italic;
+ return Doc.UserDoc().italic;
+});
+
+
+
+
+/** INK
+ * setActiveInkTool
+ * setStrokeWidth
+ * setStrokeColor
+ **/
+
+Scripting.addGlobal(function setActiveInkTool(tool: string, checkResult?: boolean) {
+ if (checkResult) {
+ return Doc.UserDoc().activeInkTool === tool && GestureOverlay.Instance.InkShape === "" || GestureOverlay.Instance.InkShape === tool;
+ }
+ if (tool === "circle") {
+ Doc.UserDoc().activeInkTool = "pen";
+ GestureOverlay.Instance.InkShape = tool;
+ } else if (tool === "square") {
+ Doc.UserDoc().activeInkTool = "pen";
+ GestureOverlay.Instance.InkShape = tool;
+ } else if (tool === "line") {
+ Doc.UserDoc().activeInkTool = "pen";
+ GestureOverlay.Instance.InkShape = tool;
+ } else if (tool) {
+ if (Doc.UserDoc().activeInkTool === tool && GestureOverlay.Instance.InkShape === "" || GestureOverlay.Instance.InkShape === tool){
+ GestureOverlay.Instance.InkShape = "";
+ Doc.UserDoc().activeInkTool = InkTool.None;
+ } else {
+ Doc.UserDoc().activeInkTool = tool;
+ }
+ } else {
+ Doc.UserDoc().activeInkTool = InkTool.None;
+ }
+});
+
+Scripting.addGlobal(function setStrokeWidth(width: number, checkResult?: boolean) {
+ if (checkResult) {
+ const selected = SelectionManager.Views().length ? SelectionManager.Views()[0].rootDoc : undefined;
+ if (selected && selected.type === DocumentType.INK){
+ return Number(selected.strokeWidth);
+ } else {
+ const width: number = NumCast(Doc.UserDoc().activeInkWidth);
+ return width;
+ }
+ }
+ Doc.UserDoc().activeInkWidth = width;
+ SelectionManager.Views().filter(i => StrCast(i.rootDoc.type) === DocumentType.INK).map(i => i.rootDoc.strokeWidth = Number(width));
+});
+
+// toggle: Set overlay status of selected document
+Scripting.addGlobal(function setStrokeColor(color?: string, checkResult?: boolean) {
+ if (checkResult) {
+ const selected = SelectionManager.Views().length ? SelectionManager.Views()[0].rootDoc : undefined;
+ if (selected && selected.type === DocumentType.INK){
+ return selected.color;
+ } else {
+ const color: string = StrCast(Doc.UserDoc().activeInkColor);
+ return color;
+ }
+ }
+ SetActiveInkColor(StrCast(color));
+ SelectionManager.Views().filter(i => StrCast(i.rootDoc.type) === DocumentType.INK).map(i => i.rootDoc.color = String(color));
+});
+
+
+/** WEB
+ * webSetURL
+ **/
+Scripting.addGlobal(function webSetURL(url: string, checkResult?: boolean) {
+ const selected = SelectionManager.Views().length ? SelectionManager.Views()[0].rootDoc : undefined;
+ console.log("URL: ", url);
+ if (checkResult && selected && selected.type === DocumentType.WEB){
+ return Cast(selected.data, WebField, null).url;
+ }
+ else if (selected && selected.type === DocumentType.WEB){
+ selected.data = url;
+ }
+}); \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 140d39929..d1027dfd7 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -11,7 +11,7 @@ import { ReplaceStep } from 'prosemirror-transform';
import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { DateField } from '../../../../fields/DateField';
-import { AclAdmin, AclEdit, DataSym, Doc, DocListCast, DocListCastAsync, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym } from "../../../../fields/Doc";
+import { AclAdmin, AclEdit, AclSelfEdit, DataSym, Doc, DocListCast, DocListCastAsync, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym, AclAugment } from "../../../../fields/Doc";
import { documentSchema } from '../../../../fields/documentSchemas';
import { Id } from '../../../../fields/FieldSymbols';
import { InkTool } from '../../../../fields/InkField';
@@ -120,7 +120,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
public get EditorView() { return this._editorView; }
public get SidebarKey() { return this.fieldKey + "-sidebar"; }
- @computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._sidebarWidthPercent, "0%"); }
+ @computed get sidebarWidthPercent() { return this._showSidebar ? "20%" : StrCast(this.layoutDoc._sidebarWidthPercent, "0%"); }
@computed get sidebarColor() { return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "#e4e4e4")); }
@computed get autoHeight() { return this.layoutDoc._autoHeight && !this.props.ignoreAutoHeight; }
@computed get textHeight() { return NumCast(this.rootDoc[this.fieldKey + "-height"]); }
@@ -215,9 +215,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
AnchorMenu.Instance.Status = "marquee";
AnchorMenu.Instance.Highlight = action((color: string, isLinkButton: boolean) => {
this._editorView?.state && RichTextMenu.Instance.insertHighlight(color, this._editorView.state, this._editorView?.dispatch);
- console.log("highlight")
return undefined;
});
+ AnchorMenu.Instance.onMakeAnchor = this.getAnchor;
/**
* This function is used by the PDFmenu to create an anchor highlight and a new linked text annotation.
* It also initiates a Drag/Drop interaction to place the text annotation.
@@ -254,7 +254,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
const removeSelection = (json: string | undefined) => json?.indexOf("\"storedMarks\"") === -1 ?
json?.replace(/"selection":.*/, "") : json?.replace(/"selection":"\"storedMarks\""/, "\"storedMarks\"");
- if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) {
+ if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin || effectiveAcl === AclSelfEdit) {
const accumTags = [] as string[];
state.tr.doc.nodesBetween(0, state.doc.content.size, (node: any, pos: number, parent: any) => {
if (node.type === schema.nodes.dashField && node.attrs.fieldKey.startsWith("#")) {
@@ -371,7 +371,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex;
const anchor = Docs.Create.TextanchorDocument();
const alink = DocUtils.MakeLink({ doc: anchor }, { doc: target }, "automatic")!;
- const allAnchors = [{ href: Utils.prepend("/doc/" + anchor[Id]), title: "a link", anchorId: anchor[Id] }];
+ const allAnchors = [{ href: Doc.localServerPath(anchor), title: "a link", anchorId: anchor[Id] }];
const link = this._editorView!.state.schema.marks.linkAnchor.create({ allAnchors, title: "auto link", location });
tr = tr.addMark(flattened[i].from, flattened[i].to, link);
});
@@ -432,6 +432,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
this.ProseRef = ele;
this._dropDisposer?.();
ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.layoutDoc));
+ // if (this.autoHeight) this.tryUpdateScrollHeight();
}
@undoBatch
@@ -535,7 +536,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" });
const min = Math.round(Date.now() / 1000 / 60);
numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-min-" + (min - i), { opacity: ((10 - i - 1) / 10).toString() }));
- setTimeout(() => this.updateHighlights());
+ setTimeout(this.updateHighlights);
}
if (FormattedTextBox._highlights.indexOf("By Recent Hour") !== -1) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" });
@@ -544,11 +545,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
}
+ @observable _showSidebar = false;
+ @computed get SidebarShown() { return this._showSidebar || this.layoutDoc._showSidebar ? true : false; }
+
@action
- toggleSidebar = () => {
+ toggleSidebar = (preview: boolean = false) => {
const prevWidth = this.sidebarWidth();
- this.layoutDoc._showSidebar = ((this.layoutDoc._sidebarWidthPercent = StrCast(this.layoutDoc._sidebarWidthPercent, "0%") === "0%" ? "50%" : "0%")) !== "0%";
- this.layoutDoc._width = this.layoutDoc._showSidebar ? NumCast(this.layoutDoc._width) * 2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth);
+ if (preview) this._showSidebar = true;
+ else this.layoutDoc._showSidebar = ((this.layoutDoc._sidebarWidthPercent = StrCast(this.layoutDoc._sidebarWidthPercent, "0%") === "0%" ? "50%" : "0%")) !== "0%";
+
+ this.layoutDoc._width = !preview && this.SidebarShown ? NumCast(this.layoutDoc._width) * 2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth);
}
sidebarDown = (e: React.PointerEvent) => {
setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, () => setTimeout(this.toggleSidebar), false);
@@ -705,7 +711,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
let tr = state.tr.addMark(sel.from, sel.to, splitter);
if (sel.from !== sel.to) {
const anchor = anchorDoc ?? Docs.Create.TextanchorDocument({ title: this._editorView?.state.doc.textBetween(sel.from, sel.to) });
- const href = targetHref ?? Utils.prepend("/doc/" + anchor[Id]);
+ const href = targetHref ?? Doc.localServerPath(anchor);
if (anchor !== anchorDoc) this.addDocument(anchor);
tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => {
if (node.firstChild === null && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) {
@@ -726,6 +732,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
scrollFocus = (textAnchor: Doc, smooth: boolean) => {
+ if (DocListCast(this.Document[this.fieldKey + "-sidebar"]).includes(textAnchor) && !this.SidebarShown) {
+ this.toggleSidebar(!smooth);
+ }
const textAnchorId = textAnchor[Id];
const findAnchorFrag = (frag: Fragment, editor: EditorView) => {
const nodes: Node[] = [];
@@ -874,7 +883,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
},
);
- if (this._recording) setTimeout(() => this.recordDictation());
+ if (this._recording) setTimeout(this.recordDictation);
}
var quickScroll: string | undefined = "";
this._disposers.scroll = reaction(() => NumCast(this.layoutDoc._scrollTop),
@@ -1042,7 +1051,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
const marks = [...node.marks];
const linkIndex = marks.findIndex(mark => mark.type.name === "link");
- const allLinks = [{ href: Utils.prepend(`/doc/${linkId}`), title, linkId }];
+ const allLinks = [{ href: Doc.globalServerPath(linkId), title, linkId }];
const link = view.state.schema.mark(view.state.schema.marks.linkAnchor, { allLinks, location: "add:right", title, docref: true });
marks.splice(linkIndex === -1 ? 0 : linkIndex, 1, link);
return node.mark(marks);
@@ -1394,6 +1403,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
this._rules!.EnteringStyle = false;
}
e.stopPropagation();
+ for (var i = state.selection.from; i < state.selection.to; i++) {
+ const node = state.doc.resolve(i);
+ if (node?.marks?.().some(mark => mark.type === schema.marks.user_mark &&
+ mark.attrs.userid !== Doc.CurrentUserEmail) &&
+ [AclAugment, AclSelfEdit].includes(GetEffectiveAcl(this.rootDoc))) {
+ e.preventDefault();
+ }
+ }
switch (e.key) {
case "Escape":
this._editorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from)));
@@ -1423,16 +1440,24 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
}
}
- tryUpdateScrollHeight() {
+ tryUpdateScrollHeight = () => {
if (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath())) {
const margins = 2 * NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0);
- const proseHeight = !this.ProseRef ? 0 : Array.from(this.ProseRef.children[0].children).reduce((p, child) => p + Number(getComputedStyle(child).height.replace("px", "")), margins);
- const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight);
- if (scrollHeight && this.props.renderDepth && !this.props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
- const setScrollHeight = () => this.rootDoc[this.fieldKey + "-scrollHeight"] = scrollHeight;
- if (this.rootDoc === this.layoutDoc.doc || this.layoutDoc.resolvedDataDoc) {
- setScrollHeight();
- } else setTimeout(setScrollHeight, 10); // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template. So set a timeout and hope its arrived...
+ const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined;
+ if (children) {
+ var proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + Number(getComputedStyle(child).height.replace("px", "")), margins);
+ var scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight);
+ if (scrollHeight && this.props.renderDepth && !this.props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
+ const setScrollHeight = () => this.rootDoc[this.fieldKey + "-scrollHeight"] = scrollHeight;
+ if (this.rootDoc === this.layoutDoc.doc || this.layoutDoc.resolvedDataDoc) {
+ setScrollHeight();
+ setTimeout(() => {
+ proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + Number(getComputedStyle(child).height.replace("px", "")), margins);
+ scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight);
+ scrollHeight && setScrollHeight();
+ }, 10);
+ } else setTimeout(setScrollHeight, 10); // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template. So set a timeout and hope its arrived...
+ }
}
}
}
@@ -1468,10 +1493,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
return ComponentTag === CollectionStackingView ?
<SidebarAnnos ref={this._sidebarRef}
{...this.props}
- fieldKey={this.annotationKey}
+ fieldKey={this.fieldKey}
rootDoc={this.rootDoc}
layoutDoc={this.layoutDoc}
dataDoc={this.dataDoc}
+ nativeWidth={NumCast(this.layoutDoc._nativeWidth)}
+ showSidebar={this.SidebarShown}
PanelWidth={this.sidebarWidth}
setHeight={this.setSidebarHeight}
sidebarAddDocument={this.sidebarAddDocument}
@@ -1569,7 +1596,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}}
/>
</div>
- {(this.props.noSidebar || this.Document._noSidebar) || this.props.dontSelectOnLoad || !this.layoutDoc._showSidebar || this.sidebarWidthPercent === "0%" ? (null) : this.sidebarCollection}
+ {(this.props.noSidebar || this.Document._noSidebar) || this.props.dontSelectOnLoad || !this.SidebarShown || this.sidebarWidthPercent === "0%" ? (null) : this.sidebarCollection}
{(this.props.noSidebar || this.Document._noSidebar) || this.props.dontSelectOnLoad || this.Document._singleLine ? (null) : this.sidebarHandle}
{!this.layoutDoc._showAudio ? (null) : this.audioHandle}
</div>
diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
index d5c77786c..76a5675de 100644
--- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
+++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
@@ -7,13 +7,14 @@ import { splitListItem, wrapInList, } from "prosemirror-schema-list";
import { EditorState, Transaction, TextSelection } from "prosemirror-state";
import { SelectionManager } from "../../../util/SelectionManager";
import { NumCast, BoolCast, Cast, StrCast } from "../../../../fields/Types";
-import { Doc, DataSym, DocListCast } from "../../../../fields/Doc";
+import { Doc, DataSym, DocListCast, AclAugment, AclSelfEdit } from "../../../../fields/Doc";
import { FormattedTextBox } from "./FormattedTextBox";
import { Id } from "../../../../fields/FieldSymbols";
import { Docs } from "../../../documents/Documents";
import { Utils } from "../../../../Utils";
import { listSpec } from "../../../../fields/Schema";
import { List } from "../../../../fields/List";
+import { GetEffectiveAcl } from "../../../../fields/util";
const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false;
@@ -70,25 +71,42 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
return false;
};
+ const canEdit = (state: any) => {
+ switch (GetEffectiveAcl(props.Document)) {
+ case AclAugment: return false;
+ case AclSelfEdit:
+ for (var i = state.selection.from; i < state.selection.to; i++) {
+ const marks = state.doc.resolve(i)?.marks?.();
+ if (marks?.some((mark: any) => mark.type === schema.marks.user_mark && mark.attrs.userid !== Doc.CurrentUserEmail)) {
+ return false;
+ }
+ }
+ break;
+ }
+ return true;
+ };
+
+ const toggleEditableMark = (mark: any) => (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && toggleMark(mark)(state, dispatch);
+
//History commands
bind("Mod-z", undo);
bind("Shift-Mod-z", redo);
!mac && bind("Mod-y", redo);
//Commands to modify Mark
- bind("Mod-b", toggleMark(schema.marks.strong));
- bind("Mod-B", toggleMark(schema.marks.strong));
+ bind("Mod-b", toggleEditableMark(schema.marks.strong));
+ bind("Mod-B", toggleEditableMark(schema.marks.strong));
- bind("Mod-e", toggleMark(schema.marks.em));
- bind("Mod-E", toggleMark(schema.marks.em));
+ bind("Mod-e", toggleEditableMark(schema.marks.em));
+ bind("Mod-E", toggleEditableMark(schema.marks.em));
- bind("Mod-*", toggleMark(schema.marks.code));
+ bind("Mod-*", toggleEditableMark(schema.marks.code));
- bind("Mod-u", toggleMark(schema.marks.underline));
- bind("Mod-U", toggleMark(schema.marks.underline));
+ bind("Mod-u", toggleEditableMark(schema.marks.underline));
+ bind("Mod-U", toggleEditableMark(schema.marks.underline));
//Commands for lists
- bind("Ctrl-i", wrapInList(schema.nodes.ordered_list));
+ bind("Ctrl-i", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && wrapInList(schema.nodes.ordered_list)(state, dispatch as any));
bind("Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
/// bcz; Argh!! replace layotuTEmpalteString with a onTab prop conditionally handles Tab);
@@ -96,6 +114,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
if (!props.LayoutTemplateString) return addTextBox(false, true);
return true;
}
+ if (!canEdit(state)) return true;
const ref = state.selection;
const range = ref.$from.blockRange(ref.$to);
const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
@@ -121,6 +140,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
bind("Shift-Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
/// bcz; Argh!! replace with a onShiftTab prop conditionally handles Tab);
if (props.Document._singleLine) return true;
+ if (!canEdit(state)) return true;
const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
if (!liftListItem(schema.nodes.list_item)(state.tr, (tx2: Transaction) => {
@@ -140,24 +160,19 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
});
//Commands to modify BlockType
- bind("Ctrl->", wrapIn(schema.nodes.blockquote));
- bind("Alt-\\", setBlockType(schema.nodes.paragraph));
- bind("Shift-Ctrl-\\", setBlockType(schema.nodes.code_block));
+ bind("Ctrl->", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit((state) && wrapIn(schema.nodes.blockquote)(state, dispatch as any)));
+ bind("Alt-\\", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && setBlockType(schema.nodes.paragraph)(state, dispatch as any));
+ bind("Shift-Ctrl-\\", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && setBlockType(schema.nodes.code_block)(state, dispatch as any));
- bind("Ctrl-m", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
- dispatch(state.tr.replaceSelectionWith(schema.nodes.equation.create({ fieldKey: "math" + Utils.GenerateGuid() })));
- });
+ bind("Ctrl-m", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && dispatch(state.tr.replaceSelectionWith(schema.nodes.equation.create({ fieldKey: "math" + Utils.GenerateGuid() }))));
for (let i = 1; i <= 6; i++) {
- bind("Shift-Ctrl-" + i, setBlockType(schema.nodes.heading, { level: i }));
+ bind("Shift-Ctrl-" + i, (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && setBlockType(schema.nodes.heading, { level: i })(state, dispatch as any));
}
//Command to create a horizontal break line
const hr = schema.nodes.horizontal_rule;
- bind("Mod-_", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
- dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView());
- return true;
- });
+ bind("Mod-_", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView()));
//Command to unselect all
bind("Escape", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
@@ -173,13 +188,15 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
};
//Command to create a text document to the right of the selected textbox
- bind("Alt-Enter", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => addTextBox(false, true));
+ bind("Alt-Enter", () => addTextBox(false, true));
//Command to create a text document to the bottom of the selected textbox
- bind("Ctrl-Enter", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => addTextBox(true, true));
+ bind("Ctrl-Enter", () => addTextBox(true, true));
// backspace = chainCommands(deleteSelection, joinBackward, selectNodeBackward);
bind("Backspace", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => {
+ if (!canEdit(state)) return true;
+
if (!deleteSelection(state, (tx: Transaction<Schema<any, any>>) => {
dispatch(updateBullets(tx, schema));
})) {
@@ -200,6 +217,9 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
//command to break line
bind("Enter", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => {
if (addTextBox(true, false)) return true;
+
+ if (!canEdit(state)) return true;
+
const trange = state.selection.$from.blockRange(state.selection.$to);
const path = (state.selection.$from as any).path;
const depth = trange ? liftTarget(trange) : undefined;
@@ -238,18 +258,19 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
//Command to create a blank space
bind("Space", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ if (!canEdit(state)) return true;
const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
dispatch(splitMetadata(marks, state.tr));
return false;
});
- bind("Alt-ArrowUp", joinUp);
- bind("Alt-ArrowDown", joinDown);
- bind("Mod-BracketLeft", lift);
+ bind("Alt-ArrowUp", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && joinUp(state, dispatch as any));
+ bind("Alt-ArrowDown", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && joinDown(state, dispatch as any));
+ bind("Mod-BracketLeft", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && lift(state, dispatch as any));
const cmd = chainCommands(exitCode, (state, dispatch) => {
if (dispatch) {
- dispatch(state.tr.replaceSelectionWith(schema.nodes.hard_break.create()).scrollIntoView());
+ canEdit(state) && dispatch(state.tr.replaceSelectionWith(schema.nodes.hard_break.create()).scrollIntoView());
return true;
}
return false;
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 2523dda38..c081b8e26 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -277,6 +277,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
const found = new Set<Mark>();
const { from, to } = state.selection as TextSelection;
state.doc.nodesBetween(from, to, (node) => node.marks.forEach(m => found.add(m)));
+ console.log("Marks: " + found);
return found;
}
@@ -821,8 +822,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
if (link) {
const href = link.attrs.allAnchors.length > 0 ? link.attrs.allAnchors[0].href : undefined;
if (href) {
- if (href.indexOf(Utils.prepend("/doc/")) === 0) {
- const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+ if (href.indexOf(Doc.localServerPath()) === 0) {
+ const linkclicked = href.replace(Doc.localServerPath(), "").split("?")[0];
if (linkclicked) {
const linkDoc = await DocServer.GetRefField(linkclicked);
if (linkDoc instanceof Doc) {
@@ -864,8 +865,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
const allAnchors = linkAnchor.attrs.allAnchors.slice();
this.TextView.RemoveAnchorFromSelection(allAnchors);
// bcz: Argh ... this will remove the link from the document even it's anchored somewhere else in the text which happens if only part of the anchor text was selected.
- allAnchors.filter((aref: any) => aref?.href.indexOf(Utils.prepend("/doc/")) === 0).forEach((aref: any) => {
- const anchorId = aref.href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+ allAnchors.filter((aref: any) => aref?.href.indexOf(Doc.localServerPath()) === 0).forEach((aref: any) => {
+ const anchorId = aref.href.replace(Doc.localServerPath(), "").split("?")[0];
anchorId && DocServer.GetRefField(anchorId).then(linkDoc => LinkManager.Instance.deleteLink(linkDoc as Doc));
});
}
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 5cb9866f8..cfc3e75cc 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -605,7 +605,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
this.layoutDoc.presStatus = PresStatus.Edit;
Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocs as Doc), undefined, this.rootDoc);
CollectionDockingView.AddSplit(this.rootDoc, "right");
- } else if (this.layoutDoc.context && docView) {
+ } else if ((true || this.layoutDoc.context) && docView) {
console.log("case 2");
this.layoutDoc.presStatus = PresStatus.Edit;
clearTimeout(this._presTimer);
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index 70ca19842..75e3f81fb 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -45,6 +45,8 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
@observable public Highlighting: boolean = false;
@observable public Status: "marquee" | "annotation" | "" = "";
+ public onMakeAnchor: () => Opt<Doc> = () => undefined; // Method to get anchor from text search
+
public OnClick: (e: PointerEvent) => void = unimplementedFunction;
public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction;
public Highlight: (color: string, isPushpin: boolean) => Opt<Doc> = (color: string, isPushpin: boolean) => undefined;
@@ -65,7 +67,11 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
componentDidMount() {
this._disposer = reaction(() => SelectionManager.Views(),
- selected => AnchorMenu.Instance.fadeOut(true));
+ selected => {
+ this._showLinkPopup = false;
+ console.log("unmount");
+ AnchorMenu.Instance.fadeOut(true)
+ });
}
pointerDown = (e: React.PointerEvent) => {
@@ -85,7 +91,6 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
@action
toggleLinkPopup = (e: React.MouseEvent) => {
//ignore the potential null type error because this method cannot be called unless the user selects text and clicks the link button
- console.log(window.getSelection().toString())
//change popup visibility field to visible
this._showLinkPopup = !this._showLinkPopup;
}
@@ -146,14 +151,13 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
<FontAwesomeIcon icon="comment-alt" size="lg" />
</button>
</Tooltip>,
-
- //NOTE: link popup is currently incomplete
- // <Tooltip key="link" title={<div className="dash-tooltip">{"Link selected text to document or URL"}</div>}>
- // <button className="antimodeMenu-button link" onPointerDown={this.toggleLinkPopup} style={{}}>
- // <FontAwesomeIcon icon="link" size="lg" />
- // </button>
- // </Tooltip>,
- // <LinkPopup showPopup={this._showLinkPopup} />
+ //NOTE: link popup is currently in progress
+ <Tooltip key="link" title={<div className="dash-tooltip">{"Link selected text to document"}</div>}>
+ <button className="antimodeMenu-button link" onPointerDown={this.toggleLinkPopup} style={{}}>
+ <FontAwesomeIcon icon="link" size="lg" />
+ </button>
+ </Tooltip>,
+ <LinkPopup showPopup={this._showLinkPopup} linkFrom={this.onMakeAnchor} />
] : [
<Tooltip key="trash" title={<div className="dash-tooltip">{"Remove Link Anchor"}</div>}>
<button className="antimodeMenu-button" onPointerDown={this.Delete}>
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index e8c7a4ab0..734d9127c 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -9,7 +9,7 @@ import { createSchema } from "../../../fields/Schema";
import { Cast, NumCast, ScriptCast, StrCast } from "../../../fields/Types";
import { PdfField } from "../../../fields/URLField";
import { TraceMobx } from "../../../fields/util";
-import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, OmitKeys, smoothScroll, Utils, returnFalse } from "../../../Utils";
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, OmitKeys, smoothScroll, Utils, returnFalse, returnEmptyString, returnEmptyFilter } from "../../../Utils";
import { DocUtils } from "../../documents/Documents";
import { Networking } from "../../Network";
import { CurrentUserUtils } from "../../util/CurrentUserUtils";
@@ -46,7 +46,6 @@ interface IViewerProps extends FieldViewProps {
loaded?: (nw: number, nh: number, np: number) => void;
setPdfViewer: (view: PDFViewer) => void;
ContentScaling?: () => number;
- sidebarWidth: () => number;
anchorMenuClick?: () => undefined | ((anchor: Doc) => void);
}
@@ -70,7 +69,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
private _pdfViewer: any;
private _styleRule: any; // stylesheet rule for making hyperlinks clickable
private _retries = 0; // number of times tried to create the PDF viewer
- private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void);
+ private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void);
private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
private _disposers: { [name: string]: IReactionDisposer } = {};
private _viewer: React.RefObject<HTMLDivElement> = React.createRef();
@@ -183,17 +182,18 @@ export class PDFViewer extends React.Component<IViewerProps> {
scrollFocus = (doc: Doc, smooth: boolean) => {
const mainCont = this._mainCont.current;
let focusSpeed: Opt<number>;
- if (doc !== this.props.rootDoc && mainCont && this._pdfViewer) {
+ if (doc !== this.props.rootDoc && mainCont) {
const windowHeight = this.props.PanelHeight() / (this.props.scaling?.() || 1);
const scrollTo = Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.props.layoutDoc._scrollTop), windowHeight, .1 * windowHeight);
if (scrollTo !== undefined) {
focusSpeed = 500;
- if (smooth) smoothScroll(focusSpeed, mainCont, scrollTo);
+ if (!this._pdfViewer) this._initialScroll = scrollTo;
+ else if (smooth) smoothScroll(focusSpeed, mainCont, scrollTo);
else this._mainCont.current?.scrollTo({ top: Math.abs(scrollTo || 0) });
}
} else {
- this._initialScroll = NumCast(doc.y);
+ this._initialScroll = NumCast(this.props.layoutDoc._scrollTop);
}
return focusSpeed;
}
@@ -371,10 +371,11 @@ export class PDFViewer extends React.Component<IViewerProps> {
this._downY = e.clientY;
if ((this.props.Document._viewScale || 1) !== 1) return;
if ((e.button !== 0 || e.altKey) && this.props.isContentActive(true)) {
- this._setPreviewCursor?.(e.clientX, e.clientY, true);
+ this._setPreviewCursor?.(e.clientX, e.clientY, true, false);
}
if (!e.altKey && e.button === 0 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool)) {
this.props.select(false);
+ MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
this._marqueeing = [e.clientX, e.clientY];
if (e.target && ((e.target as any).className.includes("endOfContent") || ((e.target as any).parentElement.className !== "textLayer"))) {
this._textSelecting = false;
@@ -382,10 +383,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
} else {
// if textLayer is hit, then we select text instead of using a marquee so clear out the marquee.
setTimeout(action(() => this._marqueeing = undefined), 100); // bcz: hack .. anchor menu is setup within MarqueeAnnotator so we need to at least create the marqueeAnnotator even though we aren't using it.
- // clear out old marquees and initialize menu for new selection
- AnchorMenu.Instance.Status = "marquee";
- Array.from(this._savedAnnotations.values()).forEach(v => v.forEach(a => a.remove()));
- this._savedAnnotations.clear();
+
this._styleRule = addStyleSheetRule(PDFViewer._annotationStyle, "htmlAnnotation", { "pointer-events": "none" });
document.addEventListener("pointerup", this.onSelectEnd);
document.addEventListener("pointermove", this.onSelectMove);
@@ -454,12 +452,12 @@ export class PDFViewer extends React.Component<IViewerProps> {
if (this._setPreviewCursor && e.button === 0 &&
Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD &&
Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
- this._setPreviewCursor(e.clientX, e.clientY, false);
+ this._setPreviewCursor(e.clientX, e.clientY, false, false);
}
// e.stopPropagation(); // bcz: not sure why this was here. We need to allow the DocumentView to get clicks to process doubleClicks
}
- setPreviewCursor = (func?: (x: number, y: number, drag: boolean) => void) => this._setPreviewCursor = func;
+ setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => this._setPreviewCursor = func;
getCoverImage = () => {
if (!this.props.Document[HeightSym]() || !Doc.NativeHeight(this.props.Document)) {
@@ -508,13 +506,10 @@ export class PDFViewer extends React.Component<IViewerProps> {
overlayTransform = () => this.scrollXf().scale(1 / this._zoomed);
panelWidth = () => this.props.PanelWidth() / (this.props.scaling?.() || 1); // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0);
panelHeight = () => this.props.PanelHeight() / (this.props.scaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document);
+ transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()];
+ opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()];
@computed get overlayLayer() {
- return <div className={`pdfViewerDash-overlay${CurrentUserUtils.SelectedTool !== InkTool.None || SnappingManager.GetIsDragging() ? "-inking" : ""}`}
- style={{
- pointerEvents: SnappingManager.GetIsDragging() ? "all" : undefined,
- mixBlendMode: this.allAnnotations.some(anno => anno.mixBlendMode) ? "hard-light" : undefined,
- transform: `scale(${this._zoomed})`
- }}>
+ const renderAnnotations = (docFilters?: () => string[]) =>
<CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
isAnnotationOverlay={true}
isContentActive={returnFalse}
@@ -526,10 +521,30 @@ export class PDFViewer extends React.Component<IViewerProps> {
select={emptyFunction}
ContentScaling={this.contentZoom}
bringToFront={emptyFunction}
+ docFilters={docFilters || this.props.docFilters}
+ dontRenderDocuments={docFilters ? false : true}
CollectionView={undefined}
ScreenToLocalTransform={this.overlayTransform}
renderDepth={this.props.renderDepth + 1}
childPointerEvents={true} />
+ return <div>
+ <div className={`pdfViewerDash-overlay${CurrentUserUtils.SelectedTool !== InkTool.None || SnappingManager.GetIsDragging() ? "-inking" : ""}`}
+ style={{
+ pointerEvents: SnappingManager.GetIsDragging() ? "all" : undefined,
+ mixBlendMode: "multiply",
+ transform: `scale(${this._zoomed})`
+ }}>
+ {renderAnnotations(this.transparentFilter)}
+ </div>
+ <div className={`pdfViewerDash-overlay${CurrentUserUtils.SelectedTool !== InkTool.None || SnappingManager.GetIsDragging() ? "-inking" : ""}`}
+ style={{
+ pointerEvents: SnappingManager.GetIsDragging() ? "all" : undefined,
+ mixBlendMode: this.allAnnotations.some(anno => anno.mixBlendMode) ? "hard-light" : undefined,
+ transform: `scale(${this._zoomed})`
+ }}>
+ {renderAnnotations(this.opaqueFilter)}
+ {renderAnnotations()}
+ </div>
</div>;
}
@computed get pdfViewerDiv() {
@@ -550,7 +565,6 @@ export class PDFViewer extends React.Component<IViewerProps> {
onScroll={this.onScroll} onWheel={this.onZoomWheel} onPointerDown={this.onPointerDown} onClick={this.onClick}
style={{
overflowX: this._zoomed !== 1 ? "scroll" : undefined,
- width: !this.props.Document._fitWidth && (window.screen.width > 600) ? Doc.NativeWidth(this.props.Document) - this.props.sidebarWidth() / this.contentScaling : `calc(${100 / this.contentScaling}% - ${this.props.sidebarWidth() / this.contentScaling}px)`,
height: !this.props.Document._fitWidth && (window.screen.width > 600) ? Doc.NativeHeight(this.props.Document) : `${100 / this.contentScaling}%`,
transform: `scale(${this.contentScaling})`
}} >
diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss
index 6a2fe6f19..2586ef2ee 100644
--- a/src/client/views/search/SearchBox.scss
+++ b/src/client/views/search/SearchBox.scss
@@ -2,141 +2,109 @@
@import "./NaviconButton.scss";
.searchBox-container {
- display: flex;
- flex-direction: column;
width: 100%;
height: 100%;
- position: relative;
font-size: 10px;
line-height: 1;
- overflow-y: auto;
- overflow-x: visible;
- background: lightgrey;
- overflow: visible;
+ background: none;
z-index: 1000;
+ padding: 0px;
+ cursor: default;
.searchBox-bar {
- height: $searchpanel-height;
+ width: 100%;
+ height: 35px;
display: flex;
justify-content: center;
align-items: center;
- background-color: $dark-gray;
+ background-color: none;
+ padding: 5px;
- .searchBox-lozenges {
- position: absolute;
- left: 15;
- display: flex;
-
- .searchBox-lozenge-user,
- .searchBox-lozenge-dashboard,
- .searchBox-lozenge {
- height: 18px;
- padding: 4px;
- margin-right: 5px;
- display: flex;
- align-items: center;
- border: grey 1px solid;
- .searchBox-logoff,
- .searchBox-dashboards {
- border-radius: 3px;
- background: olivedrab;
- color: white;
- display: none;
- margin-left: 5px;
- padding: 1px 2px 1px 2px;
- cursor: pointer;
- }
- .searchBox-logoff {
- background: red;
- }
-
- .searchBox-dashSelect{
- border: none;
- background-color: transparent;
+ .searchBox-type {
+ display: block;
+ width: 55px;
+ outline: none;
+ padding: 1px 5px 1px 5px;
+ color: black;
+ height: 25px;
+ border: 1px solid black;
+ border-right: 0px;
+ }
- &:hover {
- cursor: pointer;
- }
- }
- }
- .searchBox-lozenge-user:hover {
- .searchBox-logoff {
- display:inline-block;
- }
- }
- .searchBox-lozenge-dashboard:hover {
- .searchBox-dashboards {
- display:inline-block;
- }
- }
+ .searchBox-input {
+ display: block;
+ width: calc(100% - 55px);
+ outline: none;
+ padding: 1px 5px 1px 5px;
+ color: black;
+ height: 25px;
+ border: 1px solid black;
}
- .searchBox-query {
- position: relative;
+ }
+
+ .searchBox-results-container {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ height: 100%;
+ justify-content: "center";
+
+ .searchBox-results-count {
display: flex;
- width: 450;
+ color: gray;
+ margin-left: 5px;
}
- .searchBox-barChild {
+
+ .searchBox-results-scroll-view {
+ margin-top: 10px;
+ display: inline-block;
+ width: 100%;
+ height: calc(100% - 55px);
+ overflow-y: scroll;
- &.searchBox-collection {
- flex: 0 1 auto;
- margin-left: 2px;
- margin-right: 2px
- }
+ .searchBox-results-scroll-view-result {
+ display: inline-block;
+ vertical-align: middle;
+ width: 100%;
+ height: 50px;
+ cursor: pointer;
+ font-size: 15px;
+ padding: 11px;
- &.searchBox-input {
- margin:5px;
- border-radius:20px;
- border:$dark-gray;
- display: block;
- width: 130px;
- -webkit-transition: width 0.4s;
- transition: width 0.4s;
- align-self: stretch;
- outline:none;
- &:focus {
- width: 500px;
- outline:none;
+ &.searchBox-results-scroll-view-result-selected {
+ background: #999;
}
- }
- &.searchBox-filter {
- align-self: stretch;
- button{
- transform:none;
- &:hover {
- transform: none;
- }
+
+ .searchBox-result-title {
+ display: relative;
+ float: left;
+ width: calc(100% - 60px);
+ text-align: left;
}
- }
- &.searchBox-submit {
- margin-left: 2px;
- margin-right: 2px
- }
+ .searchBox-result-type {
+ font-size: 12px;
+ margin-top: 6px;
+ display: relative;
+ float: right;
+ width: 60px;
+ text-align: right;
+ color: #222;
+ }
- &.searchBox-close {
- color: $white;
- max-height: $searchpanel-height;
+ .searchBox-result-keys {
+ font-size: 10px;
+ margin-top: 1px;
+ display: relative;
+ float: left;
+ width: 100%;
+ text-align: left;
+ color: #555;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
}
}
- }
-}
-
-.searchBox-results {
- display: flex;
- flex-direction: column;
- top: 300px;
- display: flex;
- flex-direction: column;
- height: 100%;
- overflow: visible;
-
- .no-result {
- width: 500px;
- background: $light-gray;
- padding: 10px;
- height: 50px;
- text-transform: uppercase;
- text-align: left;
- font-weight: bold;
}
} \ No newline at end of file
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 6a2325342..07186fe8d 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -1,601 +1,348 @@
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Tooltip } from '@material-ui/core';
-import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
+import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { Doc, DocListCast, Field, Opt, DocListCastAsync } from '../../../fields/Doc';
+import { Doc, DocListCast, DocListCastAsync, Field } from '../../../fields/Doc';
import { documentSchema } from "../../../fields/documentSchemas";
-import { Copy, Id } from '../../../fields/FieldSymbols';
-import { List } from '../../../fields/List';
-import { createSchema, listSpec, makeInterface } from '../../../fields/Schema';
-import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
-import { Cast, NumCast, StrCast } from '../../../fields/Types';
-import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents, Utils } from '../../../Utils';
-import { Docs } from '../../documents/Documents';
+import { Id } from '../../../fields/FieldSymbols';
+import { createSchema, makeInterface } from '../../../fields/Schema';
+import { StrCast } from '../../../fields/Types';
import { DocumentType } from "../../documents/DocumentTypes";
-import { CurrentUserUtils } from "../../util/CurrentUserUtils";
-import { SetupDrag } from '../../util/DragManager';
-import { SearchUtil } from '../../util/SearchUtil';
-import { Transform } from '../../util/Transform';
import { CollectionDockingView } from "../collections/CollectionDockingView";
-import { CollectionSchemaView, ColumnType } from "../collections/collectionSchema/CollectionSchemaView";
-import { CollectionViewType } from '../collections/CollectionView';
import { ViewBoxBaseComponent } from "../DocComponent";
import { FieldView, FieldViewProps } from '../nodes/FieldView';
import "./SearchBox.scss";
-import { undoBatch } from "../../util/UndoManager";
-import { DocServer } from "../../DocServer";
-import { MainView } from "../MainView";
+import { DocumentManager } from '../../util/DocumentManager';
+import { DocUtils } from '../../documents/Documents';
-export const searchSchema = createSchema({ Document: Doc });
+export const searchSchema = createSchema({
+ Document: Doc
+});
type SearchBoxDocument = makeInterface<[typeof documentSchema, typeof searchSchema]>;
const SearchBoxDocument = makeInterface(documentSchema, searchSchema);
+export interface SearchBoxProps extends FieldViewProps {
+ linkSearch: boolean;
+ linkFrom?: (() => Doc | undefined) | undefined;
+}
+
+/**
+ * This is the SearchBox component. It represents the search box input and results in
+ * the search panel on the left side of the screen.
+ */
@observer
-export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDocument>(SearchBoxDocument) {
+export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDocument>(SearchBoxDocument) {
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(SearchBox, fieldKey); }
public static Instance: SearchBox;
- private _allIcons: string[] = [DocumentType.INK, DocumentType.AUDIO, DocumentType.COL, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.RTF, DocumentType.VID, DocumentType.WEB];
- private _numResultsPerPage = 500;
- private _numTotalResults = -1;
- private _endIndex = -1;
- private _lockPromise?: Promise<void>;
- private _resultsSet = new Map<Doc, number>();
private _inputRef = React.createRef<HTMLInputElement>();
- private _maxSearchIndex: number = 0;
- private _curRequest?: Promise<any> = undefined;
- private _disposers: { [name: string]: IReactionDisposer } = {};
- private _blockedTypes = [DocumentType.PRESELEMENT, DocumentType.KVP, DocumentType.FILTER, DocumentType.SEARCH, DocumentType.SEARCHITEM, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING];
-
- private docsforfilter: Doc[] | undefined = [];
- private realTotalResults: number = 0;
- private newsearchstring = "";
- private collectionRef = React.createRef<HTMLDivElement>();
-
-
- @observable _undoBackground: string | undefined = "";
- @observable _icons: string[] = this._allIcons;
- @observable _results: [Doc, string[], string[]][] = [];
- @observable _visibleElements: JSX.Element[] = [];
- @observable _visibleDocuments: Doc[] = [];
+
+ @observable _searchString = "";
+ @observable _docTypeString = "all";
+ @observable _results: [Doc, string[]][] = [];
+ @observable _selectedResult: Doc | undefined = undefined;
@observable _deletedDocsStatus: boolean = false;
@observable _onlyAliases: boolean = true;
- @observable _searchbarOpen = false;
- @observable _searchFullDB = "DB"; // "DB" means searh the entire database. "My Stuff" adds a flag that selects only documents that the current user has authored
- @observable _noResults = "";
- @observable _pageStart = 0;
- @observable open = false;
- @observable children = 0;
- @computed get filter() { return this._results?.length && (this.currentSelectedCollection?.props.Document._searchFilterDocs || this.currentSelectedCollection?.props.Document._docFilters); }
+ /**
+ * This is the constructor for the SearchBox class.
+ */
constructor(props: any) {
super(props);
SearchBox.Instance = this;
}
+ /**
+ * This method is called when the SearchBox component is first mounted. When the user opens
+ * the search panel, the search input box is automatically selected. This allows the user to
+ * type in the search input box immediately, without needing clicking on it first.
+ */
componentDidMount = action(() => {
if (this._inputRef.current) {
this._inputRef.current.focus();
}
- this._disposers.filters = reaction(() => this.props.Document._docFilters,
- (filters: any) => this.setSearchFilter(this.currentSelectedCollection, !this.filter ? undefined : this.docsforfilter));
});
+ /**
+ * This method is called when the SearchBox component is about to be unmounted. When the user
+ * closes the search panel, the search and its results are reset.
+ */
componentWillUnmount() {
- Object.values(this._disposers).forEach(disposer => disposer?.());
+ this.resetSearch();
}
- @computed get currentSelectedCollection() { return CollectionDockingView.Instance; }
-
- onChange = action((e: React.ChangeEvent<HTMLInputElement>) => {
- this.newsearchstring = e.target.value;
- if (e.target.value === "") {
- console.log("Reset start");
- this.docsforfilter = undefined;
- this.setSearchFilter(this.currentSelectedCollection, undefined);
- this.resetSearch(false);
-
- this.open = false;
- this._results = [];
- this._resultsSet.clear();
- this._visibleElements = [];
- this._numTotalResults = -1;
- this._endIndex = -1;
- this._curRequest = undefined;
- this._maxSearchIndex = 0;
- }
+ /**
+ * This method is called when the text in the search input box is modified by the user. The
+ * _searchString is updated to the new value of the text in the input box and submitSearch
+ * is called to update the search results accordingly.
+ *
+ * (Note: There is no longer a need to press enter to submit a search. Any update to the input
+ * causes a search to be submitted automatically.)
+ */
+ onInputChange = action((e: React.ChangeEvent<HTMLInputElement>) => {
+ this._searchString = e.target.value;
+ this.submitSearch();
});
- enter = action((e: React.KeyboardEvent | undefined) => {
- if (!e || e.key === "Enter") {
- this.layoutDoc._searchString = this.newsearchstring;
- this._pageStart = 0;
- this.open = StrCast(this.layoutDoc._searchString) !== "" || this._searchFullDB !== "DB";
- this.submitSearch();
- }
+ /**
+ * This method is called when the option in the select drop-down menu is changed. The
+ * _docTypeString is updated to the new value of the option in the drop-down menu. This
+ * is used to filter the results of the search to documents of a specific type.
+ *
+ * (Note: This doesn't affect the results array, so there is no need to submit a new
+ * search here. The results of the search on the _searchString query are simply filtered
+ * by type directly before rendering them.)
+ */
+ onSelectChange = action((e: React.ChangeEvent<HTMLSelectElement>) => {
+ this._docTypeString = e.target.value;
});
- getFinalQuery(query: string): string {
- //alters the query so it looks in the correct fields
- //if this is true, then not all of the field boxes are checked
- //TODO: data
- const initialfilters = Cast(this.props.Document._docFilters, listSpec("string"), []);
-
- const filters: string[] = [];
-
- for (const initFilter of initialfilters) {
- const fields = initFilter.split(":");
- if (fields[2] !== undefined) {
- filters.push(fields[0]);
- filters.push(fields[1]);
- filters.push(fields[2]);
- }
- }
-
- const finalfilters: { [key: string]: string[] } = {};
-
- for (let i = 0; i < filters.length; i = i++) {
- const fields = filters[i].split(":");
- if (finalfilters[fields[0]] !== undefined) {
- finalfilters[fields[0]].push(fields[1]);
- }
- else {
- finalfilters[fields[0]] = [fields[1]];
- }
- }
+ /**
+ * @param {Doc} doc - doc of the search result that has been clicked on
+ *
+ * This method is called when the user clicks on a search result. The _selectedResult is
+ * updated accordingly and the doc is highlighted with the selectElement method.
+ */
+ onResultClick = action((doc: Doc) => {
+ this.selectElement(doc);
+ this._selectedResult = doc;
+ });
- for (const key in finalfilters) {
- const values = finalfilters[key];
- if (values.length === 1) {
- const mod = "_t:";
- const newWords: string[] = [];
- const oldWords = values[0].split(" ");
- oldWords.forEach((word, i) => i === 0 ? newWords.push(key + mod + word) : newWords.push("AND " + key + mod + word));
- query = `(${query}) AND (${newWords.join(" ")})`;
- }
- else {
- for (let i = 0; i < values.length; i++) {
- const mod = "_t:";
- const newWords: string[] = [];
- const oldWords = values[i].split(" ");
- oldWords.forEach((word, i) => i === 0 ? newWords.push(key + mod + word) : newWords.push("AND " + key + mod + word));
- const v = "(" + newWords.join(" ") + ")";
- if (i === 0) {
- query = `(${query}) AND (${v}` + (values.length === 1 ? ")" : "");
- }
- else query = query + " OR " + v + (i === values.length - 1 ? ")" : "");
- }
+ makeLink = action((linkTo: Doc) => {
+ console.log("[makeLink-1] got here!")
+ console.log(linkTo.title);
+ if (this.props.linkFrom){
+ const linkFrom = this.props.linkFrom();
+ if (linkFrom){
+ console.log(linkFrom.title);
+ DocUtils.MakeLink({doc: linkFrom}, {doc:linkTo});
}
}
+ });
- return query.replace(/-\s+/g, '');
- }
-
- @action
- filterDocsByType(docs: Doc[]) {
- const finalDocs: Doc[] = [];
- docs.forEach(doc => {
- const layoutresult = StrCast(doc.type, "string") as DocumentType;
- if (layoutresult && !this._blockedTypes.includes(layoutresult) && this._icons.includes(layoutresult)) {
- finalDocs.push(doc);
- }
- });
- return finalDocs;
- }
-
- static async foreachRecursiveDocAsync(docs: Doc[], func: (doc: Doc) => void) {
+ /**
+ * @param {Doc[]} docs - docs to be searched through recursively
+ * @param {number, Doc => void} func - function to be called on each doc
+ *
+ * This method iterates through an array of docs and all docs within those docs, calling
+ * the function func on each doc.
+ */
+ static foreachRecursiveDoc(docs: Doc[], func: (depth: number, doc: Doc) => void) {
let newarray: Doc[] = [];
+ var depth = 0;
while (docs.length > 0) {
newarray = [];
- await Promise.all(docs.filter(d => d).map(async d => {
+ docs.filter(d => d).forEach(d => {
const fieldKey = Doc.LayoutFieldKey(d);
const annos = !Field.toString(Doc.LayoutField(d) as Field).includes("CollectionView");
const data = d[annos ? fieldKey + "-annotations" : fieldKey];
- const docs = await DocListCastAsync(data);
- docs && newarray.push(...docs);
- func(d);
- }));
+ data && newarray.push(...DocListCast(data));
+ func(depth, d);
+ });
docs = newarray;
+ depth++;
}
}
- static foreachRecursiveDoc(docs: Doc[], func: (doc: Doc) => void) {
+
+ /**
+ * @param {Doc[]} docs - docs to be searched through recursively
+ * @param {number, Doc => void} func - function to be called on each doc
+ *
+ * This method iterates asynchronously through an array of docs and all docs within those
+ * docs, calling the function func on each doc.
+ */
+ static async foreachRecursiveDocAsync(docs: Doc[], func: (depth: number, doc: Doc) => void) {
let newarray: Doc[] = [];
+ var depth = 0;
while (docs.length > 0) {
newarray = [];
- docs.filter(d => d).forEach(d => {
+ await Promise.all(docs.filter(d => d).map(async d => {
const fieldKey = Doc.LayoutFieldKey(d);
const annos = !Field.toString(Doc.LayoutField(d) as Field).includes("CollectionView");
const data = d[annos ? fieldKey + "-annotations" : fieldKey];
- data && newarray.push(...DocListCast(data));
- func(d);
- });
+ const docs = await DocListCastAsync(data);
+ docs && newarray.push(...docs);
+ func(depth, d);
+ }));
docs = newarray;
+ depth++;
}
}
+ /**
+ * @param {String} type - string representing the type of a doc
+ *
+ * This method converts a doc type string of any length to a 3-letter doc type string in
+ * which the first letter is capitalized. This is used when displaying the type on the
+ * right side of each search result.
+ */
+ static formatType(type: String): String {
+ if (type == "pdf") {
+ return "PDF";
+ }
+ else if (type == "image") {
+ return "Img";
+ }
+
+ return type.charAt(0).toUpperCase() + type.substring(1, 3);
+ }
+
+ /**
+ * @param {String} query - search query string
+ *
+ * This method searches the CollectionDockingView instance for a certain query and puts
+ * the matching results in the results array. Docs are considered to be matching results
+ * when the query is a substring of many different pieces of its metadata (title, text,
+ * author, etc).
+ */
@action
searchCollection(query: string) {
- const selectedCollection = this.currentSelectedCollection;//SelectionManager.SelectedDocuments()[0];
+ const blockedTypes = [DocumentType.PRESELEMENT, DocumentType.KVP, DocumentType.FILTER, DocumentType.SEARCH, DocumentType.SEARCHITEM, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING];
+ const blockedKeys = ["x", "y", "proto", "width", "autoHeight", "acl-Override", "acl-Public", "context", "zIndex", "height", "text-scrollHeight", "text-height", "cloneFieldFilter", "isPrototype", "text-annotations",
+ "dragFactory-count", "text-noTemplate", "aliases", "system", "layoutKey", "baseProto", "xMargin", "yMargin", "links", "layout", "layout_keyValue", "fitWidth", "viewType", "title-custom",
+ "panX", "panY", "viewScale"]
+ const collection = CollectionDockingView.Instance;
query = query.toLowerCase();
- if (selectedCollection !== undefined) {
- // this._currentSelectedCollection = selectedCollection;
- const docs = DocListCast(selectedCollection.dataDoc[Doc.LayoutFieldKey(selectedCollection.dataDoc)]);
- const found: [Doc, string[], string[]][] = [];
- SearchBox.foreachRecursiveDoc(docs, (doc: Doc) => {
- const hlights = new Set<string>();
- SearchBox.documentKeys(doc).forEach(key => Field.toString(doc[key] as Field).toLowerCase().includes(query) && hlights.add(key));
- Array.from(hlights.keys()).length > 0 && found.push([doc, Array.from(hlights.keys()), []]);
+ this._results = []
+ this._selectedResult = undefined
+
+ if (collection !== undefined) {
+ const docs = DocListCast(collection.rootDoc[Doc.LayoutFieldKey(collection.rootDoc)]);
+ const docIDs: String[] = []
+ console.log(docs.length)
+ SearchBox.foreachRecursiveDoc(docs, (depth: number, doc: Doc) => {
+ const dtype = StrCast(doc.type, "string") as DocumentType;
+ if (dtype && !blockedTypes.includes(dtype) && !docIDs.includes(doc[Id]) && depth > 0) {
+ const hlights = new Set<string>();
+ SearchBox.documentKeys(doc).forEach(key => Field.toString(doc[key] as Field).toLowerCase().includes(query) && hlights.add(key));
+ blockedKeys.forEach(key => {
+ hlights.delete(key);
+ })
+ Array.from(hlights.keys()).length > 0 && this._results.push([doc, Array.from(hlights.keys())]);
+ }
+ docIDs.push(doc[Id])
});
-
- this._results = found;
- this.docsforfilter = this._results.map(r => r[0]);
- this.setSearchFilter(selectedCollection, this.filter && found.length ? this.docsforfilter : undefined);
- this._numTotalResults = found.length;
- this.realTotalResults = found.length;
- }
- else {
- this._noResults = "No collection selected :(";
}
-
}
+ /**
+ * @param {Doc} doc - doc for which keys are returned
+ *
+ * This method returns a list of a document doc's keys.
+ */
static documentKeys(doc: Doc) {
const keys: { [key: string]: boolean } = {};
- // bcz: ugh. this is untracked since otherwise a large collection of documents will blast the server for all their fields.
- // then as each document's fields come back, we update the documents _proxies. Each time we do this, the whole schema will be
- // invalidated and re-rendered. This workaround will inquire all of the document fields before the options button is clicked.
- // then by the time the options button is clicked, all of the fields should be in place. If a new field is added while this menu
- // is displayed (unlikely) it won't show up until something else changes.
- //TODO Types
Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => keys[key] = false));
return Array.from(Object.keys(keys));
}
+ /**
+ * This method submits a search with the _searchString as its query and updates
+ * the results array accordingly.
+ */
@action
submitSearch = async () => {
- this.resetSearch(false);
-
- //this.props.Document._docFilters = new List();
- this._noResults = "";
+ this.resetSearch();
- this.dataDoc[this.fieldKey] = new List<Doc>([]);
- this.children = 0;
- let query = StrCast(this.layoutDoc._searchString);
+ let query = StrCast(this._searchString);
Doc.SetSearchQuery(query);
- this._searchFullDB && (query = this.getFinalQuery(query));
this._results = [];
- this._resultsSet.clear();
- this._visibleElements = [];
- this._visibleDocuments = [];
-
- if (query || this._searchFullDB === "My Stuff") {
- this._endIndex = 12;
- this._maxSearchIndex = 0;
- this._numTotalResults = -1;
- this._searchFullDB ? await this.searchDatabase(query) : this.searchCollection(query);
- runInAction(() => {
- this.open = this._searchbarOpen = true;
- this.resultsScrolled();
- });
- }
- }
- getAllResults = async (query: string) => {
- return SearchUtil.Search(query, true, { fq: this.filterQuery, start: 0, rows: 10000000 });
- }
-
- private get filterQuery() {
- const baseExpr = "NOT system_b:true";
- const authorExpr = this._searchFullDB === "My Stuff" ? ` author_t:${Doc.CurrentUserEmail}` : undefined;
- const includeDeleted = this._deletedDocsStatus ? "" : " NOT deleted_b:true";
- const typeExpr = this._onlyAliases ? "NOT {!join from=id to=proto_i}type_t:*" : `(type_t:* OR {!join from=id to=proto_i}type_t:*) ${this._blockedTypes.map(type => `NOT ({!join from=id to=proto_i}type_t:${type}) AND NOT type_t:${type}`).join(" AND ")}`;
- // fq: type_t:collection OR {!join from=id to=proto_i}type_t:collection q:text_t:hello
- return [baseExpr, authorExpr, includeDeleted, typeExpr].filter(q => q).join(" AND ").replace(/AND $/, "");
- }
-
- @computed get primarySort() {
- const suffixMap = (type: ColumnType) => {
- switch (type) {
- case ColumnType.Date: return "_d";
- case ColumnType.String: return "_t";
- case ColumnType.Boolean: return "_b";
- case ColumnType.Number: return "_n";
- }
- };
- const headers = Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []);
- return headers.reduce((p: Opt<string>, header: SchemaHeaderField) => p || (header.desc !== undefined && suffixMap(header.type) ? (header.heading + suffixMap(header.type) + (header.desc ? " desc" : " asc")) : undefined), undefined);
- }
-
- searchDatabase = async (query: string) => {
- this._lockPromise && (await this._lockPromise);
- this._lockPromise = new Promise(async res => {
- while (this._results.length <= this._endIndex && (this._numTotalResults === -1 || this._maxSearchIndex < this._numTotalResults)) {
- this._curRequest = SearchUtil.Search(query, true, { onlyAliases: true, allowAliases: true, /*sort: this.primarySort,*/ fq: this.filterQuery, start: 0, rows: this._numResultsPerPage, hl: "on", "hl.fl": "*", }).then(action(async (res: SearchUtil.DocSearchResult) => {
- // happens at the beginning
- this.realTotalResults = res.numFound <= 0 ? 0 : res.numFound;
- if (res.numFound !== this._numTotalResults && this._numTotalResults === -1) {
- this._numTotalResults = res.numFound;
- }
- const highlighting = res.highlighting || {};
- const highlightList = res.docs.map(doc => highlighting[doc[Id]]);
- const lines = new Map<string, string[]>();
- res.docs.map((doc, i) => lines.set(doc[Id], res.lines[i]));
- const docs = res.docs;
- const highlights: typeof res.highlighting = {};
- docs.forEach((doc, index) => highlights[doc[Id]] = highlightList[index]);
- const filteredDocs = this.filterDocsByType(docs);
-
- runInAction(() => filteredDocs.forEach((doc, i) => {
- const index = this._resultsSet.get(doc);
- const highlight = highlights[doc[Id]];
- const line = lines.get(doc[Id]) || [];
- const hlights = highlight ? Object.keys(highlight).map(key => key.substring(0, key.length - 2)).filter(k => k) : [];
- // if (this.findCommonElements(hlights)) {
- // }
- if (index === undefined) {
- this._resultsSet.set(doc, this._results.length);
- this._results.push([doc, hlights, line]);
- } else {
- this._results[index][1].push(...hlights);
- this._results[index][2].push(...line);
- }
-
- }));
-
- this._curRequest = undefined;
- }));
- this._maxSearchIndex += this._numResultsPerPage;
-
- await this._curRequest;
- }
-
- this.resultsScrolled();
-
- const selectedCollection = this.currentSelectedCollection;//SelectionManager.SelectedDocuments()[0];
- this.docsforfilter = this._results.map(r => r[0]);
- this.setSearchFilter(selectedCollection, this.filter ? this.docsforfilter : undefined);
- res();
- });
- return this._lockPromise;
- }
-
- startDragCollection = async () => {
- const res = await this.getAllResults(this.getFinalQuery(StrCast(this.layoutDoc._searchString)));
- const filtered = this.filterDocsByType(res.docs);
- const docs = filtered.map(doc => Doc.GetT(doc, "isPrototype", "boolean", true) ? Doc.MakeDelegate(doc) : Doc.MakeAlias(doc));
- let x = 0;
- let y = 0;
- for (const doc of docs.map(d => Doc.Layout(d))) {
- doc.x = x;
- doc.y = y;
- const size = 200;
- const aspect = Doc.NativeHeight(doc) / (Doc.NativeWidth(doc) || 1);
- if (aspect > 1) {
- doc._height = size;
- doc._width = size / aspect;
- } else if (aspect > 0) {
- doc._width = size;
- doc._height = size * aspect;
- } else {
- doc._width = size;
- doc._height = size;
- }
- x += 250;
- if (x > 1000) {
- x = 0;
- y += 300;
- }
+ if (query) {
+ this.searchCollection(query);
}
- const headers = Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []).map(h => { const v = h[Copy](); v.color = "#f1efeb"; return v; });
- return Docs.Create.SchemaDocument(headers, DocListCast(this.dataDoc[this.fieldKey]), { _autoHeight: true, _viewType: CollectionViewType.Schema, title: StrCast(this.layoutDoc._searchString) });
}
- @action.bound
- openSearch(e: React.SyntheticEvent) {
- e.stopPropagation();
- this._results.forEach(result => Doc.BrushDoc(result[0]));
- }
-
- resetSearch = action((close: boolean) => {
+ /**
+ * This method resets the search by iterating through each result and removing all
+ * brushes and highlights. All search matches are cleared as well.
+ */
+ resetSearch = action(() => {
this._results.forEach(result => {
Doc.UnBrushDoc(result[0]);
+ Doc.UnHighlightDoc(result[0]);
Doc.ClearSearchMatches();
});
- close && (this.open = this._searchbarOpen = false);
});
- @action.bound
- closeResults() {
- this._results = [];
- this._resultsSet.clear();
- this._visibleElements = [];
- this._visibleDocuments = [];
- this._numTotalResults = -1;
- this._endIndex = -1;
- this._curRequest = undefined;
+ /**
+ * @param {Doc} doc - doc to be selected
+ *
+ * This method selects a doc by either jumping to it (centering/zooming in on it)
+ * or opening it in a new tab.
+ */
+ selectElement = async (doc: Doc) => {
+ await DocumentManager.Instance.jumpToDocument(doc, true);
}
- @action
- resultsScrolled = (e?: React.UIEvent<HTMLDivElement>) => {
- this._endIndex = 30;
- const headers = new Set<string>(["title", "author", "text", "type", "data", "*lastModified", "context"]);
-
- if (this._numTotalResults <= this._maxSearchIndex) {
- this._numTotalResults = this._results.length;
- }
-
- // only hit right at the beginning
- // visibleElements is all of the elements (even the ones you can't see)
- if (this._visibleElements.length !== this._numTotalResults) {
- // undefined until a searchitem is put in there
- this._visibleElements = Array<JSX.Element>(this._numTotalResults === -1 ? 0 : this._numTotalResults);
- this._visibleDocuments = Array<Doc>(this._numTotalResults === -1 ? 0 : this._numTotalResults);
- }
- let max = this._numResultsPerPage;
- max > this._results.length ? max = this._results.length : console.log("");
- for (let i = this._pageStart; i < max; i++) {
- //if the index is out of the window then put a placeholder in
- //should ones that have already been found get set to placeholders?
-
- let result: [Doc, string[], string[]] | undefined = undefined;
-
- result = this._results[i];
- if (result) {
- const highlights = Array.from([...Array.from(new Set(result[1]).values())]);
- const lines = new List<string>(result[2]);
- highlights.forEach((item) => headers.add(item));
- Doc.SetSearchMatch(result[0], { searchMatch: 1 });
- if (i < this._visibleDocuments.length) {
- this._visibleDocuments[i] = result[0];
- Doc.BrushDoc(result[0]);
- Doc.AddDocToList(this.dataDoc, this.props.fieldKey, result[0]);
- this.children++;
- }
- }
- }
- if (this.props.Document._schemaHeaders === undefined) {
- this.props.Document._schemaHeaders = new List<SchemaHeaderField>([new SchemaHeaderField("title", "#f1efeb")]);
- }
- if (this._maxSearchIndex >= this._numTotalResults) {
- this._visibleElements.length = this._results.length;
- this._visibleDocuments.length = this._results.length;
- }
+ /**
+ * This method returns a JSX list of the options in the select drop-down menu, which
+ * is used to filter the types of documents that appear in the search results.
+ */
+ @computed
+ public get selectOptions() {
+ const selectValues = ["all", "rtf", "image", "pdf", "web", "video", "audio", "collection"]
+
+ return selectValues.map(value => {
+ return <option key={value} value={value}>{SearchBox.formatType(value)}</option>
+ })
}
- getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65);// listBox padding-left and pres-box-cont minHeight
- panelHeight = () => this.props.PanelHeight();
- selectElement = (doc: Doc) => { /* this.gotoDocument(this.childDocs.indexOf(doc), NumCasst(this.layoutDoc._itemIndex)); */ };
- returnHeight = () => NumCast(this.layoutDoc._height);
- returnLength = () => Math.min(window.innerWidth, 51 + 205 * Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []).length);
+ /**
+ * This method renders the search input box, select drop-down menu, and search results.
+ */
+ render() {
+ var validResults = 0;
- @action
- changeSearchScope = (scope: string) => {
- this.docsforfilter = undefined;
- this.setSearchFilter(this.currentSelectedCollection, undefined);
- this._searchFullDB = scope;
- this.dataDoc[this.fieldKey] = new List<Doc>([]);
- this.submitSearch();
- }
+ const isLinkSearch:boolean = this.props.linkSearch;
- @computed get scopeButtons() {
- return <div style={{ height: 25, paddingLeft: "4px", paddingRight: "4px" }}>
- <form className="beta" style={{ justifyContent: "space-evenly", display: "flex" }}>
- <div style={{ display: "contents" }}>
- <div className="radio" style={{ margin: 0 }}>
- <label style={{ fontSize: 12, marginTop: 6 }} >
- <input type="radio" style={{ marginLeft: -16, marginTop: -1 }} checked={!this._searchFullDB} onChange={() => this.changeSearchScope("")} />
- Dashboard
- </label>
- </div>
- <div className="radio" style={{ margin: 0 }}>
- <label style={{ fontSize: 12, marginTop: 6 }} >
- <input type="radio" style={{ marginLeft: -16, marginTop: -1 }} checked={this._searchFullDB?.length ? true : false} onChange={() => this.changeSearchScope("DB")} />
- DB
- <span onClick={action(() => this._searchFullDB = this._searchFullDB === "My Stuff" ? "DB" : "My Stuff")}>
- {this._searchFullDB === "My Stuff" ? "(me)" : "(full)"}
- </span>
- </label>
- </div>
- </div>
- </form>
- </div>;
- }
- setSearchFilter = action((collectionView: { props: { Document: Doc } }, docsForFilter: Doc[] | undefined) => {
- if (collectionView) {
- const docFilters = Cast(this.props.Document._docFilters, listSpec("string"), null);
- collectionView.props.Document._searchFilterDocs = docsForFilter?.length ? new List<Doc>(docsForFilter) : undefined;
- collectionView.props.Document._docFilters = docsForFilter?.length && docFilters?.length ? new List<string>(docFilters) : undefined;
- }
- });
+ const results = this._results.map(result => {
+ var className = "searchBox-results-scroll-view-result";
- render() {
- const myDashboards = DocListCast(CurrentUserUtils.MyDashboards.data);
- return (
- <div style={{ pointerEvents: "all" }} className="searchBox-container">
- <div className="searchBox-bar" style={{ background: SearchBox.Instance._undoBackground }}>
- <div className="searchBox-lozenges" >
- <div className="searchBox-lozenge-user">
- {`${Doc.CurrentUserEmail}`}
- <div className="searchBox-logoff" onClick={() => window.location.assign(Utils.prepend("/logout"))}>
- Logoff
- </div>
+ if (this._selectedResult == result[0]) {
+ className += " searchBox-results-scroll-view-result-selected"
+ }
+
+ if (this._docTypeString == "all" || this._docTypeString == result[0].type) {
+ validResults++;
+ return (
+ <div key={result[0][Id]} onClick={isLinkSearch ? () => this.makeLink(result[0]) : () => this.onResultClick(result[0])} className={className}>
+ <div className="searchBox-result-title">
+ {result[0].title}
</div>
- <div className="searchBox-lozenge" onClick={() => DocServer.UPDATE_SERVER_CACHE()}>
- {`UI project`}
+ <div className="searchBox-result-type">
+ {SearchBox.formatType(StrCast(result[0].type))}
</div>
- <div className="searchBox-lozenge-dashboard" >
- <select className="searchBox-dashSelect" onChange={e => CurrentUserUtils.openDashboard(Doc.UserDoc(), myDashboards[Number(e.target.value)])}
- value={myDashboards.indexOf(CurrentUserUtils.ActiveDashboard)}>
- {myDashboards.map((dash, i) => <option key={dash[Id]} value={i}> {StrCast(dash.title)} </option>)}
- </select>
- <div className="searchBox-dashboards" onClick={undoBatch(() => CurrentUserUtils.createNewDashboard(Doc.UserDoc()))}>
- New
- </div>
- <div className="searchBox-dashboards" onClick={undoBatch(() => CurrentUserUtils.snapshotDashboard(Doc.UserDoc()))}>
- Snapshot
- </div>
+ <div className="searchBox-result-keys">
+ {result[1].join(", ")}
</div>
</div>
- <div className="searchBox-query" >
- <input defaultValue={""} autoComplete="off" onChange={this.onChange} type="text" placeholder="Search..." id="search-input" ref={this._inputRef}
- className="searchBox-barChild searchBox-input" onKeyPress={this.enter}
- style={{ padding: 1, paddingLeft: 20, paddingRight: 60, color: "black", height: 20, width: 250 }} />
- <div style={{ display: "flex", alignItems: "center" }}>
- <div style={{ position: "absolute", left: 10 }}>
- <Tooltip title={<div className="dash-tooltip" >drag search results as collection</div>}>
- <div ref={this.collectionRef}><FontAwesomeIcon onPointerDown={SetupDrag(this.collectionRef, () => StrCast(this.layoutDoc._searchString) ? this.startDragCollection() : undefined)} icon={"search"} size="lg"
- style={{ cursor: "hand", color: "black", padding: 1, position: "relative" }} /></div>
- </Tooltip>
- </div>
- <div style={{ position: "absolute", left: Doc.UserDoc().noviceMode ? 220 : 200, width: 30, zIndex: 9000, color: "grey", background: "white", }}>
- {`${this._results.length}` + " of " + `${this.realTotalResults}`}
- </div>
- {Doc.UserDoc().noviceMode ? (null) : <div style={{ cursor: "default", left: 235, position: "absolute", }}>
- <Tooltip title={<div className="dash-tooltip" >only display documents matching search</div>} >
- <div>
- <FontAwesomeIcon icon={"filter"} size="lg"
- style={{ cursor: "hand", padding: 1, backgroundColor: this.filter ? "white" : "lightgray", color: this.filter ? "black" : "white" }}
- onPointerDown={e => { e.stopPropagation(); SetupDrag(this.collectionRef, () => this.layoutDoc._searchString ? this.startDragCollection() : undefined); }}
- onClick={action(() => this.setSearchFilter(this.currentSelectedCollection, this.filter ? undefined : this.docsforfilter))} />
- </div>
- </Tooltip>
- </div>}
- {this.scopeButtons}
- </div>
- </div >
+ )
+ }
+
+ return null;
+ })
+
+ results.filter(result => result);
+
+ return (
+ <div style={{ pointerEvents: "all" }} className="searchBox-container">
+ <div className="searchBox-bar" >
+ {isLinkSearch ? (null) : <select name="type" id="searchBox-type" className="searchBox-type" onChange={this.onSelectChange}>
+ {this.selectOptions}
+ </select>}
+ <input defaultValue={""} autoComplete="off" onChange={this.onInputChange} type="text" placeholder="Search..." id="search-input" className="searchBox-input" style={{width: isLinkSearch ? "100%" : undefined, borderRadius: isLinkSearch ? "5px" : undefined}} ref={this._inputRef} />
</div >
- {!this._searchbarOpen ? (null) :
- <div style={{ zIndex: 20000, color: "black" }} ref={(r) => r?.focus()}>
- <div style={{ display: "flex", justifyContent: "center", }}>
- <div style={{ display: this.open ? "flex" : "none", overflow: "auto", position: "absolute" }}>
- <CollectionSchemaView {...this.props}
- CollectionView={undefined}
- addDocument={returnFalse}
- Document={this.props.Document}
- moveDocument={returnFalse}
- removeDocument={returnFalse}
- PanelHeight={this.open ? this.returnHeight : returnZero}
- PanelWidth={this.open ? this.returnLength : returnZero}
- scrollOverflow={length > window.innerWidth || this.children > 6 ? true : false}
- focus={this.selectElement}
- ScreenToLocalTransform={Transform.Identity}
- />
- <div style={{ position: "absolute", right: 5, bottom: 7, width: 15, height: 15, }}
- onPointerDown={e => setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => {
- this.props.Document._height = NumCast(this.props.Document._height) + delta[1];
- return false;
- }, returnFalse, emptyFunction)}
- >
- <FontAwesomeIcon icon="grip-lines" size="lg" />
- </div>
- </div>
- </div>
+ <div className="searchBox-results-container">
+ <div className="searchBox-results-count">
+ {`${validResults}` + " result" + (validResults == 1 ? "" : "s")}
</div>
- }
+ <div className="searchBox-results-scroll-view">
+ {results}
+ </div>
+ </div>
</div >
);
}
diff --git a/src/client/views/topbar/TopBar.scss b/src/client/views/topbar/TopBar.scss
index ac6ec9b30..05aeb177c 100644
--- a/src/client/views/topbar/TopBar.scss
+++ b/src/client/views/topbar/TopBar.scss
@@ -22,7 +22,7 @@
.topBar-icon {
cursor: pointer;
- font-size: 12px;
+ font-size: 12.5px;
font-family: 'Roboto';
width: fit-content;
display: flex;
@@ -31,18 +31,20 @@
align-items: center;
justify-self: center;
align-self: center;
- border-radius: 5px;
+ border-radius: $standard-border-radius;
padding: 5px;
transition: linear 0.1s;
color: $black;
background-color: $light-gray;
- }
- .topBar-icon:hover {
- background-color: $light-blue;
+ &:hover {
+ background-color: $light-blue;
+ }
}
-
+
+
+
.topbar-center {
grid-column: 2;
display: inline-flex;
@@ -50,12 +52,16 @@
align-items: center;
gap: 5px;
+ .topbar-dashboards {
+ display: flex;
+ flex-direction: row;
+ gap: 5px;
+ }
+
.topbar-lozenge-dashboard {
display: flex;
- .topbar-dashboards {
- display: inline-flex;
- }
+
.topbar-dashSelect {
border: none;
@@ -88,7 +94,7 @@
font-family: 'Roboto';
position: relative;
display: flex;
- width: 450;
+ width: fit-content;
gap: 5px;
.topBar-icon:hover {
@@ -152,9 +158,9 @@
}
&.topbar-input {
- margin:5px;
- border-radius:20px;
- border:$dark-gray;
+ margin: 5px;
+ border-radius: 20px;
+ border: $dark-gray;
display: block;
width: 130px;
-webkit-transition: width 0.4s;
diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx
index 05edb975c..4dbd5e22c 100644
--- a/src/client/views/topbar/TopBar.tsx
+++ b/src/client/views/topbar/TopBar.tsx
@@ -28,7 +28,7 @@ export class TopBar extends React.Component {
{`${Doc.CurrentUserEmail}`}
</div>
<div className="topbar-icon" onClick={() => window.location.assign(Utils.prepend("/logout"))}>
- {"Sign out"}
+ {"Log out"}
</div>
</div>
<div className="topbar-center" >
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 976bd5ee1..17f41fac8 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -21,9 +21,11 @@ import { listSpec } from "./Schema";
import { ComputedField, ScriptField } from "./ScriptField";
import { Cast, FieldValue, NumCast, StrCast, ToConstructor } from "./Types";
import { AudioField, ImageField, PdfField, VideoField, WebField } from "./URLField";
-import { deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, updateFunction } from "./util";
+import { deleteProperty, GetEffectiveAcl, getField, getter, inheritParentAcls, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, updateFunction } from "./util";
import JSZip = require("jszip");
+import { CurrentUserUtils } from "../client/util/CurrentUserUtils";
import { IconProp } from "@fortawesome/fontawesome-svg-core";
+import Color = require("color");
export namespace Field {
export function toKeyValueString(doc: Doc, key: string): string {
@@ -54,6 +56,9 @@ export namespace Field {
|| (field instanceof RefField)
|| (includeUndefined && field === undefined);
}
+ export function Copy(field: any) {
+ return field instanceof ObjectField ? ObjectField.MakeCopy(field) : field;
+ }
}
export type Field = number | string | boolean | ObjectField | RefField;
export type Opt<T> = T | undefined;
@@ -61,10 +66,10 @@ export type FieldWaiting<T extends RefField = RefField> = T extends undefined ?
export type FieldResult<T extends Field = Field> = Opt<T> | FieldWaiting<Extract<T, RefField>>;
/**
- * Cast any field to either a List of Docs or undefined if the given field isn't a List of Docs.
- * If a default value is given, that will be returned instead of undefined.
- * If a default value is given, the returned value should not be modified as it might be a temporary value.
- * If no default value is given, and the returned value is not undefined, it can be safely modified.
+ * Cast any field to either a List of Docs or undefined if the given field isn't a List of Docs.
+ * If a default value is given, that will be returned instead of undefined.
+ * If a default value is given, the returned value should not be modified as it might be a temporary value.
+ * If no default value is given, and the returned value is not undefined, it can be safely modified.
*/
export function DocListCastAsync(field: FieldResult): Promise<Doc[] | undefined>;
export function DocListCastAsync(field: FieldResult, defaultValue: Doc[]): Promise<Doc[]>;
@@ -89,7 +94,8 @@ export const DirectLinksSym = Symbol("DirectLinks");
export const AclUnset = Symbol("AclUnset");
export const AclPrivate = Symbol("AclOwnerOnly");
export const AclReadonly = Symbol("AclReadOnly");
-export const AclAddonly = Symbol("AclAddonly");
+export const AclAugment = Symbol("AclAugment");
+export const AclSelfEdit = Symbol("AclSelfEdit");
export const AclEdit = Symbol("AclEdit");
export const AclAdmin = Symbol("AclAdmin");
export const UpdatingFromServer = Symbol("UpdatingFromServer");
@@ -101,7 +107,8 @@ const AclMap = new Map<string, symbol>([
["None", AclUnset],
[SharingPermissions.None, AclPrivate],
[SharingPermissions.View, AclReadonly],
- [SharingPermissions.Add, AclAddonly],
+ [SharingPermissions.Augment, AclAugment],
+ [SharingPermissions.SelfEdit, AclSelfEdit],
[SharingPermissions.Edit, AclEdit],
[SharingPermissions.Admin, AclAdmin]
]);
@@ -251,7 +258,8 @@ export class Doc extends RefField {
DocServer.GetRefField(this[Id], true);
}
};
- if (sameAuthor || fKey.startsWith("acl") || DocServer.getFieldWriteMode(fKey) !== DocServer.WriteMode.Playground) {
+ const writeMode = DocServer.getFieldWriteMode(fKey);
+ if (fKey.startsWith("acl") || writeMode !== DocServer.WriteMode.Playground) {
delete this[CachedUpdates][fKey];
await fn();
} else {
@@ -365,13 +373,13 @@ export namespace Doc {
/**
* This function is intended to model Object.assign({}, {}) [https://mzl.la/1Mo3l21], which copies
* the values of the properties of a source object into the target.
- *
+ *
* This is just a specific, Dash-authored version that serves the same role for our
* Doc class.
- *
- * @param doc the target document into which you'd like to insert the new fields
+ *
+ * @param doc the target document into which you'd like to insert the new fields
* @param fields the fields to project onto the target. Its type signature defines a mapping from some string key
- * to a potentially undefined field, where each entry in this mapping is optional.
+ * to a potentially undefined field, where each entry in this mapping is optional.
*/
export function assign<K extends string>(doc: Doc, fields: Partial<Record<K, Opt<Field>>>, skipUndefineds: boolean = false, isInitializing = false) {
isInitializing && (doc[Initializing] = true);
@@ -398,7 +406,7 @@ export namespace Doc {
}
// Gets the data document for the document. Note: this is mis-named -- it does not specifically
- // return the doc's proto, but rather recursively searches through the proto inheritance chain
+ // return the doc's proto, but rather recursively searches through the proto inheritance chain
// and returns the document who's proto is undefined or whose proto is marked as a base prototype ('isPrototype').
export function GetProto(doc: Doc): Doc {
if (doc instanceof Promise) {
@@ -424,6 +432,9 @@ export namespace Doc {
return Array.from(results);
}
+ /**
+ * @returns the index of doc toFind in list of docs, -1 otherwise
+ */
export function IndexOf(toFind: Doc, list: Doc[], allowProtos: boolean = true) {
let index = list.reduce((p, v, i) => (v instanceof Doc && v === toFind) ? i : p, -1);
index = allowProtos && index !== -1 ? index : list.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, toFind)) ? i : p, -1);
@@ -530,13 +541,13 @@ export namespace Doc {
const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
const field = ProxyField.WithoutProxy(() => doc[key]);
const copyObjectField = async (field: ObjectField) => {
- const list = Cast(doc[key], listSpec(Doc));
+ 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 => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch)));
!dontCreate && assignKey(new List<Doc>(clones));
} else if (doc[key] instanceof Doc) {
- assignKey(key.includes("layout[") ? undefined : key.startsWith("layout") ? doc[key] as Doc : await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch)); // 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, linkMap, rtfs, exclusions, dontCreate, asBranch)); // reference documents except copy documents that are expanded template fields
} else {
!dontCreate && assignKey(ObjectField.MakeCopy(field));
if (field instanceof RichTextField) {
@@ -562,7 +573,7 @@ export namespace Doc {
} else if (field instanceof ObjectField) {
await copyObjectField(field);
} else if (field instanceof Promise) {
- debugger; //This shouldn't happend...
+ debugger; //This shouldn't happen...
} else {
assignKey(field);
}
@@ -582,11 +593,10 @@ export namespace Doc {
}
return copy;
}
- export async function MakeClone(doc: Doc, dontCreate: boolean = false, asBranch = false) {
- const cloneMap = new Map<string, Doc>();
+ export async function MakeClone(doc: Doc, dontCreate: boolean = false, asBranch = false, cloneMap: Map<string, Doc> = new Map()) {
const linkMap = new Map<Doc, Doc>();
const rtfMap: { copy: Doc, key: string, field: RichTextField }[] = [];
- const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ["context", "annotationOn", "cloneOf", "branches", "branchOf"], dontCreate, asBranch);
+ const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ["cloneOf", "branches", "branchOf"], dontCreate, asBranch);
Array.from(linkMap.entries()).map((links: Doc[]) => LinkManager.Instance.addLink(links[1], true));
rtfMap.map(({ copy, key, field }) => {
const replacer = (match: any, attr: string, id: string, offset: any, string: any) => {
@@ -597,7 +607,7 @@ export namespace Doc {
const mapped = cloneMap.get(id);
return href + (mapped ? mapped[Id] : id);
};
- const regex = `(${Utils.prepend("/doc/")})([^"]*)`;
+ const regex = `(${Doc.localServerPath()})([^"]*)`;
const re = new RegExp(regex, "g");
copy[key] = new RichTextField(field.Data.replace(/("textId":|"audioId":|"anchorId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text);
});
@@ -667,14 +677,14 @@ export namespace Doc {
const _pendingMap: Map<string, boolean> = new Map();
//
// Returns an expanded template layout for a target data document if there is a template relationship
- // between the two. If so, the layoutDoc is expanded into a new document that inherits the properties
+ // between the two. If so, the layoutDoc is expanded into a new document that inherits the properties
// of the original layout while allowing for individual layout properties to be overridden in the expanded layout.
// templateArgs should be equivalent to the layout key that generates the template since that's where the template parameters are stored in ()'s at the end of the key.
// NOTE: the template will have references to "@params" -- the template arguments will be assigned to the '@params' field
// so that when the @params key is accessed, it will be rewritten as the key that is stored in the 'params' field and
// the derefence will then occur on the rootDocument (the original document).
// in the future, field references could be written as @<someparam> and then arguments would be passed in the layout key as:
- // layout_mytemplate(somparam=somearg).
+ // layout_mytemplate(somparam=somearg).
// then any references to @someparam would be rewritten as accesses to 'somearg' on the rootDocument
export function expandTemplateLayout(templateLayoutDoc: Doc, targetDoc?: Doc, templateArgs?: string) {
const args = templateArgs?.match(/\(([a-zA-Z0-9._\-]*)\)/)?.[1].replace("()", "") || StrCast(templateLayoutDoc.PARAMS);
@@ -775,7 +785,7 @@ export namespace Doc {
copy[key] = cfield[Copy]();// ComputedField.MakeFunction(cfield.script.originalScript);
} else if (field instanceof ObjectField) {
copy[key] = doc[key] instanceof Doc ?
- key.includes("layout[") ? undefined : doc[key] : // reference documents except remove documents that are expanded teplate fields
+ key.includes("layout[") ? undefined : doc[key] : // reference documents except remove documents that are expanded teplate fields
ObjectField.MakeCopy(field);
} else if (field instanceof Promise) {
debugger; //This shouldn't happend...
@@ -897,6 +907,16 @@ export namespace Doc {
return true;
}
+
+ // converts a document id to a url path on the server
+ export function globalServerPath(doc: Doc | string = ""): string {
+ return Utils.prepend("/doc/" + (doc instanceof Doc ? doc[Id] : doc));
+ }
+ // converts a document id to a url path on the server
+ export function localServerPath(doc?: Doc): string {
+ return "/doc/" + (doc ? doc[Id] : "");
+ }
+
export function overlapping(doc1: Doc, doc2: Doc, clusterDistance: number) {
const doc2Layout = Doc.Layout(doc2);
const doc1Layout = Doc.Layout(doc1);
@@ -928,7 +948,7 @@ export namespace Doc {
}
// the document containing the view layout information - will be the Document itself unless the Document has
- // a layout field or 'layout' is given.
+ // a layout field or 'layout' is given.
export function Layout(doc: Doc, layout?: Doc): Doc {
const overrideLayout = layout && Cast(doc[`${StrCast(layout.isTemplateForField, "data")}-layout[` + layout[Id] + "]"], Doc, null);
return overrideLayout || doc[LayoutSym] || doc;
@@ -1068,6 +1088,10 @@ export namespace Doc {
}
export function matchFieldValue(doc: Doc, key: string, value: any): boolean {
+ if (Utils.HasTransparencyFilter(value)) {
+ const isTransparent = (color: string) => color !== "" && (Color(color).alpha() !== 1);
+ return isTransparent(StrCast(doc[key]));
+ }
const fieldVal = doc[key];
if (Cast(fieldVal, listSpec("string"), []).length) {
const vals = Cast(fieldVal, listSpec("string"), []);
@@ -1107,7 +1131,7 @@ export namespace Doc {
}
// filters document in a container collection:
- // all documents with the specified value for the specified key are included/excluded
+ // all documents with the specified value for the specified key are included/excluded
// based on the modifiers :"check", "x", undefined
export function setDocFilter(container: Opt<Doc>, key: string, value: any, modifiers: "remove" | "match" | "check" | "x", toggle?: boolean, fieldSuffix?: string, append: boolean = true) {
if (!container) return;
@@ -1178,6 +1202,9 @@ export namespace Doc {
dragFactory["dragFactory-count"] = NumCast(dragFactory["dragFactory-count"]) + 1;
Doc.SetInPlace(ndoc, "title", ndoc.title + " " + NumCast(dragFactory["dragFactory-count"]).toString(), true);
}
+
+ if (ndoc) inheritParentAcls(CurrentUserUtils.ActiveDashboard, ndoc);
+
return ndoc;
}
export function delegateDragFactory(dragFactory: Doc) {
@@ -1196,7 +1223,7 @@ export namespace Doc {
case DocumentType.RTF: return "sticky-note";
case DocumentType.COL:
const folder: IconProp = isOpen ? "folder-open" : "folder";
- const chevron: IconProp = isOpen ? "chevron-down" : "chevron-right"
+ const chevron: IconProp = isOpen ? "chevron-down" : "chevron-right";
return !doc?.isFolder ? folder : chevron;
case DocumentType.WEB: return "globe-asia";
case DocumentType.SCREENSHOT: return "photo-video";
@@ -1231,39 +1258,39 @@ export namespace Doc {
/**
* This function takes any valid JSON(-like) data, i.e. parsed or unparsed, and at arbitrarily
* deep levels of nesting, converts the data and structure into nested documents with the appropriate fields.
- *
+ *
* After building a hierarchy within / below a top-level document, it then returns that top-level parent.
- *
+ *
* If we've received a string, treat it like valid JSON and try to parse it into an object. If this fails, the
* string is invalid JSON, so we should assume that the input is the result of a JSON.parse()
* call that returned a regular string value to be stored as a Field.
- *
+ *
* If we've received something other than a string, since the caller might also pass in the results of a
* JSON.parse() call, valid input might be an object, an array (still typeof object), a boolean or a number.
* Anything else (like a function, etc. passed in naively as any) is meaningless for this operation.
- *
+ *
* All TS/JS objects get converted directly to documents, directly preserving the key value structure. Everything else,
* lacking the key value structure, gets stored as a field in a wrapper document.
- *
+ *
* @param data for convenience and flexibility, either a valid JSON string to be parsed,
* or the result of any JSON.parse() call.
* @param title an optional title to give to the highest parent document in the hierarchy.
* If whether this function creates a new document or appendToExisting is specified and that document already has a title,
* because this title field can be left undefined for the opposite behavior, including a title will overwrite the existing title.
* @param appendToExisting **if specified**, there are two cases, both of which return the target document:
- *
+ *
* 1) the json to be converted can be represented as a document, in which case the target document will act as the root
* of the tree and receive all the conversion results as new fields on itself
* 2) the json can't be represented as a document, in which case the function will assign the field-level conversion
* results to either the specified key on the target document, or to its "json" key by default.
- *
+ *
* If not specified, the function creates and returns a new entirely generic document (different from the Doc.Create calls)
* to act as the root of the tree.
- *
+ *
* One might choose to specify this field if you want to write to a document returned from a Document.Create function call,
* say a TreeView document that will be rendered, not just an untyped, identityless doc that would otherwise be created
* from a default call to new Doc.
- *
+ *
* @param excludeEmptyObjects whether non-primitive objects (TypeScript objects and arrays) should be converted even
* if they contain no data. By default, empty objects and arrays are ignored.
*/
@@ -1299,7 +1326,7 @@ export namespace Doc {
* For each value of the object, recursively convert it to its appropriate field value
* and store the field at the appropriate key in the document if it is not undefined
* @param object the object to convert
- * @returns the object mapped from JSON to field values, where each mapping
+ * @returns the object mapped from JSON to field values, where each mapping
* might involve arbitrary recursion (since toField might itself call convertObject)
*/
const convertObject = (object: any, excludeEmptyObjects: boolean, title?: string, target?: Doc): Opt<Doc> => {
@@ -1323,10 +1350,10 @@ export namespace Doc {
};
/**
- * For each element in the list, recursively convert it to a document or other field
+ * For each element in the list, recursively convert it to a document or other field
* and push the field to the list if it is not undefined
* @param list the list to convert
- * @returns the list mapped from JSON to field values, where each mapping
+ * @returns the list mapped from JSON to field values, where each mapping
* might involve arbitrary recursion (since toField might itself call convertList)
*/
const convertList = (list: Array<any>, excludeEmptyObjects: boolean): Opt<List<Field>> => {
@@ -1365,7 +1392,7 @@ 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 copyDragFactory(dragFactory: Doc) { return Doc.copyDragFactory(dragFactory); });
Scripting.addGlobal(function delegateDragFactory(dragFactory: Doc) { return Doc.delegateDragFactory(dragFactory); });
-Scripting.addGlobal(function copyField(field: any) { return field instanceof ObjectField ? ObjectField.MakeCopy(field) : field; });
+Scripting.addGlobal(function copyField(field: any) { return Field.Copy(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); });
Scripting.addGlobal(function sameDocs(doc1: any, doc2: any) { return Doc.AreProtosEqual(doc1, doc2); });
diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts
index 1270a2dab..f16e143d8 100644
--- a/src/fields/InkField.ts
+++ b/src/fields/InkField.ts
@@ -1,8 +1,8 @@
+import { createSimpleSchema, list, object, serializable } from "serializr";
+import { Scripting } from "../client/util/Scripting";
import { Deserializable } from "../client/util/SerializationHelper";
-import { serializable, custom, createSimpleSchema, list, object, map } from "serializr";
+import { Copy, ToScriptString, ToString } from "./FieldSymbols";
import { ObjectField } from "./ObjectField";
-import { Copy, ToScriptString, ToString, Update } from "./FieldSymbols";
-import { Scripting } from "../client/util/Scripting";
// Helps keep track of the current ink tool in use.
export enum InkTool {
diff --git a/src/fields/List.ts b/src/fields/List.ts
index 215dff34b..93a8d1d60 100644
--- a/src/fields/List.ts
+++ b/src/fields/List.ts
@@ -1,4 +1,4 @@
-import { action, observable, runInAction } from "mobx";
+import { action, observable } from "mobx";
import { alias, list, serializable } from "serializr";
import { DocServer } from "../client/DocServer";
import { Scripting } from "../client/util/Scripting";
@@ -264,24 +264,19 @@ class ListImpl<T extends Field> extends ObjectField {
// this requests all ProxyFields at the same time to avoid the overhead
// of separate network requests and separate updates to the React dom.
private __realFields() {
- const waiting = this.__fields.filter(f => f instanceof ProxyField && f.promisedValue());
- const promised = waiting.map(f => f instanceof ProxyField ? f.promisedValue() : "");
+ const promised = this.__fields.filter(f => f instanceof ProxyField && f.promisedValue()).map(f => ({ field: f as any, promisedFieldId: (f instanceof ProxyField) ? f.promisedValue() : "" }));
// if we find any ProxyFields that don't have a current value, then
// start the server request for all of them
if (promised.length) {
- const promise = DocServer.GetRefFields(promised);
+ const batchPromise = DocServer.GetRefFields(promised.map(p => p.promisedFieldId));
// as soon as we get the fields from the server, set all the list values in one
// action to generate one React dom update.
- promise.then(fields => runInAction(() => {
- waiting.map((w, i) => w instanceof ProxyField && w.setValue(fields[promised[i]]));
- }));
+ batchPromise.then(pfields => promised.forEach(p => p.field.setValue(pfields[p.promisedFieldId])));
// we also have to mark all lists items with this promise so that any calls to them
- // will await the batch request.
- // This counts on the handler for 'promise' in the call above being invoked before the
+ // will await the batch request and return the requested field value.
+ // This assumes the handler for 'promise' in the call above being invoked before the
// handler for 'promise' in the lines below.
- waiting.map((w, i) => {
- w instanceof ProxyField && w.setPromise(promise.then(fields => fields[promised[i]]));
- });
+ promised.forEach(p => p.field.setPromise(batchPromise.then(pfields => pfields[p.promisedFieldId])));
}
return this.__fields.map(toRealField);
}
diff --git a/src/fields/URLField.ts b/src/fields/URLField.ts
index fb71160ca..d96e8a70a 100644
--- a/src/fields/URLField.ts
+++ b/src/fields/URLField.ts
@@ -3,14 +3,17 @@ import { serializable, custom } from "serializr";
import { ObjectField } from "./ObjectField";
import { ToScriptString, ToString, Copy } from "./FieldSymbols";
import { Scripting, scriptingGlobal } from "../client/util/Scripting";
+import { Utils } from "../Utils";
function url() {
return custom(
function (value: URL) {
- return value.href;
+ return value.origin === window.location.origin ?
+ value.pathname :
+ value.href;
},
function (jsonValue: string) {
- return new URL(jsonValue);
+ return new URL(jsonValue, window.location.origin);
}
);
}
@@ -24,15 +27,21 @@ export abstract class URLField extends ObjectField {
constructor(url: URL | string) {
super();
if (typeof url === "string") {
- url = new URL(url);
+ url = url.startsWith("http") ? new URL(url) : new URL(url, window.location.origin);
}
this.url = url;
}
[ToScriptString]() {
+ if (Utils.prepend(this.url.pathname) === this.url.href) {
+ return `new ${this.constructor.name}("${this.url.pathname}")`;
+ }
return `new ${this.constructor.name}("${this.url.href}")`;
}
[ToString]() {
+ if (Utils.prepend(this.url.pathname) === this.url.href) {
+ return this.url.pathname;
+ }
return this.url.href;
}
diff --git a/src/fields/util.ts b/src/fields/util.ts
index ea91cc057..439c4d333 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -1,5 +1,5 @@
import { UndoManager } from "../client/util/UndoManager";
-import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, DataSym, DocListCast, AclAdmin, HeightSym, WidthSym, updateCachedAcls, AclUnset, DocListCastAsync, ForceServerWrite, Initializing } from "./Doc";
+import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAugment, AclSym, DataSym, DocListCast, AclAdmin, HeightSym, WidthSym, updateCachedAcls, AclUnset, DocListCastAsync, ForceServerWrite, Initializing, AclSelfEdit } from "./Doc";
import { SerializationHelper } from "../client/util/SerializationHelper";
import { ProxyField, PrefetchProxy } from "./Proxy";
import { RefField } from "./RefField";
@@ -14,6 +14,7 @@ import CursorField from "./CursorField";
import { List } from "./List";
import { SnappingManager } from "../client/util/SnappingManager";
import { computedFn } from "mobx-utils";
+import { RichTextField } from "./RichTextField";
function _readOnlySetter(): never {
throw new Error("Documents can't be modified in read-only mode");
@@ -77,7 +78,9 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
const fromServer = target[UpdatingFromServer];
const sameAuthor = fromServer || (receiver.author === Doc.CurrentUserEmail);
const writeToDoc = sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || (writeMode !== DocServer.WriteMode.LiveReadonly);
- const writeToServer = (sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || writeMode === DocServer.WriteMode.Default) && !DocServer.Control.isReadOnly();// && !playgroundMode;
+ const writeToServer =
+ (sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || (effectiveAcl === AclSelfEdit && (value instanceof RichTextField))) &&
+ !DocServer.Control.isReadOnly();
if (writeToDoc) {
if (value === undefined) {
@@ -131,6 +134,19 @@ export function denormalizeEmail(email: string) {
// playgroundMode = !playgroundMode;
// }
+
+/**
+ * Copies parent's acl fields to the child
+ */
+export function inheritParentAcls(parent: Doc, child: Doc) {
+ const dataDoc = parent[DataSym];
+ for (const key of Object.keys(dataDoc)) {
+ // if the default acl mode is private, then don't inherit the acl-Public permission, but set it to private.
+ const permission = (key === "acl-Public" && Doc.UserDoc().defaultAclPrivate) ? AclPrivate : dataDoc[key];
+ key.startsWith("acl") && distributeAcls(key, permission, child);
+ }
+}
+
/**
* These are the various levels of access a user can have to a document.
*
@@ -146,9 +162,10 @@ export function denormalizeEmail(email: string) {
*/
export enum SharingPermissions {
Admin = "Admin",
- Edit = "Can Edit",
- Add = "Can Augment",
- View = "Can View",
+ Edit = "Edit",
+ SelfEdit = "Self Edit",
+ Augment = "Augment",
+ View = "View",
None = "Not Shared"
}
@@ -165,7 +182,7 @@ export function GetEffectiveAcl(target: any, user?: string): symbol {
function getPropAcl(target: any, prop: string | symbol | number) {
if (prop === UpdatingFromServer || prop === Initializing || target[UpdatingFromServer] || prop === AclSym) return AclAdmin; // requesting the UpdatingFromServer prop or AclSym must always go through to keep the local DB consistent
- if (prop && DocServer.PlaygroundFields?.includes(prop.toString())) return AclEdit; // playground props are always editable
+ if (prop && DocServer.IsPlaygroundField(prop.toString())) return AclEdit; // playground props are always editable
return GetEffectiveAcl(target);
}
@@ -181,7 +198,8 @@ function getEffectiveAcl(target: any, user?: string): symbol {
HierarchyMapping = HierarchyMapping || new Map<symbol, number>([
[AclPrivate, 0],
[AclReadonly, 1],
- [AclAddonly, 2],
+ [AclAugment, 2],
+ [AclSelfEdit, 2.5],
[AclEdit, 3],
[AclAdmin, 4]
]);
@@ -215,7 +233,7 @@ function getEffectiveAcl(target: any, user?: string): symbol {
* @param inheritingFromCollection whether the target is being assigned rights after being dragged into a collection (and so is inheriting the acls from the collection)
* inheritingFromCollection is not currently being used but could be used if acl assignment defaults change
*/
-export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, inheritingFromCollection?: boolean, visited?: Doc[]) {
+export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, inheritingFromCollection?: boolean, visited?: Doc[], isDashboard?: boolean) {
if (!visited) visited = [] as Doc[];
if (visited.includes(target)) return;
visited.push(target);
@@ -224,6 +242,7 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
["Not Shared", 0],
["Can View", 1],
["Can Augment", 2],
+ ["Self Edit", 2.5],
["Can Edit", 3],
["Admin", 4]
]);
@@ -236,6 +255,12 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
if (GetEffectiveAcl(target) === AclAdmin && (!inheritingFromCollection || !target[key] || HierarchyMapping.get(StrCast(target[key]))! > HierarchyMapping.get(acl)!)) {
target[key] = acl;
layoutDocChanged = true;
+
+ if (isDashboard) {
+ DocListCastAsync(target[Doc.LayoutFieldKey(target)]).then(docs => {
+ docs?.forEach(d => distributeAcls(key, acl, d, inheritingFromCollection, visited));
+ });
+ }
}
if (dataDoc && (!inheritingFromCollection || !dataDoc[key] || HierarchyMapping.get(StrCast(dataDoc[key]))! > HierarchyMapping.get(acl)!)) {
@@ -245,28 +270,26 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
dataDocChanged = true;
}
- // maps over the aliases of the document
+ // maps over the links of the document
const links = DocListCast(dataDoc.links);
links.forEach(link => distributeAcls(key, acl, link, inheritingFromCollection, visited));
// maps over the children of the document
- DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc)]).map(d => {
- // if (GetEffectiveAcl(d) === AclAdmin && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) {
+ DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + (isDashboard ? "-all" : "")]).map(d => {
distributeAcls(key, acl, d, inheritingFromCollection, visited);
// }
const data = d[DataSym];
- if (data) {// && GetEffectiveAcl(data) === AclAdmin && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) {
+ if (data) {
distributeAcls(key, acl, data, inheritingFromCollection, visited);
}
});
// maps over the annotations of the document
DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + "-annotations"]).map(d => {
- // if (GetEffectiveAcl(d) === AclAdmin && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) {
distributeAcls(key, acl, d, inheritingFromCollection, visited);
// }
const data = d[DataSym];
- if (data) {// && GetEffectiveAcl(data) === AclAdmin && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) {
+ if (data) {
distributeAcls(key, acl, data, inheritingFromCollection, visited);
}
});
@@ -279,7 +302,7 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean {
let prop = in_prop;
const effectiveAcl = getPropAcl(target, prop);
- if (effectiveAcl !== AclEdit && effectiveAcl !== AclAdmin) return true;
+ if (effectiveAcl !== AclEdit && effectiveAcl !== AclAdmin && !(effectiveAcl === AclSelfEdit && value instanceof RichTextField)) return true;
// if you're trying to change an acl but don't have Admin access / you're trying to change it to something that isn't an acceptable acl, you can't
if (typeof prop === "string" && prop.startsWith("acl") && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined, "None"].includes(value))) return true;
// if (typeof prop === "string" && prop.startsWith("acl") && !["Can Edit", "Can Augment", "Can View", "Not Shared", undefined].includes(value)) return true;
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index 98696496f..f910d765e 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -50,7 +50,7 @@ export class Uploader extends React.Component<ImageUploadProps> {
if (result instanceof Error) {
return;
}
- const path = Utils.prepend(result.accessPaths.agnostic.client);
+ const path = result.accessPaths.agnostic.client;
let doc = null;
// Case 1: File is a video
if (file.type === "video/mp4") {
diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts
index e40f2b8e5..0f4a067fc 100644
--- a/src/server/server_Initialization.ts
+++ b/src/server/server_Initialization.ts
@@ -142,8 +142,9 @@ function registerCorsProxy(server: express.Express) {
const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
server.use("/corsProxy", async (req, res) => {
- const requrl = decodeURIComponent(req.url.substring(1));
const referer = req.headers.referer ? decodeURIComponent(req.headers.referer) : "";
+ const requrlraw = decodeURIComponent(req.url.substring(1));
+ const requrl = requrlraw.startsWith("/") ? referer + requrlraw : requrlraw;
// cors weirdness here...
// if the referer is a cors page and the cors() route (I think) redirected to /corsProxy/<path> and the requested url path was relative,
// then we redirect again to the cors referer and just add the relative path.
diff --git a/src/server/websocket.ts b/src/server/websocket.ts
index 4ae97913f..224a9eefb 100644
--- a/src/server/websocket.ts
+++ b/src/server/websocket.ts
@@ -283,7 +283,7 @@ export namespace WebSocket {
return;
}
const curList = (curListItems as any)?.fields?.[updatefield.replace("fields.", "")]?.fields.filter((item: any) => item !== undefined) || [];
- diff.diff.$set[updatefield].fields = [...curList, ...newListItems.filter((newItem: any) => newItem === null || !curList.some((curItem: any) => curItem.fieldId ? curItem.fieldId === newItem.fieldId : curItem.heading ? curItem.heading === newItem.heading : curItem === newItem))];
+ diff.diff.$set[updatefield].fields = [...curList, ...newListItems];//, ...newListItems.filter((newItem: any) => newItem === null || !curList.some((curItem: any) => curItem.fieldId ? curItem.fieldId === newItem.fieldId : curItem.heading ? curItem.heading === newItem.heading : curItem === newItem))];
const sendBack = diff.diff.length !== diff.diff.$set[updatefield].fields.length;
delete diff.diff.length;
Database.Instance.update(diff.id, diff.diff,