diff options
Diffstat (limited to 'src')
175 files changed, 6180 insertions, 4639 deletions
diff --git a/src/Utils.ts b/src/Utils.ts index ef9c51b8b..de3b13f63 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -67,7 +67,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 +115,24 @@ export namespace Utils { return { r: r, g: g, b: b, a: a }; } + const isTransparentFunctionHack = "isTransparent(__value__)"; + export 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 : "") + ")"; } @@ -191,11 +208,12 @@ export namespace Utils { return { h: h, s: s, l: l }; } - export function scrollIntoView(targetY: number, targetHgt: number, scrollTop: number, contextHgt: number) { - if (scrollTop + contextHgt < targetY + targetHgt * 1.1) { - return Math.ceil(targetY + targetHgt * 1.1 - contextHgt); - } else if (scrollTop > targetY - targetHgt * .1) { - return Math.max(0, Math.floor(targetY - targetHgt * .1)); + export function scrollIntoView(targetY: number, targetHgt: number, scrollTop: number, contextHgt: number, minSpacing: number) { + if (scrollTop + contextHgt < targetY + minSpacing + targetHgt) { + return Math.ceil(targetY + minSpacing + targetHgt - contextHgt); + } + if (scrollTop > targetY - minSpacing - targetHgt) { + return Math.max(0, Math.floor(targetY - minSpacing - targetHgt)); } } @@ -390,8 +408,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) { @@ -439,6 +456,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; diff --git a/src/client/ClientRecommender.scss b/src/client/ClientRecommender.scss index 3f9102f15..178c7fdab 100644 --- a/src/client/ClientRecommender.scss +++ b/src/client/ClientRecommender.scss @@ -1,4 +1,4 @@ -// @import "/views/globalCssVariables.scss"; +// @import "/views/global/globalCssVariables.scss"; .space{ border: 1px dashed blue; 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/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index 8565784b4..dba7ff907 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -18,7 +18,7 @@ export enum DocumentType { LABEL = "label", // simple text label BUTTON = "button", // onClick button WEBCAM = "webcam", // webcam - HTMLANCHOR = "htmlanchor", // text selection anchor in PDF/Web + MARKER = "marker", // generic marker document not intended to be viewed independently of its context (e.g., for text selections in PDF/Web/RTF) DATE = "date", // calendar view of a date SCRIPTING = "script", // script editor EQUATION = "equation", // equation editor @@ -40,6 +40,4 @@ export enum DocumentType { LINKDB = "linkdb", // database of links ??? why do we have this SCRIPTDB = "scriptdb", // database of scripts GROUPDB = "groupdb", // database of groups - - TEXTANCHOR = "textanchor" // selection of text in a text box }
\ No newline at end of file diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 24682cbd0..a1ffddb98 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,53 +13,52 @@ 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"; -import { DFLT_IMAGE_NATIVE_DIM } from "../views/globalCssVariables.scss"; +import { DFLT_IMAGE_NATIVE_DIM } from "../views/global/globalCssVariables.scss"; import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkingStroke } from "../views/InkingStroke"; 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 { 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/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/presentationview/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", "")); @@ -270,6 +269,8 @@ export class DocumentOptions { 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 { @@ -427,7 +428,7 @@ export namespace Docs { [DocumentType.PRESELEMENT, { layout: { view: PresElementBox, dataField: defaultDataKey } }], - [DocumentType.HTMLANCHOR, { + [DocumentType.MARKER, { layout: { view: CollectionView, dataField: defaultDataKey }, options: { links: ComputedField.MakeFunction("links(self)") as any, hideLinkButton: true } }], @@ -452,10 +453,6 @@ export namespace Docs { layout: { view: EmptyBox, dataField: defaultDataKey }, options: { links: ComputedField.MakeFunction("links(self)") as any } }], - [DocumentType.TEXTANCHOR, { - layout: { view: EmptyBox, dataField: defaultDataKey }, - options: { targetDropAction: "move", links: ComputedField.MakeFunction("links(self)") as any, hideLinkButton: true } - }] ]); const suffix = "Proto"; @@ -554,84 +551,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 @@ -659,8 +578,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>(); @@ -668,18 +589,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.TEXTANCHOR, DocumentType.LABEL].includes(viewDoc.type as any) && DocUtils.MakeLinkToActiveAudio(() => viewDoc); + ![DocumentType.LINK, DocumentType.MARKER, DocumentType.LABEL].includes(viewDoc.type as any) && DocUtils.MakeLinkToActiveAudio(() => viewDoc); - !Doc.IsSystem(dataDoc) && ![DocumentType.HTMLANCHOR, DocumentType.KVP, DocumentType.LINK, DocumentType.LINKANCHOR, DocumentType.TEXTANCHOR].includes(proto.type as any) && + !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 }); } @@ -693,11 +616,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 = {}) { @@ -713,7 +636,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 }); } @@ -772,25 +695,35 @@ 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); + const width = options._width || undefined; + const height = options._height || undefined; + const nwid = options._nativeWidth || undefined; + const nhght = options._nativeHeight || undefined; + if (!nhght && width && height && nwid) options._nativeHeight = Number(nwid) * Number(height) / Number(width); + 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); + const width = options._width || undefined; + const height = options._height || undefined; + const nwid = options._nativeWidth || undefined; + const nhght = options._nativeHeight || undefined; + if (!nhght && width && height && nwid) options._nativeHeight = Number(nwid) * Number(height) / Number(width); + return InstanceFromProto(Prototypes.get(DocumentType.WEB), url ? new WebField(url) : undefined, options); } export function HtmlDocument(html: string, options: DocumentOptions = {}) { @@ -801,17 +734,22 @@ 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.TEXTANCHOR), 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.HTMLANCHOR), new List(documents), options, id); + return InstanceFromProto(Prototypes.get(DocumentType.MARKER), new List(documents), options, id); } export function PileDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { @@ -838,8 +776,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) { @@ -889,8 +827,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); } @@ -1113,8 +1051,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, @@ -1329,16 +1267,12 @@ export namespace DocUtils { d._timecodeToShow = undefined; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection }); }); - if (x !== undefined && y !== undefined) { - const newCollection = Docs.Create.PileDocument(docList, { title: "pileup", x: x - 55, y: y - 55, _width: 110, _height: 100, _overflow: "visible" }); - newCollection.x = NumCast(newCollection.x) + NumCast(newCollection._width) / 2 - 55; - newCollection.y = NumCast(newCollection.y) + NumCast(newCollection._height) / 2 - 55; - newCollection._width = newCollection._height = 110; - //newCollection.borderRounding = "40px"; - newCollection._jitterRotation = 10; - newCollection._backgroundColor = "gray"; - return newCollection; - } + const newCollection = Docs.Create.PileDocument(docList, { title: "pileup", x: (x || 0) - 55, y: (y || 0) - 55, _width: 110, _height: 100, _overflow: "visible" }); + newCollection.x = NumCast(newCollection.x) + NumCast(newCollection._width) / 2 - 55; + newCollection.y = NumCast(newCollection.y) + NumCast(newCollection._height) / 2 - 55; + newCollection._width = newCollection._height = 110; + newCollection._jitterRotation = 10; + return newCollection; } export function LeavePushpin(doc: Doc, annotationField: string) { @@ -1436,4 +1370,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/goldenLayout.js b/src/client/goldenLayout.js index 9cfea7f3f..238f1ac0a 100644 --- a/src/client/goldenLayout.js +++ b/src/client/goldenLayout.js @@ -82,16 +82,16 @@ return target; }; - /** - * This is based on Paul Irish's shim, but looks quite odd in comparison. Why? - * Because - * a) it shouldn't affect the global requestAnimationFrame function - * b) it shouldn't pass on the time that has passed - * - * @param {Function} fn - * - * @returns {void} - */ + /** + * This is based on Paul Irish's shim, but looks quite odd in comparison. Why? + * Because + * a) it shouldn't affect the global requestAnimationFrame function + * b) it shouldn't pass on the time that has passed + * + * @param {Function} fn + * + * @returns {void} + */ lm.utils.animFrame = function (fn) { return (window.requestAnimationFrame || window.webkitRequestAnimationFrame || @@ -178,16 +178,16 @@ .replace('.', ''); }; - /** - * A basic XSS filter. It is ultimately up to the - * implementing developer to make sure their particular - * applications and usecases are save from cross site scripting attacks - * - * @param {String} input - * @param {Boolean} keepTags - * - * @returns {String} filtered input - */ + /** + * A basic XSS filter. It is ultimately up to the + * implementing developer to make sure their particular + * applications and usecases are save from cross site scripting attacks + * + * @param {String} input + * @param {Boolean} keepTags + * + * @returns {String} filtered input + */ lm.utils.filterXss = function (input, keepTags) { var output = input @@ -206,41 +206,41 @@ } }; - /** - * Removes html tags from a string - * - * @param {String} input - * - * @returns {String} input without tags - */ + /** + * Removes html tags from a string + * + * @param {String} input + * + * @returns {String} input without tags + */ lm.utils.stripTags = function (input) { return $.trim(input.replace(/(<([^>]+)>)/ig, '')); }; - /** - * A generic and very fast EventEmitter - * implementation. On top of emitting the - * actual event it emits an - * - * lm.utils.EventEmitter.ALL_EVENT - * - * event for every event triggered. This allows - * to hook into it and proxy events forwards - * - * @constructor - */ + /** + * A generic and very fast EventEmitter + * implementation. On top of emitting the + * actual event it emits an + * + * lm.utils.EventEmitter.ALL_EVENT + * + * event for every event triggered. This allows + * to hook into it and proxy events forwards + * + * @constructor + */ lm.utils.EventEmitter = function () { this._mSubscriptions = {}; this._mSubscriptions[lm.utils.EventEmitter.ALL_EVENT] = []; - /** - * Listen for events - * - * @param {String} sEvent The name of the event to listen to - * @param {Function} fCallback The callback to execute when the event occurs - * @param {[Object]} oContext The value of the this pointer within the callback function - * - * @returns {void} - */ + /** + * Listen for events + * + * @param {String} sEvent The name of the event to listen to + * @param {Function} fCallback The callback to execute when the event occurs + * @param {[Object]} oContext The value of the this pointer within the callback function + * + * @returns {void} + */ this.on = function (sEvent, fCallback, oContext) { if (!lm.utils.isFunction(fCallback)) { throw new Error('Tried to listen to event ' + sEvent + ' with non-function callback ' + fCallback); @@ -253,14 +253,14 @@ this._mSubscriptions[sEvent].push({ fn: fCallback, ctx: oContext }); }; - /** - * Emit an event and notify listeners - * - * @param {String} sEvent The name of the event - * @param {Mixed} various additional arguments that will be passed to the listener - * - * @returns {void} - */ + /** + * Emit an event and notify listeners + * + * @param {String} sEvent The name of the event + * @param {Mixed} various additional arguments that will be passed to the listener + * + * @returns {void} + */ this.emit = function (sEvent) { var i, ctx, args; @@ -286,15 +286,15 @@ } }; - /** - * Removes a listener for an event, or all listeners if no callback and context is provided. - * - * @param {String} sEvent The name of the event - * @param {Function} fCallback The previously registered callback method (optional) - * @param {Object} oContext The previously registered context (optional) - * - * @returns {void} - */ + /** + * Removes a listener for an event, or all listeners if no callback and context is provided. + * + * @param {String} sEvent The name of the event + * @param {Function} fCallback The previously registered callback method (optional) + * @param {Object} oContext The previously registered context (optional) + * + * @returns {void} + */ this.unbind = function (sEvent, fCallback, oContext) { if (!this._mSubscriptions[sEvent]) { throw new Error('No subscribtions to unsubscribe for event ' + sEvent); @@ -318,28 +318,28 @@ } }; - /** - * Alias for unbind - */ + /** + * Alias for unbind + */ this.off = this.unbind; - /** - * Alias for emit - */ + /** + * Alias for emit + */ this.trigger = this.emit; }; - /** - * The name of the event that's triggered for every other event - * - * usage - * - * myEmitter.on( lm.utils.EventEmitter.ALL_EVENT, function( eventName, argsArray ){ - * //do stuff - * }); - * - * @type {String} - */ + /** + * The name of the event that's triggered for every other event + * + * usage + * + * myEmitter.on( lm.utils.EventEmitter.ALL_EVENT, function( eventName, argsArray ){ + * //do stuff + * }); + * + * @type {String} + */ lm.utils.EventEmitter.ALL_EVENT = '__all'; lm.utils.DragListener = function (eElement, nButtonCode) { lm.utils.EventEmitter.call(this); @@ -349,14 +349,14 @@ this._eBody = $(document.body); this._nButtonCode = nButtonCode || 0; - /** - * The delay after which to start the drag in milliseconds - */ + /** + * The delay after which to start the drag in milliseconds + */ this._nDelay = 200; - /** - * The distance the mouse needs to be moved to qualify as a drag - */ + /** + * The distance the mouse needs to be moved to qualify as a drag + */ this._nDistance = 10;//TODO - works better with delay only this._nX = 0; @@ -459,16 +459,16 @@ }; } }); - /** - * The main class that will be exposed as GoldenLayout. - * - * @public - * @constructor - * @param {GoldenLayout config} config - * @param {[DOM element container]} container Can be a jQuery selector string or a Dom element. Defaults to body - * - * @returns {VOID} - */ + /** + * The main class that will be exposed as GoldenLayout. + * + * @public + * @constructor + * @param {GoldenLayout config} config + * @param {[DOM element container]} container Can be a jQuery selector string or a Dom element. Defaults to body + * + * @returns {VOID} + */ lm.LayoutManager = function (config, container) { if (!$ || typeof $.noConflict !== 'function') { @@ -521,59 +521,59 @@ }; }; - /** - * Hook that allows to access private classes - */ + /** + * Hook that allows to access private classes + */ lm.LayoutManager.__lm = lm; - /** - * Takes a GoldenLayout configuration object and - * replaces its keys and values recursively with - * one letter codes - * - * @static - * @public - * @param {Object} config A GoldenLayout config object - * - * @returns {Object} minified config - */ + /** + * Takes a GoldenLayout configuration object and + * replaces its keys and values recursively with + * one letter codes + * + * @static + * @public + * @param {Object} config A GoldenLayout config object + * + * @returns {Object} minified config + */ lm.LayoutManager.minifyConfig = function (config) { return (new lm.utils.ConfigMinifier()).minifyConfig(config); }; - /** - * Takes a configuration Object that was previously minified - * using minifyConfig and returns its original version - * - * @static - * @public - * @param {Object} minifiedConfig - * - * @returns {Object} the original configuration - */ + /** + * Takes a configuration Object that was previously minified + * using minifyConfig and returns its original version + * + * @static + * @public + * @param {Object} minifiedConfig + * + * @returns {Object} the original configuration + */ lm.LayoutManager.unminifyConfig = function (config) { return (new lm.utils.ConfigMinifier()).unminifyConfig(config); }; lm.utils.copy(lm.LayoutManager.prototype, { - /** - * Register a component with the layout manager. If a configuration node - * of type component is reached it will look up componentName and create the - * associated component - * - * { - * type: "component", - * componentName: "EquityNewsFeed", - * componentState: { "feedTopic": "us-bluechips" } - * } - * - * @public - * @param {String} name - * @param {Function} constructor - * - * @returns {void} - */ + /** + * Register a component with the layout manager. If a configuration node + * of type component is reached it will look up componentName and create the + * associated component + * + * { + * type: "component", + * componentName: "EquityNewsFeed", + * componentState: { "feedTopic": "us-bluechips" } + * } + * + * @public + * @param {String} name + * @param {Function} constructor + * + * @returns {void} + */ registerComponent: function (name, constructor) { if (typeof constructor !== 'function') { throw new Error('Please register a constructor function'); @@ -586,12 +586,12 @@ this._components[name] = constructor; }, - /** - * Creates a layout configuration object based on the the current state - * - * @public - * @returns {Object} GoldenLayout configuration - */ + /** + * Creates a layout configuration object based on the the current state + * + * @public + * @returns {Object} GoldenLayout configuration + */ toConfig: function (root) { var config, next, i; @@ -603,18 +603,18 @@ throw new Error('Root must be a ContentItem'); } - /* - * settings & labels - */ + /* + * settings & labels + */ config = { settings: lm.utils.copy({}, this.config.settings), dimensions: lm.utils.copy({}, this.config.dimensions), labels: lm.utils.copy({}, this.config.labels) }; - /* - * Content - */ + /* + * Content + */ config.content = []; next = function (configNode, item) { var key, i; @@ -641,30 +641,30 @@ next(config, this.root); } - /* - * Retrieve config for subwindows - */ + /* + * Retrieve config for subwindows + */ this._$reconcilePopoutWindows(); config.openPopouts = []; for (i = 0; i < this.openPopouts.length; i++) { config.openPopouts.push(this.openPopouts[i].toConfig()); } - /* - * Add maximised item - */ + /* + * Add maximised item + */ config.maximisedItemId = this._maximisedItem ? '__glMaximised' : null; return config; }, - /** - * Returns a previously registered component - * - * @public - * @param {String} name The name used - * - * @returns {Function} - */ + /** + * Returns a previously registered component + * + * @public + * @param {String} name The name used + * + * @returns {Function} + */ getComponent: function (name) { if (this._components[name] === undefined) { throw new lm.errors.ConfigurationError('Unknown component "' + name + '"'); @@ -673,44 +673,44 @@ return this._components[name]; }, - /** - * Creates the actual layout. Must be called after all initial components - * are registered. Recurses through the configuration and sets up - * the item tree. - * - * If called before the document is ready it adds itself as a listener - * to the document.ready event - * - * @public - * - * @returns {void} - */ + /** + * Creates the actual layout. Must be called after all initial components + * are registered. Recurses through the configuration and sets up + * the item tree. + * + * If called before the document is ready it adds itself as a listener + * to the document.ready event + * + * @public + * + * @returns {void} + */ init: function () { - /** - * Create the popout windows straight away. If popouts are blocked - * an error is thrown on the same 'thread' rather than a timeout and can - * be caught. This also prevents any further initilisation from taking place. - */ + /** + * Create the popout windows straight away. If popouts are blocked + * an error is thrown on the same 'thread' rather than a timeout and can + * be caught. This also prevents any further initilisation from taking place. + */ if (this._subWindowsCreated === false) { this._createSubWindows(); this._subWindowsCreated = true; } - /** - * If the document isn't ready yet, wait for it. - */ + /** + * If the document isn't ready yet, wait for it. + */ if (document.readyState === 'loading' || document.body === null) { $(document).ready(lm.utils.fnBind(this.init, this)); return; } - /** - * If this is a subwindow, wait a few milliseconds for the original - * page's js calls to be executed, then replace the bodies content - * with GoldenLayout - */ + /** + * If this is a subwindow, wait a few milliseconds for the original + * page's js calls to be executed, then replace the bodies content + * with GoldenLayout + */ if (this.isSubWindow === true && this._creationTimeoutPassed === false) { setTimeout(lm.utils.fnBind(this.init, this), 7); this._creationTimeoutPassed = true; @@ -732,15 +732,15 @@ this.emit('initialised'); }, - /** - * Updates the layout managers size - * - * @public - * @param {[int]} width height in pixels - * @param {[int]} height width in pixels - * - * @returns {void} - */ + /** + * Updates the layout managers size + * + * @public + * @param {[int]} width height in pixels + * @param {[int]} height width in pixels + * + * @returns {void} + */ updateSize: function (width, height) { if (arguments.length === 2) { this.width = width; @@ -763,13 +763,13 @@ } }, - /** - * Destroys the LayoutManager instance itself as well as every ContentItem - * within it. After this is called nothing should be left of the LayoutManager. - * - * @public - * @returns {void} - */ + /** + * Destroys the LayoutManager instance itself as well as every ContentItem + * within it. After this is called nothing should be left of the LayoutManager. + * + * @public + * @returns {void} + */ destroy: function () { if (this.isInitialised === false) { return; @@ -793,16 +793,16 @@ this._dragSources = []; }, - /** - * Recursively creates new item tree structures based on a provided - * ItemConfiguration object - * - * @public - * @param {Object} config ItemConfig - * @param {[ContentItem]} parent The item the newly created item should be a child of - * - * @returns {lm.items.ContentItem} - */ + /** + * Recursively creates new item tree structures based on a provided + * ItemConfiguration object + * + * @public + * @param {Object} config ItemConfig + * @param {[ContentItem]} parent The item the newly created item should be a child of + * + * @returns {lm.items.ContentItem} + */ createContentItem: function (config, parent) { var typeErrorMsg, contentItem; @@ -823,9 +823,9 @@ } - /** - * We add an additional stack around every component that's not within a stack anyways. - */ + /** + * We add an additional stack around every component that's not within a stack anyways. + */ if ( // If this is a component config.type === 'component' && @@ -851,17 +851,17 @@ return contentItem; }, - /** - * Creates a popout window with the specified content and dimensions - * - * @param {Object|lm.itemsAbstractContentItem} configOrContentItem - * @param {[Object]} dimensions A map with width, height, left and top - * @param {[String]} parentId the id of the element this item will be appended to - * when popIn is called - * @param {[Number]} indexInParent The position of this item within its parent element + /** + * Creates a popout window with the specified content and dimensions + * + * @param {Object|lm.itemsAbstractContentItem} configOrContentItem + * @param {[Object]} dimensions A map with width, height, left and top + * @param {[String]} parentId the id of the element this item will be appended to + * when popIn is called + * @param {[Number]} indexInParent The position of this item within its parent element - * @returns {lm.controls.BrowserPopout} - */ + * @returns {lm.controls.BrowserPopout} + */ createPopout: function (configOrContentItem, dimensions, parentId, indexInParent) { var config = configOrContentItem, isItem = configOrContentItem instanceof lm.items.AbstractContentItem, @@ -879,14 +879,14 @@ config = this.toConfig(configOrContentItem).content; parentId = lm.utils.getUniqueId(); - /** - * If the item is the only component within a stack or for some - * other reason the only child of its parent the parent will be destroyed - * when the child is removed. - * - * In order to support this we move up the tree until we find something - * that will remain after the item is being popped out - */ + /** + * If the item is the only component within a stack or for some + * other reason the only child of its parent the parent will be destroyed + * when the child is removed. + * + * In order to support this we move up the tree until we find something + * that will remain after the item is being popped out + */ parent = configOrContentItem.parent; child = configOrContentItem; while (parent.contentItems.length === 1 && !parent.isRoot) { @@ -946,16 +946,16 @@ return browserPopout; }, - /** - * Attaches DragListener to any given DOM element - * and turns it into a way of creating new ContentItems - * by 'dragging' the DOM element into the layout - * - * @param {jQuery DOM element} element - * @param {Object|Function} itemConfig for the new item to be created, or a function which will provide it - * - * @returns {void} - */ + /** + * Attaches DragListener to any given DOM element + * and turns it into a way of creating new ContentItems + * by 'dragging' the DOM element into the layout + * + * @param {jQuery DOM element} element + * @param {Object|Function} itemConfig for the new item to be created, or a function which will provide it + * + * @returns {void} + */ createDragSource: function (element, itemConfig) { this.config.settings.constrainDragToContainer = false; var dragSource = new lm.controls.DragSource($(element), itemConfig, this); @@ -964,17 +964,17 @@ return dragSource; }, - /** - * Programmatically selects an item. This deselects - * the currently selected item, selects the specified item - * and emits a selectionChanged event - * - * @param {lm.item.AbstractContentItem} item# - * @param {[Boolean]} _$silent Wheather to notify the item of its selection - * @event selectionChanged - * - * @returns {VOID} - */ + /** + * Programmatically selects an item. This deselects + * the currently selected item, selects the specified item + * and emits a selectionChanged event + * + * @param {lm.item.AbstractContentItem} item# + * @param {[Boolean]} _$silent Wheather to notify the item of its selection + * @event selectionChanged + * + * @returns {VOID} + */ selectItem: function (item, _$silent) { if (this.config.settings.selectionEnabled !== true) { @@ -998,9 +998,9 @@ this.emit('selectionChanged', item); }, - /************************* - * PACKAGE PRIVATE - *************************/ + /************************* + * PACKAGE PRIVATE + *************************/ _$maximiseItem: function (contentItem) { if (this._maximisedItem !== null) { this._$minimiseItem(this._maximisedItem); @@ -1028,20 +1028,20 @@ this.emit('stateChanged'); }, - /** - * This method is used to get around sandboxed iframe restrictions. - * If 'allow-top-navigation' is not specified in the iframe's 'sandbox' attribute - * (as is the case with codepens) the parent window is forbidden from calling certain - * methods on the child, such as window.close() or setting document.location.href. - * - * This prevented GoldenLayout popouts from popping in in codepens. The fix is to call - * _$closeWindow on the child window's gl instance which (after a timeout to disconnect - * the invoking method from the close call) closes itself. - * - * @packagePrivate - * - * @returns {void} - */ + /** + * This method is used to get around sandboxed iframe restrictions. + * If 'allow-top-navigation' is not specified in the iframe's 'sandbox' attribute + * (as is the case with codepens) the parent window is forbidden from calling certain + * methods on the child, such as window.close() or setting document.location.href. + * + * This prevented GoldenLayout popouts from popping in in codepens. The fix is to call + * _$closeWindow on the child window's gl instance which (after a timeout to disconnect + * the invoking method from the close call) closes itself. + * + * @packagePrivate + * + * @returns {void} + */ _$closeWindow: function () { window.setTimeout(function () { window.close(); @@ -1088,13 +1088,13 @@ var i, area, allContentItems = this._getAllContentItems(); this._itemAreas = []; - /** - * If the last item is dragged out, highlight the entire container size to - * allow to re-drop it. allContentItems[ 0 ] === this.root at this point - * - * Don't include root into the possible drop areas though otherwise since it - * will used for every gap in the layout, e.g. splitters - */ + /** + * If the last item is dragged out, highlight the entire container size to + * allow to re-drop it. allContentItems[ 0 ] === this.root at this point + * + * Don't include root into the possible drop areas though otherwise since it + * will used for every gap in the layout, e.g. splitters + */ if (allContentItems.length === 1) { this._itemAreas.push(this.root._$getArea()); return; @@ -1124,18 +1124,18 @@ } }, - /** - * Takes a contentItem or a configuration and optionally a parent - * item and returns an initialised instance of the contentItem. - * If the contentItem is a function, it is first called - * - * @packagePrivate - * - * @param {lm.items.AbtractContentItem|Object|Function} contentItemOrConfig - * @param {lm.items.AbtractContentItem} parent Only necessary when passing in config - * - * @returns {lm.items.AbtractContentItem} - */ + /** + * Takes a contentItem or a configuration and optionally a parent + * item and returns an initialised instance of the contentItem. + * If the contentItem is a function, it is first called + * + * @packagePrivate + * + * @param {lm.items.AbtractContentItem|Object|Function} contentItemOrConfig + * @param {lm.items.AbtractContentItem} parent Only necessary when passing in config + * + * @returns {lm.items.AbtractContentItem} + */ _$normalizeContentItem: function (contentItemOrConfig, parent) { if (!contentItemOrConfig) { throw new Error('No content item defined'); @@ -1158,15 +1158,15 @@ } }, - /** - * Iterates through the array of open popout windows and removes the ones - * that are effectively closed. This is necessary due to the lack of reliably - * listening for window.close / unload events in a cross browser compatible fashion. - * - * @packagePrivate - * - * @returns {void} - */ + /** + * Iterates through the array of open popout windows and removes the ones + * that are effectively closed. This is necessary due to the lack of reliably + * listening for window.close / unload events in a cross browser compatible fashion. + * + * @packagePrivate + * + * @returns {void} + */ _$reconcilePopoutWindows: function () { var openPopouts = [], i; @@ -1185,17 +1185,17 @@ }, - /*************************** - * PRIVATE - ***************************/ - /** - * Returns a flattened array of all content items, - * regardles of level or type - * - * @private - * - * @returns {void} - */ + /*************************** + * PRIVATE + ***************************/ + /** + * Returns a flattened array of all content items, + * regardles of level or type + * + * @private + * + * @returns {void} + */ _getAllContentItems: function () { var allContentItems = []; @@ -1214,13 +1214,13 @@ return allContentItems; }, - /** - * Binds to DOM/BOM events on init - * - * @private - * - * @returns {void} - */ + /** + * Binds to DOM/BOM events on init + * + * @private + * + * @returns {void} + */ _bindEvents: function () { if (this._isFullPage) { $(window).resize(this._resizeFunction); @@ -1228,27 +1228,27 @@ $(window).on('unload beforeunload', this._unloadFunction); }, - /** - * Debounces resize events - * - * @private - * - * @returns {void} - */ + /** + * Debounces resize events + * + * @private + * + * @returns {void} + */ _onResize: function () { clearTimeout(this._resizeTimeoutId); this._resizeTimeoutId = setTimeout(lm.utils.fnBind(this.updateSize, this), 100); }, - /** - * Extends the default config with the user specific settings and applies - * derivations. Please note that there's a seperate method (AbstractContentItem._extendItemNode) - * that deals with the extension of item configs - * - * @param {Object} config - * @static - * @returns {Object} config - */ + /** + * Extends the default config with the user specific settings and applies + * derivations. Please note that there's a seperate method (AbstractContentItem._extendItemNode) + * that deals with the extension of item configs + * + * @param {Object} config + * @static + * @returns {Object} config + */ _createConfig: function (config) { var windowConfigKey = lm.utils.getQueryStringParam('gl-window'); @@ -1283,14 +1283,14 @@ return config; }, - /** - * This is executed when GoldenLayout detects that it is run - * within a previously opened popout window. - * - * @private - * - * @returns {void} - */ + /** + * This is executed when GoldenLayout detects that it is run + * within a previously opened popout window. + * + * @private + * + * @returns {void} + */ _adjustToWindowMode: function () { var popInButton = $('<div class="lm_popin" title="' + this.config.labels.popin + '">' + '<div class="lm_icon"></div>' + @@ -1310,26 +1310,26 @@ .css('visibility', 'visible') .append(popInButton); - /* - * This seems a bit pointless, but actually causes a reflow/re-evaluation getting around - * slickgrid's "Cannot find stylesheet." bug in chrome - */ + /* + * This seems a bit pointless, but actually causes a reflow/re-evaluation getting around + * slickgrid's "Cannot find stylesheet." bug in chrome + */ var x = document.body.offsetHeight; // jshint ignore:line - /* - * Expose this instance on the window object - * to allow the opening window to interact with - * it - */ + /* + * Expose this instance on the window object + * to allow the opening window to interact with + * it + */ window.__glInstance = this; }, - /** - * Creates Subwindows (if there are any). Throws an error - * if popouts are blocked. - * - * @returns {void} - */ + /** + * Creates Subwindows (if there are any). Throws an error + * if popouts are blocked. + * + * @returns {void} + */ _createSubWindows: function () { var i, popout; @@ -1345,13 +1345,13 @@ } }, - /** - * Determines what element the layout will be created in - * - * @private - * - * @returns {void} - */ + /** + * Determines what element the layout will be created in + * + * @private + * + * @returns {void} + */ _setContainer: function () { var container = $(this.container || 'body'); @@ -1377,13 +1377,13 @@ this.container = container; }, - /** - * Kicks of the initial, recursive creation chain - * - * @param {Object} config GoldenLayout Config - * - * @returns {void} - */ + /** + * Kicks of the initial, recursive creation chain + * + * @param {Object} config GoldenLayout Config + * + * @returns {void} + */ _create: function (config) { var errorMsg; @@ -1410,12 +1410,12 @@ } }, - /** - * Called when the window is closed or the user navigates away - * from the page - * - * @returns {void} - */ + /** + * Called when the window is closed or the user navigates away + * from the page + * + * @returns {void} + */ _onUnload: function () { if (this.config.settings.closePopoutsOnUnload === true) { for (var i = 0; i < this.openPopouts.length; i++) { @@ -1424,11 +1424,11 @@ } }, - /** - * Adjusts the number of columns to be lower to fit the screen and still maintain minItemWidth. - * - * @returns {void} - */ + /** + * Adjusts the number of columns to be lower to fit the screen and still maintain minItemWidth. + * + * @returns {void} + */ _adjustColumnsResponsive: function () { // If there is no min width set, or not content items, do nothing. @@ -1470,21 +1470,21 @@ this._updatingColumnsResponsive = false; }, - /** - * Determines if responsive layout should be used. - * - * @returns {bool} - True if responsive layout should be used; otherwise false. - */ + /** + * Determines if responsive layout should be used. + * + * @returns {bool} - True if responsive layout should be used; otherwise false. + */ _useResponsiveLayout: function () { return this.config.settings && (this.config.settings.responsiveMode == 'always' || (this.config.settings.responsiveMode == 'onload' && this._firstLoad)); }, - /** - * Adds all children of a node to another container recursively. - * @param {object} container - Container to add child content items to. - * @param {object} node - Node to search for content items. - * @returns {void} - */ + /** + * Adds all children of a node to another container recursively. + * @param {object} container - Container to add child content items to. + * @param {object} node - Node to search for content items. + * @returns {void} + */ _addChildContentItemsToContainer: function (container, node) { if (node.type === 'stack') { node.contentItems.forEach(function (item) { @@ -1499,10 +1499,10 @@ } }, - /** - * Finds all the stack containers. - * @returns {array} - The found stack containers. - */ + /** + * Finds all the stack containers. + * @returns {array} - The found stack containers. + */ _findAllStackContainers: function () { var stackContainers = []; this._findAllStackContainersRecursive(stackContainers, this.root); @@ -1510,14 +1510,14 @@ return stackContainers; }, - /** - * Finds all the stack containers. - * - * @param {array} - Set of containers to populate. - * @param {object} - Current node to process. - * - * @returns {void} - */ + /** + * Finds all the stack containers. + * + * @param {array} - Set of containers to populate. + * @param {object} - Current node to process. + * + * @returns {void} + */ _findAllStackContainersRecursive: function (stackContainers, node) { node.contentItems.forEach(lm.utils.fnBind(function (item) { if (item.type == 'stack') { @@ -1530,9 +1530,9 @@ } }); - /** - * Expose the Layoutmanager as the single entrypoint using UMD - */ + /** + * Expose the Layoutmanager as the single entrypoint using UMD + */ (function () { /* global define */ if (typeof define === 'function' && define.amd) { @@ -1567,15 +1567,15 @@ showCloseIcon: true, responsiveMode: 'onload', // Can be onload, always, or none. tabOverlapAllowance: 0, // maximum pixel overlap per tab - reorderOnTabMenuClick: true, + reorderOnTabMenuClick: false, //do not reorder! - horizontal scroll tabControlOffset: 10 }, dimensions: { - borderWidth: 5, + borderWidth: 3, borderGrabWidth: 5, minItemHeight: 10, - minItemWidth: 10, - headerHeight: 20, + minItemWidth: 20, + headerHeight: 27, dragProxyWidth: 300, dragProxyHeight: 200 }, @@ -1611,36 +1611,36 @@ lm.utils.copy(lm.container.ItemContainer.prototype, { - /** - * Get the inner DOM element the container's content - * is intended to live in - * - * @returns {DOM element} - */ + /** + * Get the inner DOM element the container's content + * is intended to live in + * + * @returns {DOM element} + */ getElement: function () { return this._contentElement; }, - /** - * Hide the container. Notifies the containers content first - * and then hides the DOM node. If the container is already hidden - * this should have no effect - * - * @returns {void} - */ + /** + * Hide the container. Notifies the containers content first + * and then hides the DOM node. If the container is already hidden + * this should have no effect + * + * @returns {void} + */ hide: function () { this.emit('hide'); this.isHidden = true; this._element.hide(); }, - /** - * Shows a previously hidden container. Notifies the - * containers content first and then shows the DOM element. - * If the container is already visible this has no effect. - * - * @returns {void} - */ + /** + * Shows a previously hidden container. Notifies the + * containers content first and then shows the DOM element. + * If the container is already visible this has no effect. + * + * @returns {void} + */ show: function () { this.emit('show'); this.isHidden = false; @@ -1651,19 +1651,19 @@ } }, - /** - * Set the size from within the container. Traverses up - * the item tree until it finds a row or column element - * and resizes its items accordingly. - * - * If this container isn't a descendant of a row or column - * it returns false - * @todo Rework!!! - * @param {Number} width The new width in pixel - * @param {Number} height The new height in pixel - * - * @returns {Boolean} resizeSuccesful - */ + /** + * Set the size from within the container. Traverses up + * the item tree until it finds a row or column element + * and resizes its items accordingly. + * + * If this container isn't a descendant of a row or column + * it returns false + * @todo Rework!!! + * @param {Number} width The new width in pixel + * @param {Number} height The new height in pixel + * + * @returns {Boolean} resizeSuccesful + */ setSize: function (width, height) { var rowOrColumn = this.parent, rowOrColumnChild = this, @@ -1679,9 +1679,9 @@ rowOrColumn = rowOrColumn.parent; - /** - * No row or column has been found - */ + /** + * No row or column has been found + */ if (rowOrColumn.isRoot) { return false; } @@ -1707,13 +1707,13 @@ return true; }, - /** - * Closes the container if it is closable. Can be called by - * both the component within at as well as the contentItem containing - * it. Emits a close event before the container itself is closed. - * - * @returns {void} - */ + /** + * Closes the container if it is closable. Can be called by + * both the component within at as well as the contentItem containing + * it. Emits a close event before the container itself is closed. + * + * @returns {void} + */ close: function () { if (this._config.isClosable) { this.emit('close'); @@ -1721,55 +1721,55 @@ } }, - /** - * Returns the current state object - * - * @returns {Object} state - */ + /** + * Returns the current state object + * + * @returns {Object} state + */ getState: function () { return this._config.componentState; }, - /** - * Merges the provided state into the current one - * - * @param {Object} state - * - * @returns {void} - */ + /** + * Merges the provided state into the current one + * + * @param {Object} state + * + * @returns {void} + */ extendState: function (state) { this.setState($.extend(true, this.getState(), state)); }, - /** - * Notifies the layout manager of a stateupdate - * - * @param {serialisable} state - */ + /** + * Notifies the layout manager of a stateupdate + * + * @param {serialisable} state + */ setState: function (state) { this._config.componentState = state; this.parent.emitBubblingEvent('stateChanged'); }, - /** - * Set's the components title - * - * @param {String} title - */ + /** + * Set's the components title + * + * @param {String} title + */ setTitle: function (title) { this.parent.setTitle(title); }, - /** - * Set's the containers size. Called by the container's component. - * To set the size programmatically from within the container please - * use the public setSize method - * - * @param {[Int]} width in px - * @param {[Int]} height in px - * - * @returns {void} - */ + /** + * Set's the containers size. Called by the container's component. + * To set the size programmatically from within the container please + * use the public setSize method + * + * @param {[Int]} width in px + * @param {[Int]} height in px + * + * @returns {void} + */ _$setSize: function (width, height) { if (width !== this.width || height !== this.height) { this.width = width; @@ -1784,22 +1784,22 @@ } }); - /** - * Pops a content item out into a new browser window. - * This is achieved by - * - * - Creating a new configuration with the content item as root element - * - Serializing and minifying the configuration - * - Opening the current window's URL with the configuration as a GET parameter - * - GoldenLayout when opened in the new window will look for the GET parameter - * and use it instead of the provided configuration - * - * @param {Object} config GoldenLayout item config - * @param {Object} dimensions A map with width, height, top and left - * @param {String} parentId The id of the element the item will be appended to on popIn - * @param {Number} indexInParent The position of this element within its parent - * @param {lm.LayoutManager} layoutManager - */ + /** + * Pops a content item out into a new browser window. + * This is achieved by + * + * - Creating a new configuration with the content item as root element + * - Serializing and minifying the configuration + * - Opening the current window's URL with the configuration as a GET parameter + * - GoldenLayout when opened in the new window will look for the GET parameter + * and use it instead of the provided configuration + * + * @param {Object} config GoldenLayout item config + * @param {Object} dimensions A map with width, height, top and left + * @param {String} parentId The id of the element the item will be appended to on popIn + * @param {Number} indexInParent The position of this element within its parent + * @param {lm.LayoutManager} layoutManager + */ lm.controls.BrowserPopout = function (config, dimensions, parentId, indexInParent, layoutManager) { lm.utils.EventEmitter.call(this); this.isInitialised = false; @@ -1853,10 +1853,10 @@ } }, - /** - * Returns the popped out item to its original position. If the original - * parent isn't available anymore it falls back to the layout's topmost element - */ + /** + * Returns the popped out item to its original position. If the original + * parent isn't available anymore it falls back to the layout's topmost element + */ popIn: function () { var childConfig, parentItem, @@ -1864,22 +1864,22 @@ if (this._parentId) { - /* - * The $.extend call seems a bit pointless, but it's crucial to - * copy the config returned by this.getGlInstance().toConfig() - * onto a new object. Internet Explorer keeps the references - * to objects on the child window, resulting in the following error - * once the child window is closed: - * - * The callee (server [not server application]) is not available and disappeared - */ + /* + * The $.extend call seems a bit pointless, but it's crucial to + * copy the config returned by this.getGlInstance().toConfig() + * onto a new object. Internet Explorer keeps the references + * to objects on the child window, resulting in the following error + * once the child window is closed: + * + * The callee (server [not server application]) is not available and disappeared + */ childConfig = $.extend(true, {}, this.getGlInstance().toConfig()).content[0]; parentItem = this._layoutManager.root.getItemsById(this._parentId)[0]; - /* - * Fallback if parentItem is not available. Either add it to the topmost - * item or make it the topmost item if the layout is empty - */ + /* + * Fallback if parentItem is not available. Either add it to the topmost + * item or make it the topmost item if the layout is empty + */ if (!parentItem) { if (this._layoutManager.root.contentItems.length > 0) { parentItem = this._layoutManager.root.contentItems[0]; @@ -1894,28 +1894,28 @@ this.close(); }, - /** - * Creates the URL and window parameter - * and opens a new window - * - * @private - * - * @returns {void} - */ + /** + * Creates the URL and window parameter + * and opens a new window + * + * @private + * + * @returns {void} + */ _createWindow: function () { var checkReadyInterval, url = this._createUrl(), - /** - * Bogus title to prevent re-usage of existing window with the - * same title. The actual title will be set by the new window's - * GoldenLayout instance if it detects that it is in subWindowMode - */ + /** + * Bogus title to prevent re-usage of existing window with the + * same title. The actual title will be set by the new window's + * GoldenLayout instance if it detects that it is in subWindowMode + */ title = Math.floor(Math.random() * 1000000).toString(36), - /** - * The options as used in the window.open string - */ + /** + * The options as used in the window.open string + */ options = this._serializeWindowOptions({ width: this._dimensions.width, height: this._dimensions.height, @@ -1946,12 +1946,12 @@ .on('load', lm.utils.fnBind(this._positionWindow, this)) .on('unload beforeunload', lm.utils.fnBind(this._onClose, this)); - /** - * Polling the childwindow to find out if GoldenLayout has been initialised - * doesn't seem optimal, but the alternatives - adding a callback to the parent - * window or raising an event on the window object - both would introduce knowledge - * about the parent to the child window which we'd rather avoid - */ + /** + * Polling the childwindow to find out if GoldenLayout has been initialised + * doesn't seem optimal, but the alternatives - adding a callback to the parent + * window or raising an event on the window object - both would introduce knowledge + * about the parent to the child window which we'd rather avoid + */ checkReadyInterval = setInterval(lm.utils.fnBind(function () { if (this._popoutWindow.__glInstance && this._popoutWindow.__glInstance.isInitialised) { this._onInitialised(); @@ -1960,13 +1960,13 @@ }, this), 10); }, - /** - * Serialises a map of key:values to a window options string - * - * @param {Object} windowOptions - * - * @returns {String} serialised window options - */ + /** + * Serialises a map of key:values to a window options string + * + * @param {Object} windowOptions + * + * @returns {String} serialised window options + */ _serializeWindowOptions: function (windowOptions) { var windowOptionsString = [], key; @@ -1977,12 +1977,12 @@ return windowOptionsString.join(','); }, - /** - * Creates the URL for the new window, including the - * config GET parameter - * - * @returns {String} URL - */ + /** + * Creates the URL for the new window, including the + * config GET parameter + * + * @returns {String} URL + */ _createUrl: function () { var config = { content: this._config }, storageKey = 'gl-window-config-' + lm.utils.getUniqueId(), @@ -2008,57 +2008,57 @@ } }, - /** - * Move the newly created window roughly to - * where the component used to be. - * - * @private - * - * @returns {void} - */ + /** + * Move the newly created window roughly to + * where the component used to be. + * + * @private + * + * @returns {void} + */ _positionWindow: function () { this._popoutWindow.moveTo(this._dimensions.left, this._dimensions.top); this._popoutWindow.focus(); }, - /** - * Callback when the new window is opened and the GoldenLayout instance - * within it is initialised - * - * @returns {void} - */ + /** + * Callback when the new window is opened and the GoldenLayout instance + * within it is initialised + * + * @returns {void} + */ _onInitialised: function () { this.isInitialised = true; this.getGlInstance().on('popIn', this.popIn, this); this.emit('initialised'); }, - /** - * Invoked 50ms after the window unload event - * - * @private - * - * @returns {void} - */ + /** + * Invoked 50ms after the window unload event + * + * @private + * + * @returns {void} + */ _onClose: function () { setTimeout(lm.utils.fnBind(this.emit, this, ['closed']), 50); } }); - /** - * This class creates a temporary container - * for the component whilst it is being dragged - * and handles drag events - * - * @constructor - * @private - * - * @param {Number} x The initial x position - * @param {Number} y The initial y position - * @param {lm.utils.DragListener} dragListener - * @param {lm.LayoutManager} layoutManager - * @param {lm.item.AbstractContentItem} contentItem - * @param {lm.item.AbstractContentItem} originalParent - */ + /** + * This class creates a temporary container + * for the component whilst it is being dragged + * and handles drag events + * + * @constructor + * @private + * + * @param {Number} x The initial x position + * @param {Number} y The initial y position + * @param {lm.utils.DragListener} dragListener + * @param {lm.LayoutManager} layoutManager + * @param {lm.item.AbstractContentItem} contentItem + * @param {lm.item.AbstractContentItem} originalParent + */ lm.controls.DragProxy = function (x, y, dragListener, layoutManager, contentItem, originalParent) { lm.utils.EventEmitter.call(this); @@ -2120,19 +2120,19 @@ lm.utils.copy(lm.controls.DragProxy.prototype, { - /** - * Callback on every mouseMove event during a drag. Determines if the drag is - * still within the valid drag area and calls the layoutManager to highlight the - * current drop area - * - * @param {Number} offsetX The difference from the original x position in px - * @param {Number} offsetY The difference from the original y position in px - * @param {jQuery DOM event} event - * - * @private - * - * @returns {void} - */ + /** + * Callback on every mouseMove event during a drag. Determines if the drag is + * still within the valid drag area and calls the layoutManager to highlight the + * current drop area + * + * @param {Number} offsetX The difference from the original x position in px + * @param {Number} offsetY The difference from the original y position in px + * @param {jQuery DOM event} event + * + * @private + * + * @returns {void} + */ _onDrag: function (offsetX, offsetY, event) { event = event.originalEvent && event.originalEvent.touches ? event.originalEvent.touches[0] : event; @@ -2148,16 +2148,16 @@ this._setDropPosition(x, y); }, - /** - * Sets the target position, highlighting the appropriate area - * - * @param {Number} x The x position in px - * @param {Number} y The y position in px - * - * @private - * - * @returns {void} - */ + /** + * Sets the target position, highlighting the appropriate area + * + * @param {Number} x The x position in px + * @param {Number} y The y position in px + * + * @private + * + * @returns {void} + */ _setDropPosition: function (x, y) { this.element.css({ left: x, top: y }); this._area = this._layoutManager._$getArea(x, y); @@ -2168,43 +2168,43 @@ } }, - /** - * Callback when the drag has finished. Determines the drop area - * and adds the child to it - * - * @private - * - * @returns {void} - */ + /** + * Callback when the drag has finished. Determines the drop area + * and adds the child to it + * + * @private + * + * @returns {void} + */ _onDrop: function () { this._layoutManager.dropTargetIndicator.hide(); - /* - * Valid drop area found - */ + /* + * Valid drop area found + */ if (this._area !== null) { this._area.contentItem._$onDrop(this._contentItem, this._area); - /** - * No valid drop area available at present, but one has been found before. - * Use it - */ + /** + * No valid drop area available at present, but one has been found before. + * Use it + */ } else if (this._lastValidArea !== null) { this._lastValidArea.contentItem._$onDrop(this._contentItem, this._lastValidArea); - /** - * No valid drop area found during the duration of the drag. Return - * content item to its original position if a original parent is provided. - * (Which is not the case if the drag had been initiated by createDragSource) - */ + /** + * No valid drop area found during the duration of the drag. Return + * content item to its original position if a original parent is provided. + * (Which is not the case if the drag had been initiated by createDragSource) + */ } else if (this._originalParent) { this._originalParent.addChild(this._contentItem); - /** - * The drag didn't ultimately end up with adding the content item to - * any container. In order to ensure clean up happens, destroy the - * content item. - */ + /** + * The drag didn't ultimately end up with adding the content item to + * any container. In order to ensure clean up happens, destroy the + * content item. + */ } else { this._contentItem._$destroy(); } @@ -2215,18 +2215,18 @@ this._layoutManager.emit('itemDropped', this._contentItem); }, - /** - * Removes the item from its original position within the tree - * - * @private - * - * @returns {void} - */ + /** + * Removes the item from its original position within the tree + * + * @private + * + * @returns {void} + */ _updateTree: function () { - /** - * parent is null if the drag had been initiated by a external drag source - */ + /** + * parent is null if the drag had been initiated by a external drag source + */ if (this._contentItem.parent) { this._contentItem.parent.removeChild(this._contentItem, true); } @@ -2234,13 +2234,13 @@ this._contentItem._$setParent(this); }, - /** - * Updates the Drag Proxie's dimensions - * - * @private - * - * @returns {void} - */ + /** + * Updates the Drag Proxie's dimensions + * + * @private + * + * @returns {void} + */ _setDimensions: function () { var dimensions = this._layoutManager.config.dimensions, width = dimensions.dragProxyWidth, @@ -2259,16 +2259,16 @@ } }); - /** - * Allows for any DOM item to create a component on drag - * start tobe dragged into the Layout - * - * @param {jQuery element} element - * @param {Object} itemConfig the configuration for the contentItem that will be created - * @param {LayoutManager} layoutManager - * - * @constructor - */ + /** + * Allows for any DOM item to create a component on drag + * start tobe dragged into the Layout + * + * @param {jQuery element} element + * @param {Object} itemConfig the configuration for the contentItem that will be created + * @param {LayoutManager} layoutManager + * + * @constructor + */ lm.controls.DragSource = function (element, itemConfig, layoutManager) { this._element = element; this._itemConfig = itemConfig; @@ -2280,11 +2280,11 @@ lm.utils.copy(lm.controls.DragSource.prototype, { - /** - * Called initially and after every drag - * - * @returns {void} - */ + /** + * Called initially and after every drag + * + * @returns {void} + */ _createDragListener: function () { if (this._dragListener !== null) { this._dragListener.destroy(); @@ -2306,14 +2306,14 @@ } }, - /** - * Callback for the DragListener's dragStart event - * - * @param {int} x the x position of the mouse on dragStart - * @param {int} y the x position of the mouse on dragStart - * - * @returns {void} - */ + /** + * Callback for the DragListener's dragStart event + * + * @param {int} x the x position of the mouse on dragStart + * @param {int} y the x position of the mouse on dragStart + * + * @returns {void} + */ _onDragStart: function (x, y) { var itemConfig = this._itemConfig; if (lm.utils.isFunction(itemConfig)) { @@ -2355,12 +2355,12 @@ this.element.hide(); } }); - /** - * This class represents a header above a Stack ContentItem. - * - * @param {lm.LayoutManager} layoutManager - * @param {lm.item.AbstractContentItem} parent - */ + /** + * This class represents a header above a Stack ContentItem. + * + * @param {lm.LayoutManager} layoutManager + * @param {lm.item.AbstractContentItem} parent + */ lm.controls.Header = function (layoutManager, parent) { lm.utils.EventEmitter.call(this); @@ -2390,24 +2390,24 @@ this._createControls(); }; + // '<ul class="lm_tabdropdown_list"></ul>', lm.controls.Header._template = [ '<div class="lm_header">', '<ul class="lm_tabs"></ul>', '<ul class="lm_controls"></ul>', - '<ul class="lm_tabdropdown_list"></ul>', '</div>' ].join(''); lm.utils.copy(lm.controls.Header.prototype, { - /** - * Creates a new tab and associates it with a contentItem - * - * @param {lm.item.AbstractContentItem} contentItem - * @param {Integer} index The position of the tab - * - * @returns {void} - */ + /** + * Creates a new tab and associates it with a contentItem + * + * @param {lm.item.AbstractContentItem} contentItem + * @param {Integer} index The position of the tab + * + * @returns {void} + */ createTab: function (contentItem, index) { var tab, i; @@ -2441,13 +2441,13 @@ this._updateTabSizes(); }, - /** - * Finds a tab based on the contentItem its associated with and removes it. - * - * @param {lm.item.AbstractContentItem} contentItem - * - * @returns {void} - */ + /** + * Finds a tab based on the contentItem its associated with and removes it. + * + * @param {lm.item.AbstractContentItem} contentItem + * + * @returns {void} + */ removeTab: function (contentItem) { for (var i = 0; i < this.tabs.length; i++) { if (this.tabs[i].contentItem === contentItem) { @@ -2460,11 +2460,11 @@ throw new Error('contentItem is not controlled by this header'); }, - /** - * The programmatical equivalent of clicking a Tab. - * - * @param {lm.item.AbstractContentItem} contentItem - */ + /** + * The programmatical equivalent of clicking a Tab. + * + * @param {lm.item.AbstractContentItem} contentItem + */ setActiveContentItem: function (contentItem) { var i, j, isActive, activeTab; @@ -2477,32 +2477,33 @@ } } - if (this.layoutManager.config.settings.reorderOnTabMenuClick) { - /** - * If the tab selected was in the dropdown, move everything down one to make way for this one to be the first. - * This will make sure the most used tabs stay visible. - */ - if (this._lastVisibleTabIndex !== -1 && this.parent.config.activeItemIndex > this._lastVisibleTabIndex) { - activeTab = this.tabs[this.parent.config.activeItemIndex]; - for (j = this.parent.config.activeItemIndex; j > 0; j--) { - this.tabs[j] = this.tabs[j - 1]; - } - this.tabs[0] = activeTab; - this.parent.config.activeItemIndex = 0; - } - } + // glr: removed for new tab manager + // if (this.layoutManager.config.settings.reorderOnTabMenuClick) { + // /** + // * If the tab selected was in the dropdown, move everything down one to make way for this one to be the first. + // * This will make sure the most used tabs stay visible. + // */ + // if (this._lastVisibleTabIndex !== -1 && this.parent.config.activeItemIndex > this._lastVisibleTabIndex) { + // activeTab = this.tabs[this.parent.config.activeItemIndex]; + // for (j = this.parent.config.activeItemIndex; j > 0; j--) { + // this.tabs[j] = this.tabs[j - 1]; + // } + // this.tabs[0] = activeTab; + // this.parent.config.activeItemIndex = 0; + // } + // } this._updateTabSizes(); this.parent.emitBubblingEvent('stateChanged'); }, - /** - * Programmatically operate with header position. - * - * @param {string} position one of ('top','left','right','bottom') to set or empty to get it. - * - * @returns {string} previous header position - */ + /** + * Programmatically operate with header position. + * + * @param {string} position one of ('top','left','right','bottom') to set or empty to get it. + * + * @returns {string} previous header position + */ position: function (position) { var previous = this.parent._header.show; if (previous && !this.parent._side) @@ -2514,14 +2515,14 @@ return previous; }, - /** - * Programmatically set closability. - * - * @package private - * @param {Boolean} isClosable Whether to enable/disable closability. - * - * @returns {Boolean} Whether the action was successful - */ + /** + * Programmatically set closability. + * + * @package private + * @param {Boolean} isClosable Whether to enable/disable closability. + * + * @returns {Boolean} Whether the action was successful + */ _$setClosable: function (isClosable) { if (this.closeButton && this._isClosable()) { this.closeButton.element[isClosable ? "show" : "hide"](); @@ -2531,13 +2532,13 @@ return false; }, - /** - * Destroys the entire header - * - * @package private - * - * @returns {void} - */ + /** + * Destroys the entire header + * + * @package private + * + * @returns {void} + */ _$destroy: function () { this.emit('destroy', this); @@ -2548,20 +2549,20 @@ this.element.remove(); }, - /** - * get settings from header - * - * @returns {string} when exists - */ + /** + * get settings from header + * + * @returns {string} when exists + */ _getHeaderSetting: function (name) { if (name in this.parent._header) return this.parent._header[name]; }, - /** - * Creates the popout, maximise and close buttons in the header's top right corner - * - * @returns {void} - */ + /** + * Creates the popout, maximise and close buttons in the header's top right corner + * + * @returns {void} + */ _createControls: function () { var closeStack, popout, @@ -2573,26 +2574,26 @@ tabDropdownLabel, showTabDropdown; - /** - * Dropdown to show additional tabs. - */ + /** + * Dropdown to show additional tabs. + */ showTabDropdown = lm.utils.fnBind(this._showAdditionalTabsDropdown, this); tabDropdownLabel = this.layoutManager.config.labels.tabDropdown; - this.tabDropdownButton = new lm.controls.HeaderButton(this, tabDropdownLabel, 'lm_tabdropdown', showTabDropdown); - this.tabDropdownButton.element.hide(); + // this.tabDropdownButton = new lm.controls.HeaderButton(this, tabDropdownLabel, 'lm_tabdropdown', showTabDropdown); + // this.tabDropdownButton.element.hide(); - /** - * Popout control to launch component in new window. - */ + /** + * Popout control to launch component in new window. + */ if (this._getHeaderSetting('popout')) { popout = lm.utils.fnBind(this._onPopoutClick, this); label = this._getHeaderSetting('popout'); new lm.controls.HeaderButton(this, label, 'lm_popout', popout); } - /** - * Maximise control - set the component to the full size of the layout - */ + /** + * Maximise control - set the component to the full size of the layout + */ if (this._getHeaderSetting('maximise')) { maximise = lm.utils.fnBind(this.parent.toggleMaximise, this.parent); maximiseLabel = this._getHeaderSetting('maximise'); @@ -2608,9 +2609,9 @@ }); } - /** - * Close button - */ + /** + * Close button + */ if (this._isClosable()) { closeStack = lm.utils.fnBind(this.parent.remove, this.parent); label = this._getHeaderSetting('close'); @@ -2618,30 +2619,30 @@ } }, - /** - * Shows drop down for additional tabs when there are too many to display. - * - * @returns {void} - */ + /** + * Shows drop down for additional tabs when there are too many to display. + * + * @returns {void} + */ _showAdditionalTabsDropdown: function () { this.tabDropdownContainer.show(); }, - /** - * Hides drop down for additional tabs when there are too many to display. - * - * @returns {void} - */ + /** + * Hides drop down for additional tabs when there are too many to display. + * + * @returns {void} + */ _hideAdditionalTabsDropdown: function (e) { this.tabDropdownContainer.hide(); }, - /** - * Checks whether the header is closable based on the parent config and - * the global config. - * - * @returns {Boolean} Whether the header is closable. - */ + /** + * Checks whether the header is closable based on the parent config and + * the global config. + * + * @returns {Boolean} Whether the header is closable. + */ _isClosable: function () { return this.parent.config.isClosable && this.layoutManager.config.settings.showCloseIcon; }, @@ -2655,32 +2656,29 @@ }, - /** - * Invoked when the header's background is clicked (not it's tabs or controls) - * - * @param {jQuery DOM event} event - * - * @returns {void} - */ + /** + * Invoked when the header's background is clicked (not it's tabs or controls) + * + * @param {jQuery DOM event} event + * + * @returns {void} + */ _onHeaderClick: function (event) { if (event.target === this.element[0]) { this.parent.select(); } }, - /** - * Pushes the tabs to the tab dropdown if the available space is not sufficient - * - * @returns {void} - */ + /** + * Pushes the tabs to the tab dropdown if the available space is not sufficient + * + * @returns {void} + */ _updateTabSizes: function (showTabMenu) { if (this.tabs.length === 0) { return; } - //Show the menu based on function argument - this.tabDropdownButton.element.toggle(showTabMenu === true); - var size = function (val) { return val ? 'width' : 'height'; }; @@ -2835,14 +2833,14 @@ } }); - /** - * Represents an individual tab within a Stack's header - * - * @param {lm.controls.Header} header - * @param {lm.items.AbstractContentItem} contentItem - * - * @constructor - */ + /** + * Represents an individual tab within a Stack's header + * + * @param {lm.controls.Header} header + * @param {lm.items.AbstractContentItem} contentItem + * + * @constructor + */ lm.controls.Tab = function (header, contentItem) { this.header = header; this.contentItem = contentItem; @@ -2888,37 +2886,37 @@ } }; - /** - * The tab's html template - * - * @type {String} - */ + /** + * The tab's html template + * + * @type {String} + */ lm.controls.Tab._template = '<li class="lm_tab"><i class="lm_left"></i>' + '<div class="lm_title_wrap"><input class="lm_title"/></div><div class="lm_close_tab"></div>' + '<i class="lm_right"></i></li>'; lm.utils.copy(lm.controls.Tab.prototype, { - /** - * Sets the tab's title to the provided string and sets - * its title attribute to a pure text representation (without - * html tags) of the same string. - * - * @public - * @param {String} title can contain html - */ + /** + * Sets the tab's title to the provided string and sets + * its title attribute to a pure text representation (without + * html tags) of the same string. + * + * @public + * @param {String} title can contain html + */ setTitle: function (title) { this.element.attr('title', lm.utils.stripTags(title)); this.titleElement.html(title); }, - /** - * Sets this tab's active state. To programmatically - * switch tabs, use header.setActiveContentItem( item ) instead. - * - * @public - * @param {Boolean} isActive - */ + /** + * Sets this tab's active state. To programmatically + * switch tabs, use header.setActiveContentItem( item ) instead. + * + * @public + * @param {Boolean} isActive + */ setActive: function (isActive) { if (isActive === this.isActive) { return; @@ -2932,12 +2930,12 @@ } }, - /** - * Destroys the tab - * - * @private - * @returns {void} - */ + /** + * Destroys the tab + * + * @private + * @returns {void} + */ _$destroy: function () { this._layoutManager.emit('tabDestroyed', this); this.element.off('mousedown touchstart', this._onTabClickFn); @@ -2950,15 +2948,15 @@ this.element.remove(); }, - /** - * Callback for the DragListener - * - * @param {Number} x The tabs absolute x position - * @param {Number} y The tabs absolute y position - * - * @private - * @returns {void} - */ + /** + * Callback for the DragListener + * + * @param {Number} x The tabs absolute x position + * @param {Number} y The tabs absolute y position + * + * @private + * @returns {void} + */ _onDragStart: function (x, y) { if (this.contentItem.parent.isMaximised === true) { this.contentItem.parent.toggleMaximise(); @@ -2973,14 +2971,14 @@ ); }, - /** - * Callback when the tab is clicked - * - * @param {jQuery DOM event} event - * - * @private - * @returns {void} - */ + /** + * Callback when the tab is clicked + * + * @param {jQuery DOM event} event + * + * @private + * @returns {void} + */ _onTabClick: function (event) { // left mouse button or tap if (event.button === 0 || event.type === 'touchstart') { @@ -2995,30 +2993,30 @@ } }, - /** - * Callback when the tab's close button is - * clicked - * - * @param {jQuery DOM event} event - * - * @private - * @returns {void} - */ + /** + * Callback when the tab's close button is + * clicked + * + * @param {jQuery DOM event} event + * + * @private + * @returns {void} + */ _onCloseClick: function (event) { event.stopPropagation(); this.header.parent.removeChild(this.contentItem); }, - /** - * Callback to capture tab close button mousedown - * to prevent tab from activating. - * - * @param (jQuery DOM event) event - * - * @private - * @returns {void} - */ + /** + * Callback to capture tab close button mousedown + * to prevent tab from activating. + * + * @param (jQuery DOM event) event + * + * @private + * @returns {void} + */ _onCloseMousedown: function (event) { event.stopPropagation(); } @@ -3040,9 +3038,9 @@ }, transitionElements: function (fromElement, toElement) { - /** - * TODO - This is not quite as cool as expected. Review. - */ + /** + * TODO - This is not quite as cool as expected. Review. + */ return; this._toElement = toElement; this._animationStartTime = lm.utils.now(); @@ -3096,27 +3094,27 @@ lm.errors.ConfigurationError.prototype = new Error(); - /** - * This is the baseclass that all content items inherit from. - * Most methods provide a subset of what the sub-classes do. - * - * It also provides a number of functions for tree traversal - * - * @param {lm.LayoutManager} layoutManager - * @param {item node configuration} config - * @param {lm.item} parent - * - * @event stateChanged - * @event beforeItemDestroyed - * @event itemDestroyed - * @event itemCreated - * @event componentCreated - * @event rowCreated - * @event columnCreated - * @event stackCreated - * - * @constructor - */ + /** + * This is the baseclass that all content items inherit from. + * Most methods provide a subset of what the sub-classes do. + * + * It also provides a number of functions for tree traversal + * + * @param {lm.LayoutManager} layoutManager + * @param {item node configuration} config + * @param {lm.item} parent + * + * @event stateChanged + * @event beforeItemDestroyed + * @event itemDestroyed + * @event itemCreated + * @event componentCreated + * @event rowCreated + * @event columnCreated + * @event stackCreated + * + * @constructor + */ lm.items.AbstractContentItem = function (layoutManager, config, parent) { lm.utils.EventEmitter.call(this); @@ -3146,26 +3144,26 @@ lm.utils.copy(lm.items.AbstractContentItem.prototype, { - /** - * Set the size of the component and its children, called recursively - * - * @abstract - * @returns void - */ + /** + * Set the size of the component and its children, called recursively + * + * @abstract + * @returns void + */ setSize: function () { throw new Error('Abstract Method'); }, - /** - * Calls a method recursively downwards on the tree - * - * @param {String} functionName the name of the function to be called - * @param {[Array]}functionArguments optional arguments that are passed to every function - * @param {[bool]} bottomUp Call methods from bottom to top, defaults to false - * @param {[bool]} skipSelf Don't invoke the method on the class that calls it, defaults to false - * - * @returns {void} - */ + /** + * Calls a method recursively downwards on the tree + * + * @param {String} functionName the name of the function to be called + * @param {[Array]}functionArguments optional arguments that are passed to every function + * @param {[bool]} bottomUp Call methods from bottom to top, defaults to false + * @param {[bool]} skipSelf Don't invoke the method on the class that calls it, defaults to false + * + * @returns {void} + */ callDownwards: function (functionName, functionArguments, bottomUp, skipSelf) { var i; @@ -3180,53 +3178,53 @@ } }, - /** - * Removes a child node (and its children) from the tree - * - * @param {lm.items.ContentItem} contentItem - * - * @returns {void} - */ + /** + * Removes a child node (and its children) from the tree + * + * @param {lm.items.ContentItem} contentItem + * + * @returns {void} + */ removeChild: function (contentItem, keepChild) { - /* - * Get the position of the item that's to be removed within all content items this node contains - */ + /* + * Get the position of the item that's to be removed within all content items this node contains + */ var index = lm.utils.indexOf(contentItem, this.contentItems); - /* - * Make sure the content item to be removed is actually a child of this item - */ + /* + * Make sure the content item to be removed is actually a child of this item + */ if (index === -1) { throw new Error('Can\'t remove child item. Unknown content item'); } - /** - * Call ._$destroy on the content item. This also calls ._$destroy on all its children - */ + /** + * Call ._$destroy on the content item. This also calls ._$destroy on all its children + */ if (keepChild !== true) { this.contentItems[index]._$destroy(); } - /** - * Remove the content item from this nodes array of children - */ + /** + * Remove the content item from this nodes array of children + */ this.contentItems.splice(index, 1); - /** - * Remove the item from the configuration - */ + /** + * Remove the item from the configuration + */ this.config.content.splice(index, 1); - /** - * If this node still contains other content items, adjust their size - */ + /** + * If this node still contains other content items, adjust their size + */ if (this.contentItems.length > 0) { this.callDownwards('setSize'); - /** - * If this was the last content item, remove this node as well - */ + /** + * If this was the last content item, remove this node as well + */ } else if (!(this instanceof lm.items.Root) && this.config.isClosable === true) { const stack = this; const rowOrCol = stack.parent; @@ -3244,14 +3242,14 @@ } }, - /** - * Sets up the tree structure for the newly added child - * The responsibility for the actual DOM manipulations lies - * with the concrete item - * - * @param {lm.items.AbstractContentItem} contentItem - * @param {[Int]} index If omitted item will be appended - */ + /** + * Sets up the tree structure for the newly added child + * The responsibility for the actual DOM manipulations lies + * with the concrete item + * + * @param {lm.items.AbstractContentItem} contentItem + * @param {[Int]} index If omitted item will be appended + */ addChild: function (contentItem, index) { if (index === undefined) { index = this.contentItems.length; @@ -3271,15 +3269,15 @@ } }, - /** - * Replaces oldChild with newChild. This used to use jQuery.replaceWith... which for - * some reason removes all event listeners, so isn't really an option. - * - * @param {lm.item.AbstractContentItem} oldChild - * @param {lm.item.AbstractContentItem} newChild - * - * @returns {void} - */ + /** + * Replaces oldChild with newChild. This used to use jQuery.replaceWith... which for + * some reason removes all event listeners, so isn't really an option. + * + * @param {lm.item.AbstractContentItem} oldChild + * @param {lm.item.AbstractContentItem} newChild + * + * @returns {void} + */ replaceChild: function (oldChild, newChild, _$destroyOldChild) { newChild = this.layoutManager._$normalizeContentItem(newChild); @@ -3293,23 +3291,23 @@ parentNode.replaceChild(newChild.element[0], oldChild.element[0]); - /* - * Optionally destroy the old content item - */ + /* + * Optionally destroy the old content item + */ if (_$destroyOldChild === true) { oldChild.parent = null; oldChild._$destroy(); } - /* - * Wire the new contentItem into the tree - */ + /* + * Wire the new contentItem into the tree + */ this.contentItems[index] = newChild; newChild.parent = this; - /* - * Update tab reference - */ + /* + * Update tab reference + */ if (this.isStack) { this.header.tabs[index].contentItem = newChild; } @@ -3322,33 +3320,33 @@ this.callDownwards('setSize'); }, - /** - * Convenience method. - * Shorthand for this.parent.removeChild( this ) - * - * @returns {void} - */ + /** + * Convenience method. + * Shorthand for this.parent.removeChild( this ) + * + * @returns {void} + */ remove: function () { this.parent.removeChild(this); }, - /** - * Removes the component from the layout and creates a new - * browser window with the component and its children inside - * - * @returns {lm.controls.BrowserPopout} - */ + /** + * Removes the component from the layout and creates a new + * browser window with the component and its children inside + * + * @returns {lm.controls.BrowserPopout} + */ popout: function () { var browserPopout = this.layoutManager.createPopout(this); this.emitBubblingEvent('stateChanged'); return browserPopout; }, - /** - * Maximises the Item or minimises it if it is already maximised - * - * @returns {void} - */ + /** + * Maximises the Item or minimises it if it is already maximised + * + * @returns {void} + */ toggleMaximise: function (e) { e && e.preventDefault(); if (this.isMaximised === true) { @@ -3361,11 +3359,11 @@ this.emitBubblingEvent('stateChanged'); }, - /** - * Selects the item if it is not already selected - * - * @returns {void} - */ + /** + * Selects the item if it is not already selected + * + * @returns {void} + */ select: function () { if (this.layoutManager.selectedItem !== this) { this.layoutManager.selectItem(this, true); @@ -3373,11 +3371,11 @@ } }, - /** - * De-selects the item if it is selected - * - * @returns {void} - */ + /** + * De-selects the item if it is selected + * + * @returns {void} + */ deselect: function () { if (this.layoutManager.selectedItem === this) { this.layoutManager.selectedItem = null; @@ -3385,28 +3383,28 @@ } }, - /** - * Set this component's title - * - * @public - * @param {String} title - * - * @returns {void} - */ + /** + * Set this component's title + * + * @public + * @param {String} title + * + * @returns {void} + */ setTitle: function (title) { this.config.title = title; this.emit('titleChanged', title); this.emit('stateChanged'); }, - /** - * Checks whether a provided id is present - * - * @public - * @param {String} id - * - * @returns {Boolean} isPresent - */ + /** + * Checks whether a provided id is present + * + * @public + * @param {String} id + * + * @returns {Boolean} isPresent + */ hasId: function (id) { if (!this.config.id) { return false; @@ -3417,15 +3415,15 @@ } }, - /** - * Adds an id. Adds it as a string if the component doesn't - * have an id yet or creates/uses an array - * - * @public - * @param {String} id - * - * @returns {void} - */ + /** + * Adds an id. Adds it as a string if the component doesn't + * have an id yet or creates/uses an array + * + * @public + * @param {String} id + * + * @returns {void} + */ addId: function (id) { if (this.hasId(id)) { return; @@ -3440,15 +3438,15 @@ } }, - /** - * Removes an existing id. Throws an error - * if the id is not present - * - * @public - * @param {String} id - * - * @returns {void} - */ + /** + * Removes an existing id. Throws an error + * if the id is not present + * + * @public + * @param {String} id + * + * @returns {void} + */ removeId: function (id) { if (!this.hasId(id)) { throw new Error('Id not found'); @@ -3462,9 +3460,9 @@ } }, - /**************************************** - * SELECTOR - ****************************************/ + /**************************************** + * SELECTOR + ****************************************/ getItemsByFilter: function (filter) { var result = [], next = function (contentItem) { @@ -3508,9 +3506,9 @@ return instances; }, - /**************************************** - * PACKAGE PRIVATE - ****************************************/ + /**************************************** + * PACKAGE PRIVATE + ****************************************/ _$getItemsByProperty: function (key, value) { return this.getItemsByFilter(function (item) { return item[key] === value; @@ -3555,11 +3553,11 @@ } }, - /** - * Destroys this item ands its children - * - * @returns {void} - */ + /** + * Destroys this item ands its children + * + * @returns {void} + */ _$destroy: function () { this.emitBubblingEvent('beforeItemDestroyed'); this.callDownwards('_$destroy', [], true, true); @@ -3567,17 +3565,17 @@ this.emitBubblingEvent('itemDestroyed'); }, - /** - * Returns the area the component currently occupies in the format - * - * { - * x1: int - * xy: int - * y1: int - * y2: int - * contentItem: contentItem - * } - */ + /** + * Returns the area the component currently occupies in the format + * + * { + * x1: int + * xy: int + * y1: int + * y2: int + * contentItem: contentItem + * } + */ _$getArea: function (element) { element = element || this.element; @@ -3595,17 +3593,17 @@ }; }, - /** - * The tree of content items is created in two steps: First all content items are instantiated, - * then init is called recursively from top to bottem. This is the basic init function, - * it can be used, extended or overwritten by the content items - * - * Its behaviour depends on the content item - * - * @package private - * - * @returns {void} - */ + /** + * The tree of content items is created in two steps: First all content items are instantiated, + * then init is called recursively from top to bottem. This is the basic init function, + * it can be used, extended or overwritten by the content items + * + * Its behaviour depends on the content item + * + * @package private + * + * @returns {void} + */ _$init: function () { var i; this.setSize(); @@ -3619,26 +3617,26 @@ this.emitBubblingEvent(this.type + 'Created'); }, - /** - * Emit an event that bubbles up the item tree. - * - * @param {String} name The name of the event - * - * @returns {void} - */ + /** + * Emit an event that bubbles up the item tree. + * + * @param {String} name The name of the event + * + * @returns {void} + */ emitBubblingEvent: function (name) { var event = new lm.utils.BubblingEvent(name, this); this.emit(name, event); }, - /** - * Private method, creates all content items for this node at initialisation time - * PLEASE NOTE, please see addChild for adding contentItems add runtime - * @private - * @param {configuration item node} config - * - * @returns {void} - */ + /** + * Private method, creates all content items for this node at initialisation time + * PLEASE NOTE, please see addChild for adding contentItems add runtime + * @private + * @param {configuration item node} config + * + * @returns {void} + */ _createContentItems: function (config) { var oContentItem, i; @@ -3652,13 +3650,13 @@ } }, - /** - * Extends an item configuration node with default settings - * @private - * @param {configuration item node} config - * - * @returns {configuration item node} extended config - */ + /** + * Extends an item configuration node with default settings + * @private + * @param {configuration item node} config + * + * @returns {configuration item node} extended config + */ _extendItemNode: function (config) { for (var key in lm.config.itemDefaultConfig) { @@ -3670,26 +3668,26 @@ return config; }, - /** - * Called for every event on the item tree. Decides whether the event is a bubbling - * event and propagates it to its parent - * - * @param {String} name the name of the event - * @param {lm.utils.BubblingEvent} event - * - * @returns {void} - */ + /** + * Called for every event on the item tree. Decides whether the event is a bubbling + * event and propagates it to its parent + * + * @param {String} name the name of the event + * @param {lm.utils.BubblingEvent} event + * + * @returns {void} + */ _propagateEvent: function (name, event) { if (event instanceof lm.utils.BubblingEvent && event.isPropagationStopped === false && this.isInitialised === true) { - /** - * In some cases (e.g. if an element is created from a DragSource) it - * doesn't have a parent and is not below root. If that's the case - * propagate the bubbling event from the top level of the substree directly - * to the layoutManager - */ + /** + * In some cases (e.g. if an element is created from a DragSource) it + * doesn't have a parent and is not below root. If that's the case + * propagate the bubbling event from the top level of the substree directly + * to the layoutManager + */ if (this.isRoot === false && this.parent) { this.parent.emit.apply(this.parent, Array.prototype.slice.call(arguments, 0)); } else { @@ -3698,16 +3696,16 @@ } }, - /** - * All raw events bubble up to the root element. Some events that - * are propagated to - and emitted by - the layoutManager however are - * only string-based, batched and sanitized to make them more usable - * - * @param {String} name the name of the event - * - * @private - * @returns {void} - */ + /** + * All raw events bubble up to the root element. Some events that + * are propagated to - and emitted by - the layoutManager however are + * only string-based, batched and sanitized to make them more usable + * + * @param {String} name the name of the event + * + * @private + * @returns {void} + */ _scheduleEventPropagationToLayoutManager: function (name, event) { if (lm.utils.indexOf(name, this._throttledEvents) === -1) { this.layoutManager.emit(name, event.origin); @@ -3720,25 +3718,25 @@ }, - /** - * Callback for events scheduled by _scheduleEventPropagationToLayoutManager - * - * @param {String} name the name of the event - * - * @private - * @returns {void} - */ + /** + * Callback for events scheduled by _scheduleEventPropagationToLayoutManager + * + * @param {String} name the name of the event + * + * @private + * @returns {void} + */ _propagateEventToLayoutManager: function (name, event) { this._pendingEventPropagations[name] = false; this.layoutManager.emit(name, event); } }); - /** - * @param {[type]} layoutManager [description] - * @param {[type]} config [description] - * @param {[type]} parent [description] - */ + /** + * @param {[type]} layoutManager [description] + * @param {[type]} config [description] + * @param {[type]} parent [description] + */ lm.items.Component = function (layoutManager, config, parent) { lm.items.AbstractContentItem.call(this, layoutManager, config, parent); @@ -3798,11 +3796,11 @@ lm.items.AbstractContentItem.prototype._$destroy.call(this); }, - /** - * Dragging onto a component directly is not an option - * - * @returns null - */ + /** + * Dragging onto a component directly is not an option + * + * @returns null + */ _$getArea: function () { return null; } @@ -3841,9 +3839,9 @@ this.element.width(width); this.element.height(height); - /* - * Root can be empty - */ + /* + * Root can be empty + */ if (this.contentItems[0]) { this.contentItems[0].element.width(width); this.contentItems[0].element.height(height); @@ -3917,18 +3915,18 @@ lm.utils.copy(lm.items.RowOrColumn.prototype, { - /** - * Add a new contentItem to the Row or Column - * - * @param {lm.item.AbstractContentItem} contentItem - * @param {[int]} index The position of the new item within the Row or Column. - * If no index is provided the item will be added to the end - * @param {[bool]} _$suspendResize If true the items won't be resized. This will leave the item in - * an inconsistent state and is only intended to be used if multiple - * children need to be added in one go and resize is called afterwards - * - * @returns {void} - */ + /** + * Add a new contentItem to the Row or Column + * + * @param {lm.item.AbstractContentItem} contentItem + * @param {[int]} index The position of the new item within the Row or Column. + * If no index is provided the item will be added to the end + * @param {[bool]} _$suspendResize If true the items won't be resized. This will leave the item in + * an inconsistent state and is only intended to be used if multiple + * children need to be added in one go and resize is called afterwards + * + * @returns {void} + */ addChild: function (contentItem, index, _$suspendResize) { var newItemSize, itemSize, i, splitterElement; @@ -3986,14 +3984,14 @@ }, - /** - * Removes a child of this element - * - * @param {lm.items.AbstractContentItem} contentItem - * @param {boolean} keepChild If true the child will be removed, but not destroyed - * - * @returns {void} - */ + /** + * Removes a child of this element + * + * @param {lm.items.AbstractContentItem} contentItem + * @param {boolean} keepChild If true the child will be removed, but not destroyed + * + * @returns {void} + */ removeChild: function (contentItem, keepChild) { var removedItemSize = contentItem.config[this._dimension], index = lm.utils.indexOf(contentItem, this.contentItems), @@ -4005,10 +4003,10 @@ throw new Error('Can\'t remove child. ContentItem is not child of this Row or Column'); } - /** - * Remove the splitter before the item or after if the item happens - * to be the first in the row/column - */ + /** + * Remove the splitter before the item or after if the item happens + * to be the first in the row/column + */ if (this._splitter[splitterIndex]) { this._splitter[splitterIndex]._$destroy(); this._splitter.splice(splitterIndex, 1); @@ -4019,9 +4017,9 @@ if (this.contentItems[i].config.fixed) fixedItemSize += this.contentItems[i].config[this._dimension]; } - /** - * Allocate the space that the removed item occupied to the remaining items - */ + /** + * Allocate the space that the removed item occupied to the remaining items + */ for (i = 0; i < this.contentItems.length; i++) { if (this.contentItems[i].config.fixed) ; @@ -4044,14 +4042,14 @@ } }, - /** - * Replaces a child of this Row or Column with another contentItem - * - * @param {lm.items.AbstractContentItem} oldChild - * @param {lm.items.AbstractContentItem} newChild - * - * @returns {void} - */ + /** + * Replaces a child of this Row or Column with another contentItem + * + * @param {lm.items.AbstractContentItem} oldChild + * @param {lm.items.AbstractContentItem} newChild + * + * @returns {void} + */ replaceChild: function (oldChild, newChild) { var size = oldChild.config[this._dimension]; lm.items.AbstractContentItem.prototype.replaceChild.call(this, oldChild, newChild); @@ -4060,11 +4058,11 @@ this.emitBubblingEvent('stateChanged'); }, - /** - * Called whenever the dimensions of this item or one of its parents change - * - * @returns {void} - */ + /** + * Called whenever the dimensions of this item or one of its parents change + * + * @returns {void} + */ setSize: function () { if (this.contentItems.length > 0) { this._calculateRelativeSizes(); @@ -4074,15 +4072,15 @@ this.emit('resize'); }, - /** - * Invoked recursively by the layout manager. AbstractContentItem.init appends - * the contentItem's DOM elements to the container, RowOrColumn init adds splitters - * in between them - * - * @package private - * @override AbstractContentItem._$init - * @returns {void} - */ + /** + * Invoked recursively by the layout manager. AbstractContentItem.init appends + * the contentItem's DOM elements to the container, RowOrColumn init adds splitters + * in between them + * + * @package private + * @override AbstractContentItem._$init + * @returns {void} + */ _$init: function () { if (this.isInitialised === true) return; @@ -4095,15 +4093,15 @@ } }, - /** - * Turns the relative sizes calculated by _calculateRelativeSizes into - * absolute pixel values and applies them to the children's DOM elements - * - * Assigns additional pixels to counteract Math.floor - * - * @private - * @returns {void} - */ + /** + * Turns the relative sizes calculated by _calculateRelativeSizes into + * absolute pixel values and applies them to the children's DOM elements + * + * Assigns additional pixels to counteract Math.floor + * + * @private + * @returns {void} + */ _setAbsoluteSizes: function () { var i, sizeData = this._calculateAbsoluteSizes(); @@ -4123,10 +4121,10 @@ } }, - /** - * Calculates the absolute sizes of all of the children of this Item. - * @returns {object} - Set with absolute sizes and additional pixels. - */ + /** + * Calculates the absolute sizes of all of the children of this Item. + * @returns {object} - Set with absolute sizes and additional pixels. + */ _calculateAbsoluteSizes: function () { var i, totalSplitterSize = (this.contentItems.length - 1) * this._splitterSize, @@ -4164,27 +4162,27 @@ }; }, - /** - * Calculates the relative sizes of all children of this Item. The logic - * is as follows: - * - * - Add up the total size of all items that have a configured size - * - * - If the total == 100 (check for floating point errors) - * Excellent, job done - * - * - If the total is > 100, - * set the size of items without set dimensions to 1/3 and add this to the total - * set the size off all items so that the total is hundred relative to their original size - * - * - If the total is < 100 - * If there are items without set dimensions, distribute the remainder to 100 evenly between them - * If there are no items without set dimensions, increase all items sizes relative to - * their original size so that they add up to 100 - * - * @private - * @returns {void} - */ + /** + * Calculates the relative sizes of all children of this Item. The logic + * is as follows: + * + * - Add up the total size of all items that have a configured size + * + * - If the total == 100 (check for floating point errors) + * Excellent, job done + * + * - If the total is > 100, + * set the size of items without set dimensions to 1/3 and add this to the total + * set the size off all items so that the total is hundred relative to their original size + * + * - If the total is < 100 + * If there are items without set dimensions, distribute the remainder to 100 evenly between them + * If there are no items without set dimensions, increase all items sizes relative to + * their original size so that they add up to 100 + * + * @private + * @returns {void} + */ _calculateRelativeSizes: function () { var i, @@ -4200,17 +4198,17 @@ } } - /** - * Everything adds up to hundred, all good :-) - */ + /** + * Everything adds up to hundred, all good :-) + */ if (Math.round(total) === 100) { this._respectMinItemWidth(); return; } - /** - * Allocate the remaining size to the items without a set dimension - */ + /** + * Allocate the remaining size to the items without a set dimension + */ if (Math.round(total) < 100 && itemsWithoutSetDimension.length > 0) { for (i = 0; i < itemsWithoutSetDimension.length; i++) { itemsWithoutSetDimension[i].config[dimension] = (100 - total) / itemsWithoutSetDimension.length; @@ -4219,12 +4217,12 @@ return; } - /** - * If the total is > 100, but there are also items without a set dimension left, assing 50 - * as their dimension and add it to the total - * - * This will be reset in the next step - */ + /** + * If the total is > 100, but there are also items without a set dimension left, assing 50 + * as their dimension and add it to the total + * + * This will be reset in the next step + */ if (Math.round(total) > 100) { for (i = 0; i < itemsWithoutSetDimension.length; i++) { itemsWithoutSetDimension[i].config[dimension] = 50; @@ -4232,9 +4230,9 @@ } } - /** - * Set every items size relative to 100 relative to its size to total - */ + /** + * Set every items size relative to 100 relative to its size to total + */ for (i = 0; i < this.contentItems.length; i++) { this.contentItems[i].config[dimension] = (this.contentItems[i].config[dimension] / total) * 100; } @@ -4242,10 +4240,10 @@ this._respectMinItemWidth(); }, - /** - * Adjusts the column widths to respect the dimensions minItemWidth if set. - * @returns {} - */ + /** + * Adjusts the column widths to respect the dimensions minItemWidth if set. + * @returns {} + */ _respectMinItemWidth: function () { var minItemWidth = this.layoutManager.config.dimensions ? (this.layoutManager.config.dimensions.minItemWidth || 0) : 0, sizeData = null, @@ -4266,9 +4264,9 @@ sizeData = this._calculateAbsoluteSizes(); - /** - * Figure out how much we are under the min item size total and how much room we have to use. - */ + /** + * Figure out how much we are under the min item size total and how much room we have to use. + */ for (var i = 0; i < this.contentItems.length; i++) { contentItem = this.contentItems[i]; @@ -4288,16 +4286,16 @@ allEntries.push(entry); } - /** - * If there is nothing under min, or there is not enough over to make up the difference, do nothing. - */ + /** + * If there is nothing under min, or there is not enough over to make up the difference, do nothing. + */ if (totalUnderMin === 0 || totalUnderMin > totalOverMin) { return; } - /** - * Evenly reduce all columns that are over the min item width to make up the difference. - */ + /** + * Evenly reduce all columns that are over the min item width to make up the difference. + */ reducePercent = totalUnderMin / totalOverMin; remainingWidth = totalUnderMin; for (i = 0; i < entriesOverMin.length; i++) { @@ -4307,31 +4305,31 @@ entry.width -= reducedWidth; } - /** - * Take anything remaining from the last item. - */ + /** + * Take anything remaining from the last item. + */ if (remainingWidth !== 0) { allEntries[allEntries.length - 1].width -= remainingWidth; } - /** - * Set every items size relative to 100 relative to its size to total - */ + /** + * Set every items size relative to 100 relative to its size to total + */ for (i = 0; i < this.contentItems.length; i++) { this.contentItems[i].config.width = (allEntries[i].width / sizeData.totalWidth) * 100; } }, - /** - * Instantiates a new lm.controls.Splitter, binds events to it and adds - * it to the array of splitters at the position specified as the index argument - * - * What it doesn't do though is append the splitter to the DOM - * - * @param {Int} index The position of the splitter - * - * @returns {lm.controls.Splitter} - */ + /** + * Instantiates a new lm.controls.Splitter, binds events to it and adds + * it to the array of splitters at the position specified as the index argument + * + * What it doesn't do though is append the splitter to the DOM + * + * @param {Int} index The position of the splitter + * + * @returns {lm.controls.Splitter} + */ _createSplitter: function (index) { var splitter; splitter = new lm.controls.Splitter(this._isColumn, this._splitterSize, this._splitterGrabSize); @@ -4342,16 +4340,16 @@ return splitter; }, - /** - * Locates the instance of lm.controls.Splitter in the array of - * registered splitters and returns a map containing the contentItem - * before and after the splitters, both of which are affected if the - * splitter is moved - * - * @param {lm.controls.Splitter} splitter - * - * @returns {Object} A map of contentItems that the splitter affects - */ + /** + * Locates the instance of lm.controls.Splitter in the array of + * registered splitters and returns a map containing the contentItem + * before and after the splitters, both of which are affected if the + * splitter is moved + * + * @param {lm.controls.Splitter} splitter + * + * @returns {Object} A map of contentItems that the splitter affects + */ _getItemsForSplitter: function (splitter) { var index = lm.utils.indexOf(splitter, this._splitter); @@ -4361,11 +4359,11 @@ }; }, - /** - * Gets the minimum dimensions for the given item configuration array - * @param item - * @private - */ + /** + * Gets the minimum dimensions for the given item configuration array + * @param item + * @private + */ _getMinimumDimensions: function (arr) { var minWidth = 0, minHeight = 0; @@ -4377,14 +4375,14 @@ return { horizontal: minWidth, vertical: minHeight }; }, - /** - * Invoked when a splitter's dragListener fires dragStart. Calculates the splitters - * movement area once (so that it doesn't need calculating on every mousemove event) - * - * @param {lm.controls.Splitter} splitter - * - * @returns {void} - */ + /** + * Invoked when a splitter's dragListener fires dragStart. Calculates the splitters + * movement area once (so that it doesn't need calculating on every mousemove event) + * + * @param {lm.controls.Splitter} splitter + * + * @returns {void} + */ _onSplitterDragStart: function (splitter) { var items = this._getItemsForSplitter(splitter), minSize = this.layoutManager.config.dimensions[this._isColumn ? 'minItemHeight' : 'minItemWidth']; @@ -4400,16 +4398,16 @@ this._splitterMaxPosition = items.after.element[this._dimension]() - (afterMinSize || minSize); }, - /** - * Invoked when a splitter's DragListener fires drag. Updates the splitters DOM position, - * but not the sizes of the elements the splitter controls in order to minimize resize events - * - * @param {lm.controls.Splitter} splitter - * @param {Int} offsetX Relative pixel values to the splitters original position. Can be negative - * @param {Int} offsetY Relative pixel values to the splitters original position. Can be negative - * - * @returns {void} - */ + /** + * Invoked when a splitter's DragListener fires drag. Updates the splitters DOM position, + * but not the sizes of the elements the splitter controls in order to minimize resize events + * + * @param {lm.controls.Splitter} splitter + * @param {Int} offsetX Relative pixel values to the splitters original position. Can be negative + * @param {Int} offsetY Relative pixel values to the splitters original position. Can be negative + * + * @returns {void} + */ _onSplitterDrag: function (splitter, offsetX, offsetY) { var offset = this._isColumn ? offsetY : offsetX; @@ -4419,15 +4417,15 @@ } }, - /** - * Invoked when a splitter's DragListener fires dragStop. Resets the splitters DOM position, - * and applies the new sizes to the elements before and after the splitter and their children - * on the next animation frame - * - * @param {lm.controls.Splitter} splitter - * - * @returns {void} - */ + /** + * Invoked when a splitter's DragListener fires dragStop. Resets the splitters DOM position, + * and applies the new sizes to the elements before and after the splitter and their children + * on the next animation frame + * + * @param {lm.controls.Splitter} splitter + * + * @returns {void} + */ _onSplitterDragStop: function (splitter) { var items = this._getItemsForSplitter(splitter), @@ -4575,13 +4573,13 @@ this.emitBubblingEvent('stateChanged'); }, - /** - * Validates that the stack is still closable or not. If a stack is able - * to close, but has a non closable component added to it, the stack is no - * longer closable until all components are closable. - * - * @returns {void} - */ + /** + * Validates that the stack is still closable or not. If a stack is able + * to close, but has a non closable component added to it, the stack is no + * longer closable until all components are closable. + * + * @returns {void} + */ _$validateClosability: function () { var contentItem, isClosable, @@ -4607,51 +4605,51 @@ }, - /** - * Ok, this one is going to be the tricky one: The user has dropped {contentItem} onto this stack. - * - * It was dropped on either the stacks header or the top, right, bottom or left bit of the content area - * (which one of those is stored in this._dropSegment). Now, if the user has dropped on the header the case - * is relatively clear: We add the item to the existing stack... job done (might be good to have - * tab reordering at some point, but lets not sweat it right now) - * - * If the item was dropped on the content part things are a bit more complicated. If it was dropped on either the - * top or bottom region we need to create a new column and place the items accordingly. - * Unless, of course if the stack is already within a column... in which case we want - * to add the newly created item to the existing column... - * either prepend or append it, depending on wether its top or bottom. - * - * Same thing for rows and left / right drop segments... so in total there are 9 things that can potentially happen - * (left, top, right, bottom) * is child of the right parent (row, column) + header drop - * - * @param {lm.item} contentItem - * - * @returns {void} - */ + /** + * Ok, this one is going to be the tricky one: The user has dropped {contentItem} onto this stack. + * + * It was dropped on either the stacks header or the top, right, bottom or left bit of the content area + * (which one of those is stored in this._dropSegment). Now, if the user has dropped on the header the case + * is relatively clear: We add the item to the existing stack... job done (might be good to have + * tab reordering at some point, but lets not sweat it right now) + * + * If the item was dropped on the content part things are a bit more complicated. If it was dropped on either the + * top or bottom region we need to create a new column and place the items accordingly. + * Unless, of course if the stack is already within a column... in which case we want + * to add the newly created item to the existing column... + * either prepend or append it, depending on wether its top or bottom. + * + * Same thing for rows and left / right drop segments... so in total there are 9 things that can potentially happen + * (left, top, right, bottom) * is child of the right parent (row, column) + header drop + * + * @param {lm.item} contentItem + * + * @returns {void} + */ _$onDrop: function (contentItem) { - /* - * The item was dropped on the header area. Just add it as a child of this stack and - * get the hell out of this logic - */ + /* + * The item was dropped on the header area. Just add it as a child of this stack and + * get the hell out of this logic + */ if (this._dropSegment === 'header') { this._resetHeaderDropZone(); this.addChild(contentItem, this._dropIndex); return; } - /* - * The stack is empty. Let's just add the element. - */ + /* + * The stack is empty. Let's just add the element. + */ if (this._dropSegment === 'body') { this.addChild(contentItem); return; } - /* - * The item was dropped on the top-, left-, bottom- or right- part of the content. Let's - * aggregate some conditions to make the if statements later on more readable - */ + /* + * The item was dropped on the top-, left-, bottom- or right- part of the content. Let's + * aggregate some conditions to make the if statements later on more readable + */ var isVertical = this._dropSegment === 'top' || this._dropSegment === 'bottom', isHorizontal = this._dropSegment === 'left' || this._dropSegment === 'right', insertBefore = this._dropSegment === 'top' || this._dropSegment === 'left', @@ -4662,9 +4660,9 @@ stack, rowOrColumn; - /* - * The content item can be either a component or a stack. If it is a component, wrap it into a stack - */ + /* + * The content item can be either a component or a stack. If it is a component, wrap it into a stack + */ if (contentItem.isComponent) { stack = this.layoutManager.createContentItem({ type: 'stack', @@ -4675,20 +4673,20 @@ contentItem = stack; } - /* - * If the item is dropped on top or bottom of a column or left and right of a row, it's already - * layd out in the correct way. Just add it as a child - */ + /* + * If the item is dropped on top or bottom of a column or left and right of a row, it's already + * layd out in the correct way. Just add it as a child + */ if (hasCorrectParent) { index = lm.utils.indexOf(this, this.parent.contentItems); this.parent.addChild(contentItem, insertBefore ? index : index + 1, true); this.config[dimension] *= 0.5; contentItem.config[dimension] = this.config[dimension]; this.parent.callDownwards('setSize'); - /* - * This handles items that are dropped on top or bottom of a row or left / right of a column. We need - * to create the appropriate contentItem for them to live in - */ + /* + * This handles items that are dropped on top or bottom of a row or left / right of a column. We need + * to create the appropriate contentItem for them to live in + */ } else { type = isVertical ? 'column' : 'row'; rowOrColumn = this.layoutManager.createContentItem({ type: type }, this); @@ -4703,15 +4701,15 @@ } }, - /** - * If the user hovers above the header part of the stack, indicate drop positions for tabs. - * otherwise indicate which segment of the body the dragged item would be dropped on - * - * @param {Int} x Absolute Screen X - * @param {Int} y Absolute Screen Y - * - * @returns {void} - */ + /** + * If the user hovers above the header part of the stack, indicate drop positions for tabs. + * otherwise indicate which segment of the body the dragged item would be dropped on + * + * @param {Int} x Absolute Screen X + * @param {Int} y Absolute Screen Y + * + * @returns {void} + */ _$highlightDropZone: function (x, y) { var segment, area; @@ -4761,17 +4759,17 @@ } }; - /** - * If this Stack is a parent to rows, columns or other stacks only its - * header is a valid dropzone. - */ + /** + * If this Stack is a parent to rows, columns or other stacks only its + * header is a valid dropzone. + */ if (this._activeContentItem && this._activeContentItem.isComponent === false) { return headerArea; } - /** - * Highlight the entire body if the stack is empty - */ + /** + * Highlight the entire body if the stack is empty + */ if (this.contentItems.length === 0) { this._contentAreaDimensions.body = { @@ -4875,7 +4873,7 @@ this.layoutManager.dropTargetIndicator.highlightArea({ x1: headerOffset.left, x2: headerOffset.left + 100, - y1: headerOffset.top + this.header.element.height() - 20, + y1: headerOffset.top + this.header.element.height() - 25, y2: headerOffset.top + this.header.element.height() }); @@ -4971,13 +4969,13 @@ lm.utils.BubblingEvent.prototype.stopPropagation = function () { this.isPropagationStopped = true; }; - /** - * Minifies and unminifies configs by replacing frequent keys - * and values with one letter substitutes. Config options must - * retain array position/index, add new options at the end. - * - * @constructor - */ + /** + * Minifies and unminifies configs by replacing frequent keys + * and values with one letter substitutes. Config options must + * retain array position/index, add new options at the end. + * + * @constructor + */ lm.utils.ConfigMinifier = function () { this._keys = [ 'settings', @@ -5037,154 +5035,154 @@ lm.utils.copy(lm.utils.ConfigMinifier.prototype, { - /** - * Takes a GoldenLayout configuration object and - * replaces its keys and values recursively with - * one letter counterparts - * - * @param {Object} config A GoldenLayout config object - * - * @returns {Object} minified config - */ + /** + * Takes a GoldenLayout configuration object and + * replaces its keys and values recursively with + * one letter counterparts + * + * @param {Object} config A GoldenLayout config object + * + * @returns {Object} minified config + */ minifyConfig: function (config) { var min = {}; this._nextLevel(config, min, '_min'); return min; }, - /** - * Takes a configuration Object that was previously minified - * using minifyConfig and returns its original version - * - * @param {Object} minifiedConfig - * - * @returns {Object} the original configuration - */ + /** + * Takes a configuration Object that was previously minified + * using minifyConfig and returns its original version + * + * @param {Object} minifiedConfig + * + * @returns {Object} the original configuration + */ unminifyConfig: function (minifiedConfig) { var orig = {}; this._nextLevel(minifiedConfig, orig, '_max'); return orig; }, - /** - * Recursive function, called for every level of the config structure - * - * @param {Array|Object} orig - * @param {Array|Object} min - * @param {String} translationFn - * - * @returns {void} - */ + /** + * Recursive function, called for every level of the config structure + * + * @param {Array|Object} orig + * @param {Array|Object} min + * @param {String} translationFn + * + * @returns {void} + */ _nextLevel: function (from, to, translationFn) { var key, minKey; for (key in from) { - /** - * For in returns array indices as keys, so let's cast them to numbers - */ + /** + * For in returns array indices as keys, so let's cast them to numbers + */ if (from instanceof Array) key = parseInt(key, 10); - /** - * In case something has extended Object prototypes - */ + /** + * In case something has extended Object prototypes + */ if (!from.hasOwnProperty(key)) continue; - /** - * Translate the key to a one letter substitute - */ + /** + * Translate the key to a one letter substitute + */ minKey = this[translationFn](key, this._keys); - /** - * For Arrays and Objects, create a new Array/Object - * on the minified object and recurse into it - */ + /** + * For Arrays and Objects, create a new Array/Object + * on the minified object and recurse into it + */ if (typeof from[key] === 'object') { to[minKey] = from[key] instanceof Array ? [] : {}; this._nextLevel(from[key], to[minKey], translationFn); - /** - * For primitive values (Strings, Numbers, Boolean etc.) - * minify the value - */ + /** + * For primitive values (Strings, Numbers, Boolean etc.) + * minify the value + */ } else { to[minKey] = this[translationFn](from[key], this._values); } } }, - /** - * Minifies value based on a dictionary - * - * @param {String|Boolean} value - * @param {Array<String|Boolean>} dictionary - * - * @returns {String} The minified version - */ + /** + * Minifies value based on a dictionary + * + * @param {String|Boolean} value + * @param {Array<String|Boolean>} dictionary + * + * @returns {String} The minified version + */ _min: function (value, dictionary) { - /** - * If a value actually is a single character, prefix it - * with ___ to avoid mistaking it for a minification code - */ + /** + * If a value actually is a single character, prefix it + * with ___ to avoid mistaking it for a minification code + */ if (typeof value === 'string' && value.length === 1) { return '___' + value; } var index = lm.utils.indexOf(value, dictionary); - /** - * value not found in the dictionary, return it unmodified - */ + /** + * value not found in the dictionary, return it unmodified + */ if (index === -1) { return value; - /** - * value found in dictionary, return its base36 counterpart - */ + /** + * value found in dictionary, return its base36 counterpart + */ } else { return index.toString(36); } }, _max: function (value, dictionary) { - /** - * value is a single character. Assume that it's a translation - * and return the original value from the dictionary - */ + /** + * value is a single character. Assume that it's a translation + * and return the original value from the dictionary + */ if (typeof value === 'string' && value.length === 1) { return dictionary[parseInt(value, 36)]; } - /** - * value originally was a single character and was prefixed with ___ - * to avoid mistaking it for a translation. Remove the prefix - * and return the original character - */ + /** + * value originally was a single character and was prefixed with ___ + * to avoid mistaking it for a translation. Remove the prefix + * and return the original character + */ if (typeof value === 'string' && value.substr(0, 3) === '___') { return value[3]; } - /** - * value was not minified - */ + /** + * value was not minified + */ return value; } }); - /** - * An EventEmitter singleton that propagates events - * across multiple windows. This is a little bit trickier since - * windows are allowed to open childWindows in their own right - * - * This means that we deal with a tree of windows. Hence the rules for event propagation are: - * - * - Propagate events from this layout to both parents and children - * - Propagate events from parent to this and children - * - Propagate events from children to the other children (but not the emitting one) and the parent - * - * @constructor - * - * @param {lm.LayoutManager} layoutManager - */ + /** + * An EventEmitter singleton that propagates events + * across multiple windows. This is a little bit trickier since + * windows are allowed to open childWindows in their own right + * + * This means that we deal with a tree of windows. Hence the rules for event propagation are: + * + * - Propagate events from this layout to both parents and children + * - Propagate events from parent to this and children + * - Propagate events from children to the other children (but not the emitting one) and the parent + * + * @constructor + * + * @param {lm.LayoutManager} layoutManager + */ lm.utils.EventHub = function (layoutManager) { lm.utils.EventEmitter.call(this); this._layoutManager = layoutManager; @@ -5195,15 +5193,15 @@ $(window).on('gl_child_event', this._boundOnEventFromChild); }; - /** - * Called on every event emitted on this eventHub, regardles of origin. - * - * @private - * - * @param {Mixed} - * - * @returns {void} - */ + /** + * Called on every event emitted on this eventHub, regardles of origin. + * + * @private + * + * @param {Mixed} + * + * @returns {void} + */ lm.utils.EventHub.prototype._onEventFromThis = function () { var args = Array.prototype.slice.call(arguments); @@ -5217,40 +5215,40 @@ this._childEventSource = null; }; - /** - * Called by the parent layout. - * - * @param {Array} args Event name + arguments - * - * @returns {void} - */ + /** + * Called by the parent layout. + * + * @param {Array} args Event name + arguments + * + * @returns {void} + */ lm.utils.EventHub.prototype._$onEventFromParent = function (args) { this._dontPropagateToParent = args[0]; this.emit.apply(this, args); }; - /** - * Callback for child events raised on the window - * - * @param {DOMEvent} event - * @private - * - * @returns {void} - */ + /** + * Callback for child events raised on the window + * + * @param {DOMEvent} event + * @private + * + * @returns {void} + */ lm.utils.EventHub.prototype._onEventFromChild = function (event) { this._childEventSource = event.originalEvent.__gl; this.emit.apply(this, event.originalEvent.__glArgs); }; - /** - * Propagates the event to the parent by emitting - * it on the parent's DOM window - * - * @param {Array} args Event name + arguments - * @private - * - * @returns {void} - */ + /** + * Propagates the event to the parent by emitting + * it on the parent's DOM window + * + * @param {Array} args Event name + arguments + * @private + * + * @returns {void} + */ lm.utils.EventHub.prototype._propagateToParent = function (args) { var event, eventName = 'gl_child_event'; @@ -5274,14 +5272,14 @@ } }; - /** - * Propagate events to children - * - * @param {Array} args Event name + arguments - * @private - * - * @returns {void} - */ + /** + * Propagate events to children + * + * @param {Array} args Event name + arguments + * @private + * + * @returns {void} + */ lm.utils.EventHub.prototype._propagateToChildren = function (args) { var childGl, i; @@ -5295,25 +5293,25 @@ }; - /** - * Destroys the EventHub - * - * @public - * @returns {void} - */ + /** + * Destroys the EventHub + * + * @public + * @returns {void} + */ lm.utils.EventHub.prototype.destroy = function () { $(window).off('gl_child_event', this._boundOnEventFromChild); }; - /** - * A specialised GoldenLayout component that binds GoldenLayout container - * lifecycle events to react components - * - * @constructor - * - * @param {lm.container.ItemContainer} container - * @param {Object} state state is not required for react components - */ + /** + * A specialised GoldenLayout component that binds GoldenLayout container + * lifecycle events to react components + * + * @constructor + * + * @param {lm.container.ItemContainer} container + * @param {Object} state state is not required for react components + */ lm.utils.ReactComponentHandler = function (container, state) { this._reactComponent = null; this._originalComponentWillUpdate = null; @@ -5326,15 +5324,15 @@ lm.utils.copy(lm.utils.ReactComponentHandler.prototype, { - /** - * Creates the react class and component and hydrates it with - * the initial state - if one is present - * - * By default, react's getInitialState will be used - * - * @private - * @returns {void} - */ + /** + * Creates the react class and component and hydrates it with + * the initial state - if one is present + * + * By default, react's getInitialState will be used + * + * @private + * @returns {void} + */ _render: function () { this._reactComponent = ReactDOM.render(this._getReactComponent(), this._container.getElement()[0]); this._originalComponentWillUpdate = this._reactComponent.componentWillUpdate || function () { @@ -5345,36 +5343,36 @@ } }, - /** - * Removes the component from the DOM and thus invokes React's unmount lifecycle - * - * @private - * @returns {void} - */ + /** + * Removes the component from the DOM and thus invokes React's unmount lifecycle + * + * @private + * @returns {void} + */ _destroy: function () { ReactDOM.unmountComponentAtNode(this._container.getElement()[0]); this._container.off('open', this._render, this); this._container.off('destroy', this._destroy, this); }, - /** - * Hooks into React's state management and applies the componentstate - * to GoldenLayout - * - * @private - * @returns {void} - */ + /** + * Hooks into React's state management and applies the componentstate + * to GoldenLayout + * + * @private + * @returns {void} + */ _onUpdate: function (nextProps, nextState) { this._container.setState(nextState); this._originalComponentWillUpdate.call(this._reactComponent, nextProps, nextState); }, - /** - * Retrieves the react class from GoldenLayout's registry - * - * @private - * @returns {React.Class} - */ + /** + * Retrieves the react class from GoldenLayout's registry + * + * @private + * @returns {React.Class} + */ _getReactClass: function () { var componentName = this._container._config.component; var reactClass; @@ -5393,12 +5391,12 @@ return reactClass; }, - /** - * Copies and extends the properties array and returns the React element - * - * @private - * @returns {React.Element} - */ + /** + * Copies and extends the properties array and returns the React element + * + * @private + * @returns {React.Element} + */ _getReactComponent: function () { var defaultProps = { glEventHub: this._container.layoutManager.eventHub, diff --git a/src/client/util/CaptureManager.scss b/src/client/util/CaptureManager.scss index 71539ee1e..a5024247e 100644 --- a/src/client/util/CaptureManager.scss +++ b/src/client/util/CaptureManager.scss @@ -1,4 +1,4 @@ -@import "../views/globalCssVariables"; +@import "../views/global/globalCssVariables"; .capture-interface { //background-color: whitesmoke !important; diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 12733e815..185b8ac51 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,10 +33,10 @@ import { LinkManager } from "./LinkManager"; import { Scripting } from "./Scripting"; import { SearchUtil } from "./SearchUtil"; import { SelectionManager } from "./SelectionManager"; -import { UndoManager } from "./UndoManager"; +import { ColorScheme } from "./SettingsManager"; +import { SharingManager } from "./SharingManager"; import { SnappingManager } from "./SnappingManager"; -import { InkTool } from "../../fields/InkField"; -import { computedFn } from "mobx-utils"; +import { UndoManager } from "./UndoManager"; export let resolvedPorts: { server: number, socket: number }; @@ -44,6 +46,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; } @@ -53,6 +56,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) { @@ -380,7 +384,7 @@ export class CurrentUserUtils { ((doc.emptyPane as Doc).proto as Doc)["dragFactory-count"] = 0; } if (doc.emptySlide === undefined) { - const textDoc = Docs.Create.TreeDocument([], { title: "Slide", _viewType: CollectionViewType.Tree, _fontSize: "20px", treeViewType: "outline", _xMargin: 0, _yMargin: 0, _width: 300, _height: 200, _singleLine: true, backgroundColor: "transparent", system: true, cloneFieldFilter: new List<string>(["system"]) }); + const textDoc = Docs.Create.TreeDocument([], { title: "Slide", _viewType: CollectionViewType.Tree, _fontSize: "20px", _autoHeight: true, treeViewType: "outline", _xMargin: 0, _yMargin: 0, _width: 300, _height: 200, _singleLine: true, backgroundColor: "transparent", system: true, cloneFieldFilter: new List<string>(["system"]) }); Doc.GetProto(textDoc).title = ComputedField.MakeFunction('self.text?.Text'); FormattedTextBox.SelectOnLoad = textDoc[Id]; doc.emptySlide = textDoc; @@ -405,12 +409,23 @@ export class CurrentUserUtils { selection: { type: "text", anchor: 1, head: 1 }, storedMarks: [] }; - const headerTemplate = Docs.Create.RTFDocument(new RichTextField(JSON.stringify(json), ""), { title: "header", version: headerViewVersion, target: doc, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _autoHeight: true, system: true, cloneFieldFilter: new List<string>(["system"]) }, "header"); // text needs to be a space to allow templateText to be created + const headerTemplate = Docs.Create.RTFDocument(new RichTextField(JSON.stringify(json), ""), { + title: "text", version: headerViewVersion, target: doc, _height: 70, _headerPointerEvents: "all", + _headerHeight: 12, _headerFontSize: 9, _autoHeight: true, system: true, _fitWidth: true, + cloneFieldFilter: new List<string>(["system"]) + }, "header"); + const headerBtnHgt = 10; headerTemplate[DataSym].layout = - "<div style={'height:100%'}>" + - " <FormattedTextBox {...props} fieldKey={'header'} dontSelectOnLoad={'true'} ignoreAutoHeight={'true'} pointerEvents='{this._headerPointerEvents||`none`}' fontSize='{this._headerFontSize}px' height='{this._headerHeight}px' background='{this._headerColor||this.target.mySharedDocs.userColor}' />" + - " <FormattedTextBox {...props} fieldKey={'text'} position='absolute' top='{(this._headerHeight)*scale}px' height='calc({100/scale}% - {this._headerHeight}px)'/>" + - "</div>"; + "<HTMLdiv transformOrigin='top left' width='{100/scale}%' height='{100/scale}%' transform='scale({scale})'>" + + ` <FormattedTextBox {...props} dontScale='true' fieldKey={'text'} height='calc(100% - ${headerBtnHgt}px - {this._headerHeight}px)'/>` + + " <FormattedTextBox {...props} dontScale='true' fieldKey={'header'} dontSelectOnLoad='true' ignoreAutoHeight='true' fontSize='{this._headerFontSize}px' height='{(this._headerHeight||1)}px' background='{this._headerColor ||this.target.mySharedDocs.userColor||`lightGray`}' />" + + ` <HTMLdiv fontSize='${headerBtnHgt - 1}px' height='${headerBtnHgt}px' background='yellow' onClick={‘(this._headerHeight=scale*Math.min(Math.max(1,this._height-30),this._headerHeight===1?50:1)) && (this._autoHeightMargins=this._headerHeight+${headerBtnHgt})’} >Metadata</HTMLdiv>` + + "</HTMLdiv>"; + + // "<div style={'height:100%'}>" + + // " <FormattedTextBox {...props} fieldKey={'header'} dontSelectOnLoad={'true'} ignoreAutoHeight={'true'} pointerEvents='{this._headerPointerEvents||`none`}' fontSize='{this._headerFontSize}px' height='{this._headerHeight}px' background='{this._headerColor||this.target.mySharedDocs.userColor}' />" + + // " <FormattedTextBox {...props} fieldKey={'text'} position='absolute' top='{(this._headerHeight)*scale}px' height='calc({100/scale}% - {this._headerHeight}px)'/>" + + // "</div>"; (headerTemplate.proto as Doc).isTemplateDoc = makeTemplate(headerTemplate.proto as Doc, true, "headerView"); doc.emptyHeader = headerTemplate; ((doc.emptyHeader as Doc).proto as Doc)["dragFactory-count"] = 0; @@ -445,7 +460,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); @@ -461,7 +476,7 @@ export class CurrentUserUtils { { toolTip: "Tap to create a videoWall", title: "Wall", icon: "photo-video", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyWall as Doc }, { toolTip: "Tap to create an audio recorder in a new pane, drag for an audio recorder", title: "Audio", icon: "microphone", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyAudio as Doc, noviceMode: true }, { toolTip: "Tap to create a button in a new pane, drag for a button", title: "Button", icon: "bolt", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyButton as Doc }, - { toolTip: "Tap to create a presentation in a new pane, drag for a presentation", title: "Trails", icon: "pres-trail", click: 'openOnRight(Doc.UserDoc().activePresentation = copyDragFactory(this.dragFactory))', drag: `Doc.UserDoc().activePresentation = copyDragFactory(this.dragFactory)`, dragFactory: doc.emptyPresentation as Doc, noviceMode: true }, + // { toolTip: "Tap to create a presentation in a new pane, drag for a presentation", title: "Trails", icon: "pres-trail", click: 'openOnRight(Doc.UserDoc().activePresentation = copyDragFactory(this.dragFactory))', drag: `Doc.UserDoc().activePresentation = copyDragFactory(this.dragFactory)`, dragFactory: doc.emptyPresentation as Doc, noviceMode: true }, { toolTip: "Tap to create a scripting box in a new pane, drag for a scripting box", title: "Script", icon: "terminal", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyScript as Doc }, { toolTip: "Tap to create a mobile view in a new pane, drag for a mobile view", title: "Phone", icon: "mobile", click: 'openOnRight(Doc.UserDoc().activeMobileMenu)', drag: 'this.dragFactory', dragFactory: doc.activeMobileMenu as Doc }, { toolTip: "Tap to create a custom header note document, drag for a custom header note", title: "Custom", icon: "window-maximize", click: 'openOnRight(delegateDragFactory(this.dragFactory))', drag: 'delegateDragFactory(this.dragFactory)', dragFactory: doc.emptyHeader as Doc }, @@ -516,27 +531,19 @@ 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: "Help", target: undefined as any, icon: "question-circle", click: 'selectMainMenu(self)' }, - { title: "Settings", target: undefined as any, icon: "cog", 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 @@ -550,23 +557,37 @@ export class CurrentUserUtils { dontUndo: true, title, target, - backgroundColor: "black", _dropAction: "alias", _removeDropProperties: new List<string>(["dropAction", "_stayInCollection"]), _width: 60, _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", _chromeHidden: true, + backgroundColor: Colors.DARK_GRAY, + boxShadow: "rgba(0,0,0,0)", dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), - backgroundColor: "black", ignoreClick: true, + ignoreClick: true, _gridGap: 0, _yMargin: 0, _yPadding: 0, _xMargin: 0, _autoHeight: false, _width: 60, _columnWidth: 60, _lockedPosition: true, system: true @@ -576,8 +597,6 @@ export class CurrentUserUtils { PromiseValue(Cast(doc.menuStack, Doc)).then(stack => { stack && PromiseValue(stack.data).then(btns => { DocListCastAsync(btns).then(bts => bts?.forEach(btn => { - btn.color = "white"; - btn._backgroundColor = ""; btn.dontUndo = true; btn.system = true; if (btn.title === "Catalog" || btn.title === "My Files") { // migration from Catalog to My Files @@ -736,7 +755,7 @@ 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, + system: true, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true, boxShadow: "0 0", })) as any as Doc; doc.myTools = toolsStack; @@ -748,14 +767,19 @@ export class CurrentUserUtils { await doc.myDashboards; if (doc.myDashboards === undefined) { doc.myDashboards = new PrefetchProxy(Docs.Create.TreeDocument([], { - title: "My Dashboards", _height: 400, childHideLinkButton: true, + 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; } @@ -764,7 +788,7 @@ export class CurrentUserUtils { await doc.myPresentations; if (doc.myPresentations === undefined) { doc.myPresentations = new PrefetchProxy(Docs.Create.TreeDocument([], { - title: "My Presentations", _height: 100, + title: "My Trails", _showTitle: "title", _height: 100, 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 @@ -783,7 +807,7 @@ export class CurrentUserUtils { doc.myFileOrphans = Docs.Create.TreeDocument([], { title: "Unfiled", _stayInCollection: true, system: true, isFolder: true }); doc.myFileRoot = Docs.Create.TreeDocument([], { title: "file root", _stayInCollection: true, system: true, isFolder: true }); doc.myFilesystem = new PrefetchProxy(Docs.Create.TreeDocument([doc.myFileRoot as Doc, doc.myFileOrphans as Doc], { - title: "My Documents", _height: 100, + title: "My Documents", _showTitle: "title", _height: 100, treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, childDropAction: "alias", treeViewTruncateTitleWidth: 150, ignoreClick: true, isFolder: true, treeViewType: "fileSystem", childHideLinkButton: true, @@ -797,7 +821,7 @@ export class CurrentUserUtils { // setup Recently Closed library item if (doc.myRecentlyClosedDocs === undefined) { doc.myRecentlyClosedDocs = new PrefetchProxy(Docs.Create.TreeDocument([], { - title: "Recently Closed", treeViewShowClearButton: true, childHideLinkButton: true, + title: "Recently Closed", _showTitle: "title", treeViewShowClearButton: true, 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 @@ -807,6 +831,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) { @@ -828,7 +853,7 @@ export class CurrentUserUtils { doc.treeViewOpen = true; doc.treeViewExpandedView = "fields"; doc.myUserDoc = new PrefetchProxy(Docs.Create.TreeDocument([doc], { - treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, title: "My UserDoc", + treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, title: "My UserDoc", _showTitle: "title", treeViewTruncateTitleWidth: 150, ignoreClick: true, _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true })) as any as Doc; @@ -848,11 +873,11 @@ export class CurrentUserUtils { static async setupSidebarButtons(doc: Doc) { CurrentUserUtils.setupSidebarContainer(doc); await CurrentUserUtils.setupToolsBtnPanel(doc); + CurrentUserUtils.setupImportSidebar(doc); CurrentUserUtils.setupDashboards(doc); CurrentUserUtils.setupPresentations(doc); CurrentUserUtils.setupFilesystem(doc); CurrentUserUtils.setupRecentlyClosedDocs(doc); - // CurrentUserUtils.setupFilterDocs(doc); CurrentUserUtils.setupUserDoc(doc); } @@ -880,6 +905,7 @@ export class CurrentUserUtils { (doc["dockedBtn-undo"] as Doc).dontUndo = true; (doc["dockedBtn-redo"] as Doc).dontUndo = true; } + // sets up the default set of documents to be shown in the Overlay layer static setupOverlays(doc: Doc) { if (doc.myOverlayDocs === undefined) { @@ -902,23 +928,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, _chromeHidden: true, + 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); } @@ -928,14 +961,24 @@ export class CurrentUserUtils { static setupImportSidebar(doc: Doc) { if (doc.myImportDocs === undefined) { doc.myImportDocs = new PrefetchProxy(Docs.Create.StackingDocument([], { - title: "My ImportDocuments", _forceActive: true, ignoreClick: true, _showTitle: "title", _stayInCollection: true, _hideContextMenu: true, childLimitHeight: 0, + title: "My ImportDocuments", _forceActive: true, ignoreClick: true, _stayInCollection: true, _hideContextMenu: true, childLimitHeight: 0, childDropAction: "alias", _autoHeight: true, _yMargin: 50, _gridGap: 15, _lockedPosition: true, system: true, _chromeHidden: true, })); } 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 }); - doc.myImportPanel = new PrefetchProxy(Docs.Create.StackingDocument([newUpload, uploads], { title: "My ImportPanel", _yMargin: 20, ignoreClick: true, _chromeHidden: true, _stayInCollection: true, _hideContextMenu: true, _lockedPosition: true, 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; } } @@ -996,10 +1039,14 @@ export class CurrentUserUtils { const mygroups = groups?.filter(group => JSON.parse(StrCast(group.members)).includes(Doc.CurrentUserEmail)) || []; SnappingManager.SetCachedGroups(["Public", ...mygroups?.map(g => StrCast(g.title))]); }, { fireImmediately: true }); + // Document properties on load doc.system = true; + doc.darkScheme = ColorScheme.Dark; doc.noviceMode = doc.noviceMode === undefined ? "true" : doc.noviceMode; doc.title = Doc.CurrentUserEmail; 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.activeInkBezier = StrCast(doc.activeInkBezier, "0"); @@ -1011,7 +1058,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); @@ -1023,8 +1070,8 @@ export class CurrentUserUtils { 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.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.setupDockedButtons(doc); // the bottom bar of font icons await this.setupSidebarButtons(doc); // the pop-out left sidebar of tools/panels @@ -1188,14 +1235,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(`self.darkScheme = !self.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); @@ -1247,5 +1308,51 @@ Scripting.addGlobal(function links(doc: any) { return new List(LinkManager.Insta "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"); -Scripting.addGlobal(function toggleComicMode() { Doc.UserDoc().renderStyle = Doc.UserDoc().renderStyle === "comic" ? undefined : "comic"; }, - "toggle between regular rendeing and an informal sketch/comic style"); +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 []; +}); diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 304215a8f..cb0ee411c 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -28,7 +28,7 @@ export class DocumentManager { DocListCast(view.rootDoc.links).forEach(link => { const whichOtherAnchor = view.props.LayoutTemplateString?.includes("anchor2") ? "anchor1" : "anchor2"; const otherDoc = link && (link[whichOtherAnchor] as Doc); - const otherDocAnno = otherDoc?.type === DocumentType.TEXTANCHOR ? otherDoc.annotationOn as Doc : undefined; + const otherDocAnno = DocumentType.MARKER === otherDoc?.type ? otherDoc.annotationOn as Doc : undefined; otherDoc && DocumentManager.Instance.DocumentViews?.filter(dv => Doc.AreProtosEqual(dv.rootDoc, otherDoc) || Doc.AreProtosEqual(dv.rootDoc, otherDocAnno)). forEach(otherView => { if (otherView.rootDoc.type !== DocumentType.LINK || otherView.props.LayoutTemplateString !== view.props.LayoutTemplateString) { @@ -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 focusView = !docView && targetDoc.type === DocumentType.TEXTANCHOR && annoContainerView ? annoContainerView : docView; - if (!docView && annoContainerView && !focusView) { + 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) { 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 d8c2f913e..f7ef9ae6f 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -1,4 +1,4 @@ -import { action, observable, runInAction } from "mobx"; +import { action } from "mobx"; import { DateField } from "../../fields/DateField"; import { Doc, Field, Opt } from "../../fields/Doc"; import { List } from "../../fields/List"; @@ -7,14 +7,23 @@ import { listSpec } from "../../fields/Schema"; import { SchemaHeaderField } from "../../fields/SchemaHeaderField"; import { ScriptField } from "../../fields/ScriptField"; import { Cast, NumCast, ScriptCast, StrCast } from "../../fields/Types"; -import { emptyFunction, returnTrue } from "../../Utils"; +import { emptyFunction } from "../../Utils"; import { Docs, DocUtils } from "../documents/Documents"; -import * as globalCssVariables from "../views/globalCssVariables.scss"; -import { UndoManager } from "./UndoManager"; -import { SnappingManager } from "./SnappingManager"; +import * as globalCssVariables from "../views/global/globalCssVariables.scss"; import { DocumentView } from "../views/nodes/DocumentView"; +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, @@ -210,16 +219,16 @@ export namespace DragManager { dropDoc instanceof Doc && DocUtils.MakeLinkToActiveAudio(() => dropDoc); return dropDoc; }; - const finishDrag = (e: DragCompleteEvent) => { + const finishDrag = async (e: DragCompleteEvent) => { const docDragData = e.docDragData; dropEvent?.(); // glr: optional additional function to be called - in this case with presentation trails if (docDragData && !docDragData.droppedDocuments.length) { docDragData.dropAction = dragData.userDropAction || dragData.dropAction; docDragData.droppedDocuments = - dragData.draggedDocuments.map(d => !dragData.isSelectionMove && !dragData.userDropAction && ScriptCast(d.onDragStart) ? addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result) : + await Promise.all(dragData.draggedDocuments.map(async d => !dragData.isSelectionMove && !dragData.userDropAction && ScriptCast(d.onDragStart) ? addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result) : docDragData.dropAction === "alias" ? Doc.MakeAlias(d) : docDragData.dropAction === "proto" ? Doc.GetProto(d) : - docDragData.dropAction === "copy" ? Doc.MakeClone(d) : d); + docDragData.dropAction === "copy" ? (await Doc.MakeClone(d)).clone : d)); !["same", "proto"].includes(docDragData.dropAction as any) && docDragData.droppedDocuments.forEach((drop: Doc, i: number) => { const dragProps = Cast(dragData.draggedDocuments[i].removeDropProperties, listSpec("string"), []); const remProps = (dragData?.removeDropProperties || []).concat(Array.from(dragProps)); @@ -340,7 +349,7 @@ export namespace DragManager { dragLabel.style.zIndex = "100001"; dragLabel.style.fontSize = "10px"; dragLabel.style.position = "absolute"; - dragLabel.innerText = "press 'a' to embed on drop"; // bcz: need to move this to a status bar + dragLabel.innerText = "drag titlebar to embed on drop"; // bcz: need to move this to a status bar dragDiv.appendChild(dragLabel); DragManager.Root().appendChild(dragDiv); } @@ -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); @@ -427,7 +436,7 @@ export namespace DragManager { SnappingManager.clearSnapLines(); batch.end(); }); - const moveHandler = (e: PointerEvent) => { + const moveHandler = async (e: PointerEvent) => { e.preventDefault(); // required or dragging text menu link item ends up dragging the link button as native drag/drop if (dragData instanceof DocumentDragData) { dragData.userDropAction = e.ctrlKey && e.altKey ? "copy" : e.ctrlKey ? "alias" : dragData.defaultDropAction; @@ -438,7 +447,7 @@ export namespace DragManager { dragData.removeDocument?.(dragData.draggedDocuments[0]); } AbortDrag(); - finishDrag?.(new DragCompleteEvent(true, dragData)); + await finishDrag?.(new DragCompleteEvent(true, dragData)); DragManager.StartWindowDrag?.({ pageX: e.pageX, pageY: e.pageY, @@ -510,14 +519,13 @@ export namespace DragManager { ); }; 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); - endDrag(); + 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); } - 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: { @@ -531,8 +539,9 @@ export namespace DragManager { } }; target.dispatchEvent(new CustomEvent<DropEvent>("dashPreDrop", dropArgs)); - finishDrag?.(complete); + 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 01d00db30..e009fb3a9 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -208,9 +208,10 @@ export namespace InteractionUtils { <polyline points={strpts} style={{ - filter: drawHalo ? "url(#inkSelectionHalo)" : undefined, + // 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 3c3d5c3b8..84ff3a3ff 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -1,15 +1,15 @@ +import { observable, observe, action, reaction, computed } from "mobx"; import { computedFn } from "mobx-utils"; -import { Doc, DocListCast, Opt, DirectLinksSym, Field } from "../../fields/Doc"; -import { BoolCast, Cast, StrCast, PromiseValue } from "../../fields/Types"; +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"; import { DocumentManager } from "./DocumentManager"; import { SharingManager } from "./SharingManager"; import { UndoManager } from "./UndoManager"; -import { observe, observable, reaction } from "mobx"; -import { listSpec } from "../../fields/Schema"; -import { List } from "../../fields/List"; -import { ProxyField } from "../../fields/Proxy"; type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => void) => void; /* @@ -27,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 = (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); - } + 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): any => { + 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); - } + 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 watchUserLinks = (userLinks: List<Doc>) => { + 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))); @@ -65,18 +73,47 @@ 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 addLink(linkDoc: Doc) { - return Doc.AddDocToList(Doc.LinkDBDoc(), "data", linkDoc); + + 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); + } } public deleteLink(linkDoc: Doc) { return Doc.RemoveDocFromList(Doc.LinkDBDoc(), "data", linkDoc); } public deleteAllLinksOnAnchor(anchor: Doc) { LinkManager.Instance.relatedLinker(anchor).forEach(linkDoc => LinkManager.Instance.deleteLink(linkDoc)); } @@ -85,25 +122,13 @@ export class LinkManager { public getAllDirectLinks(anchor: Doc): Doc[] { return Array.from(Doc.GetProto(anchor)[DirectLinksSym]); } // finds all links that contain the given anchor - public getAllLinks(): Doc[] { return []; }//this.allLinks(); } - - // allLinks = computedFn(function allLinks(this: any): Doc[] { - // const linkData = Doc.LinkDBDoc().data; - // const lset = new Set<Doc>(DocListCast(linkData)); - // SharingManager.Instance.users.forEach(user => DocListCast(user.linkDatabase?.data).forEach(doc => lset.add(doc))); - // LinkManager.Instance.allLinks().filter(link => { - // const a1 = Cast(link?.anchor1, Doc, null); - // const a2 = Cast(link?.anchor2, Doc, null); - // return link && ((a1?.author !== undefined && a2?.author !== undefined) || link.author === Doc.CurrentUserEmail) && (Doc.AreProtosEqual(anchor, a1) || Doc.AreProtosEqual(anchor, a2) || Doc.AreProtosEqual(link, anchor)); - // }); - // return Array.from(lset); - // }, true); 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 @@ -147,7 +172,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 c3c3083be..f981f84cd 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -181,7 +181,7 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an if (batch) { batch.end(); } - onError?.(error); + onError?.(script + " " + error); return { success: false, error, result: errorVal }; } }; diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 00f0894c7..0cfaebbf2 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -1,6 +1,7 @@ import { action, observable, ObservableMap } from "mobx"; import { computedFn } from "mobx-utils"; import { Doc, Opt } from "../../fields/Doc"; +import { DocumentType } from "../documents/DocumentTypes"; import { CollectionSchemaView } from "../views/collections/collectionSchema/CollectionSchemaView"; import { CollectionViewType } from "../views/collections/CollectionView"; import { DocumentView } from "../views/nodes/DocumentView"; @@ -22,7 +23,7 @@ export namespace SelectionManager { @action SelectView(docView: DocumentView, ctrlPressed: boolean): void { // if doc is not in SelectedDocuments, add it - if (!manager.SelectedViews.get(docView)) { + if (!manager.SelectedViews.get(docView) && docView.props.Document.type !== DocumentType.MARKER) { if (!ctrlPressed) { this.DeselectAll(); } diff --git a/src/client/util/SettingsManager.scss b/src/client/util/SettingsManager.scss index d8342ea56..c9db94419 100644 --- a/src/client/util/SettingsManager.scss +++ b/src/client/util/SettingsManager.scss @@ -1,4 +1,4 @@ -@import "../views/globalCssVariables"; +@import "../views/global/globalCssVariables"; .settings-interface { //background-color: whitesmoke !important; @@ -264,7 +264,7 @@ //margin-top: 4px; &:hover { - background: $main-accent; + background: $medium-gray; } } diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index 777394b05..bd91db779 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -18,6 +18,12 @@ const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; +export enum ColorScheme { + Dark = "Dark", + Light = "Light", + System = "Match System" +} + @observer export class SettingsManager extends React.Component<{}> { public static Instance: SettingsManager; @@ -32,7 +38,7 @@ export class SettingsManager extends React.Component<{}> { @observable activeTab = "Accounts"; @computed get backgroundColor() { return Doc.UserDoc().activeCollectionBackground; } - + @computed get colorScheme() { return Doc.UserDoc().colorScheme; } constructor(props: {}) { super(props); @@ -69,6 +75,28 @@ export class SettingsManager extends React.Component<{}> { else DocServer.Control.makeEditable(); }); + @undoBatch + @action + changeColorScheme = action((e: React.ChangeEvent) => { + const scheme: ColorScheme = (e.currentTarget as any).value; + switch (scheme) { + case ColorScheme.Light: + Doc.UserDoc().colorScheme = ColorScheme.Light; + addStyleSheetRule(SettingsManager._settingsStyle, "lm_header", { background: "#d3d3d3 !important" }); + break; + case ColorScheme.Dark: + Doc.UserDoc().colorScheme = ColorScheme.Dark; + addStyleSheetRule(SettingsManager._settingsStyle, "lm_header", { background: "black !important" }); + break; + case ColorScheme.System: default: + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => { + Doc.UserDoc().colorScheme = e.matches ? ColorScheme.Dark : ColorScheme.Light; + }); + break; + } + }); + + @computed get colorsContent() { const colorBox = (func: (color: ColorState) => void) => <SketchPicker onChange={func} color={StrCast(this.backgroundColor)} presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', @@ -90,8 +118,7 @@ export class SettingsManager extends React.Component<{}> { </Flyout> </div>; - const fontFamilies = ["Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]; - const fontSizes = ["7px", "8px", "9px", "10px", "12px", "14px", "16px", "18px", "20px", "24px", "32px", "48px", "72px"]; + const colorSchemes = [ColorScheme.Light, ColorScheme.Dark, ColorScheme.System]; return <div className="colors-content"> <div className="preferences-color"> @@ -102,14 +129,11 @@ export class SettingsManager extends React.Component<{}> { <div className="preferences-color-text">Border/Header Color</div> {userColorFlyout} </div> - <div className="preferences-font"> - <div className="preferences-font-text">Default Font</div> - <div className="preferences-font-controls"> - <select className="size-select" onChange={this.changeFontSize} value={StrCast(Doc.UserDoc().fontSize, "7px")}> - {fontSizes.map(size => <option key={size} value={size} defaultValue={StrCast(Doc.UserDoc().fontSize)}> {size} </option>)} - </select> - <select className="font-select" onChange={this.changeFontFamily} value={StrCast(Doc.UserDoc().fontFamily, "Times New Roman")} > - {fontFamilies.map(font => <option key={font} value={font} defaultValue={StrCast(Doc.UserDoc().fontFamily)}> {font} </option>)} + <div className="preferences-colorScheme"> + <div className="preferences-color-text">Color Scheme</div> + <div className="preferences-color-controls"> + <select className="scheme-select" onChange={this.changeColorScheme} defaultValue={StrCast(Doc.UserDoc().colorScheme)}> + {colorSchemes.map(scheme => <option key={scheme} value={scheme}> {scheme} </option>)} </select> </div> </div> @@ -132,6 +156,16 @@ export class SettingsManager extends React.Component<{}> { checked={BoolCast(Doc.UserDoc()._raiseWhenDragged)} /> <div className="preferences-check">Raise on drag</div> </div> + <div> + <input type="checkbox" onChange={e => Doc.UserDoc()._showLabel = !Doc.UserDoc()._showLabel} + checked={BoolCast(Doc.UserDoc()._showLabel)} /> + <div className="preferences-check">Show tool button labels</div> + </div> + <div> + <input type="checkbox" onChange={e => Doc.UserDoc()._showMenuLabel = !Doc.UserDoc()._showMenuLabel} + checked={BoolCast(Doc.UserDoc()._showMenuLabel)} /> + <div className="preferences-check">Show menu button labels</div> + </div> </div>; } @@ -149,6 +183,27 @@ export class SettingsManager extends React.Component<{}> { </div>; } + @computed get textContent() { + + const fontFamilies = ["Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text", "Roboto"]; + const fontSizes = ["7px", "8px", "9px", "10px", "12px", "14px", "16px", "18px", "20px", "24px", "32px", "48px", "72px"]; + + return ( + <div className="tab-content appearances-content"> + <div className="preferences-font"> + <div className="preferences-font-text">Default Font</div> + <div className="preferences-font-controls"> + <select className="size-select" onChange={this.changeFontSize} value={StrCast(Doc.UserDoc().fontSize, "7px")}> + {fontSizes.map(size => <option key={size} value={size} defaultValue={StrCast(Doc.UserDoc().fontSize)}> {size} </option>)} + </select> + <select className="font-select" onChange={this.changeFontFamily} value={StrCast(Doc.UserDoc().fontFamily, "Times New Roman")} > + {fontFamilies.map(font => <option key={font} value={font} defaultValue={StrCast(Doc.UserDoc().fontFamily)}> {font} </option>)} + </select> + </div> + </div> + </div>); + } + @action changeVal = (e: React.ChangeEvent, pass: string) => { const value = (e.target as any).value; @@ -213,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> @@ -228,7 +284,7 @@ export class SettingsManager extends React.Component<{}> { // { title: "Accounts", ele: this.accountsContent }, { title: "Preferences", ele: this.preferencesContent }]; const tabs = [{ title: "Accounts", ele: this.accountsContent }, { title: "Modes", ele: this.modesContent }, - { title: "Appearance", ele: this.appearanceContent }]; + { title: "Appearance", ele: this.appearanceContent }, { title: "Text", ele: this.textContent }]; return <div className="settings-interface"> <div className="settings-panel"> 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 Binary files differindex 33e624ef4..e4ac87aad 100644 --- a/src/client/views/.DS_Store +++ b/src/client/views/.DS_Store diff --git a/src/client/views/AntimodeMenu.scss b/src/client/views/AntimodeMenu.scss index a275901be..8a0e5480e 100644 --- a/src/client/views/AntimodeMenu.scss +++ b/src/client/views/AntimodeMenu.scss @@ -1,14 +1,14 @@ -@import "./globalCssVariables"; +@import "./global/globalCssVariables"; .antimodeMenu-cont { position: absolute; z-index: 10001; height: $antimodemenu-height; - background: #323232; - box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25); + background: $dark-gray; + border-bottom: $standard-border; + // box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25); // border-radius: 0px 6px 6px 6px; - z-index: 1001; display: flex; &.with-rows { diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss index b514de5f2..795529780 100644 --- a/src/client/views/ContextMenu.scss +++ b/src/client/views/ContextMenu.scss @@ -1,10 +1,10 @@ -@import "globalCssVariables"; +@import "global/globalCssVariables"; .contextMenu-cont { position: absolute; display: flex; z-index: $contextMenu-zindex; - box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw; + box-shadow: $medium-gray 0.2vw 0.2vw 0.4vw; flex-direction: column; background: whitesmoke; padding-top: 10px; @@ -14,17 +14,17 @@ } // .contextMenu-item:first-child { -// background: $intermediate-color; -// color: $light-color; +// background: $medium-gray; +// color: $white; // } // .contextMenu-item:first-child::placeholder { -// color: $light-color; +// color: $white; // } // .contextMenu-item:first-child:hover { -// background: $intermediate-color; -// color: $light-color; +// background: $medium-gray; +// color: $white; // } .contextMenu-subMenu-cont { @@ -94,7 +94,7 @@ .contextMenu-item:hover { border-width: .11px; border-style: none; - border-color: $intermediate-color; // rgb(187, 186, 186); + border-color: $medium-gray; // rgb(187, 186, 186); border-bottom-style: solid; border-top-style: solid; @@ -122,7 +122,7 @@ transition: all .1s; border-width: .11px; border-style: none; - border-color: $intermediate-color; // rgb(187, 186, 186); + border-color: $medium-gray; // rgb(187, 186, 186); // padding: 10px 0px 10px 0px; white-space: nowrap; font-size: 13px; @@ -137,7 +137,7 @@ .contextMenu-item:hover { transition: all 0.1s ease; - background: $lighter-alt-accent; + background: $light-blue; } .contextMenu-description { diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx index d96de72e3..c4fabbf99 100644 --- a/src/client/views/ContextMenu.tsx +++ b/src/client/views/ContextMenu.tsx @@ -218,7 +218,7 @@ export class ContextMenu extends React.Component { @computed get menuItems() { if (!this._searchString) { - return this._items.map(item => <ContextMenuItem {...item} noexpand={this.itemsNeedSearch ? true : (item as any).noexpand} key={item.description} closeMenu={this.closeMenu} />); + return this._items.map((item, ind) => <ContextMenuItem {...item} noexpand={this.itemsNeedSearch ? true : (item as any).noexpand} key={ind + item.description} closeMenu={this.closeMenu} />); } return this.filteredViews; } diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index a878a7afb..33dff9da5 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -1,17 +1,17 @@ -import { Doc, Opt, DataSym, AclReadonly, AclAddonly, AclPrivate, AclEdit, AclSym, DocListCastAsync, DocListCast, AclAdmin } from '../../fields/Doc'; -import { Touchable } from './Touchable'; -import { computed, action, observable } from 'mobx'; -import { Cast, BoolCast, ScriptCast } from '../../fields/Types'; +import { action, computed, observable } from 'mobx'; +import { DateField } from '../../fields/DateField'; +import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, AclSym, DataSym, Doc, DocListCast, Opt } from '../../fields/Doc'; import { InkTool } from '../../fields/InkField'; -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 { CurrentUserUtils } from '../util/CurrentUserUtils'; -import { DocUtils } from '../documents/Documents'; +import { Cast, ScriptCast } from '../../fields/Types'; +import { denormalizeEmail, distributeAcls, GetEffectiveAcl, inheritParentAcls, SharingPermissions } from '../../fields/util'; import { returnFalse } from '../../Utils'; +import { DocUtils } from '../documents/Documents'; +import { CurrentUserUtils } from '../util/CurrentUserUtils'; +import { InteractionUtils } from '../util/InteractionUtils'; import { UndoManager } from '../util/UndoManager'; +import { Touchable } from './Touchable'; /// DocComponent returns a generic React base class used by views that don't have 'fieldKey' props (e.g.,CollectionFreeFormDocumentView, DocumentView) @@ -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,19 +107,13 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T // key where data is stored @computed get fieldKey() { return this.props.fieldKey; } - private AclMap = new Map<symbol, string>([ - [AclPrivate, SharingPermissions.None], - [AclReadonly, SharingPermissions.View], - [AclAddonly, SharingPermissions.Add], - [AclEdit, SharingPermissions.Edit], - [AclAdmin, SharingPermissions.Admin] - ]); + isAnyChildContentActive = () => this._isAnyChildContentActive; lookupField = (field: string) => ScriptCast((this.layoutDoc as any).lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field }).result; styleFromLayoutString = (scale: number) => { const style: { [key: string]: any } = {}; - const divKeys = ["width", "height", "fontSize", "left", "background", "top", "pointerEvents", "position"]; + const divKeys = ["width", "height", "fontSize", "transform", "left", "background", "left", "right", "top", "bottom", "pointerEvents", "position"]; const replacer = (match: any, expr: string, offset: any, string: any) => { // bcz: this executes a script to convert a property expression string: { script } into a value return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: "number" })?.script.run({ self: this.rootDoc, this: this.layoutDoc, scale }).result as string || ""; }; @@ -132,13 +126,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); @@ -154,7 +148,10 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T leavePushpin && DocUtils.LeavePushpin(doc, annotationKey ?? this.annotationKey); Doc.RemoveDocFromList(targetDataDoc, annotationKey ?? this.annotationKey, doc); doc.context = undefined; - recent && Doc.AddDocToList(recent, "data", doc, undefined, true, true); + if (recent) { + Doc.RemoveDocFromList(recent, "data", doc); + Doc.AddDocToList(recent, "data", doc, undefined, true, true); + } }); this.props.select(false); return true; @@ -198,17 +195,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); }); @@ -219,10 +215,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())); } @@ -232,10 +230,6 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T } whenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged(this._isAnyChildContentActive = isActive)); - isContentActive = (outsideReaction?: boolean) => (CurrentUserUtils.SelectedTool !== InkTool.None || - (this.props.isContentActive?.() || this.props.Document.forceActive || - this.props.isSelected(outsideReaction) || this._isAnyChildContentActive || - this.props.rootSelected(outsideReaction)) ? true : false) } return Component; }
\ No newline at end of file diff --git a/src/client/views/DocumentButtonBar.scss b/src/client/views/DocumentButtonBar.scss index 09ae14016..a112f4745 100644 --- a/src/client/views/DocumentButtonBar.scss +++ b/src/client/views/DocumentButtonBar.scss @@ -1,4 +1,4 @@ -@import "globalCssVariables"; +@import "global/globalCssVariables"; $linkGap : 3px; @@ -7,13 +7,13 @@ $linkGap : 3px; } .documentButtonBar-linkButton-empty:hover { - background: $main-accent; + background: $medium-gray; transform: scale(1.05); cursor: pointer; } .documentButtonBar-linkButton-nonempty:hover { - background: $main-accent; + background: $medium-gray; transform: scale(1.05); cursor: pointer; } @@ -25,8 +25,8 @@ $linkGap : 3px; border-radius: 50%; opacity: 0.9; pointer-events: auto; - background-color: $dark-color; - color: $light-color; + background-color: $dark-gray; + color: $white; text-transform: uppercase; letter-spacing: 2px; font-size: 75%; @@ -37,39 +37,60 @@ $linkGap : 3px; align-items: center; &:hover { - background: $main-accent; + background: $medium-gray; transform: scale(1.05); cursor: pointer; } } .documentButtonBar { - margin-top: $linkGap; - grid-column: 1/4; - width: max-content; - height: auto; display: flex; flex-direction: row; } .documentButtonBar-button { - pointer-events: auto; - padding-right: 5px; - width: 25px; + cursor: pointer; + display: flex; + width: 30px; + height: 30px; + align-content: center; + justify-content: center; + align-items: center; } +// depracated (now use .documentButtonBar-icon) for standard buttons .documentButtonBar-linker { height: 20px; width: 20px; text-align: center; border-radius: 50%; pointer-events: auto; - background-color: $dark-color; + background-color: $dark-gray; + border: none; + transition: 0.2s ease all; + + &:hover { + background-color: $medium-gray; + } +} + +.documentButtonBar-icon { + height: 80%; + width: 80%; + font-size: 100%; + text-align: center; + border-radius: 50%; + pointer-events: auto; + background-color: $dark-gray; border: none; transition: 0.2s ease all; + display: flex; + align-content: center; + justify-content: center; + align-items: center; &:hover { - background-color: $main-accent; + background-color: $black; } } diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index a5d80cd22..5f09a322c 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -3,7 +3,7 @@ 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 } from "../../fields/Doc"; +import { Doc, DocCastAsync } from "../../fields/Doc"; import { RichTextField } from '../../fields/RichTextField'; import { Cast, NumCast, StrCast } from "../../fields/Types"; import { emptyFunction, setupMoveUpEvents, simulateMouseClick } from "../../Utils"; @@ -24,7 +24,7 @@ import { DocumentView } from './nodes/DocumentView'; import { GoogleRef } from "./nodes/formattedText/FormattedTextBox"; import { TemplateMenu } from "./TemplateMenu"; import React = require("react"); -import { PresBox } from './nodes/PresBox'; +import { PresBox } from './nodes/trails/PresBox'; import { undoBatch } from '../util/UndoManager'; import { CollectionViewType } from './collections/CollectionView'; const higflyout = require("@hig/flyout"); @@ -110,7 +110,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV const animation = this.isAnimatingPulse ? "shadow-pulse 1s linear infinite" : "none"; return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{`${published ? "Push" : "Publish"} to Google Docs`}</div></>}> <div - className="documentButtonBar-linker" + className="documentButtonBar-button" style={{ animation }} onClick={async () => { await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(); @@ -139,7 +139,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV return !targetDoc || !dataDoc || !dataDoc[GoogleRef] ? (null) : <Tooltip title={<><div className="dash-tooltip">{title}</div></>}> - <div className="documentButtonBar-linker" + <div className="documentButtonBar-button" style={{ backgroundColor: this.pullColor }} onPointerEnter={action(e => { if (e.altKey) { @@ -188,8 +188,8 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV const targetDoc = this.view0?.props.Document; return !targetDoc ? (null) : <Tooltip title={ <div className="dash-tooltip">{"follow primary link on click"}</div>}> - <div className="documentButtonBar-linker" - style={{ color: targetDoc.isLinkButton ? "black" : "white", backgroundColor: targetDoc.isLinkButton ? "white" : "black" }} + <div className="documentButtonBar-icon" + style={{ color: targetDoc.isLinkButton ? "black" : "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> @@ -200,7 +200,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV const targetDoc = this.view0?.props.Document; return !targetDoc ? (null) : <Tooltip title={ <div className="dash-tooltip">{SelectionManager.Views().length > 1 ? "Pin multiple documents to presentation" : "Pin to presentation"}</div>}> - <div className="documentButtonBar-linker" + <div className="documentButtonBar-icon" style={{ color: "white" }} onClick={undoBatch(e => this.props.views().map(view => view && TabDocView.PinDoc(view.props.Document, { setPosition: e.shiftKey ? true : undefined })))}> <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" /> @@ -243,7 +243,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV const presPinWithViewIcon = <img src="/assets/pinWithView.png" style={{ margin: "auto", width: 17, transform: 'translate(0, 1px)' }} />; const targetDoc = this.view0?.props.Document; return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Pin with current view"}</div></>}> - <div className="documentButtonBar-linker" onClick={() => this.pinWithView(targetDoc)}> + <div className="documentButtonBar-icon" onClick={() => this.pinWithView(targetDoc)}> {presPinWithViewIcon} </div> </Tooltip>; @@ -253,8 +253,8 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV get shareButton() { const targetDoc = this.view0?.props.Document; return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Open Sharing Manager"}</div></>}> - <div className="documentButtonBar-linker" style={{ color: "white" }} onClick={e => SharingManager.Instance.open(this.view0, targetDoc)}> - <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="users" /> + <div className="documentButtonBar-icon" style={{ color: "white" }} onClick={e => SharingManager.Instance.open(this.view0, targetDoc)}> + <FontAwesomeIcon className="documentdecorations-icon" icon="users" /> </div></Tooltip >; } @@ -262,8 +262,8 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV get menuButton() { const targetDoc = this.view0?.props.Document; return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{`Open Context Menu`}</div></>}> - <div className="documentButtonBar-linker" style={{ color: "white", cursor: "pointer" }} onClick={e => this.openContextMenu(e)}> - <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="bars" /> + <div className="documentButtonBar-icon" style={{ color: "white", cursor: "pointer" }} onClick={e => this.openContextMenu(e)}> + <FontAwesomeIcon className="documentdecorations-icon" icon="bars" /> </div></Tooltip >; } @@ -271,9 +271,9 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV get moreButton() { const targetDoc = this.view0?.props.Document; return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{`${CurrentUserUtils.propertiesWidth > 0 ? "Close" : "Open"} Properties Panel`}</div></>}> - <div className="documentButtonBar-linker" style={{ color: "white", cursor: "e-resize" }} onClick={action(e => + <div className="documentButtonBar-icon" style={{ color: "white", cursor: "e-resize" }} onClick={action(e => CurrentUserUtils.propertiesWidth = CurrentUserUtils.propertiesWidth > 0 ? 0 : 250)}> - <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="ellipsis-h" + <FontAwesomeIcon className="documentdecorations-icon" icon="ellipsis-h" /> </div></Tooltip >; } @@ -286,7 +286,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={<MetadataEntryMenu docs={this.props.views().filter(dv => dv).map(dv => dv!.props.Document)} suggestWithFunction /> /* tfs: @bcz This might need to be the data document? */}> <div className={"documentButtonBar-linkButton-" + "empty"} onPointerDown={e => e.stopPropagation()} > - {<FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="sm" />} + {<FontAwesomeIcon className="documentdecorations-icon" icon="tag" />} </div> </Flyout> </div></Tooltip>; @@ -348,16 +348,17 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV if (!this.view0) return (null); const isText = this.view0.props.Document[this.view0.LayoutFieldKey] instanceof RichTextField; + const doc = this.view0?.props.Document; const considerPull = isText && this.considerGoogleDocsPull; const considerPush = isText && this.considerGoogleDocsPush; return <div className="documentButtonBar"> <div className="documentButtonBar-button"> <DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={true} /> </div> - {DocumentLinksButton.StartLink || !Doc.UserDoc()["documentLinksButton-fullMenu"] ? <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"> + {/*!Doc.UserDoc()["documentLinksButton-fullMenu"] ? (null) : <div className="documentButtonBar-button"> {this.templateButton} </div> /*<div className="documentButtonBar-button"> diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index db2d56aa8..316f63240 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -1,4 +1,4 @@ -@import "globalCssVariables"; +@import "global/globalCssVariables"; $linkGap : 3px; @@ -49,7 +49,7 @@ $linkGap : 3px; .documentDecorations-bottomResizer, .documentDecorations-rightResizer { pointer-events: auto; - background: $alt-accent; + background: $medium-gray; opacity: 0.1; &:hover { opacity: 1; @@ -251,19 +251,18 @@ $linkGap : 3px; } .linkButton-empty:hover { - background: $main-accent; + background: $medium-gray; transform: scale(1.05); cursor: pointer; } .linkButton-nonempty:hover { - background: $main-accent; + background: $medium-gray; transform: scale(1.05); cursor: pointer; } .link-button-container { - padding: $linkGap; border-radius: 10px; width: max-content; height: auto; @@ -271,7 +270,10 @@ $linkGap : 3px; flex-direction: row; z-index: 998; position: absolute; - background: $alt-accent; + justify-content: center; + align-items: center; + gap: 5px; + background: $medium-gray; } .linkButtonWrapper { @@ -286,8 +288,8 @@ $linkGap : 3px; text-align: center; border-radius: 50%; pointer-events: auto; - color: $dark-color; - border: $dark-color 1px solid; + color: $dark-gray; + border: $dark-gray 1px solid; } .linkButton-linker:hover { @@ -302,8 +304,8 @@ $linkGap : 3px; border-radius: 50%; opacity: 0.9; pointer-events: auto; - background-color: $dark-color; - color: $light-color; + background-color: $dark-gray; + color: $white; text-transform: uppercase; letter-spacing: 2px; font-size: 75%; @@ -314,7 +316,7 @@ $linkGap : 3px; align-items: center; &:hover { - background: $main-accent; + background: $medium-gray; transform: scale(1.05); cursor: pointer; } @@ -334,7 +336,7 @@ $linkGap : 3px; } .documentdecorations-icon { - margin-top: 3px; + margin: 0px; } .templating-button, .docDecs-tagButton { @@ -343,13 +345,13 @@ $linkGap : 3px; border-radius: 50%; opacity: 0.9; font-size: 14; - background-color: $dark-color; - color: $light-color; + background-color: $dark-gray; + color: $white; text-align: center; cursor: pointer; &:hover { - background: $main-accent; + background: $medium-gray; transform: scale(1.05); } } @@ -365,7 +367,7 @@ $linkGap : 3px; width: max-content; font-family: $sans-serif; font-size: 12px; - background-color: $light-color-secondary; + background-color: $light-gray; padding: 2px 12px; list-style: none; diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index bf939d57c..118d2e7c7 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 }> { @@ -201,7 +202,7 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b (e: PointerEvent, down: number[], delta: number[]) => { const movement = { X: delta[0], Y: e.clientY - down[1] }; const angle = Math.max(1, Math.abs(movement.Y / 10)); - InkStrokeProperties.Instance?.rotate(2 * movement.X / angle * (Math.PI / 180)); + InkStrokeProperties.Instance?.rotateInk(2 * movement.X / angle * (Math.PI / 180)); return false; }, () => this._rotateUndo?.end(), @@ -235,7 +236,7 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b this._resizeUndo = UndoManager.StartBatch("DocDecs resize"); this._snapX = e.pageX; this._snapY = e.pageY; - DragManager.docsBeingDragged.forEach(doc => this._dragHeights.set(doc, { start: NumCast(doc._height), lowest: NumCast(doc._height) })); + SelectionManager.Views().forEach(docView => this._dragHeights.set(docView.layoutDoc, { start: NumCast(docView.rootDoc._height), lowest: NumCast(docView.rootDoc._height) })); } onPointerMove = (e: PointerEvent, down: number[], move: number[]): boolean => { @@ -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)) }); @@ -382,7 +384,7 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b SnappingManager.clearSnapLines(); // detect autoHeight gesture and apply - DragManager.docsBeingDragged.map(doc => ({ doc, hgts: this._dragHeights.get(doc) })) + SelectionManager.Views().map(docView => ({ doc: docView.layoutDoc, hgts: this._dragHeights.get(docView.layoutDoc) })) .filter(pair => pair.hgts && pair.hgts.lowest < pair.hgts.start && pair.hgts.lowest <= 20) .forEach(pair => pair.doc._autoHeight = true); //need to change points for resize, or else rotation/control points will fail. 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/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index 491bf18b2..bbf21f22c 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -634,7 +634,7 @@ export class GestureOverlay extends Touchable { } else { this._points = []; } - CollectionFreeFormViewChrome.Instance.primCreated(); + CollectionFreeFormViewChrome.Instance?.primCreated(); } makePolygon = (shape: string, gesture: boolean) => { @@ -726,39 +726,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 cbaa706e0..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"; @@ -89,8 +88,6 @@ export class KeyManager { private unmodified = action((keyname: string, e: KeyboardEvent) => { switch (keyname) { - case "a": SnappingManager.GetIsDragging() && (DragManager.CanEmbed = true); - break; case "u": if (document.activeElement?.tagName === "INPUT" || document.activeElement?.tagName === "TEXTAREA") { return { stopPropagation: false, preventDefault: false }; @@ -116,7 +113,7 @@ export class KeyManager { case "escape": DocumentLinksButton.StartLink = undefined; DocumentLinksButton.StartLinkView = undefined; - InkStrokeProperties.Instance && (InkStrokeProperties.Instance._controlBtn = false); + InkStrokeProperties.Instance && (InkStrokeProperties.Instance._controlButton = false); CurrentUserUtils.SelectedTool = InkTool.None; var doDeselect = true; if (SnappingManager.GetIsDragging()) { @@ -225,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 new file mode 100644 index 000000000..6213a4075 --- /dev/null +++ b/src/client/views/InkControls.tsx @@ -0,0 +1,142 @@ +import React = require("react"); +import { observable, action } from "mobx"; +import { observer } from "mobx-react"; +import { InkStrokeProperties } from "./InkStrokeProperties"; +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[]; + ScreenToLocalTransform: () => Transform; +} + +@observer +export class InkControls extends React.Component<InkControlProps> { + @observable private _overControl = -1; + @observable private _overAddPoint = -1; + + /** + * Handles the movement of a selected control point when the user clicks and drags. + * @param controlIndex The index of the currently selected control point. + */ + @action + onControlDown = (e: React.PointerEvent, controlIndex: number): void => { + if (InkStrokeProperties.Instance) { + InkStrokeProperties.Instance.moveControl(0, 0, 1); + 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 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(), + action((e: PointerEvent, doubleTap: boolean | undefined) => { + if (doubleTap && brokenIndices && brokenIndices.includes(controlIndex)) { + InkStrokeProperties.Instance?.snapHandleTangent(controlIndex, handleIndexA, handleIndexB); + } + })); + } + } + + /** + * Deletes the currently selected point. + */ + @action + onDelete = (e: KeyboardEvent) => { + if (["-", "Backspace", "Delete"].includes(e.key)) { + if (InkStrokeProperties.Instance?.deletePoints()) e.stopPropagation(); + } + } + + /** + * Changes the current selected control point. + */ + @action + changeCurrPoint = (i: number) => { + if (InkStrokeProperties.Instance) { + InkStrokeProperties.Instance._currentPoint = i; + document.addEventListener("keydown", this.onDelete, true); + } + } + + /** + * Updates whether a user has hovered over a particular control point or point that could be added + * on click. + */ + @action onEnterControl = (i: number) => { this._overControl = i; }; + @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); + + // Accessing the current ink's data and extracting all control points. + const data = this.props.data; + const controlPoints: ControlPoint[] = []; + if (data.length >= 4) { + for (let i = 0; i <= data.length - 4; i += 4) { + controlPoints.push({ X: data[i].X, Y: data[i].Y, I: i }); + controlPoints.push({ X: data[i + 3].X, Y: data[i + 3].Y, I: i + 3 }); + } + } + const addedPoints = this.props.addedPoints; + 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 / 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={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> + )} + </> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/InkHandles.tsx b/src/client/views/InkHandles.tsx new file mode 100644 index 000000000..0b24c3c32 --- /dev/null +++ b/src/client/views/InkHandles.tsx @@ -0,0 +1,127 @@ +import React = require("react"); +import { observable, action } from "mobx"; +import { observer } from "mobx-react"; +import { InkStrokeProperties } from "./InkStrokeProperties"; +import { setupMoveUpEvents, emptyFunction } from "../../Utils"; +import { UndoManager } from "../util/UndoManager"; +import { InkData, HandlePoint, HandleLine } from "../../fields/InkField"; +import { Transform } from "../util/Transform"; +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; +} + +@observer +export class InkHandles extends React.Component<InkHandlesProps> { + /** + * Handles the movement of a selected handle point when the user clicks and drags. + * @param handleNum The index of the currently selected handle point. + */ + onHandleDown = (e: React.PointerEvent, handleIndex: number): void => { + if (InkStrokeProperties.Instance) { + InkStrokeProperties.Instance.moveControl(0, 0, 1); + const controlUndo = UndoManager.StartBatch("DocDecs set radius"); + const screenScale = this.props.ScreenToLocalTransform().Scale; + const order = handleIndex % 4; + const oppositeHandleIndex = order === 1 ? handleIndex - 3 : handleIndex + 3; + const controlIndex = order === 1 ? handleIndex - 1 : handleIndex + 2; + document.addEventListener("keydown", (e: KeyboardEvent) => this.onBreakTangent(e, controlIndex), true); + setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => { + InkStrokeProperties.Instance?.moveHandle(-delta[0] * screenScale, -delta[1] * screenScale, handleIndex, oppositeHandleIndex, controlIndex); + return false; + }, () => controlUndo?.end(), emptyFunction + ); + } + } + + /** + * Breaks tangent handle movement when ‘Alt’ key is held down. Adds the current handle index and + * its matching (opposite) handle to a list of broken handle indices. + * @param handleNum The index of the currently selected handle point. + */ + @action + 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)) { + brokenIndices.push(controlIndex); + } + doc.brokenInkIndices = brokenIndices; + } + } + } + + render() { + const formatInstance = InkStrokeProperties.Instance; + if (!formatInstance) return (null); + + // 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) { + for (let i = 0; i <= data.length - 4; i += 4) { + handlePoints.push({ X: data[i + 1].X, Y: data[i + 1].Y, I: i + 1, dot1: i, dot2: i === 0 ? i : i - 1 }); + handlePoints.push({ X: data[i + 2].X, Y: data[i + 2].Y, I: i + 2, dot1: i + 3, dot2: i === data.length ? i + 3 : i + 4 }); + } + // Adding first and last (single) handle lines. + handleLines.push({ X1: data[0].X, Y1: data[0].Y, X2: data[0].X, Y2: data[0].Y, X3: data[1].X, Y3: data[1].Y, dot1: 0, dot2: 0 }); + handleLines.push({ X1: data[data.length - 2].X, Y1: data[data.length - 2].Y, X2: data[data.length - 1].X, Y2: data[data.length - 1].Y, X3: data[data.length - 1].X, Y3: data[data.length - 1].Y, dot1: data.length - 1, dot2: data.length - 1 }); + for (let i = 2; i < data.length - 4; i += 4) { + 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] = this.props.format; + + return ( + <> + {handlePoints.map((pts, i) => + <svg height="10" width="10" key={`hdl${i}`}> + <circle + cx={(pts.X - left - strokeWidth / 2) * scaleX + strokeWidth / 2} + cy={(pts.Y - top - strokeWidth / 2) * scaleY + strokeWidth / 2} + r={strokeWidth / 2} + strokeWidth={0} + fill={Colors.MEDIUM_BLUE} + onPointerDown={(e) => this.onHandleDown(e, pts.I)} + pointerEvents="all" + cursor="default" + display={(pts.dot1 === formatInstance._currentPoint || pts.dot2 === formatInstance._currentPoint) ? "inherit" : "none"} /> + </svg>)} + {handleLines.map((pts, i) => + <svg height="100" width="100" key={`line${i}`}> + <line + x1={(pts.X1 - left - strokeWidth / 2) * scaleX + strokeWidth / 2} + y1={(pts.Y1 - top - strokeWidth / 2) * scaleY + strokeWidth / 2} + x2={(pts.X2 - left - strokeWidth / 2) * scaleX + strokeWidth / 2} + y2={(pts.Y2 - top - strokeWidth / 2) * scaleY + strokeWidth / 2} + stroke={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={Colors.MEDIUM_BLUE} + strokeWidth={strokeWidth / 4} + display={(pts.dot1 === formatInstance._currentPoint || pts.dot2 === formatInstance._currentPoint) ? "inherit" : "none"} /> + </svg>)} + </> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/InkStroke.scss b/src/client/views/InkStroke.scss new file mode 100644 index 000000000..812a79bd5 --- /dev/null +++ b/src/client/views/InkStroke.scss @@ -0,0 +1,11 @@ +.inkStroke { + mix-blend-mode: multiply; + stroke-linejoin: round; + stroke-linecap: round; + overflow: visible !important; + transform-origin: top left; + + svg:not(:root) { + overflow: visible !important; + } +} diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index b13b04f68..d527b2a05 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -1,124 +1,44 @@ import { action, computed, observable } from "mobx"; -import { ColorState } from 'react-color'; -import { Doc, Field, Opt } from "../../fields/Doc"; +import { Doc, DocListCast, Field, Opt } from "../../fields/Doc"; import { Document } from "../../fields/documentSchemas"; -import { InkField, InkData } from "../../fields/InkField"; +import { InkField, InkData, PointData, ControlPoint } from "../../fields/InkField"; +import { List } from "../../fields/List"; +import { listSpec } from "../../fields/Schema"; import { Cast, NumCast } from "../../fields/Types"; import { DocumentType } from "../documents/DocumentTypes"; import { SelectionManager } from "../util/SelectionManager"; import { undoBatch } from "../util/UndoManager"; -import { bool } from "sharp"; export class InkStrokeProperties { static Instance: InkStrokeProperties | undefined; - private _lastFill = "#D0021B"; - private _lastLine = "#D0021B"; - private _lastDash = "2"; - private _inkDocs: { x: number, y: number, width: number, height: number }[] = []; - @observable _lock = false; - @observable _controlBtn = false; - @observable _currPoint = -1; + @observable _controlButton = false; + @observable _currentPoint = -1; - getField(key: string) { - return this.selectedInk?.reduce((p, i) => - (p === undefined || (p && p === i.rootDoc[key])) && i.rootDoc[key] !== "0" ? Field.toString(i.rootDoc[key] as Field) : "", undefined as Opt<string>); + constructor() { + InkStrokeProperties.Instance = this; } @computed get selectedInk() { const inks = SelectionManager.Views().filter(i => Document(i.rootDoc).type === DocumentType.INK); return inks.length ? inks : undefined; } - @computed get unFilled() { return this.selectedInk?.reduce((p, i) => p && !i.rootDoc.fillColor ? true : false, true) || false; } - @computed get unStrokd() { return this.selectedInk?.reduce((p, i) => p && !i.rootDoc.color ? true : false, true) || false; } - @computed get solidFil() { return this.selectedInk?.reduce((p, i) => p && i.rootDoc.fillColor ? true : false, true) || false; } - @computed get solidStk() { return this.selectedInk?.reduce((p, i) => p && i.rootDoc.color && (!i.rootDoc.strokeDash || i.rootDoc.strokeDash === "0") ? true : false, true) || false; } - @computed get dashdStk() { return !this.unStrokd && this.getField("strokeDash") || ""; } - @computed get colorFil() { const ccol = this.getField("fillColor") || ""; ccol && (this._lastFill = ccol); return ccol; } - @computed get colorStk() { const ccol = this.getField("color") || ""; ccol && (this._lastLine = ccol); return ccol; } - @computed get widthStk() { return this.getField("strokeWidth") || "1"; } - @computed get markHead() { return this.getField("strokeStartMarker") || ""; } - @computed get markTail() { return this.getField("strokeEndMarker") || ""; } - @computed get shapeHgt() { return this.getField("_height"); } - @computed get shapeWid() { return this.getField("_width"); } - @computed get shapeXps() { return this.getField("x"); } - @computed get shapeYps() { return this.getField("y"); } - @computed get shapeRot() { return this.getField("rotation"); } - set unFilled(value) { this.colorFil = value ? "" : this._lastFill; } - set solidFil(value) { this.unFilled = !value; } - set colorFil(value) { value && (this._lastFill = value); this.selectedInk?.forEach(i => i.rootDoc.fillColor = value ? value : undefined); } - set colorStk(value) { value && (this._lastLine = value); this.selectedInk?.forEach(i => i.rootDoc.color = value ? value : undefined); } - set markHead(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeStartMarker = value); } - set markTail(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeEndMarker = value); } - set unStrokd(value) { this.colorStk = value ? "" : this._lastLine; } - set solidStk(value) { this.dashdStk = ""; this.unStrokd = !value; } - set dashdStk(value) { - value && (this._lastDash = value) && (this.unStrokd = false); - this.selectedInk?.forEach(i => i.rootDoc.strokeDash = value ? this._lastDash : undefined); - } - set shapeXps(value) { this.selectedInk?.forEach(i => i.rootDoc.x = Number(value)); } - set shapeYps(value) { this.selectedInk?.forEach(i => i.rootDoc.y = Number(value)); } - set shapeRot(value) { this.selectedInk?.forEach(i => i.rootDoc.rotation = Number(value)); } - set widthStk(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeWidth = Number(value)); } - set shapeWid(value) { - this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => { - const oldWidth = NumCast(i.rootDoc._width); - i.rootDoc._width = Number(value); - this._lock && (i.rootDoc._height = (i.rootDoc._width * NumCast(i.rootDoc._height)) / oldWidth); - }); - } - set shapeHgt(value) { - this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => { - const oldHeight = NumCast(i.rootDoc._height); - i.rootDoc._height = Number(value); - this._lock && (i.rootDoc._width = (i.rootDoc._height * NumCast(i.rootDoc._width)) / oldHeight); - }); - } - - constructor() { - InkStrokeProperties.Instance = this; - } - @undoBatch - @action - addPoints = (x: number, y: number, pts: { X: number, Y: number }[], index: number, control: { X: number, Y: number }[]) => { - this.selectedInk?.forEach(action(inkView => { - if (this.selectedInk?.length === 1) { - const doc = Document(inkView.rootDoc); - if (doc.type === DocumentType.INK) { - const ink = Cast(doc.data, InkField)?.inkData; - if (ink) { - const newPoints: { X: number, Y: number }[] = []; - var counter = 0; - for (var k = 0; k < index; k++) { - control.forEach(pt => (pts[k].X === pt.X && pts[k].Y === pt.Y) && counter++); - } - //decide where to put the new coordinate - const spNum = Math.floor(counter / 2) * 4 + 2; - - for (var i = 0; i < spNum; i++) { - ink[i] && newPoints.push({ X: ink[i].X, Y: ink[i].Y }); - } - for (var j = 0; j < 4; j++) { - newPoints.push({ X: x, Y: y }); - - } - for (var i = spNum; i < ink.length; i++) { - newPoints.push({ X: ink[i].X, Y: ink[i].Y }); - } - this._currPoint = -1; - Doc.GetProto(doc).data = new InkField(newPoints); - } - } - } - })); + getField(key: string) { + return this.selectedInk?.reduce((p, i) => + (p === undefined || (p && p === i.rootDoc[key])) && i.rootDoc[key] !== "0" ? Field.toString(i.rootDoc[key] as Field) : "", undefined as Opt<string>); } + /** + * Helper function that enables other functions to be applied to a particular ink instance. + * @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) => { var appliedFunc = false; this.selectedInk?.forEach(action(inkView => { - if (this.selectedInk?.length === 1 && (!requireCurrPoint || this._currPoint !== -1)) { + if (this.selectedInk?.length === 1 && (!requireCurrPoint || this._currentPoint !== -1)) { const doc = Document(inkView.rootDoc); if (doc.type === DocumentType.INK && doc.width && doc.height) { const ink = Cast(doc.data, InkField)?.inkData; @@ -145,17 +65,136 @@ export class InkStrokeProperties { return appliedFunc; } + /** + * Adds a new control point to the ink instance when editing its format. + * @param index The index of the new point. + * @param control The list of all control points of the ink. + */ + @undoBatch + @action + addPoints = (x: number, y: number, points: InkData, index: number, controls: { X: number, Y: number }[]) => { + this.applyFunction((doc: Doc, ink: InkData) => { + const newControl = { X: x, Y: y }; + const newPoints: InkData = []; + let [counter, start, end] = [0, 0, 0]; + for (let k = 0; k < points.length; k++) { + if (end === 0) { + controls.forEach((control) => { + if (control.X === points[k].X && control.Y === points[k].Y) { + if (k < index) { + counter++; + start = k; + } else if (k > index) { + end = k; + } + } + }); + } + } + 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); + 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 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) { + // Adjusting the magnitude of the right handle line of the left neighboring control point. + const [leftControl, leftHandle] = [points[start], ink[i]]; + const scaledVector = this.getScaledHandlePoint(true, start, end, index, leftControl, leftHandle); + leftHandle && newPoints.push({ X: leftControl.X - scaledVector.X, Y: leftControl.Y - scaledVector.Y }); + } 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) => { + if (control >= newIndex) { + return control + 4; + } else { + return control; + } + })); + } + doc.brokenInkIndices = brokenIndices; + this._currentPoint = -1; + return newPoints; + }); + } + + /** + * Scales a handle point of a control point that is adjacent to a newly added one. + * @param isLeft Determines if the current control point is on the left or right side of the newly added one. + * @param start Beginning index of curve from the left control point to the newly added one. + * @param end Final index of curve from the newly added control point to its right neighbor. + */ + getScaledHandlePoint(isLeft: boolean, start: number, end: number, index: number, control: PointData, handle: PointData) { + const prevSize = end - start; + const newSize = isLeft ? index - start : end - index; + const handleVector = { X: control.X - handle.X, Y: control.Y - handle.Y }; + const scaledVector = { X: handleVector.X * (newSize / prevSize), Y: handleVector.Y * (newSize / prevSize) }; + return scaledVector; + } + + /** + * Determines the position of the handle points of a newly added control point by finding the + * tangent vectors to the split curve at the new control. Given the properties of Bézier curves, + * the tangent vector to a control point is equivalent to the first/last (depending on the direction + * of the curve) leg of the Bézier curve's derivative. + * (Source: https://pages.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/Bezier/bezier-der.html) + * + * @param C The curve represented by all points from the previous control until the newly added point. + * @param D The curve represented by all points from the newly added point to the next control. + * @param newControl The newly added control point. + */ + 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))); + // 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) { + handleSizeA *= 3; + handleSizeB *= 3; + } + if (Math.abs(handleSizeA - handleSizeB) < 50) { + handleSizeA *= 5; + handleSizeB *= 5; + } else if (Math.abs(handleSizeA - handleSizeB) < 150) { + handleSizeA *= 2; + 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) }; + // 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 }; + const handleB = { X: newControl.X + dD.X, Y: newControl.Y + dD.Y }; + return [handleA, handleB]; + } + + /** + * Deletes the current control point of the selected ink instance. + */ @undoBatch @action deletePoints = () => this.applyFunction((doc: Doc, ink: InkData) => { - var newPoints: { X: number, Y: number }[] = []; - const toRemove = Math.floor(((this._currPoint + 2) / 4)); - for (var i = 0; i < ink.length; i++) { + const newPoints: { X: number, Y: number }[] = []; + const toRemove = Math.floor(((this._currentPoint + 2) / 4)); + for (let i = 0; i < ink.length; i++) { if (Math.floor((i + 2) / 4) !== toRemove && (toRemove !== 0 || i > 3)) { newPoints.push({ X: ink[i].X, Y: ink[i].Y }); } } - this._currPoint = -1; + this._currentPoint = -1; if (newPoints.length < 4) return undefined; if (newPoints.length === 4) { const newerPoints: { X: number, Y: number }[] = []; @@ -166,12 +205,16 @@ export class InkStrokeProperties { return newerPoints; } return newPoints; - }, true); + }, true) + /** + * Rotates the entire selected ink instance. + * @param angle The angle at which to rotate the ink in radians. + */ @undoBatch @action - rotate = (angle: number) => { - this.applyFunction((doc: Doc, ink: InkData, ptsXscale: number, ptsYscale: number) => { + rotateInk = (angle: number) => { + this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => { const oldXrange = (xs => ({ coord: NumCast(doc.x), min: Math.min(...xs), max: Math.max(...xs) }))(ink.map(p => p.X)); const oldYrange = (ys => ({ coord: NumCast(doc.y), min: Math.min(...ys), max: Math.max(...ys) }))(ink.map(p => p.Y)); const centerPoint = { X: (oldXrange.min + oldXrange.max) / 2, Y: (oldYrange.min + oldYrange.max) / 2 }; @@ -186,42 +229,121 @@ export class InkStrokeProperties { }); } + /** + * Handles the movement/scaling of a control point. + */ @undoBatch @action - control = (xDiff: number, yDiff: number, controlNum: number) => - this.applyFunction((doc: Doc, ink: InkData, ptsXscale: number, ptsYscale: number) => { + moveControl = (deltaX: number, deltaY: number, controlIndex: number) => + this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => { const newPoints: { X: number, Y: number }[] = []; - const order = controlNum % 4; + const order = controlIndex % 4; for (var i = 0; i < ink.length; i++) { - newPoints.push( - (controlNum === i || - (order === 0 && i === controlNum + 1) || - (order === 0 && controlNum !== 0 && i === controlNum - 2) || - (order === 0 && controlNum !== 0 && i === controlNum - 1) || - (order === 3 && i === controlNum - 1) || - (order === 3 && controlNum !== ink.length - 1 && i === controlNum + 1) || - (order === 3 && controlNum !== ink.length - 1 && i === controlNum + 2) || - ((ink[0].X === ink[ink.length - 1].X) && (ink[0].Y === ink[ink.length - 1].Y) && (i === 0 || i === ink.length - 1) && (controlNum === 0 || controlNum === ink.length - 1)) - ) ? - { X: ink[i].X - xDiff / ptsXscale, Y: ink[i].Y - yDiff / ptsYscale } : - { X: ink[i].X, Y: ink[i].Y }); + const leftHandlePoint = order === 0 && i === controlIndex + 1; + const rightHandlePoint = order === 0 && controlIndex !== 0 && i === controlIndex - 2; + if (controlIndex === i || + leftHandlePoint || + rightHandlePoint || + (order === 0 && controlIndex !== 0 && i === controlIndex - 1) || + (order === 3 && i === controlIndex - 1) || + (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 1) || + (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 2) || + ((ink[0].X === ink[ink.length - 1].X) && (ink[0].Y === ink[ink.length - 1].Y) && (i === 0 || i === ink.length - 1) && (controlIndex === 0 || controlIndex === ink.length - 1))) { + newPoints.push({ X: ink[i].X - deltaX / xScale, Y: ink[i].Y - deltaY / yScale }); + } else { + newPoints.push({ X: ink[i].X, Y: ink[i].Y }); + } } 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) => { + 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; + } }); + } - @undoBatch + /** + * Rotates the target point about the origin point for a given angle (radians). + */ @action - switchStk = (color: ColorState) => { - const val = String(color.hex); - this.colorStk = val; - return true; + rotatePoint = (target: PointData, origin: PointData, angle: number) => { + 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; + } + + /** + * Finds the angle (in radians) between two inputted vectors. + * + * α = arccos(a·b / |a|·|b|), where a and b are both vectors. + */ + angleBetweenTwoVectors = (vectorA: PointData, vectorB: PointData) => { + const magnitudeA = Math.sqrt(vectorA.X * vectorA.X + vectorA.Y * vectorA.Y); + const magnitudeB = Math.sqrt(vectorB.X * vectorB.X + vectorB.Y * vectorB.Y); + // Normalizing the vectors. + vectorA = { X: vectorA.X / magnitudeA, Y: vectorA.Y / magnitudeA }; + vectorB = { X: vectorB.X / magnitudeB, Y: vectorB.Y / magnitudeB }; + const dotProduct = vectorB.X * vectorA.X + vectorB.Y * vectorA.Y; + return Math.acos(dotProduct); } + /** + * Finds the angle difference (in radians) between two vectors relative to an arbitrary origin. + */ + angleChange = (a: PointData, b: PointData, origin: PointData) => { + // Finding vector representation of inputted points relative to new origin. + const vectorA = { X: a.X - origin.X, Y: a.Y - origin.Y }; + const vectorB = { X: b.X - origin.X, Y: b.Y - origin.Y }; + const crossProduct = vectorB.X * vectorA.Y - vectorB.Y * vectorA.X; + // Determining whether rotation is clockwise or counterclockwise. + const sign = crossProduct < 0 ? 1 : -1; + const theta = this.angleBetweenTwoVectors(vectorA, vectorB); + return sign * theta; + } + + /** + * Handles the movement/scaling of a handle point. + */ @undoBatch @action - switchFil = (color: ColorState) => { - const val = String(color.hex); - this.colorFil = val; - return true; - } + moveHandle = (deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) => + this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => { + const oldHandlePoint = ink[handleIndex]; + let oppositeHandlePoint = ink[oppositeHandleIndex]; + const controlPoint = ink[controlIndex]; + const newHandlePoint = { X: ink[handleIndex].X - deltaX / xScale, Y: ink[handleIndex].Y - deltaY / yScale }; + ink[handleIndex] = newHandlePoint; + const brokenIndices = Cast(doc.brokenInkIndices, listSpec("number")); + // Rotate opposite handle if user hasn't held 'Alt' key or not first/final control (which have only 1 handle). + if ((!brokenIndices || !brokenIndices?.includes(controlIndex)) && handleIndex !== 1 && handleIndex !== ink.length - 2) { + const angle = this.angleChange(oldHandlePoint, newHandlePoint, controlPoint); + oppositeHandlePoint = this.rotatePoint(oppositeHandlePoint, controlPoint, angle); + ink[oppositeHandleIndex] = oppositeHandlePoint; + } + return ink; + }) }
\ No newline at end of file diff --git a/src/client/views/InkingStroke.scss b/src/client/views/InkingStroke.scss deleted file mode 100644 index 30ab1967e..000000000 --- a/src/client/views/InkingStroke.scss +++ /dev/null @@ -1,11 +0,0 @@ -.inkingStroke { - mix-blend-mode: multiply; - stroke-linejoin: round; - stroke-linecap: round; - overflow: visible !important; - transform-origin: top left; - - svg:not(:root) { - overflow: visible !important; - } -}
\ No newline at end of file diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 449019ca8..5fc159f14 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -1,4 +1,5 @@ -import { action } from "mobx"; +import React = require("react"); +import { action, observable } from "mobx"; import { observer } from "mobx-react"; import { Doc } from "../../fields/Doc"; import { documentSchema } from "../../fields/documentSchemas"; @@ -10,25 +11,36 @@ import { setupMoveUpEvents, emptyFunction, returnFalse } from "../../Utils"; import { CognitiveServices } from "../cognitive_services/CognitiveServices"; import { InteractionUtils } from "../util/InteractionUtils"; import { Scripting } from "../util/Scripting"; -import { UndoManager } from "../util/UndoManager"; import { ContextMenu } from "./ContextMenu"; import { ViewBoxBaseComponent } from "./DocComponent"; -import "./InkingStroke.scss"; +import "./InkStroke.scss"; import { FieldView, FieldViewProps } from "./nodes/FieldView"; -import React = require("react"); 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); @observer export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocument>(InkDocument) { - private _controlUndo?: UndoManager.Batch; + static readonly MaskDim = 50000; + @observable private _properties?: InkStrokeProperties; + + constructor(props: FieldViewProps & InkDocument) { + super(props); + + this._properties = InkStrokeProperties.Instance; + } - public static LayoutString(fieldStr: string) { return FieldView.LayoutString(InkingStroke, fieldStr); } + public static LayoutString(fieldStr: string) { + return FieldView.LayoutString(InkingStroke, fieldStr); + } - private analyzeStrokes = () => { + analyzeStrokes() { const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? []; CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], [data]); } @@ -41,149 +53,67 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume inkDoc._stayInCollection = inkDoc.isInkMask ? true : undefined; }); - @action - onControlDown = (e: React.PointerEvent, controlNum: number): void => { - if (InkStrokeProperties.Instance) { - InkStrokeProperties.Instance.control(0, 0, 1); - const controlUndo = UndoManager.StartBatch("DocDecs set radius"); - const screenScale = this.props.ScreenToLocalTransform().Scale; - setupMoveUpEvents(this, e, - (e: PointerEvent, down: number[], delta: number[]) => { - InkStrokeProperties.Instance?.control(-delta[0] * screenScale, -delta[1] * screenScale, controlNum); - return false; - }, - () => controlUndo?.end(), emptyFunction); - } - } - - @action - changeCurrPoint = (i: number) => { - if (InkStrokeProperties.Instance) { - InkStrokeProperties.Instance._currPoint = i; - document.addEventListener("keydown", this.delPts, true); + /** + * Handles the movement of the entire ink object when the user clicks and drags. + */ + onPointerDown = (e: React.PointerEvent) => { + if (this.props.isSelected(true)) { + setupMoveUpEvents(this, e, returnFalse, emptyFunction, + action((e: PointerEvent, doubleTap: boolean | undefined) => + doubleTap && this._properties && (this._properties._controlButton = true)) + ); } } + /** + * Ensures the ink controls and handles aren't rendered when the current ink stroke is reselected. + */ @action - delPts = (e: KeyboardEvent) => { - if (["-", "Backspace", "Delete"].includes(e.key)) { - if (InkStrokeProperties.Instance?.deletePoints()) e.stopPropagation(); + toggleControlButton = () => { + if (!this.props.isSelected() && this._properties) { + this._properties._controlButton = false; } } - onPointerDown = (e: React.PointerEvent) => { - if (this.props.isSelected(true)) { - setupMoveUpEvents(this, e, returnFalse, emptyFunction, action((e: PointerEvent, doubleTap: boolean | undefined) => - doubleTap && InkStrokeProperties.Instance && (InkStrokeProperties.Instance._controlBtn = true))); - } - } - - public static MaskDim = 50000; render() { TraceMobx(); - const formatInstance = InkStrokeProperties.Instance; - if (!formatInstance) return (null); + 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 strokeWidth = Number(StrCast(this.layoutDoc.strokeWidth, ActiveInkWidth())); + const inkDoc: Doc = this.layoutDoc; const strokeWidth = Number(this.layoutDoc.strokeWidth); - const xs = data.map(p => p.X); - const ys = data.map(p => p.Y); - const lineTop = Math.min(...ys); - const lineBot = Math.max(...ys); - const lineLft = Math.min(...xs); - const lineRgt = Math.max(...xs); - const left = lineLft - strokeWidth / 2; + const lineTop = Math.min(...data.map(p => p.Y)); + const lineBottom = Math.max(...data.map(p => p.Y)); + const lineLeft = Math.min(...data.map(p => p.X)); + const lineRight = Math.max(...data.map(p => p.X)); + const left = lineLeft - strokeWidth / 2; const top = lineTop - strokeWidth / 2; - const right = lineRgt + strokeWidth / 2; - const bottom = lineBot + strokeWidth / 2; + const right = lineRight + strokeWidth / 2; + const bottom = lineBottom + strokeWidth / 2; const width = Math.max(1, right - left); const height = Math.max(1, bottom - top); const scaleX = width === strokeWidth ? 1 : (this.props.PanelWidth() - strokeWidth) / (width - strokeWidth); const scaleY = height === strokeWidth ? 1 : (this.props.PanelHeight() - strokeWidth) / (height - strokeWidth); const strokeColor = StrCast(this.layoutDoc.color, ""); - - const points = 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 && lineBot - lineTop > 1 && lineRgt - lineLft > 1, false); - - const hpoints = 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); - - //points for adding - const apoints = 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 controlPoints: { X: number, Y: number, I: number }[] = []; - const handlePoints: { X: number, Y: number, I: number, dot1: number, dot2: number }[] = []; - const handleLine: { X1: number, Y1: number, X2: number, Y2: number, X3: number, Y3: number, dot1: number, dot2: number }[] = []; - if (data.length >= 4) { - for (var i = 0; i <= data.length - 4; i += 4) { - controlPoints.push({ X: data[i].X, Y: data[i].Y, I: i }); - controlPoints.push({ X: data[i + 3].X, Y: data[i + 3].Y, I: i + 3 }); - handlePoints.push({ X: data[i + 1].X, Y: data[i + 1].Y, I: i + 1, dot1: i, dot2: i === 0 ? i : i - 1 }); - handlePoints.push({ X: data[i + 2].X, Y: data[i + 2].Y, I: i + 2, dot1: i + 3, dot2: i === data.length ? i + 3 : i + 4 }); - } - - handleLine.push({ X1: data[0].X, Y1: data[0].Y, X2: data[0].X, Y2: data[0].Y, X3: data[1].X, Y3: data[1].Y, dot1: 0, dot2: 0 }); - for (var i = 2; i < data.length - 4; i += 4) { - - handleLine.push({ X1: data[i].X, Y1: data[i].Y, X2: data[i + 1].X, Y2: data[i + 1].Y, X3: data[i + 3].X, Y3: data[i + 3].Y, dot1: i + 1, dot2: i + 2 }); - - } - handleLine.push({ X1: data[data.length - 2].X, Y1: data[data.length - 2].Y, X2: data[data.length - 1].X, Y2: data[data.length - 1].Y, X3: data[data.length - 1].X, Y3: data[data.length - 1].Y, dot1: data.length - 1, dot2: data.length - 1 }); - - for (var i = 0; i <= data.length - 4; i += 4) { - handlePoints.push({ X: data[i + 1].X, Y: data[i + 1].Y, I: i + 1, dot1: i, dot2: i === 0 ? i : i - 1 }); - handlePoints.push({ X: data[i + 2].X, Y: data[i + 2].Y, I: i + 2, dot1: i + 3, dot2: i === data.length ? i + 3 : i + 4 }); - } - } - // if (data.length <= 4) { - // handlePoints = []; - // handleLine = []; - // controlPoints = []; - // for (var i = 0; i < data.length; i++) { - // controlPoints.push({ X: data[i].X, Y: data[i].Y, I: i }); - // } - - // } const dotsize = Math.max(width * scaleX, height * scaleY) / 40; - const addpoints = apoints.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="invisible" strokeWidth={dotsize / 2} fill="invisible" - onPointerDown={(e) => { formatInstance.addPoints(pts.X, pts.Y, apoints, i, controlPoints); }} pointerEvents="all" cursor="all-scroll" - /> - </svg>); - const handles = handlePoints.map((pts, i) => - <svg height="10" width="10" key={`hdl${i}`}> - <circle cx={(pts.X - left - strokeWidth / 2) * scaleX + strokeWidth / 2} cy={(pts.Y - top - strokeWidth / 2) * scaleY + strokeWidth / 2} r={strokeWidth} strokeWidth={0} fill="green" - onPointerDown={(e) => this.onControlDown(e, pts.I)} pointerEvents="all" cursor="default" display={(pts.dot1 === formatInstance._currPoint || pts.dot2 === formatInstance._currPoint) ? "inherit" : "none"} /> - </svg>); - - const controls = controlPoints.map((pts, i) => - <svg height="10" width="10" key={`ctrl${i}`}> - <circle cx={(pts.X - left - strokeWidth / 2) * scaleX + strokeWidth / 2} cy={(pts.Y - top - strokeWidth / 2) * scaleY + strokeWidth / 2} r={strokeWidth / 2} strokeWidth={0} fill="red" - onPointerDown={(e) => { this.changeCurrPoint(pts.I); this.onControlDown(e, pts.I); }} pointerEvents="all" cursor="default" - /> - </svg>); - const handleLines = handleLine.map((pts, i) => - <svg height="100" width="100" key={`line${i}`} > - <line x1={(pts.X1 - left - strokeWidth / 2) * scaleX + strokeWidth / 2} y1={(pts.Y1 - top - strokeWidth / 2) * scaleY + strokeWidth / 2} - x2={(pts.X2 - left - strokeWidth / 2) * scaleX + strokeWidth / 2} y2={(pts.Y2 - top - strokeWidth / 2) * scaleY + strokeWidth / 2} stroke="green" strokeWidth={dotsize / 6} - display={(pts.dot1 === formatInstance._currPoint || pts.dot2 === formatInstance._currPoint) ? "inherit" : "none"} /> - <line x1={(pts.X2 - left - strokeWidth / 2) * scaleX + strokeWidth / 2} y1={(pts.Y2 - top - strokeWidth / 2) * scaleY + strokeWidth / 2} - x2={(pts.X3 - left - strokeWidth / 2) * scaleX + strokeWidth / 2} y2={(pts.Y3 - top - strokeWidth / 2) * scaleY + strokeWidth / 2} stroke="green" strokeWidth={dotsize / 6} - display={(pts.dot1 === formatInstance._currPoint || pts.dot2 === formatInstance._currPoint) ? "inherit" : "none"} /> - </svg>); - + // 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); + // Thin blue line indicating that the current ink stroke is selected. + 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, "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); return ( - <svg className="inkingStroke" + <svg className="inkStroke" style={{ pointerEvents: this.props.Document.isInkMask && this.props.layerProvider?.(this.props.Document) !== false ? "all" : "none", transform: this.props.Document.isInkMask ? `translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined, @@ -196,19 +126,29 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume if (cm) { !Doc.UserDoc().noviceMode && cm.addItem({ description: "Recognize Writing", event: this.analyzeStrokes, icon: "paint-brush" }); cm.addItem({ description: "Toggle Mask", event: () => InkingStroke.toggleMask(this.rootDoc), icon: "paint-brush" }); - cm.addItem({ description: "Edit Points", event: action(() => formatInstance._controlBtn = !formatInstance._controlBtn), icon: "paint-brush" }); - //cm.addItem({ description: "Format Shape...", event: this.formatShape, icon: "paint-brush" }); + cm.addItem({ description: "Edit Points", event: action(() => { if (this._properties) { this._properties._controlButton = !this._properties._controlButton; } }), icon: "paint-brush" }); } }} - ><defs> - </defs> - {hpoints} - {points} - {formatInstance._controlBtn && this.props.isSelected() ? addpoints : ""} - {formatInstance._controlBtn && this.props.isSelected() ? handleLines : ""} - {formatInstance._controlBtn && this.props.isSelected() ? handles : ""} - {formatInstance._controlBtn && this.props.isSelected() ? controls : ""} - + > + + {clickableLine} + {inkLine} + {this.props.isSelected() ? selectedLine : ""} + {this.props.isSelected() && this._properties?._controlButton ? + <> + <InkControls + inkDoc={inkDoc} + data={data} + addedPoints={addedPoints} + format={[left, top, scaleX, scaleY, strokeWidth]} + ScreenToLocalTransform={this.props.ScreenToLocalTransform} /> + <InkHandles + inkDoc={inkDoc} + data={data} + shape={InkShape} + format={[left, top, scaleX, scaleY, strokeWidth]} + ScreenToLocalTransform={this.props.ScreenToLocalTransform} /> + </> : ""} </svg> ); } diff --git a/src/client/views/LightboxView.scss b/src/client/views/LightboxView.scss index 4ea2dc2d6..5d42cd97f 100644 --- a/src/client/views/LightboxView.scss +++ b/src/client/views/LightboxView.scss @@ -1,3 +1,32 @@ + + .lightboxView-navBtn { + margin: auto; + position: absolute; + right: 10; + top: 10; + background: transparent; + border-radius: 8; + color:white; + opacity: 0.7; + width: 35; + &:hover { + opacity: 1; + } + } + .lightboxView-tabBtn { + margin: auto; + position: absolute; + right: 35; + top: 10; + background: transparent; + border-radius: 8; + color:white; + opacity: 0.7; + width: 35; + &:hover { + opacity: 1; + } + } .lightboxView-frame { position: absolute; top: 0; left: 0; @@ -15,7 +44,6 @@ position: relative; background: transparent; border-radius: 8; - color:white; opacity: 0.7; width: 35; &:hover { diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx index ce36d9182..88739fe91 100644 --- a/src/client/views/LightboxView.tsx +++ b/src/client/views/LightboxView.tsx @@ -1,19 +1,20 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable, trace } from 'mobx'; +import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import "normalize.css"; import * as React from 'react'; import { Doc, DocListCast, Opt } from '../../fields/Doc'; import { Cast, NumCast, StrCast } from '../../fields/Types'; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnTrue, returnFalse } from '../../Utils'; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../Utils'; import { DocUtils } from '../documents/Documents'; import { DocumentManager } from '../util/DocumentManager'; import { LinkManager } from '../util/LinkManager'; import { SelectionManager } from '../util/SelectionManager'; import { Transform } from '../util/Transform'; +import { CollectionDockingView } from './collections/CollectionDockingView'; import { TabDocView } from './collections/TabDocView'; import "./LightboxView.scss"; -import { DocumentView, ViewAdjustment } from './nodes/DocumentView'; +import { DocumentView } from './nodes/DocumentView'; import { DefaultStyleProvider, wavyBorderPath } from './StyleProvider'; interface LightboxViewProps { @@ -160,7 +161,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { const { doc, target } = LightboxView._history?.lastElement(); const docView = DocumentManager.Instance.getLightboxDocumentView(target || doc); if (docView) { - LightboxView._docTarget = undefined; + LightboxView._docTarget = target; const focusSpeed = 1000; doc._viewTransition = `transform ${focusSpeed}ms`; if (!target) docView.ComponentView?.shrinkWrap?.(); @@ -197,7 +198,6 @@ export class LightboxView extends React.Component<LightboxViewProps> { TabDocView.PinDoc(coll, { hidePresBox: true }); } } - setTimeout(LightboxView.Next); } future = () => LightboxView._future; @@ -228,7 +228,6 @@ export class LightboxView extends React.Component<LightboxViewProps> { const targetView = target && DocumentManager.Instance.getLightboxDocumentView(target); if (doc === r.props.Document && (!target || target === doc)) r.ComponentView?.shrinkWrap?.(); else target && targetView?.focus(target, { willZoom: true, scale: 0.9, instant: true }); - LightboxView._docTarget = undefined; })); })} Document={LightboxView.LightboxDoc} @@ -270,7 +269,16 @@ export class LightboxView extends React.Component<LightboxViewProps> { LightboxView.Next(); })} <LightboxTourBtn navBtn={this.navBtn} future={this.future} stepInto={this.stepInto} tourMap={this.tourMap} /> - <div className="lightboxView-navBtn" title={"toggle fit width"} style={{ position: "absolute", right: 10, top: 10, color: "white" }} + <div className="lightboxView-tabBtn" title={"open in tab"} + onClick={e => { + e.stopPropagation(); + CollectionDockingView.AddSplit(LightboxView._docTarget || LightboxView._doc!, "onRight"); + SelectionManager.DeselectAll(); + LightboxView.SetLightboxDoc(undefined); + }}> + <FontAwesomeIcon icon={"file-download"} size="2x" /> + </div> + <div className="lightboxView-navBtn" title={"toggle fit width"} onClick={e => { e.stopPropagation(); LightboxView.LightboxDoc!._fitWidth = !LightboxView.LightboxDoc!._fitWidth; }}> <FontAwesomeIcon icon={"arrows-alt-h"} size="2x" /> </div> diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss index b1ad4868c..c8e64b5c4 100644 --- a/src/client/views/Main.scss +++ b/src/client/views/Main.scss @@ -1,4 +1,4 @@ -@import "globalCssVariables"; +@import "global/globalCssVariables"; @import "nodeModuleOverrides"; :root { @@ -54,7 +54,7 @@ button { background: black; outline: none; border: 0px; - color: $light-color; + color: $white; text-transform: uppercase; letter-spacing: 2px; font-size: 75%; @@ -63,7 +63,7 @@ button { } button:hover { - background: $main-accent; + background: $medium-gray; transform: scale(1.05); cursor: pointer; } 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 3f04a0f3a..d913f2069 100644 --- a/src/client/views/MainView.scss +++ b/src/client/views/MainView.scss @@ -1,4 +1,4 @@ -@import "globalCssVariables"; +@import "global/globalCssVariables"; @import "nodeModuleOverrides"; @@ -22,10 +22,6 @@ height: 100%; } -.mainContent-div-flyout { - left: calc(-1 * var(--flyoutHandleWidth)); -} - // add nodes menu. Note that the + button is actually an input label, not an actual button. .mainView-docButtons { position: absolute; @@ -56,50 +52,50 @@ touch-action: none; .searchBox-container { - background: lightgray; + background: $light-gray; } } .mainView-container { - color: black; + color: $dark-gray; .lm_title { - background: #cacaca; - color: black; + background: $light-gray; + color: $dark-gray; } } .mainView-container-dark { - color: lightgray; + color: $light-gray; .lm_goldenlayout { - background: dimgray; + background: $medium-gray; } .lm_title { - background: black; + background: $dark-gray; color: unset; } .marquee { - border-color: white; + border-color: $white; } #search-input { - background: lightgray; + background: $light-gray; } .searchBox-container { - background: rgb(45, 45, 45); + background: $dark-gray; } .contextMenu-cont, .contextMenu-item { - background: dimGray; + background: $medium-gray; } .contextMenu-item:hover { - background: gray; + background: $medium-gray; } } @@ -111,14 +107,21 @@ user-select: none; } +.properties-container { + height: 100%; + position: relative; + left: 100%; + top: calc(-100% - 36px); + z-index: 3000; +} + .mainView-propertiesDragger { //background-color: rgb(140, 139, 139); - background-color: lightgrey; + background-color: $light-gray; height: 55px; width: 17px; position: absolute; top: 50%; - border: 1px black solid; border-radius: 0; border-top-left-radius: 10px; border-bottom-left-radius: 10px; @@ -141,18 +144,6 @@ } } -.mainiView-propertiesView { - display: flex; - flex-direction: column; - height: 100%; - position: absolute; - right: 0; - top: 0; - border-left: solid 1px; - z-index: 100000; - cursor: auto; -} - .mainView-innerContent, .mainView-innerContent-dark { display: contents; flex-direction: row; @@ -163,43 +154,43 @@ flex-direction: column; position: relative; height: 100%; - background: dimgray; + background: $medium-gray; .documentView-node-topmost { - background: lightgrey; + background: $light-gray; } } .propertiesView { - right: 0; + left: 0; position: absolute; z-index: 2; - background-color: rgb(159, 159, 159); + background-color: $light-gray; .editable-title { - background-color: lightgrey; + background-color: $light-gray; } } } .mainView-libraryHandle { - background-color: lightgrey; + background-color: $light-gray; } .mainView-innerContent-dark { .propertiesView { background-color: #252525; input { - background-color: dimgrey; + background-color: $medium-gray; } .propertiesView-sharingTable { - background-color: dimgrey; + background-color: $medium-gray; } .editable-title { - background-color: dimgrey; + background-color: $medium-gray; } .propertiesView-field { - background-color: dimgrey; + background-color: $medium-gray; } } .mainView-propertiesDragger, @@ -209,17 +200,18 @@ } .mainView-container-dark { .contextMenu-cont { - background: dimgrey; - color: white; + background: $medium-gray; + color: $white; input::placeholder { - color:white; + color:$white; } } } .mainView-menuPanel { min-width: var(--menuPanelWidth); - background-color: #121721; + background-color: $dark-gray; + border-right: $standard-border; .collectionStackingView { scrollbar-width: none; @@ -233,13 +225,13 @@ padding: 7px; padding-left: 7px; width: 100%; - background: black; + background: $dark-gray; .mainView-menuPanel-button-wrap { width: 45px; /* padding: 5px; */ touch-action: none; - background: black; + background: $dark-gray; transform-origin: top left; /* margin-bottom: 5px; */ margin-top: 5px; @@ -247,7 +239,7 @@ border-radius: 8px; &:hover { - background: rgb(61, 61, 61); + background: $black; cursor: pointer; } } @@ -419,31 +411,4 @@ display: block; width: 500px; height: 1000px; -} - -.lm_drag_tab { - padding: 0; - width: 15px !important; - height: 15px !important; - position: relative !important; - display: inline-flex !important; - align-items: center; - top: 0 !important; - right: unset !important; - left: 0 !important; -} -.lm_close_tab { - padding: 0; - width: 15px !important; - height: 15px !important; - position: relative !important; - display: inline-flex !important; - align-items: center; - top: 0 !important; - right: unset !important; - left: 0 !important; -} -.lm_tab, .lm_tab_active { - display: flex !important; - padding-right: 0 !important; }
\ No newline at end of file diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 4eeb1fc95..8b5e18fb2 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 './globalCssVariables.scss'; +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,13 +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'; 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; @@ -78,7 +78,7 @@ export class MainView extends React.Component { @observable private _sidebarContent: any = this.userDoc?.sidebar; @observable private _flyoutWidth: number = 0; - @computed private get topOffset() { return (CollectionMenu.Instance?.Pinned ? 35 : 0) + Number(SEARCH_PANEL_HEIGHT.replace("px", "")); } + @computed private get topOffset() { return Number(SEARCH_PANEL_HEIGHT.replace("px", "")); } //TODO remove @computed private get leftOffset() { return this.menuPanelWidth() - 2; } @computed private get userDoc() { return Doc.UserDoc(); } @computed private get darkScheme() { return BoolCast(CurrentUserUtils.ActiveDashboard?.darkScheme); } @@ -103,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'); @@ -178,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(); } @@ -192,7 +189,7 @@ export class MainView extends React.Component { initEventListeners = () => { window.addEventListener("drop", e => e.preventDefault(), false); // prevent default behavior of navigating to a new web page window.addEventListener("dragover", e => e.preventDefault(), false); - document.addEventListener("pointermove", action(e => SearchBox.Instance._undoBackground = UndoManager.batchCounter ? "#000000a8" : undefined)); + // document.addEventListener("pointermove", action(e => SearchBox.Instance._undoBackground = UndoManager.batchCounter ? "#000000a8" : undefined)); document.addEventListener("pointerdown", this.globalPointerDown); document.addEventListener("click", (e: MouseEvent) => { if (!e.cancelBubble) { @@ -242,8 +239,9 @@ export class MainView extends React.Component { } getPWidth = () => this._panelWidth - this.propertiesWidth(); - getPHeight = () => this._panelHeight; + getPHeight = () => this._panelHeight - (CollectionMenu.Instance?.Pinned ? 35 : 0); getContentsHeight = () => this._panelHeight; + getMenuPanelHeight = () => this._panelHeight + (CollectionMenu.Instance?.Pinned ? 35 : 0); @computed get mainDocView() { return <DocumentView key="main" @@ -275,10 +273,12 @@ export class MainView extends React.Component { @computed get dockingContent() { return <div key="docking" className={`mainContent-div${this._flyoutWidth ? "-flyout" : ""}`} onDrop={e => { e.stopPropagation(); e.preventDefault(); }} + // style={{ minWidth: `calc(100% - ${this._flyoutWidth + this.menuPanelWidth() + this.propertiesWidth()}px)`, width: `calc(100% - ${this._flyoutWidth + this.propertiesWidth()}px)` }}> + // FIXME update with property panel width style={{ minWidth: `calc(100% - ${this._flyoutWidth + this.menuPanelWidth() + this.propertiesWidth()}px)`, transform: LightboxView.LightboxDoc ? "scale(0.0001)" : undefined, - width: `calc(100% - ${this._flyoutWidth + this.menuPanelWidth() + this.propertiesWidth()}px)` + //TODO:glr width: `calc(100% - ${this._flyoutWidth + this.menuPanelWidth() + this.propertiesWidth()}px)` }}> {!this.mainContainer ? (null) : this.mainDocView} </div>; @@ -358,7 +358,7 @@ export class MainView extends React.Component { removeDocument={returnFalse} ScreenToLocalTransform={this.sidebarScreenToLocal} PanelWidth={this.menuPanelWidth} - PanelHeight={this.getContentsHeight} + PanelHeight={this.getMenuPanelHeight} renderDepth={0} docViewPath={returnEmptyDoclist} focus={DocUtils.DefaultFocus} @@ -387,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: @@ -401,20 +397,27 @@ export class MainView extends React.Component { } @computed get mainInnerContent() { + const width = this.propertiesWidth() + this._flyoutWidth + this.menuPanelWidth(); + const transform = this._flyoutWidth ? 'translate(-28px, 0px)' : undefined; return <> {this.menuPanel} <div key="inner" className={`mainView-innerContent${this.darkScheme ? "-dark" : ""}`}> {this.flyout} - <div className="mainView-libraryHandle" style={{ display: !this._flyoutWidth ? "none" : undefined, }} onPointerDown={this.onFlyoutPointerDown} > + <div className="mainView-libraryHandle" style={{ display: !this._flyoutWidth ? "none" : undefined }} onPointerDown={this.onFlyoutPointerDown} > <FontAwesomeIcon icon="chevron-left" color={this.darkScheme ? "white" : "black"} style={{ opacity: "50%" }} size="sm" /> </div> + <div className="mainView-innerContainer" style={{ width: `calc(100% - ${width}px)`, transform: transform }}> + <CollectionMenu /> - {this.dockingContent} + {this.dockingContent} - <div className="mainView-propertiesDragger" key="props" onPointerDown={this.onPropertiesPointerDown} style={{ right: this.propertiesWidth() - 1 }}> - <FontAwesomeIcon icon={this.propertiesWidth() < 10 ? "chevron-left" : "chevron-right"} color={this.darkScheme ? "white" : "black"} size="sm" /> + <div className="mainView-propertiesDragger" key="props" onPointerDown={this.onPropertiesPointerDown} style={{ right: this._flyoutWidth ? 0 : this.propertiesWidth() - 1 }}> + <FontAwesomeIcon icon={this.propertiesWidth() < 10 ? "chevron-left" : "chevron-right"} color={this.darkScheme ? Colors.WHITE : Colors.BLACK} size="sm" /> + </div> + <div className="properties-container"> + {this.propertiesWidth() < 10 ? (null) : <PropertiesView styleProvider={DefaultStyleProvider} width={this.propertiesWidth()} height={this.getContentsHeight()} />} + </div> </div> - {this.propertiesWidth() < 10 ? (null) : <PropertiesView styleProvider={DefaultStyleProvider} width={this.propertiesWidth()} height={this.getContentsHeight()} />} </div> </>; } @@ -470,6 +473,7 @@ export class MainView extends React.Component { bringToFront={emptyFunction} select={emptyFunction} isContentActive={returnFalse} + isAnyChildContentActive={returnFalse} isSelected={returnFalse} docViewPath={returnEmptyDoclist} moveDocument={this.moveButtonDoc} @@ -523,37 +527,10 @@ export class MainView extends React.Component { </svg>; } - @computed get search() { + @computed get topbar() { TraceMobx(); - return <div className="mainView-searchPanel"> - <SearchBox Document={CurrentUserUtils.MySearchPanelDoc} - DataDoc={CurrentUserUtils.MySearchPanelDoc} - fieldKey="data" - dropAction="move" - isSelected={returnTrue} - isContentActive={returnTrue} - select={returnTrue} - setHeight={returnFalse} - addDocument={undefined} - addDocTab={this.addDocTabFunc} - pinToPres={emptyFunction} - rootSelected={returnTrue} - styleProvider={DefaultStyleProvider} - layerProvider={undefined} - removeDocument={undefined} - ScreenToLocalTransform={Transform.Identity} - PanelWidth={this.getPWidth} - PanelHeight={this.getPHeight} - renderDepth={0} - focus={DocUtils.DefaultFocus} - docViewPath={returnEmptyDoclist} - whenChildContentsActiveChanged={emptyFunction} - bringToFront={emptyFunction} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} /> + return <div className="mainView-topbar"> + <TopBar /> </div>; } @@ -604,8 +581,7 @@ export class MainView extends React.Component { <GroupManager /> <GoogleAuthenticationManager /> <DocumentDecorations boundsLeft={this.leftOffset} boundsTop={this.topOffset} /> - {this.search} - <CollectionMenu /> + {this.topbar} {LinkDescriptionPopup.descriptionPopup ? <LinkDescriptionPopup /> : null} {DocumentLinksButton.LinkEditorDocView ? <LinkMenu docView={DocumentLinksButton.LinkEditorDocView} changeFlyout={emptyFunction} /> : (null)} {LinkDocPreview.LinkInfo ? <LinkDocPreview {...LinkDocPreview.LinkInfo} /> : (null)} @@ -623,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/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx index d2074d653..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() { @@ -65,10 +63,11 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { doc.addEventListener("pointermove", this.onSelectMove); doc.addEventListener("pointerup", this.onSelectEnd); - AnchorMenu.Instance.OnClick = (e: PointerEvent) => { - this.props.anchorMenuClick?.()?.(this.highlight("rgba(173, 216, 230, 0.75)", true)); - }; + 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. @@ -103,11 +102,13 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { @undoBatch @action - makeAnnotationDocument = (color: string, isLinkButton?: boolean): Opt<Doc> => { - if (this.props.savedAnnotations.size === 0) return undefined; - if ((Array.from(this.props.savedAnnotations.values())[0][0] as any).marqueeing) { + makeAnnotationDocument = (color: string, isLinkButton?: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>): Opt<Doc> => { + const savedAnnoMap = savedAnnotations ?? this.props.savedAnnotations; + if (savedAnnoMap.size === 0) return undefined; + const savedAnnos = Array.from(savedAnnoMap.values())[0]; + if (savedAnnos.length && (savedAnnos[0] as any).marqueeing) { const scale = this.props.scaling?.() || 1; - const anno = Array.from(this.props.savedAnnotations.values())[0][0]; + const anno = savedAnnos[0]; const containerOffset = this.props.containerOffset?.() || [0, 0]; const marqueeAnno = Docs.Create.FreeformDocument([], { _isLinkButton: isLinkButton, backgroundColor: color, annotationOn: this.props.rootDoc, title: "Annotation on " + this.props.rootDoc.title }); marqueeAnno.x = (parseInt(anno.style.left || "0") - containerOffset[0]) / scale; @@ -115,15 +116,17 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { marqueeAnno._height = parseInt(anno.style.height || "0") / scale; marqueeAnno._width = parseInt(anno.style.width || "0") / scale; anno.remove(); - this.props.savedAnnotations.clear(); + savedAnnoMap.clear(); return marqueeAnno; } - const textRegionAnno = Docs.Create.HTMLAnchorDocument([], { annotationOn: this.props.rootDoc, title: "Selection on " + this.props.rootDoc.title, _width: 1, _height: 1 }); + const textRegionAnno = Docs.Create.HTMLAnchorDocument([], { annotationOn: this.props.rootDoc, backgroundColor: "transparent", title: "Selection on " + this.props.rootDoc.title }); + let minX = Number.MAX_VALUE; let maxX = -Number.MAX_VALUE; let minY = Number.MAX_VALUE; + let maxY = -Number.MIN_VALUE; const annoDocs: Doc[] = []; - this.props.savedAnnotations.forEach((value: HTMLDivElement[], key: number) => value.map(anno => { + savedAnnoMap.forEach((value: HTMLDivElement[], key: number) => value.map(anno => { const textRegion = new Doc(); textRegion.x = parseInt(anno.style.left ?? "0"); textRegion.y = parseInt(anno.style.top ?? "0"); @@ -134,23 +137,27 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { annoDocs.push(textRegion); anno.remove(); minY = Math.min(NumCast(textRegion.y), minY); + minX = Math.min(NumCast(textRegion.x), minX); + maxY = Math.max(NumCast(textRegion.y) + NumCast(textRegion._height), maxY); maxX = Math.max(NumCast(textRegion.x) + NumCast(textRegion._width), maxX); })); const textRegionAnnoProto = Doc.GetProto(textRegionAnno); textRegionAnnoProto.y = Math.max(minY, 0); - textRegionAnnoProto.x = Math.max(maxX, 0); + textRegionAnnoProto.x = Math.max(minX, 0); + textRegionAnnoProto.height = Math.max(maxY, 0) - Math.max(minY, 0); + textRegionAnnoProto.width = Math.max(maxX, 0) - Math.max(minX, 0); // mainAnnoDocProto.text = this._selectionText; textRegionAnnoProto.textInlineAnnotations = new List<Doc>(annoDocs); - this.props.savedAnnotations.clear(); + savedAnnoMap.clear(); return textRegionAnno; } @action - highlight = (color: string, isLinkButton: boolean) => { + 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); - annotationDoc && this.props.addDocument(annotationDoc); + 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/PropertiesButtons.scss b/src/client/views/PropertiesButtons.scss index 29d2bfcb7..484522bc7 100644 --- a/src/client/views/PropertiesButtons.scss +++ b/src/client/views/PropertiesButtons.scss @@ -1,4 +1,4 @@ -@import "globalCssVariables"; +@import "global/globalCssVariables"; $linkGap : 3px; @@ -7,13 +7,13 @@ $linkGap : 3px; } .propertiesButtons-linkButton-empty:hover { - background: $main-accent; + background: $medium-gray; transform: scale(1.05); cursor: pointer; } .propertiesButtons-linkButton-nonempty:hover { - background: $main-accent; + background: $medium-gray; transform: scale(1.05); cursor: pointer; } @@ -24,7 +24,7 @@ $linkGap : 3px; width: 29px; border-radius: 6px; pointer-events: auto; - background-color: #121721; + background-color: $dark-gray; color: #fcfbf7; text-transform: uppercase; letter-spacing: 2px; @@ -38,18 +38,18 @@ $linkGap : 3px; margin-left: 4px; &:hover { - background: $main-accent; + background: $medium-gray; transform: scale(1.05); cursor: pointer; } } .propertiesButtons-linkButton-empty.toggle-on { background-color: white; - color: black; + color: $dark-gray; } .propertiesButtons-linkButton-empty.toggle-hover { background-color: gray; - color: black; + color: $dark-gray; } .propertiesButtons-linkButton-empty.toggle-off { color: white; @@ -111,7 +111,7 @@ $linkGap : 3px; margin-left: 4px; &:hover { - background: $main-accent; + background: $medium-gray; transform: scale(1.05); cursor: pointer; } 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 d09d949ff..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"; @@ -24,7 +24,7 @@ import { EditableView } from "./EditableView"; import { InkStrokeProperties } from "./InkStrokeProperties"; import { DocumentView, StyleProviderFunc } from "./nodes/DocumentView"; import { KeyValueBox } from "./nodes/KeyValueBox"; -import { PresBox } from "./nodes/PresBox"; +import { PresBox } from "./nodes/trails/PresBox"; import { PropertiesButtons } from "./PropertiesButtons"; import { PropertiesDocContextSelector } from "./PropertiesDocContextSelector"; import "./PropertiesView.scss"; @@ -86,7 +86,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @observable openSlideOptions: boolean = false; @observable inOptions: boolean = false; - @observable _controlBtn: boolean = false; + @observable _controlButton: boolean = false; @observable _lock: boolean = false; componentDidMount() { @@ -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] ]); @@ -540,7 +538,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { const formatInstance = InkStrokeProperties.Instance; return !formatInstance ? (null) : <div className="inking-button"> <Tooltip title={<div className="dash-tooltip">{"Edit points"}</div>}> - <div className="inking-button-points" onPointerDown={action(() => formatInstance._controlBtn = !formatInstance._controlBtn)} style={{ backgroundColor: formatInstance._controlBtn ? "black" : "" }}> + <div className="inking-button-points" onPointerDown={action(() => formatInstance._controlButton = !formatInstance._controlButton)} style={{ backgroundColor: formatInstance._controlButton ? "black" : "" }}> <FontAwesomeIcon icon="bezier-curve" color="white" size="lg" /> </div> </Tooltip> @@ -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 59ff1c340..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,36 +73,39 @@ 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) => { - if (property === StyleProp.ShowTitle) return StrCast(this.props.layoutDoc["sidebar-childShowTitle"], "title"); + if (property === StyleProp.ShowTitle) { + return doc === this.props.rootDoc ? undefined : StrCast(this.props.layoutDoc["sidebar-childShowTitle"], "title"); + } return this.props.styleProvider?.(doc, props, property); } render() { 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), @@ -116,7 +125,8 @@ 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} scaling={returnOne} @@ -129,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 9e61351c4..c9e532745 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -1,4 +1,5 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Colors } from './global/globalEnums'; import { IconProp } from '@fortawesome/fontawesome-svg-core'; import 'golden-layout/src/css/goldenlayout-base.css'; import 'golden-layout/src/css/goldenlayout-dark-theme.css'; @@ -97,16 +98,16 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps case StyleProp.Color: const docColor: Opt<string> = StrCast(doc?.[fieldKey + "color"], StrCast(doc?._color)); if (docColor) return docColor; - const backColor = backgroundCol();// || (darkScheme() ? "black" : "white"); + 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 + 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 "black"; - return "white"; + if (colsum / col.alpha() > 400 || col.alpha() < 0.25) return Colors.DARK_GRAY; + return Colors.WHITE; case StyleProp.Hidden: return BoolCast(doc?._hidden); - case StyleProp.BorderRounding: return StrCast(doc?.[fieldKey + "borderRounding"]); + case StyleProp.BorderRounding: return StrCast(doc?.[fieldKey + "borderRounding"], doc?._viewType === CollectionViewType.Pile ? "50%" : ""); case StyleProp.TitleHeight: return 15; case StyleProp.BorderPath: return comicStyle() && props?.renderDepth ? { path: wavyBorderPath(props?.PanelWidth?.() || 0, props?.PanelHeight?.() || 0), fill: wavyBorderPath(props?.PanelWidth?.() || 0, props?.PanelHeight?.() || 0, .08), width: 3 } : { path: undefined, width: 0 }; case StyleProp.JitterRotation: return comicStyle() ? random(-1, 1, NumCast(doc?.x), NumCast(doc?.y)) * ((props?.PanelWidth() || 0) > (props?.PanelHeight() || 0) ? 5 : 10) : 0; @@ -114,38 +115,38 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps doc?.type === DocumentType.RTF) && showTitle() && !StrCast(doc?.showTitle).includes(":hover") ? 15 : 0; case StyleProp.BackgroundColor: { 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() ? "dimgrey" : "lightgrey"; + 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() ? "#3e3e3e" : "white"); break; - case DocumentType.FONTICON: docColor = docColor || "black"; break; - case DocumentType.RTF: docColor = docColor || (darkScheme() ? "#2d2d2d" : "#f1efeb"); break; - case DocumentType.FILTER: docColor = docColor || (darkScheme() ? "#2d2d2d" : "rgba(105, 105, 105, 0.432)"); break; + case DocumentType.PRES: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.WHITE); break; + case DocumentType.FONTICON: docColor = docColor || Colors.DARK_GRAY; break; + case DocumentType.RTF: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY); break; + case DocumentType.FILTER: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : "rgba(105, 105, 105, 0.432)"); break; case DocumentType.INK: docColor = doc?.isInkMask ? "rgba(0,0,0,0.7)" : undefined; break; case DocumentType.SLIDER: break; case DocumentType.EQUATION: docColor = docColor || "transparent"; break; case DocumentType.LABEL: docColor = docColor || (doc.annotationOn !== undefined ? "rgba(128, 128, 128, 0.18)" : undefined); break; - case DocumentType.BUTTON: docColor = docColor || (darkScheme() ? "#2d2d2d" : "lightgray"); break; - case DocumentType.LINKANCHOR: docColor = isAnchor ? "lightblue" : "transparent"; break; - case DocumentType.LINK: docColor = docColor || "transparent"; break; + case DocumentType.BUTTON: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY); break; + case DocumentType.LINKANCHOR: docColor = isAnchor ? Colors.LIGHT_BLUE : "transparent"; break; + case DocumentType.LINK: docColor = (isAnchor ? docColor : "") || "transparent"; break; case DocumentType.IMG: case DocumentType.WEB: case DocumentType.PDF: case DocumentType.SCREENSHOT: - case DocumentType.VID: docColor = docColor || (darkScheme() ? "#2d2d2d" : "lightgray"); break; + case DocumentType.VID: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY); break; case DocumentType.COL: if (StrCast(Doc.LayoutField(doc)).includes("SliderBox")) break; - docColor = docColor ? docColor : - doc?._isGroup ? "#00000004" : // very faint highlight to show bounds of group - (Doc.IsSystem(doc) ? (darkScheme() ? "rgb(62,62,62)" : "lightgrey") : // system docs (seen in treeView) get a grayish background + docColor = docColor || + (doc?._isGroup ? "#00000004" : // very faint highlight to show bounds of group + (doc?._viewType === CollectionViewType.Pile || Doc.IsSystem(doc) ? (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY) : // system docs (seen in treeView) get a grayish background isBackground() ? "cyan" : // ?? is there a good default for a background collection doc.annotationOn ? "#00000015" : // faint interior for collections on PDFs, images, etc StrCast((props?.renderDepth || 0) > 0 ? Doc.UserDoc().activeCollectionNestedBackground : - Doc.UserDoc().activeCollectionBackground)); + Doc.UserDoc().activeCollectionBackground))); break; //if (doc._viewType !== CollectionViewType.Freeform && doc._viewType !== CollectionViewType.Time) return "rgb(62,62,62)"; - default: docColor = docColor || (darkScheme() ? "black" : "white"); break; + default: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.WHITE); break; } if (docColor && (!doc || props?.layerProvider?.(doc) === false)) docColor = Color(docColor.toLowerCase()).fade(0.5).toString(); return docColor; @@ -158,8 +159,9 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps switch (doc?.type) { case DocumentType.COL: return StrCast(doc?.boxShadow, - isBackground() || doc?._isGroup || docProps?.LayoutTemplateString ? undefined : // groups have no drop shadow -- they're supposed to be "invisible". LayoutString's imply collection is being rendered as something else (e.g., title of a Slide) - `${darkScheme() ? "rgb(30, 32, 31) " : "#9c9396 "} ${StrCast(doc.boxShadow, "0.2vw 0.2vw 0.8vw")}`); + doc?._viewType === CollectionViewType.Pile ? "4px 4px 10px 2px" : + isBackground() || doc?._isGroup || docProps?.LayoutTemplateString ? undefined : // groups have no drop shadow -- they're supposed to be "invisible". LayoutString's imply collection is being rendered as something else (e.g., title of a Slide) + `${darkScheme() ? Colors.DARK_GRAY : Colors.MEDIUM_GRAY} ${StrCast(doc.boxShadow, "0.2vw 0.2vw 0.8vw")}`); case DocumentType.LABEL: if (doc?.annotationOn !== undefined) return "black 2px 2px 1px"; @@ -172,6 +174,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps } } case StyleProp.PointerEvents: + if (doc?.type === DocumentType.MARKER) return "none"; if (props?.pointerEvents === "none") return "none"; const layer = doc && props?.layerProvider?.(doc); if (opacity() === 0 || (doc?.type === DocumentType.INK && !docProps?.treeViewDoc) || doc?.isInkMask) return "none"; diff --git a/src/client/views/TemplateMenu.scss b/src/client/views/TemplateMenu.scss index bbed8cd96..f81cbdaab 100644 --- a/src/client/views/TemplateMenu.scss +++ b/src/client/views/TemplateMenu.scss @@ -1,4 +1,4 @@ -@import "globalCssVariables"; +@import "global/globalCssVariables"; .templating-menu { position: absolute; pointer-events: auto; @@ -24,7 +24,7 @@ cursor: pointer; &:hover { - background: $main-accent; + background: $medium-gray; transform: scale(1.05); } } @@ -32,7 +32,7 @@ .template-list { font-family: $sans-serif; font-size: 12px; - background-color: $light-color-secondary; + background-color: $light-gray; padding: 2px 12px; list-style: none; position: relative; diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index 5491a81e6..ff3f92364 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -141,6 +141,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { onCheckedClick={this.scriptField} onChildClick={this.scriptField} dropAction={undefined} + isAnyChildContentActive={returnFalse} isContentActive={returnTrue} bringToFront={emptyFunction} focus={emptyFunction} diff --git a/src/client/views/_nodeModuleOverrides.scss b/src/client/views/_nodeModuleOverrides.scss index b8a7db034..fd0ac9d5c 100644 --- a/src/client/views/_nodeModuleOverrides.scss +++ b/src/client/views/_nodeModuleOverrides.scss @@ -1,8 +1,50 @@ +@import "./global/globalCssVariables"; // this file is for overriding all the css from installed node modules // goldenlayout stuff div .lm_header { - background: $dark-color; + background: $dark-gray; + overflow: hidden; + height: 27px !important; +} + +/* Width */ +.lm_header::-webkit-scrollbar { + -webkit-appearance: none; + display: none; +} + +/* Width */ +.lm_header:hover::-webkit-scrollbar { + -webkit-appearance: none; + display: block; + height: 0px; +} + +/* Track */ +.lm_header:hover::-webkit-scrollbar-track { + -webkit-appearance: none; + display: none; +} + +/* Handle */ +.lm_header:hover::-webkit-scrollbar-thumb { + -webkit-appearance: none; + background: $dark-gray; +} + +/* Handle on hover */ +.lm_header:hover::-webkit-scrollbar-thumb:hover { + -webkit-appearance: none; + background: $dark-gray; +} + +.lm_tabs { + display: flex; + position: absolute; + width: calc(100% - 60px); + overflow: scroll; + background: $dark-gray; } .lm_tab { @@ -15,7 +57,14 @@ div .lm_header { } .lm_header .lm_controls { - right: 1em !important; + align-items: center; + position: absolute; + background-color: $dark-gray; + border-radius: 5px; + display: flex; + justify-content: space-evenly; + height: 23px; + width: 65px; } // @TODO the ril__navgiation buttons in the img gallery are a lil messed up but I can't figure out diff --git a/src/client/views/animationtimeline/Keyframe.scss b/src/client/views/animationtimeline/Keyframe.scss index 84c8de287..38eb103c6 100644 --- a/src/client/views/animationtimeline/Keyframe.scss +++ b/src/client/views/animationtimeline/Keyframe.scss @@ -1,4 +1,4 @@ -@import "./../globalCssVariables.scss"; +@import "./../global/globalCssVariables.scss"; $timelineColor: #9acedf; @@ -15,11 +15,11 @@ $timelineDark: #77a1aa; height: 200px; top: 50%; position: relative; - background-color: $light-color; + background-color: $white; .menutable { tr:nth-child(odd) { - background-color: $light-color-secondary; + background-color: $light-gray; } } } @@ -67,7 +67,7 @@ $timelineDark: #77a1aa; height: 100%; position: absolute; pointer-events: none; - background: linear-gradient(to left, $timelineColor 10%, $light-color); + background: linear-gradient(to left, $timelineColor 10%, $white); } .fadeRight { @@ -75,7 +75,7 @@ $timelineDark: #77a1aa; height: 100%; position: absolute; pointer-events: none; - background: linear-gradient(to right, $timelineColor 10%, $light-color); + background: linear-gradient(to right, $timelineColor 10%, $white); } .divider { diff --git a/src/client/views/animationtimeline/Keyframe.tsx b/src/client/views/animationtimeline/Keyframe.tsx index e84022366..82b0218bf 100644 --- a/src/client/views/animationtimeline/Keyframe.tsx +++ b/src/client/views/animationtimeline/Keyframe.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import "./Keyframe.scss"; import "./Timeline.scss"; -import "../globalCssVariables.scss"; +import "../global/globalCssVariables.scss"; import { observer } from "mobx-react"; import { observable, reaction, action, IReactionDisposer, observe, computed, runInAction, trace } from "mobx"; import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../fields/Doc"; diff --git a/src/client/views/animationtimeline/Timeline.scss b/src/client/views/animationtimeline/Timeline.scss index f90249771..48422b789 100644 --- a/src/client/views/animationtimeline/Timeline.scss +++ b/src/client/views/animationtimeline/Timeline.scss @@ -1,4 +1,4 @@ -@import "./../globalCssVariables.scss"; +@import "./../global/globalCssVariables.scss"; $timelineColor: #9acedf; $timelineDark: #77a1aa; @@ -161,7 +161,7 @@ $timelineDark: #77a1aa; width: 100%; height: 300px; position: absolute; - background-color: $light-color-secondary; + background-color: $light-gray; border-bottom: 2px solid $timelineDark; transition: transform 500ms ease; @@ -251,7 +251,7 @@ $timelineDark: #77a1aa; top: 0px; width: 100px; height: 30%; - border: 1px solid $dark-color; + border: 1px solid $dark-gray; font-size: 12px; line-height: 11px; background-color: $timelineDark; diff --git a/src/client/views/animationtimeline/TimelineMenu.scss b/src/client/views/animationtimeline/TimelineMenu.scss index 7ee0a43d5..43a89419e 100644 --- a/src/client/views/animationtimeline/TimelineMenu.scss +++ b/src/client/views/animationtimeline/TimelineMenu.scss @@ -1,10 +1,10 @@ -@import "./../globalCssVariables.scss"; +@import "./../global/globalCssVariables.scss"; .timeline-menu-container{ position: absolute; display: flex; - box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw; + box-shadow: $medium-gray 0.2vw 0.2vw 0.4vw; flex-direction: column; background: whitesmoke; z-index: 10000; @@ -39,7 +39,7 @@ border-top-left-radius: 15px; border-top-right-radius: 15px; text-transform: uppercase; - background: $dark-color; + background: $dark-gray; letter-spacing: 2px; .timeline-menu-header-desc{ @@ -79,10 +79,10 @@ .timeline-menu-item:hover { border-width: .11px; border-style: none; - border-color: $intermediate-color; + border-color: $medium-gray; border-bottom-style: solid; border-top-style: solid; - background: $darker-alt-accent; + background: $medium-blue; } .timeline-menu-desc { diff --git a/src/client/views/animationtimeline/TimelineOverview.scss b/src/client/views/animationtimeline/TimelineOverview.scss index 283163ea7..c8d96c399 100644 --- a/src/client/views/animationtimeline/TimelineOverview.scss +++ b/src/client/views/animationtimeline/TimelineOverview.scss @@ -1,4 +1,4 @@ -@import "./../globalCssVariables.scss"; +@import "./../global/globalCssVariables.scss"; $timelineColor: #9acedf; $timelineDark: #77a1aa; diff --git a/src/client/views/animationtimeline/Track.scss b/src/client/views/animationtimeline/Track.scss index aec587a79..f45e0556d 100644 --- a/src/client/views/animationtimeline/Track.scss +++ b/src/client/views/animationtimeline/Track.scss @@ -1,4 +1,4 @@ -@import "./../globalCssVariables.scss"; +@import "./../global/globalCssVariables.scss"; .track-container { @@ -6,8 +6,8 @@ .inner { top: 0px; width: calc(100%); - background-color: $light-color; - border: 1px solid $dark-color; + background-color: $white; + border: 1px solid $dark-gray; position: relative; z-index: 100; } diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss index f4736eb29..77e7b86ea 100644 --- a/src/client/views/collections/CollectionDockingView.scss +++ b/src/client/views/collections/CollectionDockingView.scss @@ -1,40 +1,46 @@ -@import "../../views/globalCssVariables.scss"; +@import "../global/globalCssVariables.scss"; .lm_title { - margin-top: 3px; - border-radius: 5px; - border: solid 0px dimgray; - border-width: 2px 2px 0px; - height: 20px; - transform: translate(0px, -3px); + -webkit-appearance: none; + display: inline-block; + align-self: center; + align-items: center; + height: 100%; + overflow: hidden; + text-overflow: ellipsis; + background: transparent; + border: solid 0px transparent; cursor: grab; + color: $black; } .lm_title.focus-visible { + -webkit-appearance: none; cursor: text; } .lm_title_wrap { overflow: hidden; - height: 19px; - margin-top: -2px; - display: inline-block; + align-items: center; + align-self: center; + background: transparent; + width: max-content; + height: 100%; + display: flex; } .lm_active .lm_title { - border: solid 1px lightgray; -} - -.lm_header .lm_tab .lm_close_tab { - position: absolute; - text-align: center; + -webkit-appearance: none; + // font-weight: 700; } .lm_header .lm_tab { - padding-right: 20px; - margin-top: -1px; - border-bottom: 1px black; + padding: 0px; + opacity: 0.7; + box-shadow: none; + height: 24px; + // border-bottom: 1px black; .collectionDockingView-gear { display: none; @@ -42,9 +48,13 @@ } .lm_header .lm_tab.lm_active { - padding-right: 20px; - margin-top: 1px; - border-bottom: unset; + padding: 0; + opacity: 1; + margin: 0; + box-shadow: none; + height: 27px; + margin-right: 2px; + // border-bottom: unset; .collectionDockingView-gear { display: inline-block; @@ -55,6 +65,41 @@ display: inline; } +.lm_drag_tab { + padding: 0; + width: 15px !important; + height: 15px !important; + position: relative !important; + display: inline-flex !important; + align-items: center; + top: 0 !important; + right: unset !important; + left: 0 !important; +} + +.lm_close_tab { + padding: 0; + align-self: center; + margin-right: 5px; + background-color: black; + border-radius: 3px; + opacity: 1 !important; + width: 15px !important; + height: 15px !important; + position: relative !important; + display: inline-flex !important; + align-items: center; + top: 0 !important; + right: unset !important; + left: 0 !important; +} + +.lm_tab, +.lm_tab_active { + display: flex !important; + padding-right: 0 !important; +} + .collectiondockingview-container { width: 100%; height: 100%; @@ -82,16 +127,17 @@ } .lm_content { - background: white; + background: $white; } .lm_controls>li { - opacity: 0.6; - transform: scale(1.2); + opacity: 1; + transform: scale(1); } .lm_controls .lm_popout { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAUCAAAAABHICnvAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QAAKqNIzIAAAAHdElNRQfkCBsXMgbrEyzaAAAAT0lEQVQY02NgIAcIu8tgEW3/u4IDQ5B14/8LQlhFhckVFfCJjIyIOfP/QWpEZGSQJFS05s9fIPj3/z+YmseCTxS7CZS7DI+PsYcOjpAkDAA6H0KZxzDzlgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMC0wOC0yN1QyMzo1MDowNi0wNDowMDvgVpQAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjAtMDgtMjdUMjM6NTA6MDYtMDQ6MDBKve4oAAAAAElFTkSuQmCC) + transform: rotate(45deg); + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAQUlEQVR4nHXOQQ4AMAgCQeT/f6aXpsGK3jSTuCVJAAr7iBdoAwCKd0nwfaAdHbYERw5b44+E8JoBjEYGMBq5gAYP3usUDu2IvoUAAAAASUVORK5CYII=); } .lm_maximised .lm_controls .lm_maximise { @@ -110,7 +156,7 @@ } .flexlayout__splitter { - background-color: black; + background-color: $dark-gray; } .flexlayout__splitter:hover { @@ -179,7 +225,7 @@ position: absolute; box-sizing: border-box; background-color: #222; - color: black; + color: $dark-gray; } .flexlayout__tab_button { @@ -268,7 +314,7 @@ } .flexlayout__tab_header_outer { - background-color: black; + background-color: $dark-gray; position: absolute; left: 0; right: 0; @@ -311,8 +357,6 @@ background: transparent url("../../../../node_modules/flexlayout-react/images/restore.png") no-repeat center; } - .flexlayout__popup_menu {} - .flexlayout__popup_menu_item { padding: 2px 10px 2px 10px; color: #ddd; @@ -332,28 +376,28 @@ } .flexlayout__border_top { - background-color: black; + background-color: $dark-gray; border-bottom: 1px solid #ddd; box-sizing: border-box; overflow: hidden; } .flexlayout__border_bottom { - background-color: black; + background-color: $dark-gray; border-top: 1px solid #333; box-sizing: border-box; overflow: hidden; } .flexlayout__border_left { - background-color: black; + background-color: $dark-gray; border-right: 1px solid #333; box-sizing: border-box; overflow: hidden; } .flexlayout__border_right { - background-color: black; + background-color: $dark-gray; border-left: 1px solid #333; box-sizing: border-box; overflow: hidden; diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 388f9a909..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); })); } @@ -445,4 +469,4 @@ Scripting.addGlobal(function openInLightbox(doc: any) { LightboxView.AddDocTab(d "opens up document in a lightbox", "(doc: any)"); Scripting.addGlobal(function openOnRight(doc: any) { return CollectionDockingView.AddSplit(doc, "right"); }, "opens up document in tab on right side of the screen", "(doc: any)"); -Scripting.addGlobal(function useRightSplit(doc: any, shiftKey?: boolean) { CollectionDockingView.ReplaceTab(doc, "right", undefined, shiftKey); }); +Scripting.addGlobal(function useRightSplit(doc: any, shiftKey?: boolean) { CollectionDockingView.ReplaceTab(doc, "right", undefined, shiftKey); });
\ No newline at end of file diff --git a/src/client/views/collections/CollectionLinearView.scss b/src/client/views/collections/CollectionLinearView.scss index ca72b98a5..46e40489b 100644 --- a/src/client/views/collections/CollectionLinearView.scss +++ b/src/client/views/collections/CollectionLinearView.scss @@ -1,4 +1,4 @@ -@import "../globalCssVariables"; +@import "../global/globalCssVariables"; @import "../_nodeModuleOverrides"; .collectionLinearView-outer { @@ -12,59 +12,65 @@ align-items: center; >span { - background: $dark-color; - color: $light-color; + background: $dark-gray; + color: $white; border-radius: 18px; margin-right: 6px; cursor: pointer; } .bottomPopup-background { - padding-right: 14px; + background: $medium-blue; + display: flex; + border-radius: 10px; height: 35; - transform: translate3d(6px, 5px, 0px); - padding-top: 6.5px; - padding-bottom: 7px; - padding-left: 5px; + transform: translate3d(6px, 0px, 0px); + align-content: center; + justify-content: center; + align-items: center; } .bottomPopup-text { + color: $white; display: inline; white-space: nowrap; padding-left: 8px; - padding-right: 4px; + padding-right: 20px; vertical-align: middle; font-size: 12.5px; } .bottomPopup-descriptions { + cursor:pointer; display: inline; white-space: nowrap; padding-left: 8px; padding-right: 8px; vertical-align: middle; - background-color: lightgrey; - border-radius: 5.5px; + background-color: $light-gray; + border-radius: 3px; color: black; margin-right: 5px; } .bottomPopup-exit { + cursor:pointer; display: inline; white-space: nowrap; + margin-right: 10px; padding-left: 8px; padding-right: 8px; vertical-align: middle; - background-color: lightgrey; - border-radius: 5.5px; + background-color: $close-red; + border-radius: 3px; color: black; } >label { margin-top: "auto"; margin-bottom: "auto"; - background: $dark-color; - color: $light-color; + background: $dark-gray; + color: $white; display: inline-block; border-radius: 18px; font-size: 12.5px; @@ -82,7 +88,7 @@ } label:hover { - background: $main-accent; + background: $medium-gray; transform: scale(1.15); } diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx index e0b90304b..52c836556 100644 --- a/src/client/views/collections/CollectionLinearView.tsx +++ b/src/client/views/collections/CollectionLinearView.tsx @@ -167,24 +167,22 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { })} </div> {DocumentLinksButton.StartLink ? <span className="bottomPopup-background" style={{ - background: backgroundColor === color ? "black" : backgroundColor, pointerEvents: "all" }} onPointerDown={e => e.stopPropagation()} > <span className="bottomPopup-text" > - Creating link from: {DocumentLinksButton.AnnotationId ? "Annotation in " : " "} {StrCast(DocumentLinksButton.StartLink.title).length < 51 ? DocumentLinksButton.StartLink.title : StrCast(DocumentLinksButton.StartLink.title).slice(0, 50) + '...'} + Creating link from: <b>{DocumentLinksButton.AnnotationId ? "Annotation in " : " "} {StrCast(DocumentLinksButton.StartLink.title).length < 51 ? DocumentLinksButton.StartLink.title : StrCast(DocumentLinksButton.StartLink.title).slice(0, 50) + '...'}</b> </span> - <Tooltip title={<><div className="dash-tooltip">{LinkDescriptionPopup.showDescriptions ? "Turn off description pop-up" : - "Turn on description pop-up"} </div></>} placement="top"> + <Tooltip title={<><div className="dash-tooltip">{"Toggle description pop-up"} </div></>} placement="top"> <span className="bottomPopup-descriptions" onClick={this.changeDescriptionSetting}> Labels: {LinkDescriptionPopup.showDescriptions ? LinkDescriptionPopup.showDescriptions : "ON"} </span> </Tooltip> - <Tooltip title={<><div className="dash-tooltip">Exit link clicking mode </div></>} placement="top"> + <Tooltip title={<><div className="dash-tooltip">Exit linking mode</div></>} placement="top"> <span className="bottomPopup-exit" onClick={this.exitLongLinks}> - Clear + Stop </span> </Tooltip> diff --git a/src/client/views/collections/CollectionMenu.scss b/src/client/views/collections/CollectionMenu.scss index dc5231a3a..f04b19ef7 100644 --- a/src/client/views/collections/CollectionMenu.scss +++ b/src/client/views/collections/CollectionMenu.scss @@ -1,5 +1,4 @@ -@import "../globalCssVariables"; - +@import "../global/globalCssVariables"; .collectionMenu-cont { position: relative; @@ -8,8 +7,8 @@ opacity: 0.9; z-index: 901; transition: top .5s; - background: #323232; - color: white; + background: $dark-gray; + color: $white; transform-origin: top left; top: 0; width: 100%; @@ -18,7 +17,7 @@ border-radius: 100%; width: 18px; height: 18px; - border: solid 1px #f5f5f5; + border: solid 1px $white; display: flex; align-items: center; justify-content: center; @@ -28,7 +27,7 @@ border-radius: 100%; width: 70%; height: 70%; - background: white; + background: $white; } .collectionMenu { @@ -39,11 +38,11 @@ border: unset; .collectionMenu-divider { - height: 85%; + height: 100%; margin-left: 3px; margin-right: 3px; - width: 1.5px; - background-color: #656060; + width: 2px; + background-color: $medium-gray; } .collectionViewBaseChrome { @@ -51,11 +50,11 @@ align-items: center; .collectionViewBaseChrome-viewPicker { - font-size: 75%; - outline-color: black; - color: white; + font-size: $small-text; + outline-color: $black; + color: $white; border: none; - background: #323232; + background: $dark-gray; } .collectionViewBaseChrome-viewPicker:focus { @@ -64,16 +63,16 @@ } .collectionViewBaseChrome-viewPicker:active { - outline-color: black; + outline-color: $black; } .collectionViewBaseChrome-button { - font-size: 75%; + font-size: $small-text; text-transform: uppercase; letter-spacing: 2px; - background: rgb(238, 238, 238); - color: purple; - outline-color: black; + background: $white; + color: $pink; + outline-color: $black; border: none; padding: 12px 10px 11px 10px; margin-left: 10px; @@ -82,11 +81,11 @@ .collectionViewBaseChrome-cmdPicker { margin-left: 3px; margin-right: 0px; - font-size: 75%; + font-size: $small-text; text-transform: capitalize; - color: white; + color: $white; border: none; - background: #323232; + background: $dark-gray; } .collectionViewBaseChrome-cmdPicker:focus { @@ -105,7 +104,7 @@ overflow: hidden; .commandEntry-drop { - color: white; + color: $white; width: 30px; margin-top: auto; margin-bottom: auto; @@ -113,11 +112,11 @@ } .commandEntry-outerDiv:hover{ - background-color: rgba(0,0,0,0.2); + background-color: $drop-shadow; .collectionViewBaseChrome-viewPicker, .collectionViewBaseChrome-cmdPicker{ - background: rgb(41,41,41); + background: $dark-gray; } } @@ -142,7 +141,7 @@ height: 100%; display: flex; background: transparent; - color: grey; + color: $medium-gray; justify-content: center; } @@ -150,31 +149,31 @@ margin-left: 5px; display: grid; border: none; - border-right: solid gray 1px; + border-right: solid $medium-gray 1px; .collectionViewBaseChrome-filterIcon { position: relative; display: flex; margin: auto; - background: #323232; - color: white; + background: $dark-gray; + color: $white; width: 30px; height: 30px; align-items: center; justify-content: center; border: none; - border-right: solid gray 1px; + border-right: solid $medium-gray 1px; } .collectionViewBaseChrome-viewSpecsInput { padding: 12px 10px 11px 10px; border: 0px; - color: grey; + color: $medium-gray; text-align: center; letter-spacing: 2px; - outline-color: black; - font-size: 75%; - background: rgb(238, 238, 238); + outline-color: $black; + font-size: $small-text; + background: $white; height: 100%; width: 75px; } @@ -187,8 +186,8 @@ z-index: 100; display: flex; flex-direction: column; - background: rgb(238, 238, 238); - box-shadow: grey 2px 2px 4px; + background: $white; + box-shadow: $medium-gray 2px 2px 4px; .qs-datepicker { left: unset; @@ -204,13 +203,13 @@ .collectionViewBaseChrome-viewSpecsMenu-rowLeft, .collectionViewBaseChrome-viewSpecsMenu-rowMiddle, .collectionViewBaseChrome-viewSpecsMenu-rowRight { - font-size: 75%; + font-size: $small-text; letter-spacing: 2px; - color: grey; + color: $medium-gray; margin-left: 10px; padding: 5px; border: none; - outline-color: black; + outline-color: $black; } } @@ -236,19 +235,19 @@ margin-left: 10; .collectionGridViewChrome-viewPicker { - font-size: 75%; + font-size: $small-text; //text-transform: uppercase; //letter-spacing: 2px; - background: #121721; - color: white; - outline-color: black; - color: white; + background: $dark-gray; + color: $white; + outline-color: $black; + color: $white; border: none; - border-right: solid gray 1px; + border-right: solid $medium-gray 1px; } .collectionGridViewChrome-viewPicker:active { - outline-color: black; + outline-color: $black; } .grid-control { @@ -268,11 +267,11 @@ .collectionGridViewChrome-entryBox { width: 50%; - color: black; + color: $black; } .collectionGridViewChrome-columnButton { - color: black; + color: $black; } } } @@ -302,7 +301,7 @@ align-items: center; display: flex; grid-auto-columns: auto; - font-size: 75%; + font-size: $small-text; letter-spacing: 2px; .collectionStackingViewChrome-pivotField-label, @@ -311,7 +310,7 @@ grid-column: 1; margin-right: 7px; user-select: none; - font-family: 'Roboto'; + font-family: $sans-serif; letter-spacing: normal; } @@ -329,13 +328,13 @@ } .collectionStackingViewChrome-sortIcon:hover { - background-color: rgba(0,0,0,0.2); + background-color: $drop-shadow; } .collectionStackingViewChrome-pivotField, .collectionTreeViewChrome-pivotField, .collection3DCarouselViewChrome-scrollSpeed { - color: white; + color: $white; grid-column: 2; grid-row: 1; width: 90%; @@ -344,7 +343,7 @@ height: 80%; border-radius: 7px; align-items: center; - background: #eeeeee; + background: $white; .editable-view-input, input, @@ -352,16 +351,16 @@ .editableView-container-editing { margin: auto; border: 0px; - color: grey !important; + color: $light-gray !important; text-align: center; letter-spacing: 2px; - outline-color: black; + outline-color: $black; height: 100%; } .react-autosuggest__container { margin: 0; - color: grey; + color: $medium-gray; padding: 0px; } } @@ -407,11 +406,11 @@ } .switchToText { - color: $main-accent; + color: $medium-gray; } .switchToText:hover { - color: $dark-color; + color: $dark-gray; } } @@ -424,11 +423,11 @@ .collectionMenu-urlInput { padding: 12px 10px 11px 10px; border: 0px; - color: black; - font-size: 10px; + color: $black; + font-size: $small-text; letter-spacing: 2px; - outline-color: black; - background: rgb(238, 238, 238); + outline-color: $black; + background: $white; width: 100%; min-width: 350px; margin-right: 10px; @@ -477,10 +476,10 @@ width: 20; height: 30; bottom: 0; - background: #323232; + background: $dark-gray; display: inline-flex; align-items: center; - color: white; + color: $white; } .backKeyframe { @@ -502,13 +501,13 @@ margin: auto; } - border-right: solid gray 1px; + border-right: solid $medium-gray 1px; } } .collectionSchemaViewChrome-cont { display: flex; - font-size: 10.5px; + font-size: $small-text; .collectionSchemaViewChrome-toggle { display: flex; @@ -527,19 +526,19 @@ .collectionSchemaViewChrome-toggler { width: 100px; height: 35px; - background-color: black; + background-color: $black; position: relative; } .collectionSchemaViewChrome-togglerButton { width: 47px; height: 30px; - background-color: $light-color-secondary; + background-color: $light-gray; // position: absolute; transition: all 0.5s ease; // top: 3px; margin-top: 3px; - color: gray; + color: $medium-gray; letter-spacing: 2px; text-transform: uppercase; display: flex; @@ -579,7 +578,7 @@ } .react-autosuggest__input { - border: 1px solid #aaa; + border: 1px solid $light-gray; border-radius: 4px; width: 100%; } @@ -603,11 +602,11 @@ overflow-y: auto; max-height: 400px; width: 180px; - border: 1px solid #aaa; - background-color: #fff; - font-family: Helvetica, sans-serif; + border: 1px solid $light-gray; + background-color: $white; + font-family: $sans-serif; font-weight: 300; - font-size: 16px; + font-size: $large-header; border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; z-index: 2; @@ -625,5 +624,5 @@ } .react-autosuggest__suggestion--highlighted { - background-color: #ddd; + 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 6e6fabd0d..8f4df4a92 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -29,7 +29,7 @@ import { ActiveFillColor, ActiveInkColor, SetActiveArrowEnd, SetActiveArrowStart import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; import { DocumentView } from "../nodes/DocumentView"; import { RichTextMenu } from "../nodes/formattedText/RichTextMenu"; -import { PresBox } from "../nodes/PresBox"; +import { PresBox } from "../nodes/trails/PresBox"; import "./CollectionMenu.scss"; import { CollectionViewType, COLLECTION_BORDER_WIDTH } from "./CollectionView"; import { TabDocView } from "./TabDocView"; @@ -665,7 +665,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 +683,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> diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx index 6baf633dd..bc1407c53 100644 --- a/src/client/views/collections/CollectionPileView.tsx +++ b/src/client/views/collections/CollectionPileView.tsx @@ -1,12 +1,12 @@ -import { action, computed } from "mobx"; +import { action, computed, IReactionDisposer, reaction } from "mobx"; import { observer } from "mobx-react"; import { Doc, HeightSym, WidthSym } from "../../../fields/Doc"; import { NumCast, StrCast } from "../../../fields/Types"; -import { emptyFunction, setupMoveUpEvents, returnTrue } from "../../../Utils"; +import { emptyFunction, returnTrue, setupMoveUpEvents } from "../../../Utils"; import { DocUtils } from "../../documents/Documents"; import { SelectionManager } from "../../util/SelectionManager"; import { SnappingManager } from "../../util/SnappingManager"; -import { UndoManager, undoBatch } from "../../util/UndoManager"; +import { undoBatch, UndoManager } from "../../util/UndoManager"; import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; import "./CollectionPileView.scss"; import { CollectionSubView } from "./CollectionSubView"; @@ -15,6 +15,7 @@ import React = require("react"); @observer export class CollectionPileView extends CollectionSubView(doc => doc) { _originalChrome: any = ""; + _disposers: { [name: string]: IReactionDisposer } = {}; componentDidMount() { if (this.layoutEngine() !== "pass" && this.layoutEngine() !== "starburst") { @@ -22,9 +23,14 @@ export class CollectionPileView extends CollectionSubView(doc => doc) { } this._originalChrome = this.layoutDoc._chromeHidden; this.layoutDoc._chromeHidden = true; + + // pileups are designed to go away when they are empty. + this._disposers.selected = reaction(() => this.childDocs.length, + (num) => !num && this.props.ContainingCollectionView?.removeDocument(this.props.Document)); } componentWillUnmount() { this.layoutDoc._chromeHidden = this._originalChrome; + Object.values(this._disposers).forEach(disposer => disposer?.()); } layoutEngine = () => StrCast(this.Document._pileLayoutEngine); @@ -107,9 +113,6 @@ export class CollectionPileView extends CollectionSubView(doc => doc) { this._undoBatch?.end(); this._undoBatch = undefined; SnappingManager.SetIsDragging(false); - if (!this.childDocs.length) { - this.props.ContainingCollectionView?.removeDocument(this.props.Document); - } }, emptyFunction, e.shiftKey && this.layoutEngine() === "pass", this.layoutEngine() === "pass" && e.shiftKey); // this sets _doubleTap } diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 8f2847139..6bdeaf722 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -11,12 +11,14 @@ import { listSpec } from "../../../fields/Schema"; import { PastelSchemaPalette, SchemaHeaderField } from "../../../fields/SchemaHeaderField"; import { Cast, NumCast } from "../../../fields/Types"; import { TraceMobx } from "../../../fields/util"; -import { emptyFunction, emptyPath, returnFalse, setupMoveUpEvents, returnEmptyDoclist, returnTrue } from "../../../Utils"; +import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from "../../../Utils"; +import { DocUtils } from "../../documents/Documents"; import { SelectionManager } from "../../util/SelectionManager"; import { SnappingManager } from "../../util/SnappingManager"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; -import { COLLECTION_BORDER_WIDTH, SCHEMA_DIVIDER_WIDTH } from '../../views/globalCssVariables.scss'; +import { COLLECTION_BORDER_WIDTH, SCHEMA_DIVIDER_WIDTH } from '../../views/global/globalCssVariables.scss'; +import { SchemaTable } from "../collections/collectionSchema/SchemaTable"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import '../DocumentDecorations.scss'; @@ -24,8 +26,6 @@ import { DocumentView } from "../nodes/DocumentView"; import { DefaultStyleProvider } from "../StyleProvider"; import "./CollectionSchemaView.scss"; import { CollectionSubView } from "./CollectionSubView"; -import { SchemaTable } from "./SchemaTable"; -import { DocUtils } from "../../documents/Documents"; // bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657 export enum ColumnType { @@ -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.scss b/src/client/views/collections/CollectionStackingView.scss index 9f56a0c0e..4b123c8b6 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -1,4 +1,4 @@ -@import "../globalCssVariables"; +@import "../global/globalCssVariables"; .collectionMasonryView { display: inline; @@ -96,8 +96,8 @@ height: 2vw; width: 100%; font-family: $sans-serif; - background: $dark-color; - color: $light-color; + background: $dark-gray; + color: $white; } .collectionStackingView-columnDragger { @@ -128,7 +128,7 @@ margin-left: 2px; margin-right: 2px; margin-top: 2px; - background: $main-accent; + background: $medium-gray; height: 5px; &.active { @@ -180,11 +180,11 @@ .collectionStackingView-sectionHeader { text-align: center; margin: auto; - background: $main-accent; + background: $medium-gray; // overflow: hidden; overflow is visible so the color menu isn't hidden -ftong .editableView-input { - color: black; + color: $dark-gray; } .editableView-input:hover, @@ -205,7 +205,7 @@ display: flex; align-items: center; justify-content: center; - color: black; + color: $dark-gray; .editableView-container-editing-oneLine, .editableView-container-editing { diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 30f8e0112..4fedda1bd 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -144,7 +144,7 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, () => this.layoutDoc._columnHeaders = new List() ); this._autoHeightDisposer = reaction(() => this.layoutDoc._autoHeight, - () => this.props.setHeight(Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), + autoHeight => autoHeight && this.props.setHeight(Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), this.headerMargin + (this.isStackingView ? Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace("px", "")))) : this.refList.reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), 0))))); @@ -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} @@ -480,7 +480,7 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, if (value && this.columnHeaders) { const schemaHdrField = new SchemaHeaderField(value); this.columnHeaders.push(schemaHdrField); - DocUtils.addFieldEnumerations(undefined, this.pivotField, [{ title: value, _backgroundColor: schemaHdrField.color }]); + DocUtils.addFieldEnumerations(undefined, this.pivotField, [{ title: value, _backgroundColor: "schemaHdrField.color" }]); return true; } return false; diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 8d549bd56..b70df93da 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,8 @@ import ReactLoading from 'react-loading'; export interface SubCollectionViewProps extends CollectionViewProps { CollectionView: Opt<CollectionView>; + SetSubView?: (subView: any) => void; + isAnyChildContentActive: () => boolean; } export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: X) { @@ -30,6 +32,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 +49,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 +81,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 +115,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 +129,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; + } } } } @@ -241,7 +251,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: @undoBatch @action - protected async onExternalDrop(e: React.DragEvent, options: DocumentOptions, completed?: () => void) { + protected async onExternalDrop(e: React.DragEvent, options: DocumentOptions, completed?: (docs: Doc[]) => void) { if (e.ctrlKey) { e.stopPropagation(); // bcz: this is a hack to stop propagation when dropping an image on a text document with shift+ctrl return; @@ -303,7 +313,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 @@ -439,7 +449,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: } this.slowLoadDocuments(files, options, generatedDocuments, text, completed, e.clientX, e.clientY, addDocument).then(batch.end); } - slowLoadDocuments = async (files: (File[] | string), options: DocumentOptions, generatedDocuments: Doc[], text: string, completed: (() => void) | undefined, clientX: number, clientY: number, addDocument: (doc: Doc | Doc[]) => boolean) => { + slowLoadDocuments = async (files: (File[] | string), options: DocumentOptions, generatedDocuments: Doc[], text: string, completed: ((doc: Doc[]) => void) | undefined, clientX: number, clientY: number, addDocument: (doc: Doc | Doc[]) => boolean) => { const disposer = OverlayView.Instance.addElement( <ReactLoading type={"spinningBubbles"} color={"green"} height={250} width={250} />, { x: clientX - 125, y: clientY - 125 }); if (typeof files === "string") { @@ -448,13 +458,17 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: generatedDocuments.push(...await DocUtils.uploadFilesToDocs(files, options)); } if (generatedDocuments.length) { - const set = generatedDocuments.length > 1 && generatedDocuments.map(d => DocUtils.iconify(d)); - if (set) { - addDocument(DocUtils.pileup(generatedDocuments, options.x!, options.y!)!); - } else { - generatedDocuments.forEach(addDocument); + const isFreeformView = this.props.Document._viewType === CollectionViewType.Freeform; + const set = !isFreeformView ? generatedDocuments : + generatedDocuments.length > 1 ? generatedDocuments.map(d => { DocUtils.iconify(d); return d; }) : []; + if (completed) completed(set); + else { + if (isFreeformView && generatedDocuments.length > 1) { + addDocument(DocUtils.pileup(generatedDocuments, options.x!, options.y!)); + } else { + generatedDocuments.forEach(addDocument); + } } - completed?.(); } else { if (text && !text.includes("https://")) { addDocument(Docs.Create.TextDocument(text, { ...options, title: text.substring(0, 20), _width: 400, _height: 315 })); @@ -477,7 +491,5 @@ import { FormattedTextBox, GoogleRef } from "../nodes/formattedText/FormattedTex import { CollectionView, CollectionViewType, CollectionViewProps } from "./CollectionView"; import { SelectionManager } from "../../util/SelectionManager"; import { OverlayView } from "../OverlayView"; -import { Hypothesis } from "../../util/HypothesisUtils"; import { GetEffectiveAcl, TraceMobx } from "../../../fields/util"; -import { FilterBox } from "../nodes/FilterBox"; diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx index f41043179..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.TextanchorDocument({ + 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.scss b/src/client/views/collections/CollectionTreeView.scss index 72ab51784..d370d21ab 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -1,5 +1,8 @@ -@import "../globalCssVariables"; +@import "../global/globalCssVariables"; +.collectionTreeView-container { + transform-origin: top left; +} .collectionTreeView-dropTarget { border-width: $COLLECTION_BORDER_WIDTH; border-color: transparent; @@ -12,7 +15,7 @@ top: 0; padding-left: 10px; padding-right: 10px; - background: $light-color-secondary; + background: $light-gray; font-size: 13px; overflow: auto; user-select: none; @@ -35,12 +38,17 @@ width: max-content; } + .no-indent-outline { + padding-left: 0; + width: 100%; + } + .editableView-container { font-weight: bold; } .delete-button { - color: $intermediate-color; + color: $medium-gray; // float: right; margin-left: 15px; // margin-top: 3px; @@ -71,7 +79,7 @@ .collectionTreeView-subtitle { font-style: italic; font-size: 8pt; - color: $intermediate-color; + color: $medium-gray; } .docContainer { diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 82c8a9114..89d42439e 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -1,15 +1,16 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, reaction, IReactionDisposer, observable } from "mobx"; +import { action, computed, IReactionDisposer, observable, reaction } from "mobx"; import { observer } from "mobx-react"; -import { DataSym, Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; +import { DataSym, Doc, DocListCast, HeightSym, Opt, StrListCast, WidthSym } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; -import { List } from '../../../fields/List'; -import { Document } from '../../../fields/Schema'; +import { InkTool } from '../../../fields/InkField'; +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'; import { returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../../Utils'; import { DocUtils } from '../../documents/Documents'; +import { CurrentUserUtils } from '../../util/CurrentUserUtils'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager, dropActionType } from "../../util/DragManager"; import { SelectionManager } from '../../util/SelectionManager'; @@ -25,8 +26,6 @@ import { CollectionSubView } from "./CollectionSubView"; import "./CollectionTreeView.scss"; import { TreeView } from "./TreeView"; import React = require("react"); -import { InkTool } from '../../../fields/InkField'; -import { CurrentUserUtils } from '../../util/CurrentUserUtils'; const _global = (window /* browser */ || global /* node */) as any; export type collectionTreeViewProps = { @@ -52,6 +51,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; @@ -61,7 +61,9 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll this.props.isSelected(outsideReaction) || this._isAnyChildContentActive || this.props.rootSelected(outsideReaction)) ? true : false) + isDisposing = false; componentWillUnmount() { + this.isDisposing = true; super.componentWillUnmount(); this.treedropDisposer?.(); Object.values(this._disposers).forEach(disposer => disposer?.()); @@ -76,15 +78,16 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll refList: Set<any> = new Set(); observer: any; computeHeight = () => { - const hgt = this.paddingTop() + 26/* bcz: ugh: title bar height hack ... get ref and compute instead */ + - Array.from(this.refList).reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), 0); - this.props.setHeight(hgt); + if (this.isDisposing) return; + const bodyHeight = Array.from(this.refList).reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), this.paddingTop() + this.paddingBot()); + this.layoutDoc._autoHeightMargins = bodyHeight; + this.props.setHeight(this.documentTitleHeight() + bodyHeight); } unobserveHeight = (ref: any) => { this.refList.delete(ref); this.rootDoc.autoHeight && this.computeHeight(); } - observerHeight = (ref: any) => { + observeHeight = (ref: any) => { if (ref) { this.refList.add(ref); this.observer = new _global.ResizeObserver(action((entries: any) => { @@ -154,7 +157,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", noexpand: true, subitems: onClicks, icon: "mouse-pointer" }); } } - onTreeDrop = (e: React.DragEvent) => this.onExternalDrop(e, {}); + onTreeDrop = (e: React.DragEvent, addDocs?: (docs: Doc[]) => void) => this.onExternalDrop(e, {}, addDocs); @undoBatch makeTextCollection = (childDocs: Doc[]) => { @@ -199,6 +202,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll NativeWidth={this.documentTitleWidth} NativeHeight={this.documentTitleHeight} focus={this.props.focus} + treeViewDoc={this.props.Document} ScreenToLocalTransform={this.titleTransform} docFilters={returnEmptyFilter} docRangeFilters={returnEmptyFilter} @@ -215,6 +219,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; @@ -246,12 +255,14 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll true, this.whenChildContentsActiveChanged, this.props.dontRegisterView || Cast(this.props.Document.childDontRegisterViews, "boolean", null), - this.observerHeight, - this.unobserveHeight); + this.observeHeight, + this.unobserveHeight, + this.childContextMenuItems() + ); } @computed get titleBar() { const hideTitle = this.props.treeViewHideTitle || this.doc.treeViewHideTitle; - return hideTitle ? (null) : (this.doc.treeViewType === "outline" ? this.documentTitle : this.editableTitle)(this.treeChildren); + return hideTitle ? (null) : (this.outlineMode ? this.documentTitle : this.editableTitle)(this.treeChildren); } @computed get renderClearButton() { @@ -261,30 +272,42 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll </button> </div >; } + @computed get nativeWidth() { return Doc.NativeWidth(this.Document, undefined, true); } + @computed get nativeHeight() { return Doc.NativeHeight(this.Document, undefined, true); } + @computed get contentScaling() { + const nw = this.nativeWidth; + const nh = this.nativeHeight; + const hscale = nh ? this.props.PanelHeight() / nh : 1; + const wscale = nw ? this.props.PanelWidth() / nw : 1; + return wscale < hscale ? wscale : hscale; + } paddingX = () => NumCast(this.doc._xPadding, 15); paddingTop = () => NumCast(this.doc._yPadding, 20); + paddingBot = () => NumCast(this.doc._yPadding, 20); documentTitleWidth = () => Math.min(this.layoutDoc?.[WidthSym](), this.panelWidth()); - documentTitleHeight = () => Math.min(this.layoutDoc?.[HeightSym](), (StrCast(this.layoutDoc?._fontSize) ? Number(StrCast(this.layoutDoc?._fontSize, "32px").replace("px", "")) : NumCast(this.layoutDoc?._fontSize)) * 2); + documentTitleHeight = () => (this.layoutDoc?.[HeightSym]() || 0) - NumCast(this.layoutDoc.autoHeightMargins); titleTransform = () => this.props.ScreenToLocalTransform().translate(-NumCast(this.doc._xPadding, 10), -NumCast(this.doc._yPadding, 20)); truncateTitleWidth = () => this.treeViewtruncateTitleWidth; onChildClick = () => this.props.onChildClick?.() || ScriptCast(this.doc.onChildClick); - panelWidth = () => this.props.PanelWidth() - 2 * this.paddingX(); + panelWidth = () => (this.props.PanelWidth() - 2 * this.paddingX()) * (this.props.scaling?.() || 1); render() { TraceMobx(); const background = () => this.props.styleProvider?.(this.doc, this.props, StyleProp.BackgroundColor); const pointerEvents = () => !this.props.isContentActive() && !SnappingManager.GetIsDragging() ? "none" : undefined; return !(this.doc instanceof Doc) || !this.treeChildren ? (null) : - <div className="collectionTreeView-container" onContextMenu={this.onContextMenu}> + <div className="collectionTreeView-container" + style={this.outlineMode ? { transform: `scale(${this.contentScaling})`, width: `calc(${100 / this.contentScaling}%)` } : {}} + onContextMenu={this.onContextMenu}> <div className="collectionTreeView-dropTarget" - style={{ background: background(), paddingLeft: `${this.paddingX()}px`, paddingRight: `${this.paddingX()}px`, paddingTop: `${this.paddingTop()}px`, pointerEvents: pointerEvents() }} + style={{ background: background(), paddingLeft: `${this.paddingX()}px`, paddingRight: `${this.paddingX()}px`, paddingBottom: `${this.paddingBot()}px`, paddingTop: `${this.paddingTop()}px`, pointerEvents: pointerEvents() }} onWheel={e => e.stopPropagation()} onDrop={this.onTreeDrop} ref={this.createTreeDropTarget}> {this.titleBar} {this.renderClearButton} - <ul className="no-indent"> + <ul className={`no-indent${this.outlineMode ? "-outline" : ""}`} > {this.treeViewElements} </ul> </div > diff --git a/src/client/views/collections/CollectionView.scss b/src/client/views/collections/CollectionView.scss index a5aef86de..5db489c0a 100644 --- a/src/client/views/collections/CollectionView.scss +++ b/src/client/views/collections/CollectionView.scss @@ -1,8 +1,8 @@ -@import "../globalCssVariables"; +@import "../global/globalCssVariables"; .collectionView { border-width: 0; - border-color: $light-color-secondary; + border-color: $light-gray; border-style: solid; border-radius: 0 0 $border-radius $border-radius; box-sizing: border-box; diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index e225c4a11..229e93b9e 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,8 @@ import { CollectionTimeView } from './CollectionTimeView'; import { CollectionTreeView } from "./CollectionTreeView"; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import './CollectionView.scss'; +import { returnEmptyString } from '../../../Utils'; +import { InkTool } from '../../../fields/InkField'; export const COLLECTION_BORDER_WIDTH = 2; const path = require('path'); @@ -62,7 +64,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 +72,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 +86,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 +94,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) { @@ -112,7 +120,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab // const imageProtos = children.filter(doc => Cast(doc.data, ImageField)).map(Doc.GetProto); // const allTagged = imageProtos.length > 0 && imageProtos.every(image => image.googlePhotosTags); // return !allTagged ? (null) : <img id={"google-tags"} src={"/assets/google_tags.png"} />; - this.isContentActive(); + //this.isContentActive(); } screenToLocalTransform = () => this.props.renderDepth ? this.props.ScreenToLocalTransform() : this.props.ScreenToLocalTransform().scale(this.props.PanelWidth() / this.bodyPanelWidth()); @@ -186,15 +194,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,17 +249,25 @@ 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; + + isContentActive = (outsideReaction?: boolean) => { + return this.props.isContentActive() ? true : false; + } render() { TraceMobx(); const props: SubCollectionViewProps = { ...this.props, + SetSubView: action((subView: any) => this._subView = subView), addDocument: this.addDocument, moveDocument: this.moveDocument, removeDocument: this.removeDocument, isContentActive: this.isContentActive, + isAnyChildContentActive: this.isAnyChildContentActive, + whenChildContentsActiveChanged: this.whenChildContentsActiveChanged, PanelWidth: this.bodyPanelWidth, PanelHeight: this.props.PanelHeight, ScreenToLocalTransform: this.screenToLocalTransform, @@ -260,8 +281,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 9acbc4f85..a963f1cb9 100644 --- a/src/client/views/collections/TabDocView.scss +++ b/src/client/views/collections/TabDocView.scss @@ -1,19 +1,62 @@ input.lm_title:focus, -input.lm_title -{ +input.lm_title { max-width: unset !important; + outline: none; transition-delay: unset; - width: 100%; + width: max-content; cursor: text; } + input.lm_title { transition-delay: 0.35s; - width: 100px; + width: max-content; cursor: pointer; } -.tabDocView-drag { - margin: auto; + +.lm_iconWrap { + display: flex; + color: black; + width: 15px; + height: 15px; + align-items: center; + align-self: center; + justify-content: center; + margin: 3px; + border-radius: 20%; + + .moreInfoDot { + background-color: white; + border-radius: 100%; + width: 3px; + height: 3px; + margin: 0.5px; + } +} + +.ffMenu { + display: grid; + grid-auto-rows: 35px; + grid-auto-columns: auto auto auto auto auto; + right: 10px; + bottom: 50px; + position: absolute; + min-height: 35px; + height: max-content; + border: solid 2px black; + border-radius: 5px; + background-color: #bddbe6; + width: max-content; + min-width: 35px; + + .ffMenuButton { + display: flex; + width: 35px; + height: 35px; + align-items: center; + justify-content: center; + } } + .miniMap-hidden, .miniMap { position: absolute; @@ -37,6 +80,7 @@ input.lm_title { } } } + .miniMap-hidden { position: absolute; bottom: 0; @@ -46,7 +90,8 @@ input.lm_title { transform: translate(20px, 20px) rotate(45deg); border-radius: 30px; padding: 2px; - > svg { + + >svg { margin-top: 3px; transform: translate(0px, 7px); } diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index 7e2f7811e..117dba4de 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -1,3 +1,4 @@ +import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; import 'golden-layout/src/css/goldenlayout-base.css'; @@ -9,9 +10,8 @@ import * as ReactDOM from 'react-dom'; import { DataSym, Doc, DocListCast, DocListCastAsync, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; import { Id } from '../../../fields/FieldSymbols'; import { FieldId } from "../../../fields/RefField"; -import { Cast, NumCast, StrCast, BoolCast } from "../../../fields/Types"; -import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents, Utils } from "../../../Utils"; +import { BoolCast, Cast, NumCast, StrCast } from "../../../fields/Types"; +import { emptyFunction, lightOrDark, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents, Utils } from "../../../Utils"; import { DocServer } from "../../DocServer"; import { DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; @@ -24,15 +24,15 @@ import { Transform } from '../../util/Transform'; import { undoBatch, UndoManager } from "../../util/UndoManager"; import { LightboxView } from '../LightboxView'; import { DocFocusOptions, DocumentView, DocumentViewProps } from "../nodes/DocumentView"; -import { FieldViewProps } from '../nodes/FieldView'; -import { PinProps, PresBox, PresMovement } from '../nodes/PresBox'; +import { PinProps, PresBox, PresMovement } from '../nodes/trails'; import { DefaultLayerProvider, DefaultStyleProvider, StyleLayers, StyleProp } from '../StyleProvider'; import { CollectionDockingView } from './CollectionDockingView'; import { CollectionDockingViewMenu } from './CollectionDockingViewMenu'; import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; -import { CollectionViewType, CollectionView } from './CollectionView'; +import { CollectionView, CollectionViewType } from './CollectionView'; import "./TabDocView.scss"; import React = require("react"); +import Color = require('color'); const _global = (window /* browser */ || global /* node */) as any; interface TabDocViewProps { @@ -52,6 +52,14 @@ export class TabDocView extends React.Component<TabDocViewProps> { @computed get layoutDoc() { return this._document && Doc.Layout(this._document); } @computed get tabColor() { return StrCast(this._document?._backgroundColor, StrCast(this._document?.backgroundColor, DefaultStyleProvider(this._document, undefined, StyleProp.BackgroundColor))); } + @computed get tabTextColor() { return this._document?.type === DocumentType.PRES ? "black" : StrCast(this._document?._color, StrCast(this._document?.color, DefaultStyleProvider(this._document, undefined, StyleProp.Color))); } + // @computed get renderBounds() { + // const bounds = this._document ? Cast(this._document._renderContentBounds, listSpec("number"), [0, 0, this.returnMiniSize(), this.returnMiniSize()]) : [0, 0, 0, 0]; + // const xbounds = bounds[2] - bounds[0]; + // const ybounds = bounds[3] - bounds[1]; + // const dim = Math.max(xbounds, ybounds); + // return { l: bounds[0] + xbounds / 2 - dim / 2, t: bounds[1] + ybounds / 2 - dim / 2, cx: bounds[0] + xbounds / 2, cy: bounds[1] + ybounds / 2, dim }; + // } get stack() { return (this.props as any).glContainer.parent.parent; } get tab() { return (this.props as any).glContainer.tab; } @@ -65,15 +73,25 @@ export class TabDocView extends React.Component<TabDocViewProps> { tab.contentItem.config.fixed && (tab.contentItem.parent.config.fixed = true); tab.DashDoc = doc; CollectionDockingView.Instance.tabMap.add(tab); - + const iconType: IconProp = Doc.toIcon(doc); // setup the title element and set its size according to the # of chars in the title. Show the full title when clicked. const titleEle = tab.titleElement[0]; + const iconWrap = document.createElement("div"); + const closeWrap = document.createElement("div"); + + titleEle.size = StrCast(doc.title).length + 3; titleEle.value = doc.title; titleEle.onchange = undoBatch(action((e: any) => { titleEle.size = e.currentTarget.value.length + 3; Doc.GetProto(doc).title = e.currentTarget.value; })); + + const dragBtnDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, e => !e.defaultPrevented && DragManager.StartDocumentDrag([iconWrap], new DragManager.DocumentDragData([doc], doc.dropAction as dropActionType), e.clientX, e.clientY), returnFalse, emptyFunction); + }; + + if (tab.element[0].children[1].children.length === 1) { const toggle = document.createElement("div"); toggle.style.width = "10px"; @@ -83,18 +101,42 @@ export class TabDocView extends React.Component<TabDocViewProps> { toggle.style.borderTopRightRadius = "7px"; toggle.style.position = "relative"; toggle.style.display = "inline-block"; - toggle.style.background = "gray"; - toggle.style.borderLeft = "solid 1px black"; + toggle.style.background = "transparent"; toggle.onclick = (e: MouseEvent) => { if (tab.contentItem === tab.header.parent.getActiveContentItem()) { tab.DashDoc.activeLayer = tab.DashDoc.activeLayer ? undefined : StyleLayers.Background; } }; - tab.element[0].style.borderTopRightRadius = "8px"; - tab.element[0].children[1].appendChild(toggle); - tab._disposers.layerDisposer = reaction(() => - ({ layer: tab.DashDoc.activeLayer, color: this.tabColor }), - ({ layer, color }) => toggle.style.background = !layer ? color : "dimgrey", { fireImmediately: true }); + iconWrap.className = "lm_iconWrap"; + iconWrap.id = "lm_iconWrap"; + closeWrap.className = "lm_iconWrap"; + closeWrap.id = "lm_closeWrap"; + closeWrap.onclick = (e: MouseEvent) => { + tab.header.parent.contentItem.remove(); + Doc.AddDocToList(CurrentUserUtils.MyRecentlyClosed, "data", tab.DashDoc, undefined, true, true); + }; + const docIcon = <FontAwesomeIcon onPointerDown={dragBtnDown} icon={iconType} />; + const closeIcon = <FontAwesomeIcon icon={"times"} />; + ReactDOM.render(docIcon, iconWrap); + ReactDOM.render(closeIcon, closeWrap); + // tab.element[0].append(closeWrap); + tab.element[0].prepend(iconWrap); + tab._disposers.layerDisposer = reaction(() => ({ layer: tab.DashDoc.activeLayer, color: this.tabColor }), + ({ layer, color }) => { + const textColor = lightOrDark(this.tabColor); //not working with StyleProp.Color + titleEle.style.color = textColor; + titleEle.style.backgroundColor = "transparent"; + iconWrap.style.color = textColor; + closeWrap.style.color = textColor; + moreInfoDrag.style.backgroundColor = textColor; + tab.element[0].style.background = !layer ? color : "dimgrey"; + }, { fireImmediately: true }); + // TODO:glr fix + // tab.element[0].style.borderTopRightRadius = "8px"; + // tab.element[0].children[1].appendChild(toggle); + // tab._disposers.layerDisposer = reaction(() => + // ({ layer: tab.DashDoc.activeLayer, color: this.tabColor }), + // ({ layer, color }) => toggle.style.background = !layer ? color : "dimgrey", { fireImmediately: true }); } // shifts the focus to this tab when another tab is dragged over it tab.element[0].onmouseenter = (e: MouseEvent) => { @@ -103,13 +145,11 @@ export class TabDocView extends React.Component<TabDocViewProps> { tab.setActive(true); } }; - const dragBtnDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, e => !e.defaultPrevented && DragManager.StartDocumentDrag([dragHdl], new DragManager.DocumentDragData([doc], doc.dropAction as dropActionType), e.clientX, e.clientY), returnFalse, emptyFunction); - }; + // select the tab document when the tab is directly clicked and activate the tab whenver the tab document is selected titleEle.onpointerdown = action((e: any) => { - if (e.target.className !== "lm_close_tab") { + if (e.target.className !== "lm_iconWrap") { if (this.view) SelectionManager.SelectView(this.view, false); else this._activated = true; if (Date.now() - titleEle.lastClick < 1000) titleEle.select(); @@ -123,25 +163,29 @@ export class TabDocView extends React.Component<TabDocViewProps> { const toggle = tab.element[0].children[1].children[0] as HTMLInputElement; selected && tab.contentItem !== tab.header.parent.getActiveContentItem() && UndoManager.RunInBatch(() => tab.header.parent.setActiveContentItem(tab.contentItem), "tab switch"); - toggle.style.fontWeight = selected ? "bold" : ""; - toggle.style.textTransform = selected ? "uppercase" : ""; + // toggle.style.fontWeight = selected ? "bold" : ""; + // toggle.style.textTransform = selected ? "uppercase" : ""; })); //attach the selection doc buttons menu to the drag handle - const stack = tab.contentItem.parent; - const dragHdl = document.createElement("div"); - dragHdl.className = "lm_drag_tab"; + const stack: HTMLDivElement = tab.contentItem.parent; + const header: HTMLDivElement = tab; + stack.onscroll = action((e: any) => { + console.log('scrolling...'); + }); + const moreInfoDrag = document.createElement("div"); + moreInfoDrag.className = "lm_iconWrap"; tab._disposers.buttonDisposer = reaction(() => this.view, view => - view && [ReactDOM.render(<span className="tabDocView-drag" onPointerDown={dragBtnDown}><CollectionDockingViewMenu views={() => [view]} Stack={stack} /></span>, dragHdl), tab._disposers.buttonDisposer?.()], + view && [ReactDOM.render(<span><CollectionDockingViewMenu views={() => [view]} Stack={stack} /></span>, moreInfoDrag), tab._disposers.buttonDisposer?.()], { fireImmediately: true }); - tab.reactComponents = [dragHdl]; - tab.closeElement.before(dragHdl); + // tab.reactComponents = [moreInfoDrag]; + // tab.element[0].children[3].before(moreInfoDrag); // highlight the tab when the tab document is brushed in any part of the UI tab._disposers.reactionDisposer = reaction(() => ({ title: doc.title, degree: Doc.IsBrushedDegree(doc) }), ({ title, degree }) => { titleEle.value = title; - titleEle.style.padding = degree ? 0 : 2; - titleEle.style.border = `${["gray", "gray", "gray"][degree]} ${["none", "dashed", "solid"][degree]} 2px`; + // titleEle.style.padding = degree ? 0 : 2; + // titleEle.style.border = `${["gray", "gray", "gray"][degree]} ${["none", "dashed", "solid"][degree]} 2px`; }, { fireImmediately: true }); // clean up the tab when it is closed @@ -221,9 +265,9 @@ export class TabDocView extends React.Component<TabDocViewProps> { })).observe(this.props.glContainer._element[0]); this.props.glContainer.layoutManager.on("activeContentItemChanged", this.onActiveContentItemChanged); this.props.glContainer.tab?.isActive && this.onActiveContentItemChanged(undefined); - this._tabReaction = reaction(() => ({ selected: this.active(), title: this.tab?.titleElement[0] }), - ({ selected, title }) => title && (title.style.backgroundColor = selected ? "white" : ""), - { fireImmediately: true }); + // this._tabReaction = reaction(() => ({ selected: this.active(), title: this.tab?.titleElement[0] }), + // ({ selected, title }) => title && (title.style.backgroundColor = selected ? "white" : ""), + // { fireImmediately: true }); } componentWillUnmount() { @@ -243,10 +287,10 @@ export class TabDocView extends React.Component<TabDocViewProps> { } // adds a tab to the layout based on the locaiton parameter which can be: - // close[:{left,right,top,bottom}] - e.g., "close" will close the tab, "close:left" will close the left tab, + // close[:{left,right,top,bottom}] - e.g., "close" will close the tab, "close:left" will close the left tab, // add[:{left,right,top,bottom}] - e.g., "add" will add a tab to the current stack, "add:right" will add a tab on the right - // replace[:{left,right,top,bottom,<any string>}] - e.g., "replace" will replace the current stack contents, - // "replace:right" - will replace the stack on the right named "right" if it exists, or create a stack on the right with that name, + // replace[:{left,right,top,bottom,<any string>}] - e.g., "replace" will replace the current stack contents, + // "replace:right" - will replace the stack on the right named "right" if it exists, or create a stack on the right with that name, // "replace:monkeys" - will replace any tab that has the label 'monkeys', or a tab with that label will be created by default on the right // inPlace - will add the document to any collection along the path from the document to the docking view that has a field isInPlaceContainer. if none is found, inPlace adds a tab to current stack addDocTab = (doc: Doc, location: string) => { @@ -268,6 +312,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); @@ -304,7 +356,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} @@ -317,11 +368,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"} @@ -348,7 +399,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, @@ -422,6 +472,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} @@ -429,7 +480,8 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> { childLayoutTemplate={this.childLayoutTemplate} // bcz: Ugh .. should probably be rendering a CollectionView or the minimap should be part of the collectionFreeFormView to avoid having to set stuff like this. noOverlay={true} // don't render overlay Docs since they won't scale setHeight={returnFalse} - isContentActive={returnTrue} + isContentActive={returnFalse} + isAnyChildContentActive={returnFalse} select={emptyFunction} dropAction={undefined} isSelected={returnFalse} @@ -450,8 +502,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} /> @@ -460,4 +512,4 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> { </div> </div>; } -}
\ No newline at end of file +} diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss index 3f6fc8b0c..1ebc5873e 100644 --- a/src/client/views/collections/TreeView.scss +++ b/src/client/views/collections/TreeView.scss @@ -1,4 +1,4 @@ -@import "../globalCssVariables"; +@import "../global/globalCssVariables"; .treeView-label { max-height: 1.5em; @@ -14,7 +14,7 @@ .bullet-outline { position: relative; width: $TREE_BULLET_WIDTH; - color: $intermediate-color; + color: $medium-gray; transform: scale(0.5); display: inline-flex; align-items: center; @@ -45,7 +45,7 @@ .bullet { position: relative; width: $TREE_BULLET_WIDTH; - color: $intermediate-color; + color: $medium-gray; margin-top: 3px; transform: scale(1.3, 1.3); border: #80808030 1px solid; diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 2e98fb508..86fd21677 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'; @@ -9,7 +9,7 @@ import { listSpec } from '../../../fields/Schema'; import { ComputedField, ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, simulateMouseClick, Utils } from '../../../Utils'; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, simulateMouseClick, Utils, returnOne } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from "../../documents/DocumentTypes"; import { CurrentUserUtils } from '../../util/CurrentUserUtils'; @@ -20,7 +20,7 @@ import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; import { undoBatch, UndoManager } from '../../util/UndoManager'; import { EditableView } from "../EditableView"; -import { TREE_BULLET_WIDTH } from '../globalCssVariables.scss'; +import { TREE_BULLET_WIDTH } from '../global/globalCssVariables.scss'; import { DocumentView, DocumentViewProps, StyleProviderFunc, DocumentViewInternal } from '../nodes/DocumentView'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import { RichTextMenu } from '../nodes/formattedText/RichTextMenu'; @@ -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) { @@ -261,15 +266,19 @@ export class TreeView extends React.Component<TreeViewProps> { if (docDragData) { e.stopPropagation(); if (docDragData.draggedDocuments[0] === this.doc) return true; - const parentAddDoc = (doc: Doc | Doc[]) => this.props.addDocument(doc, undefined, before); - const canAdd = !StrCast((inside ? this.props.document : this.props.containerCollection)?.freezeChildren).includes("add") || docDragData.treeViewDoc === this.props.treeView.props.Document; - const localAdd = (doc: Doc) => Doc.AddDocToList(this.dataDoc, this.fieldKey, doc) && ((doc.context = this.doc.context) || true) ? true : false; - const addDoc = !inside ? parentAddDoc : - (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc), true as boolean); - const move = (!docDragData.dropAction || docDragData.dropAction === "proto" || docDragData.dropAction === "move" || docDragData.dropAction === "same") && docDragData.moveDocument; - if (canAdd) { - UndoManager.RunInTempBatch(() => docDragData.droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (docDragData.dropAction === "proto" ? addDoc(d) : false) : addDoc(d)) || added, false)); - } + this.dropDocuments(docDragData.droppedDocuments, before, inside, docDragData.dropAction, docDragData.moveDocument, docDragData.treeViewDoc === this.props.treeView.props.Document); + } + } + + dropDocuments(droppedDocuments: Doc[], before: boolean, inside: number | boolean, dropAction: dropActionType, moveDocument: DragManager.MoveFunction | undefined, forceAdd: boolean) { + const parentAddDoc = (doc: Doc | Doc[]) => this.props.addDocument(doc, undefined, before); + const canAdd = !StrCast((inside ? this.props.document : this.props.containerCollection)?.freezeChildren).includes("add") || forceAdd; + const localAdd = (doc: Doc) => Doc.AddDocToList(this.dataDoc, this.fieldKey, doc) && ((doc.context = this.doc.context) || true) ? true : false; + const addDoc = !inside ? parentAddDoc : + (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc), true as boolean); + const move = (!dropAction || dropAction === "proto" || dropAction === "move" || dropAction === "same") && moveDocument; + if (canAdd) { + UndoManager.RunInTempBatch(() => droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === "proto" ? addDoc(d) : false) : addDoc(d)) || added, false)); } } @@ -287,7 +296,7 @@ export class TreeView extends React.Component<TreeViewProps> { const aspect = Doc.NativeAspect(layoutDoc); if (layoutDoc._fitWidth) return Math.min(this.props.panelWidth() - treeBulletWidth(), layoutDoc[WidthSym]()); if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT * aspect, this.props.panelWidth() - treeBulletWidth())); - return Math.min(this.props.panelWidth() - treeBulletWidth(), Doc.NativeWidth(layoutDoc) ? layoutDoc[WidthSym]() : this.layoutDoc[WidthSym]()); + return Math.min((this.props.panelWidth() - treeBulletWidth()) / (this.props.treeView.props.scaling?.() || 1), Doc.NativeWidth(layoutDoc) ? layoutDoc[WidthSym]() : this.layoutDoc[WidthSym]()); } docHeight = () => { const layoutDoc = this.layoutDoc; @@ -329,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"} @@ -356,7 +365,7 @@ export class TreeView extends React.Component<TreeViewProps> { return rows; } - rtfWidth = () => Math.min(this.layoutDoc?.[WidthSym](), this.props.panelWidth() - treeBulletWidth()); + rtfWidth = () => Math.min(this.layoutDoc?.[WidthSym](), (this.props.panelWidth() - treeBulletWidth())) / (this.props.treeView.props.scaling?.() || 1); rtfHeight = () => this.rtfWidth() <= this.layoutDoc?.[WidthSym]() ? Math.min(this.layoutDoc?.[HeightSym](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT; rtfOutlineHeight = () => Math.max(this.layoutDoc?.[HeightSym](), treeBulletWidth()); expandPanelHeight = () => { @@ -411,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}> @@ -468,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; } @@ -500,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); @@ -522,7 +544,7 @@ export class TreeView extends React.Component<TreeViewProps> { switch (property.split(":")[0]) { case StyleProp.Opacity: return this.props.treeView.outlineMode ? undefined : 1; case StyleProp.BackgroundColor: return this.selected ? "#7089bb" : StrCast(doc._backgroundColor, StrCast(doc.backgroundColor)); - case StyleProp.DocContents: return testDocProps(props) && !props?.treeViewDoc ? (null) : + case StyleProp.DocContents: return this.props.treeView.outlineMode ? (null) : <div className="treeView-label" style={{ // just render a title for a tree view label (identified by treeViewDoc being set in 'props') maxWidth: props?.PanelWidth() || undefined, background: props?.styleProvider?.(doc, props, StyleProp.BackgroundColor), @@ -624,6 +646,7 @@ export class TreeView extends React.Component<TreeViewProps> { searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={this.props.treeView.props.Document} + ContentScaling={returnOne} />; const buttons = this.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.Decorations + (Doc.IsSystem(this.props.containerCollection) ? ":afterHeader" : "")); @@ -680,10 +703,12 @@ export class TreeView extends React.Component<TreeViewProps> { hideDecorationTitle={this.props.treeView.outlineMode} hideResizeHandles={this.props.treeView.outlineMode} focus={this.refocus} + ContentScaling={returnOne} hideLinkButton={BoolCast(this.props.treeView.props.Document.childHideLinkButton)} dontRegisterView={BoolCast(this.props.treeView.props.Document.childDontRegisterViews, this.props.dontRegisterView)} ScreenToLocalTransform={this.docTransform} renderDepth={this.props.renderDepth + 1} + treeViewDoc={this.props.treeView?.props.Document} rootSelected={returnTrue} layerProvider={returnTrue} docViewPath={this.props.treeView.props.docViewPath} @@ -727,12 +752,23 @@ export class TreeView extends React.Component<TreeViewProps> { </div>; } + onTreeDrop = (de: React.DragEvent) => { + const pt = [de.clientX, de.clientY]; + const rect = this._header.current!.getBoundingClientRect(); + const before = pt[1] < rect.top + rect.height / 2; + const inside = this.props.treeView.fileSysMode && !this.doc.isFolder ? false : pt[0] > Math.min(rect.left + 75, rect.left + rect.width * .75) || (!before && this.treeViewOpen && this.childDocList.length); + + const docs = this.props.treeView.onTreeDrop(de, (docs: Doc[]) => this.dropDocuments(docs, before, inside, "copy", undefined, false)); + } + render() { TraceMobx(); const hideTitle = this.doc.treeViewHideHeader || this.props.treeView.outlineMode; return this.props.renderedIds.indexOf(this.doc[Id]) !== -1 ? "<" + this.doc.title + ">" : // just print the title of documents we've previously rendered in this hierarchical path to avoid cycles <div className={`treeView-container${this.props.isContentActive() ? "-active" : ""}`} ref={this.createTreeDropTarget} + + onDrop={this.onTreeDrop} //onPointerDown={e => this.props.isContentActive(true) && SelectionManager.DeselectAll()} // bcz: this breaks entering a text filter in a filterBox since it deselects the filter's target document onKeyDown={this.onKeyDown}> <li className="collection-child"> @@ -798,7 +834,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) { @@ -863,6 +900,7 @@ export class TreeView extends React.Component<TreeViewProps> { parentTreeView={parentTreeView} observeHeight={observerHeight} unobserveHeight={unobserveHeight} + contextMenuItems={contextMenuItems} />; }); } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index afc1babeb..37444a9dc 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -126,7 +126,8 @@ export function computerStarburstLayout( replica: "" }); }); - return normalizeResults(scaleDim, 12, docMap, poolData, viewDefsToJSX, [], 0, []); + const divider = { type: "div", color: "transparent", x: -burstRadius[0] / 3, y: 0, width: 15, height: 15, payload: undefined }; + return normalizeResults(scaleDim, 12, docMap, poolData, viewDefsToJSX, [], 0, [divider]); } @@ -399,7 +400,7 @@ function normalizeResults( ): ViewDefResult[] { const grpEles = groupNames.map(gn => ({ x: gn.x, y: gn.y, width: gn.width, height: gn.height }) as ViewDefBounds); const docEles = Array.from(docMap.entries()).map(ele => ele[1]); - const aggBounds = aggregateBounds(grpEles.concat(docEles.map(de => ({ ...de, type: "doc", payload: "" }))).filter(e => e.zIndex !== -99), 0, 0); + const aggBounds = aggregateBounds(extras.concat(grpEles.concat(docEles.map(de => ({ ...de, type: "doc", payload: "" })))).filter(e => e.zIndex !== -99), 0, 0); aggBounds.r = Math.max(minWidth, aggBounds.r - aggBounds.x); const wscale = panelDim[0] / (aggBounds.r - aggBounds.x); let scale = wscale * (aggBounds.b - aggBounds.y) > panelDim[1] ? (panelDim[1]) / (aggBounds.b - aggBounds.y) : wscale; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index a8f5e6dd2..16258404d 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -2,14 +2,17 @@ import { action, computed, IReactionDisposer, observable, reaction } from "mobx" import { observer } from "mobx-react"; import { Doc } from "../../../../fields/Doc"; import { Id } from "../../../../fields/FieldSymbols"; +import { List } from "../../../../fields/List"; import { NumCast, StrCast } from "../../../../fields/Types"; import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../Utils'; import { DocumentType } from "../../../documents/DocumentTypes"; +import { LinkManager } from "../../../util/LinkManager"; import { SnappingManager } from "../../../util/SnappingManager"; import { DocumentView } from "../../nodes/DocumentView"; import "./CollectionFreeFormLinkView.scss"; import React = require("react"); + export interface CollectionFreeFormLinkViewProps { A: DocumentView; B: 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 = StrCast(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/CollectionFreeFormRemoteCursors.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss index c5b8fc5e8..5fa01b102 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss @@ -1,4 +1,4 @@ -@import "globalCssVariables"; +@import "global/globalCssVariables"; .collectionFreeFormRemoteCursors-cont { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index eb0538c41..79e063f7f 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -1,4 +1,4 @@ -@import "../../globalCssVariables"; +@import "../../global/globalCssVariables"; .collectionfreeformview-none { position: inherit; @@ -226,7 +226,7 @@ // linear-gradient(to bottom, $light-color-secondary 1px, transparent 1px); // background-size: 30px 30px; // } - border: 0px solid $light-color-secondary; + border: 0px solid $light-gray; border-radius: inherit; box-sizing: border-box; position: absolute; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index accb80c5a..fb949a36d 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,6 +1,7 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { computedFn } from "mobx-utils"; +import { DateField } from "../../../../fields/DateField"; import { Doc, HeightSym, Opt, StrListCast, WidthSym } from "../../../../fields/Doc"; import { collectionSchema, documentSchema } from "../../../../fields/documentSchemas"; import { Id } from "../../../../fields/FieldSymbols"; @@ -17,6 +18,7 @@ import { aggregateBounds, emptyFunction, intersectRect, returnFalse, setupMoveUp import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; import { DocServer } from "../../../DocServer"; import { Docs, DocUtils } from "../../../documents/Documents"; +import { DocumentType } from "../../../documents/DocumentTypes"; import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; import { DocumentManager } from "../../../util/DocumentManager"; import { DragManager, dropActionType } from "../../../util/DragManager"; @@ -28,7 +30,7 @@ import { SelectionManager } from "../../../util/SelectionManager"; import { SnappingManager } from "../../../util/SnappingManager"; import { Transform } from "../../../util/Transform"; import { undoBatch } from "../../../util/UndoManager"; -import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss"; +import { COLLECTION_BORDER_WIDTH } from "../../../views/global/globalCssVariables.scss"; import { Timeline } from "../../animationtimeline/Timeline"; import { ContextMenu } from "../../ContextMenu"; import { DocumentDecorations } from "../../DocumentDecorations"; @@ -38,7 +40,7 @@ import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDo import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment, ViewSpecPrefix } from "../../nodes/DocumentView"; import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox"; import { pageSchema } from "../../nodes/ImageBox"; -import { PresBox } from "../../nodes/PresBox"; +import { PresBox } from "../../nodes/trails/PresBox"; import { StyleLayers, StyleProp } from "../../StyleProvider"; import { CollectionDockingView } from "../CollectionDockingView"; import { CollectionSubView } from "../CollectionSubView"; @@ -48,6 +50,7 @@ import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCurso import "./CollectionFreeFormView.scss"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); +import Color = require("color"); export const panZoomSchema = createSchema({ _panX: "number", @@ -72,6 +75,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 @@ -109,13 +114,11 @@ 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); } @computed get backgroundEvents() { return this.props.layerProvider?.(this.layoutDoc) === false && SnappingManager.GetIsDragging(); } - @computed get backgroundActive() { return this.props.layerProvider?.(this.layoutDoc) === false && (this.props.ContainingCollectionView?.isContentActive() || this.props.isContentActive()); } + @computed get backgroundActive() { return this.props.layerProvider?.(this.layoutDoc) === false && this.props.isContentActive(); } @computed get fitToContentVals() { return { bounds: { ...this.contentBounds, cx: (this.contentBounds.x + this.contentBounds.r) / 2, cy: (this.contentBounds.y + this.contentBounds.b) / 2 }, @@ -158,8 +161,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); @@ -170,6 +171,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P getContainerTransform = () => this.cachedGetContainerTransform.copy(); getTransformOverlay = () => this.getContainerTransform().translate(1, 1); getActiveDocuments = () => this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout); + isAnyChildContentActive = () => this.props.isAnyChildContentActive(); addLiveTextBox = (newBox: Doc) => { FormattedTextBox.SelectOnLoad = newBox[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed this.addDocument(newBox); @@ -260,6 +262,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); @@ -1029,10 +1032,10 @@ 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} + isContentActive={returnFalse} isDocumentActive={this.props.childDocumentsActive ? this.props.isDocumentActive : this.isContentActive} focus={this.focusDocument} addDocTab={this.addDocTab} @@ -1196,15 +1199,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>([]); @@ -1446,7 +1456,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} @@ -1486,7 +1496,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P onDragOver={e => e.preventDefault()} onContextMenu={this.onContextMenu} style={{ - pointerEvents: this.backgroundEvents ? "all" : this.props.pointerEvents as any, + pointerEvents: this.props.Document.type === DocumentType.MARKER ? "none" : // bcz: ugh.. this is here to prevent markers, which render as freeform views, from grabbing events -- need a better approach. + this.backgroundEvents ? "all" : this.props.pointerEvents as any, transform: `scale(${this.contentScaling || 1})`, width: `${100 / (this.contentScaling || 1)}%`, height: this.isAnnotationOverlay && this.Document.scrollHeight ? this.Document.scrollHeight : `${100 / (this.contentScaling || 1)}%`// : this.isAnnotationOverlay ? (this.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index b1f2750c3..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"; @@ -19,7 +19,8 @@ import { Transform } from "../../../util/Transform"; import { undoBatch, UndoManager } from "../../../util/UndoManager"; import { ContextMenu } from "../../ContextMenu"; import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox"; -import { PresBox, PresMovement } from "../../nodes/PresBox"; +import { PresBox } from "../../nodes/trails/PresBox"; +import { PresMovement } from "../../nodes/trails/PresEnums"; import { PreviewCursor } from "../../PreviewCursor"; import { CollectionDockingView } from "../CollectionDockingView"; import { SubCollectionViewProps } from "../CollectionSubView"; @@ -40,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> @@ -91,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) { @@ -210,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(); // } @@ -283,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; @@ -297,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(); @@ -312,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(); } } @@ -368,8 +374,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque SelectionManager.DeselectAll(); selected.forEach(d => this.props.removeDocument?.(d)); const newCollection = DocUtils.pileup(selected, this.Bounds.left + this.Bounds.width / 2, this.Bounds.top + this.Bounds.height / 2); - this.props.addDocument?.(newCollection!); - this.props.selectDocuments([newCollection!]); + this.props.addDocument?.(newCollection); + this.props.selectDocuments([newCollection]); MarqueeOptionsMenu.Instance.fadeOut(true); this.hideMarquee(); } @@ -442,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/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/CollectionSchemaCells.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx index c8638dd12..9a3ef1422 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx @@ -26,7 +26,7 @@ import { SnappingManager } from "../../../util/SnappingManager"; import { undoBatch } from "../../../util/UndoManager"; import '../../../views/DocumentDecorations.scss'; import { EditableView } from "../../EditableView"; -import { MAX_ROW_HEIGHT } from '../../../../client/views/globalCssVariables.scss'; +import { MAX_ROW_HEIGHT } from '../../global/globalCssVariables.scss'; import { DocumentIconContainer } from "../../nodes/DocumentIcon"; import { OverlayView } from "../../OverlayView"; import "./CollectionSchemaView.scss"; diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx index 2da9409f2..52865eba6 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx @@ -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} /> diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss index e866ec079..c17b79638 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss @@ -1,7 +1,7 @@ -@import "../../globalCssVariables"; +@import "../../global/globalCssVariables.scss"; .collectionSchemaView-container { border-width: $COLLECTION_BORDER_WIDTH; - border-color: $intermediate-color; + border-color: $medium-gray; border-style: solid; border-radius: $border-radius; box-sizing: border-box; @@ -33,13 +33,13 @@ cursor: col-resize; } // .documentView-node:first-child { - // background: $light-color; + // background: $white; // } } .collectionSchemaView-searchContainer { border-width: $COLLECTION_BORDER_WIDTH; - border-color: $intermediate-color; + border-color: $medium-gray; border-style: solid; border-radius: $border-radius; box-sizing: border-box; @@ -72,7 +72,7 @@ cursor: col-resize; } // .documentView-node:first-child { - // background: $light-color; + // background: $white; // } } @@ -252,7 +252,7 @@ button.add-column { } } label { - color: $main-accent; + color: $medium-gray; font-weight: normal; letter-spacing: 2px; text-transform: uppercase; @@ -267,11 +267,11 @@ button.add-column { background-color: white; transition: background-color 0.2s; &:hover { - background-color: $light-color-secondary; + background-color: $light-gray; } &.active { font-weight: bold; - border: 2px solid $light-color-secondary; + border: 2px solid $light-gray; } svg { color: gray; @@ -284,7 +284,7 @@ button.add-column { //width: 100%; background-color: white; input { - border: 2px solid $light-color-secondary; + border: 2px solid $light-gray; padding: 3px; height: 28px; font-weight: bold; @@ -310,7 +310,7 @@ button.add-column { border-top: 0; } &:hover { - background-color: $light-color-secondary; + background-color: $light-gray; } } } @@ -336,7 +336,7 @@ button.add-column { height: 100%; background-color: white; &.row-focused .rt-td { - background-color: #bfffc0; //$light-color-secondary; + background-color: #bfffc0; //$light-gray; } &.row-wrapped { .rt-td { diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx index ef28f75c8..fed64b620 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx @@ -11,21 +11,21 @@ import { listSpec } from "../../../../fields/Schema"; import { PastelSchemaPalette, SchemaHeaderField } from "../../../../fields/SchemaHeaderField"; import { Cast, NumCast } from "../../../../fields/Types"; import { TraceMobx } from "../../../../fields/util"; -import { emptyFunction, emptyPath, returnFalse, setupMoveUpEvents, returnEmptyDoclist, returnTrue } from "../../../../Utils"; +import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from "../../../../Utils"; +import { DocUtils } from "../../../documents/Documents"; import { SelectionManager } from "../../../util/SelectionManager"; import { SnappingManager } from "../../../util/SnappingManager"; import { Transform } from "../../../util/Transform"; import { undoBatch } from "../../../util/UndoManager"; -import { COLLECTION_BORDER_WIDTH, SCHEMA_DIVIDER_WIDTH } from '../../globalCssVariables.scss'; +import '../../../views/DocumentDecorations.scss'; import { ContextMenu } from "../../ContextMenu"; import { ContextMenuProps } from "../../ContextMenuItem"; -import '../../../views/DocumentDecorations.scss'; +import { COLLECTION_BORDER_WIDTH, SCHEMA_DIVIDER_WIDTH } from '../../global/globalCssVariables.scss'; import { DocumentView } from "../../nodes/DocumentView"; import { DefaultStyleProvider } from "../../StyleProvider"; -import "./CollectionSchemaView.scss"; import { CollectionSubView } from "../CollectionSubView"; +import "./CollectionSchemaView.scss"; import { SchemaTable } from "./SchemaTable"; -import { DocUtils } from "../../../documents/Documents"; // bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657 export enum ColumnType { @@ -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 0d5c9e077..631f623c6 100644 --- a/src/client/views/collections/collectionSchema/SchemaTable.tsx +++ b/src/client/views/collections/collectionSchema/SchemaTable.tsx @@ -21,7 +21,7 @@ import { DocumentType } from "../../../documents/DocumentTypes"; import { CompileScript, Transformer, ts } from "../../../util/Scripting"; import { Transform } from "../../../util/Transform"; import { undoBatch } from "../../../util/UndoManager"; -import { COLLECTION_BORDER_WIDTH, SCHEMA_DIVIDER_WIDTH } from '../../globalCssVariables.scss'; +import { COLLECTION_BORDER_WIDTH, SCHEMA_DIVIDER_WIDTH } from '../../global/globalCssVariables.scss'; import { ContextMenu } from "../../ContextMenu"; import '../../../views/DocumentDecorations.scss'; import { DocumentView } from "../../nodes/DocumentView"; @@ -197,6 +197,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { <div onClick={e => this.changeSorting(col)} style={{ width: 21, padding: 1, display: "inline", zIndex: 1, background: "inherit", cursor: "hand" }}> <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/globalCssVariables.scss b/src/client/views/global/globalCssVariables.scss index ccc9306c4..7556f8b8a 100644 --- a/src/client/views/globalCssVariables.scss +++ b/src/client/views/global/globalCssVariables.scss @@ -1,41 +1,55 @@ -@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 -$light-color: #fcfbf7; -$light-color-secondary:#f1efeb; -//$main-accent: #61aaa3; -$main-accent: #aaaaa3; -// $alt-accent: #cdd5ec; -// $alt-accent: #cdeceb; -//$alt-accent: #59dff7; -$alt-accent: #c2c2c5; -$lighter-alt-accent: rgb(207, 220, 240); -$darker-alt-accent: #b2cef8; -$intermediate-color: #9c9396; -$dark-color: #121721; -$link-color: #add8e6; -$antimodemenu-height: 35px; +$white: #ffffff; +$light-gray: #dfdfdf; +$medium-gray: #9f9f9f; +$dark-gray: #323232; +$black: #000000; + +$light-blue: #bdddf5; +$medium-blue: #4476f7; +$pink: #e0217d; +$yellow: #f5d747; + +$close-red: #e48282; + +$drop-shadow: "#32323215"; + +//padding +$minimum-padding: 4px; +$medium-padding: 16px; +$large-padding: 32px; + +//icon sizes +$icon-size: 28px; + // fonts -$sans-serif: "Noto Sans", -sans-serif; +$sans-serif: "Roboto", sans-serif; +$large-header: 16px; +$body-text: 12px; +$small-text: 9px; // $sans-serif: "Roboto Slab", sans-serif; -$serif: "Crimson Text", -serif; + // misc values $border-radius: 0.3em; -// $search-thumnail-size: 130; +$topbar-height: 32px; +$antimodemenu-height: 36px; // dragged items $contextMenu-zindex: 100000; // context menu shows up over everything $radialMenu-zindex: 100000; // context menu shows up over everything +// borders +$standard-border: solid 1px #9f9f9f; + $searchpanel-height: 32px; $mainTextInput-zindex: 999; // then text input overlay so that it's context menu will appear over decorations, etc $docDecorations-zindex: 998; // then doc decorations appear over everything else $remoteCursors-zindex: 997; // ... not sure what level the remote cursors should go -- is this right? $COLLECTION_BORDER_WIDTH: 0; $SCHEMA_DIVIDER_WIDTH: 4; -$MINIMIZED_ICON_SIZE:25; +$MINIMIZED_ICON_SIZE: 24; $MAX_ROW_HEIGHT: 44px; $DFLT_IMAGE_NATIVE_DIM: 900px; $MENU_PANEL_WIDTH: 60px; @@ -53,4 +67,4 @@ $TREE_BULLET_WIDTH: 20px; DFLT_IMAGE_NATIVE_DIM: $DFLT_IMAGE_NATIVE_DIM; MENU_PANEL_WIDTH: $MENU_PANEL_WIDTH; TREE_BULLET_WIDTH: $TREE_BULLET_WIDTH; -}
\ No newline at end of file +} diff --git a/src/client/views/globalCssVariables.scss.d.ts b/src/client/views/global/globalCssVariables.scss.d.ts index 11e62e1eb..11e62e1eb 100644 --- a/src/client/views/globalCssVariables.scss.d.ts +++ b/src/client/views/global/globalCssVariables.scss.d.ts diff --git a/src/client/views/global/globalEnums.tsx b/src/client/views/global/globalEnums.tsx new file mode 100644 index 000000000..2aeb8e338 --- /dev/null +++ b/src/client/views/global/globalEnums.tsx @@ -0,0 +1,38 @@ +export enum Colors { + BLACK = "#000000", + DARK_GRAY = "#323232", + MEDIUM_GRAY = "#9F9F9F", + LIGHT_GRAY = "#DFDFDF", + WHITE = "#FFFFFF", + MEDIUM_BLUE = "#4476F7", + LIGHT_BLUE = "#BDDDF5", + PINK = "#E0217D", + YELLOW = "#F5D747", + DROP_SHADOW = "#32323215", +} + +export enum FontSizes { + //Bolded + LARGE_HEADER = "16px", + + //Bolded or unbolded + BODY_TEXT = "12px", + + //Bolded + SMALL_TEXT = "9px", +} + +export enum Padding { + MINIMUM_PADDING = "4px", + SMALL_PADDING = "8px", + MEDIUM_PADDING = "16px", + LARGE_PADDING = "32px", +} + +export enum IconSizes { + ICON_SIZE = "28px", +} + +export enum Borders { + STANDARD = "solid 1px #9F9F9F" +}
\ No newline at end of file diff --git a/src/client/views/linking/LinkEditor.scss b/src/client/views/linking/LinkEditor.scss index 7e6999cdc..abd413f57 100644 --- a/src/client/views/linking/LinkEditor.scss +++ b/src/client/views/linking/LinkEditor.scss @@ -1,4 +1,4 @@ -@import "../globalCssVariables"; +@import "../global/globalCssVariables"; .linkEditor { width: 100%; @@ -20,9 +20,9 @@ } .linkEditor-info { - //border-bottom: 0.5px solid $light-color-secondary; + //border-bottom: 0.5px solid $light-gray; //padding-bottom: 1px; - padding-top: 5px; + padding: 12px; padding-left: 5px; //margin-bottom: 6px; display: flex; @@ -61,7 +61,7 @@ } .linkEditor-description { - padding-left: 6.5px; + padding-left: 26px; padding-right: 6.5px; padding-bottom: 3.5px; @@ -106,10 +106,28 @@ } } +.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: 6.5px; + padding-left: 26px; padding-right: 6.5px; - padding-bottom: 6px; + padding-bottom: 15px; &:hover { cursor: pointer; @@ -195,7 +213,7 @@ } .linkEditor-group { - background-color: $light-color-secondary; + background-color: $light-gray; padding: 6px; margin: 3px 0; border-radius: 3px; @@ -254,8 +272,8 @@ } .linkEditor-option { - background-color: $light-color-secondary; - border: 1px solid $intermediate-color; + background-color: $light-gray; + border: 1px solid $medium-gray; border-top: 0; padding: 3px; cursor: pointer; @@ -272,7 +290,7 @@ .linkEditor-typeButton { background-color: transparent; - color: $dark-color; + color: $dark-gray; height: 20px; padding: 0 3px; padding-bottom: 2px; @@ -285,7 +303,7 @@ width: calc(100% - 40px); &:hover { - background-color: $light-color; + background-color: $white; } } diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx index f74b422d3..240a71c3e 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.scss b/src/client/views/linking/LinkMenu.scss index a90bf8b0a..19c6463d3 100644 --- a/src/client/views/linking/LinkMenu.scss +++ b/src/client/views/linking/LinkMenu.scss @@ -1,4 +1,4 @@ -@import "../globalCssVariables"; +@import "../global/globalCssVariables"; .linkMenu { width: auto; @@ -7,20 +7,19 @@ z-index: 2001; .linkMenu-list, - .linkMenu-listEditor - { + .linkMenu-listEditor { display: inline-block; position: relative; - border: 1px solid black; - box-shadow: 3px 3px 1.5px grey; + border: 1px solid #e4e4e4; + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23); background: white; - min-width: 170px; - max-height: 170px; + max-height: 230px; overflow-y: scroll; z-index: 10; - } - .linkMenu-list { + } + + .linkMenu-list { white-space: nowrap; overflow-x: hidden; width: 240px; @@ -46,13 +45,13 @@ } .linkMenu-group-name { + padding: 10px; &:hover { - p { - background-color: lightgray; - - } + // p { + // background-color: lightgray; + // } p.expand-one { width: calc(100% + 20px); @@ -65,10 +64,9 @@ p { width: 100%; - //padding: 4px 6px; line-height: 12px; border-radius: 5px; - font-weight: bold; + text-transform: capitalize; } .linkEditor-tableButton { diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx index c7888c5ee..53fe3f682 100644 --- a/src/client/views/linking/LinkMenu.tsx +++ b/src/client/views/linking/LinkMenu.tsx @@ -15,6 +15,9 @@ interface Props { changeFlyout: () => void; } +/** + * the outermost component for the link menu of a node that contains a list of its linked nodes + */ @observer export class LinkMenu extends React.Component<Props> { private _editorRef = React.createRef<HTMLDivElement>(); @@ -36,6 +39,11 @@ export class LinkMenu extends React.Component<Props> { } } + /** + * maps each link to a JSX element to be rendered + * @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> => { const linkItems = Array.from(groups.entries()).map(group => <LinkMenuGroup diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx index 74af78234..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,8 +56,8 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { return ( <div className="linkMenu-group" ref={this._menuRef}> - <div className="linkMenu-group-name"> - <p className={this.props.groupType === "*" || this.props.groupType === "" ? "" : "expand-one"} > {this.props.groupType}:</p> + <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"> {groupItems} diff --git a/src/client/views/linking/LinkMenuItem.scss b/src/client/views/linking/LinkMenuItem.scss index 4e13ef8c8..90722daf9 100644 --- a/src/client/views/linking/LinkMenuItem.scss +++ b/src/client/views/linking/LinkMenuItem.scss @@ -1,10 +1,10 @@ -@import "../globalCssVariables"; +@import "../global/globalCssVariables"; .linkMenu-item { - // border-top: 0.5px solid $main-accent; + // border-top: 0.5px solid $medium-gray; position: relative; display: flex; - border-bottom: 0.5px solid black; + border-top: 0.5px solid #cdcdcd; padding-left: 6.5px; padding-right: 2px; @@ -55,8 +55,8 @@ .linkMenu-destination-title { text-decoration: none; - color: rgb(85, 120, 196); - font-size: 14px; + color: #4476F7; + font-size: 16px; padding-bottom: 2px; padding-right: 4px; margin-right: 4px; @@ -76,7 +76,7 @@ text-decoration: none; font-style: italic; color: rgb(95, 97, 102); - font-size: 10px; + font-size: 9px; margin-left: 20px; max-width: 125px; height: auto; @@ -102,7 +102,7 @@ .link-metadata { padding: 0 10px 0 16px; margin-bottom: 4px; - color: $main-accent; + color: $medium-gray; font-style: italic; font-size: 10.5px; } @@ -143,8 +143,8 @@ padding-right: 6px; border-radius: 50%; pointer-events: auto; - background-color: $dark-color; - color: $light-color; + background-color: $dark-gray; + color: $white; font-size: 65%; transition: transform 0.2s; text-align: center; @@ -162,7 +162,7 @@ } &:hover { - background: $main-accent; + background: $medium-gray; cursor: pointer; } } diff --git a/src/client/views/linking/LinkPopup.scss b/src/client/views/linking/LinkPopup.scss new file mode 100644 index 000000000..60c9ebfcd --- /dev/null +++ b/src/client/views/linking/LinkPopup.scss @@ -0,0 +1,45 @@ +.linkPopup-container { + background: white; + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23); + top: 35px; + height: 200px; + width: 200px; + position: absolute; + // padding: 15px; + border-radius: 3px; + + input { + border: 1px solid #b9b9b9; + border-radius: 20px; + height: 25px; + width: 100%; + padding-left: 10px; + } + + .divider { + margin: 10px 0; + height: 20px; + width: 100%; + + .line { + height: 1px; + background-color: #b9b9b9; + width: 100%; + position: relative; + top: 12px; + } + + .divider-text { + width: 20px; + background-color: white; + text-align: center; + position: relative; + margin: auto; + } + } + + + .searchBox-container { + background: pink; + } +}
\ No newline at end of file diff --git a/src/client/views/linking/LinkPopup.tsx b/src/client/views/linking/LinkPopup.tsx new file mode 100644 index 000000000..c8be9069c --- /dev/null +++ b/src/client/views/linking/LinkPopup.tsx @@ -0,0 +1,105 @@ +import { action, observable } from 'mobx'; +import { observer } from "mobx-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 { Transform } from '../../util/Transform'; +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; + // sourceDoc: Doc; +} + +/** + * Popup component for creating links from text to Dash documents + */ + +@observer +export class LinkPopup extends React.Component<LinkPopupProps> { + @observable private linkURL: string = ""; + @observable public view?: EditorView; + + + + // TODO: should check for valid URL + @undoBatch + makeLinkToURL = (target: string, lcoation: string) => { + ((this.view as any)?.TextView as FormattedTextBox).makeLinkAnchor(undefined, "onRadd:rightight", target, target); + } + + @action + onLinkChange = (e: React.ChangeEvent<HTMLInputElement>) => { + this.linkURL = e.target.value; + } + + + getPWidth = () => 500; + getPHeight = () => 500; + + 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"> + <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> + </div> + <div className="divider"> + <div className="line"></div> + <p className="divider-text">or</p> + </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} + DataDoc={CurrentUserUtils.MySearchPanelDoc} + linkFrom={linkDoc} + linkSearch={true} + fieldKey="data" + dropAction="move" + isSelected={returnTrue} + isContentActive={returnTrue} + select={returnTrue} + setHeight={returnFalse} + addDocument={undefined} + addDocTab={returnTrue} + pinToPres={emptyFunction} + rootSelected={returnTrue} + styleProvider={DefaultStyleProvider} + layerProvider={undefined} + removeDocument={undefined} + ScreenToLocalTransform={Transform.Identity} + PanelWidth={this.getPWidth} + PanelHeight={this.getPHeight} + renderDepth={0} + focus={DocUtils.DefaultFocus} + docViewPath={returnEmptyDoclist} + whenChildContentsActiveChanged={emptyFunction} + bringToFront={emptyFunction} + docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} /> + </div> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/linking/LinkRelationshipSearch.tsx b/src/client/views/linking/LinkRelationshipSearch.tsx new file mode 100644 index 000000000..53da880e4 --- /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" }} />; + } + } + + 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 a2e36f12e..faaa887c4 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -196,7 +196,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp 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(); @@ -267,7 +267,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // returns the html audio element @computed get audio() { - return <audio ref={this.setRef} className={`audiobox-control${this.isContentActive() ? "-interactive" : ""}`}> + return <audio ref={this.setRef} className={`audiobox-control${this.props.isContentActive() ? "-interactive" : ""}`}> <source src={this.path} type="audio/mpeg" /> Not supported. </audio>; @@ -328,6 +328,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp focus={DocUtils.DefaultFocus} bringToFront={emptyFunction} CollectionView={undefined} + isAnyChildContentActive={this.isAnyChildContentActive} duration={this.duration} playFrom={this.playFrom} setTime={this.setAnchorTime} @@ -337,7 +338,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp ScreenToLocalTransform={this.timelineScreenToLocal} Play={this.Play} Pause={this.Pause} - isContentActive={this.isContentActive} playLink={this.playLink} PanelWidth={this.timelineWidth} PanelHeight={this.timelineHeight} @@ -345,7 +345,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } render() { - const interactive = SnappingManager.GetIsDragging() || this.isContentActive() ? "-interactive" : ""; + const interactive = SnappingManager.GetIsDragging() || this.props.isContentActive() ? "-interactive" : ""; return <div className="audiobox-container" onContextMenu={this.specificContextMenu} onClick={!this.path && !this._recorder ? this.recordAudioAnnotation : undefined} @@ -370,7 +370,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp RECORD </button>} </div> : - <div className="audiobox-controls" style={{ pointerEvents: this._isAnyChildContentActive || this.isContentActive() ? "all" : "none" }} > + <div className="audiobox-controls" style={{ pointerEvents: this._isAnyChildContentActive || this.props.isContentActive() ? "all" : "none" }} > <div className="audiobox-dictation" /> <div className="audiobox-player" style={{ height: `${AudioBox.heightPercent}%` }} > <div className="audiobox-playhead" style={{ width: AudioBox.playheadWidth }} title={this.mediaState === "paused" ? "play" : "pause"} onClick={this.Play}> <FontAwesomeIcon style={{ width: "100%", position: "absolute", left: "0px", top: "5px", borderWidth: "thin", borderColor: "white" }} icon={this.mediaState === "paused" ? "play" : "pause"} size={"1x"} /></div> diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 092823603..9cc4b1f9a 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -17,8 +17,8 @@ import { InkingStroke } from "../InkingStroke"; import { StyleProp } from "../StyleProvider"; import "./CollectionFreeFormDocumentView.scss"; import { DocumentView, DocumentViewProps } from "./DocumentView"; -import { FieldViewProps } from "./FieldView"; import React = require("react"); +import Color = require("color"); export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { dataProvider?: (doc: Doc, replica: string) => { x: number, y: number, zIndex?: number, opacity?: number, highlight?: boolean, z: number, transition?: string } | undefined; @@ -164,6 +164,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF PanelWidth: this.panelWidth, PanelHeight: this.panelHeight, }; + const background = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); + const mixBlendMode = StrCast(this.layoutDoc.mixBlendMode) as any || (background && Color(background).alpha() !== 1 ? "multiply" : undefined); return <div className={"collectionFreeFormDocumentView-container"} style={{ outline: this.Highlight ? "orange solid 2px" : "", @@ -172,7 +174,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF transform: this.transform, transition: this.props.dataTransition ? this.props.dataTransition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.dataTransition), zIndex: this.ZInd, - mixBlendMode: StrCast(this.layoutDoc.mixBlendMode) as any, + mixBlendMode, display: this.ZInd === -99 ? "none" : undefined }} > <DocumentView {...divProps} ref={action((r: DocumentView | null) => this._contentView = r)} /> diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index 153176afc..6708a08ee 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -1,17 +1,18 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, observable } from 'mobx'; import { observer } from "mobx-react"; -import { Doc } from '../../../fields/Doc'; +import { Doc, Opt } from '../../../fields/Doc'; import { documentSchema } from '../../../fields/documentSchemas'; import { createSchema, makeInterface } from '../../../fields/Schema'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; -import { emptyFunction, OmitKeys, setupMoveUpEvents } from '../../../Utils'; +import { emptyFunction, OmitKeys, returnFalse, setupMoveUpEvents } from '../../../Utils'; import { DragManager } from '../../util/DragManager'; import { SnappingManager } from '../../util/SnappingManager'; import { undoBatch } from '../../util/UndoManager'; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; +import { StyleProp } from '../StyleProvider'; import "./ComparisonBox.scss"; -import { DocumentView } from './DocumentView'; +import { DocumentView, DocumentViewProps } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import React = require("react"); @@ -71,6 +72,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl delete this.dataDoc[fieldKey]; } + docStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => { + if (property === StyleProp.PointerEvents) return "none"; + return this.props.styleProvider?.(doc, props, property); + } + render() { const clipWidth = NumCast(this.layoutDoc._clipWidth) + "%"; const clearButton = (which: string) => { @@ -84,6 +90,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl const whichDoc = Cast(this.dataDoc[`compareBox-${which}`], Doc, null); return whichDoc ? <> <DocumentView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} + isContentActive={returnFalse} + isDocumentActive={returnFalse} + styleProvider={this.docStyleProvider} Document={whichDoc} DataDoc={undefined} pointerEvents={"none"} /> @@ -102,7 +111,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl }; return ( - <div className={`comparisonBox${this.isContentActive() || SnappingManager.GetIsDragging() ? "-interactive" : ""}` /* change className to easily disable/enable pointer events in CSS */}> + <div className={`comparisonBox${this.props.isContentActive() || SnappingManager.GetIsDragging() ? "-interactive" : ""}` /* change className to easily disable/enable pointer events in CSS */}> {displayBox("after", 1, this.props.PanelWidth() - 3)} <div className="clip-div" style={{ width: clipWidth, transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, "gray") }}> {displayBox("before", 0, 0)} diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 34488ffbe..3d2cdf5a4 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -11,7 +11,7 @@ import { CollectionFreeFormView } from "../collections/collectionFreeForm/Collec import { CollectionSchemaView } from "../collections/collectionSchema/CollectionSchemaView"; import { CollectionView } from "../collections/CollectionView"; import { InkingStroke } from "../InkingStroke"; -import { PresElementBox } from "../presentationview/PresElementBox"; +import { PresElementBox } from "../nodes/trails/PresElementBox"; import { SearchBox } from "../search/SearchBox"; import { DashWebRTCVideo } from "../webcam/DashWebRTCVideo"; import { YoutubeBox } from "./../../apis/youtube/YoutubeBox"; @@ -32,7 +32,7 @@ import { LabelBox } from "./LabelBox"; import { LinkAnchorBox } from "./LinkAnchorBox"; import { LinkBox } from "./LinkBox"; import { PDFBox } from "./PDFBox"; -import { PresBox } from "./PresBox"; +import { PresBox } from "./trails/PresBox"; import { ScreenshotBox } from "./ScreenshotBox"; import { ScriptingBox } from "./ScriptingBox"; import { SliderBox } from "./SliderBox"; @@ -64,6 +64,7 @@ interface HTMLtagProps { htmltag: string; onClick?: ScriptField; onInput?: ScriptField; + scaling: number; } //"<HTMLdiv borderRadius='100px' onClick={this.bannerColor=this.bannerColor==='red'?'green':'red'} overflow='hidden' position='absolute' width='100%' height='100%' transform='rotate({2*this.x+this.y}deg)'> <ImageBox {...props} fieldKey={'data'}/> <HTMLspan width='200px' top='0' height='35px' textAlign='center' paddingTop='10px' transform='translate(-40px, 45px) rotate(-45deg)' position='absolute' color='{this.bannerColor===`green`?`light`:`dark`}blue' backgroundColor='{this.bannerColor===`green`?`dark`:`light`}blue'> {this.title}</HTMLspan></HTMLdiv>" @@ -82,7 +83,7 @@ interface HTMLtagProps { export class HTMLtag extends React.Component<HTMLtagProps> { click = (e: React.MouseEvent) => { const clickScript = (this.props as any).onClick as Opt<ScriptField>; - clickScript?.script.run({ this: this.props.Document, self: this.props.RootDoc }); + clickScript?.script.run({ this: this.props.Document, self: this.props.RootDoc, scale: this.props.scaling }); } onInput = (e: React.FormEvent<HTMLDivElement>) => { const onInputScript = (this.props as any).onInput as Opt<ScriptField>; @@ -90,9 +91,9 @@ export class HTMLtag extends React.Component<HTMLtagProps> { } render() { const style: { [key: string]: any } = {}; - const divKeys = OmitKeys(this.props, ["children", "htmltag", "RootDoc", "Document", "key", "onInput", "onClick", "__proto__"]).omit; + const divKeys = OmitKeys(this.props, ["children", "htmltag", "RootDoc", "scaling", "Document", "key", "onInput", "onClick", "__proto__"]).omit; const replacer = (match: any, expr: string, offset: any, string: any) => { // bcz: this executes a script to convert a propery expression string: { script } into a value - return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name })?.script.run({ self: this.props.RootDoc, this: this.props.Document }).result as string || ""; + return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: "number" })?.script.run({ self: this.props.RootDoc, this: this.props.Document, scale: this.props.scaling }).result as string || ""; }; Object.keys(divKeys).map((prop: string) => { const p = (this.props as any)[prop] as string; @@ -180,11 +181,11 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & Fo const replacer = (match: any, prefix: string, expr: string, postfix: string, offset: any, string: any) => { return prefix + (ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name })?.script.run({ this: this.props.Document }).result as string || "") + postfix; }; - layoutFrame = layoutFrame.replace(/(>[^{]*)\{([^.'][^<}]+)\}([^}]*<)/g, replacer); + layoutFrame = layoutFrame.replace(/(>[^{]*)[^=]\{([^.'][^<}]+)\}([^}]*<)/g, replacer); // replace HTML<tag> with corresponding HTML tag as in: <HTMLdiv> becomes <HTMLtag Document={props.Document} htmltag='div'> const replacer2 = (match: any, p1: string, offset: any, string: any) => { - return `<HTMLtag RootDoc={props.RootDoc} Document={props.Document} htmltag='${p1}'`; + return `<HTMLtag RootDoc={props.RootDoc} Document={props.Document} scaling='${this.props.scaling?.() || 1}' htmltag='${p1}'`; }; layoutFrame = layoutFrame.replace(/<HTML([a-zA-Z0-9_-]+)/g, replacer2); @@ -200,7 +201,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & Fo if (splits.length > 1) { const code = XRegExp.matchRecursive(splits[1], "{", "}", "", { valueNames: ["between", "left", "match", "right", "between"] }); layoutFrame = splits[0] + ` ${func}={props.${func}} ` + splits[1].substring(code[1].end + 1); - return ScriptField.MakeScript(code[1].value, { this: Doc.name, self: Doc.name, value: "string" }); + return ScriptField.MakeScript(code[1].value, { this: Doc.name, self: Doc.name, scale: "number", value: "string" }); } return undefined; // add input function to props diff --git a/src/client/views/nodes/DocumentLinksButton.scss b/src/client/views/nodes/DocumentLinksButton.scss index 735aa669f..b37b68249 100644 --- a/src/client/views/nodes/DocumentLinksButton.scss +++ b/src/client/views/nodes/DocumentLinksButton.scss @@ -1,16 +1,27 @@ -@import "../globalCssVariables.scss"; +@import "../global/globalCssVariables.scss"; +.documentLinksButton-menu { + width: 100%; + height: 100%; + position: relative; + display: flex; + align-content: center; + justify-content: center; + align-items: center; +} + .documentLinksButton-cont { min-width: 20; min-height: 20; position: absolute; } + .documentLinksButton, .documentLinksButton-endLink, .documentLinksButton-startLink { - height: 20px; - width: 20px; + height: 25px; + width: 25px; position: absolute; border-radius: 50%; opacity: 0.9; @@ -33,27 +44,43 @@ } .documentLinksButton { - background-color: black; + background-color: $dark-gray; + color: $white; font-weight: bold; + width: 80%; + height: 80%; + font-size: 100%; + transition: 0.2s ease all; &:hover { - background: $main-accent; - transform: scale(1.05); - cursor: pointer; + background-color: $black; } } -.documentLinksButton-endLink { - border: red solid 2px; +.documentLinksButton.startLink { + background-color: $medium-blue; + color: $white; + font-weight: bold; + width: 80%; + height: 80%; + font-size: 100%; + transition: 0.2s ease all; &:hover { - background: deepskyblue; - transform: scale(1.05); - cursor: pointer; + background-color: $black; } } -.documentLinksButton-startLink { - border: red solid 2px; - background-color: rgba(255, 192, 203, 0.5); +.documentLinksButton-endLink { + border: $medium-blue 2px dashed; + color: $medium-blue; + background-color: none !important; + width: 80%; + height: 80%; + font-size: 100%; + transition: 0.2s ease all; + + &:hover { + background-color: $light-blue; + } }
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index a6d07374a..7648e866e 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -20,6 +20,7 @@ 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; @@ -30,12 +31,12 @@ interface DocumentLinksButtonProps { Offset?: (number | undefined)[]; AlwaysOn?: boolean; InMenu?: boolean; - StartLink?: boolean; + StartLink?: boolean; //whether the link HAS been started (i.e. now needs to be completed) } @observer export class DocumentLinksButton extends React.Component<DocumentLinksButtonProps, {}> { private _linkButton = React.createRef<HTMLDivElement>(); - @observable public static StartLink: Doc | undefined; + @observable public static StartLink: Doc | undefined; //origin's Doc, if defined @observable public static StartLinkView: DocumentView | undefined; @observable public static AnnotationId: string | undefined; @observable public static AnnotationUri: string | undefined; @@ -45,6 +46,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp public static invisibleWebRef = React.createRef<HTMLDivElement>(); @action public static ClearLinkEditor() { DocumentLinksButton.LinkEditorDocView = undefined; } + @action @undoBatch onLinkButtonMoved = (e: PointerEvent) => { if (this.props.InMenu && this.props.StartLink) { @@ -66,6 +68,40 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp return false; } + onLinkMenuOpen = (e: React.PointerEvent): void => { + setupMoveUpEvents(this, e, this.onLinkButtonMoved, emptyFunction, action((e, doubleTap) => { + if (doubleTap) { + const rootDoc = this.props.View.rootDoc; + const docid = Doc.CurrentUserEmail + Doc.GetProto(rootDoc)[Id] + "-pivotish"; + DocServer.GetRefField(docid).then(async docx => { + const rootAlias = () => { + const rootAlias = Doc.MakeAlias(rootDoc); + rootAlias.x = rootAlias.y = 0; + return rootAlias; + }; + let wid = rootDoc[WidthSym](); + const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([rootAlias()], { title: this.props.View.Document.title + "-pivot", _width: 500, _height: 500, }, docid); + const docs = await DocListCastAsync(Doc.GetProto(target).data); + if (!target.pivotFocusish) (Doc.GetProto(target).pivotFocusish = target); + DocListCast(rootDoc.links).forEach(link => { + const other = LinkManager.getOppositeAnchor(link, rootDoc); + const otherdoc = !other ? undefined : other.annotationOn ? Cast(other.annotationOn, Doc, null) : other; + if (otherdoc && !docs?.some(d => Doc.AreProtosEqual(d, otherdoc))) { + const alias = Doc.MakeAlias(otherdoc); + alias.x = wid; + alias.y = 0; + alias._lockedPosition = false; + wid += otherdoc[WidthSym](); + Doc.AddDocToList(Doc.GetProto(target), "data", alias); + } + }); + LightboxView.SetLightboxDoc(target); + }); + } + else DocumentLinksButton.LinkEditorDocView = this.props.View; + })); + } + @undoBatch onLinkButtonDown = (e: React.PointerEvent): void => { setupMoveUpEvents(this, e, this.onLinkButtonMoved, emptyFunction, action((e, doubleTap) => { @@ -78,36 +114,6 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp DocumentLinksButton.StartLink = this.props.View.props.Document; DocumentLinksButton.StartLinkView = this.props.View; } - } else if (!this.props.InMenu) { - if (doubleTap) { - const rootDoc = this.props.View.rootDoc; - const docid = Doc.CurrentUserEmail + Doc.GetProto(rootDoc)[Id] + "-pivotish"; - DocServer.GetRefField(docid).then(async docx => { - const rootAlias = () => { - const rootAlias = Doc.MakeAlias(rootDoc); - rootAlias.x = rootAlias.y = 0; - return rootAlias; - }; - let wid = rootDoc[WidthSym](); - const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([rootAlias()], { title: this.props.View.Document.title + "-pivot", _width: 500, _height: 500, }, docid); - const docs = await DocListCastAsync(Doc.GetProto(target).data); - if (!target.pivotFocusish) (Doc.GetProto(target).pivotFocusish = target); - DocListCast(rootDoc.links).forEach(link => { - const other = LinkManager.getOppositeAnchor(link, rootDoc); - const otherdoc = !other ? undefined : other.annotationOn ? Cast(other.annotationOn, Doc, null) : other; - if (otherdoc && !docs?.some(d => Doc.AreProtosEqual(d, otherdoc))) { - const alias = Doc.MakeAlias(otherdoc); - alias.x = wid; - alias.y = 0; - alias._lockedPosition = false; - wid += otherdoc[WidthSym](); - Doc.AddDocToList(Doc.GetProto(target), "data", alias); - } - }); - LightboxView.SetLightboxDoc(target); - }); - } - else DocumentLinksButton.LinkEditorDocView = this.props.View; } })); } @@ -120,17 +126,17 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp if (DocumentLinksButton.StartLink === this.props.View.props.Document) { DocumentLinksButton.StartLink = undefined; DocumentLinksButton.StartLinkView = undefined; - } else { + } else { //if this LinkButton's Document is undefined DocumentLinksButton.StartLink = this.props.View.props.Document; DocumentLinksButton.StartLinkView = this.props.View; } - //action(() => Doc.BrushDoc(this.props.View.Document)); } else if (!this.props.InMenu) { DocumentLinksButton.LinkEditorDocView = this.props.View; } } + completeLink = (e: React.PointerEvent): void => { setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action((e, doubleTap) => { if (doubleTap && !this.props.StartLink) { @@ -141,7 +147,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp } else if (DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document) { const sourceDoc = DocumentLinksButton.StartLink; const targetDoc = this.props.View.ComponentView?.getAnchor?.() || this.props.View.Document; - const linkDoc = DocUtils.MakeLink({ doc: sourceDoc }, { doc: targetDoc }, "long drag"); + const linkDoc = DocUtils.MakeLink({ doc: sourceDoc }, { doc: targetDoc }, "links"); //why is long drag here when this is used for completing links by clicking? LinkManager.currentLink = linkDoc; @@ -184,7 +190,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp } else if (startLink !== endLink) { endLink = endLinkView?.docView?._componentView?.getAnchor?.() || endLink; startLink = DocumentLinksButton.StartLinkView?.docView?._componentView?.getAnchor?.() || startLink; - const linkDoc = DocUtils.MakeLink({ doc: startLink }, { doc: endLink }, DocumentLinksButton.AnnotationId ? "hypothes.is annotation" : "long drag", undefined, undefined, true); + const linkDoc = DocUtils.MakeLink({ doc: startLink }, { doc: endLink }, DocumentLinksButton.AnnotationId ? "hypothes.is annotation" : "link", undefined, undefined, true); LinkManager.currentLink = linkDoc; @@ -192,7 +198,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 } @@ -242,45 +248,50 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp return results; } + /** + * gets the JSX of the link button (btn used to start/complete links) OR the link-view button (btn on bottom left of each linked node) + * + * todo:glr / anh seperate functionality such as onClick onPointerDown of link menu button + */ @computed get linkButtonInner() { - const btnDim = this.props.InMenu ? "20px" : "30px"; + const btnDim = "30px"; const link = <img style={{ width: "22px", height: "16px" }} src={`/assets/${"link.png"}`} />; - - return <div className="documentLinksButton-cont" ref={this._linkButton} - style={{ left: this.props.Offset?.[0], top: this.props.Offset?.[1], right: this.props.Offset?.[2], bottom: this.props.Offset?.[3] }} - > - <div className={"documentLinksButton"} - onPointerDown={this.onLinkButtonDown} onClick={this.onLinkClick} - style={{ - backgroundColor: this.props.InMenu ? "" : "#add8e6", - color: this.props.InMenu ? "white" : "black", - width: btnDim, - height: btnDim, - }} > - {this.props.InMenu ? - this.props.StartLink ? - <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" /> - : link - : Array.from(this.filteredLinks).length} - </div> - {this.props.InMenu && !this.props.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document ? - <div className={"documentLinksButton-endLink"} + const isActive = (DocumentLinksButton.StartLink === this.props.View.props.Document) && this.props.StartLink; + return (!this.props.InMenu ? + <div className="documentLinksButton-cont" + style={{ left: this.props.Offset?.[0], top: this.props.Offset?.[1], right: this.props.Offset?.[2], bottom: this.props.Offset?.[3] }} + > + <div className={"documentLinksButton"} + onPointerDown={this.onLinkMenuOpen} onClick={this.onLinkClick} style={{ - width: btnDim, height: btnDim, - backgroundColor: DocumentLinksButton.StartLink ? "" : "grey", - opacity: DocumentLinksButton.StartLink ? "" : "50%", - border: DocumentLinksButton.StartLink ? "" : "none", - cursor: DocumentLinksButton.StartLink ? "pointer" : "default" - }} - onPointerDown={DocumentLinksButton.StartLink && this.completeLink} - onClick={e => DocumentLinksButton.StartLink && DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.props.View.props.Document, true, this.props.View)} /> - : (null) - } - {DocumentLinksButton.StartLink === this.props.View.props.Document && this.props.InMenu && this.props.StartLink ? - <div className={"documentLinksButton-startLink"} onPointerDown={this.clearLinks} onClick={this.clearLinks} style={{ width: btnDim, height: btnDim }} /> - : (null) - } - </div >; + backgroundColor: Colors.LIGHT_BLUE, + color: Colors.BLACK, + width: btnDim, + height: btnDim, + }}> + {Array.from(this.filteredLinks).length} + </div> + </div> + : + <div className="documentLinksButton-menu" ref={this._linkButton}> + {this.props.InMenu && !this.props.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document ? //if the origin node is not this node + <div className={"documentLinksButton-endLink"} + onPointerDown={DocumentLinksButton.StartLink && this.completeLink} + onClick={e => DocumentLinksButton.StartLink && DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.props.View.props.Document, true, this.props.View)}> + <FontAwesomeIcon className="documentdecorations-icon" icon="link" /> + </div> + : (null) + } + { + this.props.InMenu && this.props.StartLink ? //if link has been started from current node, then set behavior of link button to deactivate linking when clicked again + <div className={`documentLinksButton ${isActive ? `startLink` : ``}`} onPointerDown={isActive ? undefined : this.onLinkButtonDown} onClick={isActive ? this.clearLinks : this.onLinkClick}> + <FontAwesomeIcon className="documentdecorations-icon" icon="link" /> + </div> + : + (null) + } + </div> + ); } render() { @@ -290,6 +301,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp const buttonTitle = "Tap to view links; double tap to open link collection"; const title = this.props.InMenu ? menuTitle : buttonTitle; + //render circular tooltip if it isn't set to invisible and show the number of doc links the node has, and render inner-menu link button for starting/stopping links if currently in menu return !Array.from(this.filteredLinks).length && !this.props.AlwaysOn ? (null) : this.props.InMenu && (DocumentLinksButton.StartLink || this.props.StartLink) ? <Tooltip title={<div className="dash-tooltip">{title}</div>}> diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index bdbece621..7f164ca48 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -1,4 +1,4 @@ -@import "../globalCssVariables"; +@import "../global/globalCssVariables"; .documentView-effectsWrapper { border-radius: inherit; @@ -22,7 +22,7 @@ transition: outline .3s linear; cursor: grab; - // background: $light-color; //overflow: hidden; + // background: $white; //overflow: hidden; transform-origin: left top; &.minimized { @@ -147,7 +147,7 @@ .documentView-titleWrapper, .documentView-titleWrapper-hover { overflow: hidden; - color: white; + color: $black; transform-origin: top left; top: 0; width: 100%; @@ -218,6 +218,6 @@ .documentView-node:first-child { position: relative; - background: "#B59B66"; //$light-color; + background: "#B59B66"; //$white; } }
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index b861669f8..5bd6049d6 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 } from "../../../Utils"; +import { emptyFunction, hasDescendantTarget, OmitKeys, returnVal, Utils, returnTrue } from "../../../Utils"; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { Docs, DocUtils } from "../../documents/Documents"; import { DocumentType } from '../../documents/DocumentTypes'; @@ -43,7 +43,7 @@ import { DocumentLinksButton } from './DocumentLinksButton'; import "./DocumentView.scss"; import { LinkAnchorBox } from './LinkAnchorBox'; import { LinkDocPreview } from "./LinkDocPreview"; -import { PresBox } from './PresBox'; +import { PresBox } from './trails/PresBox'; import { RadialMenu } from './RadialMenu'; import React = require("react"); import { ScriptingBox } from "./ScriptingBox"; @@ -64,7 +64,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 @@ -84,10 +84,13 @@ export interface DocComponentView { reverseNativeScaling?: () => boolean; // DocumentView's setup screenToLocal based on the doc having a nativeWidth/Height. However, some content views (e.g., FreeFormView w/ fitToBox set) may ignore the native dimensions so this flags the DocumentView to not do Nativre scaling. shrinkWrap?: () => void; // requests a document to display all of its contents with no white space. currently only implemented (needed?) for freeform views menuControls?: () => JSX.Element; // controls to display in the top menu bar when the document is selected. + isAnyChildContentActive?: () => boolean; // is any child content of the document active getKeyFrameEditing?: () => boolean; // 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) 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; } export interface DocumentViewSharedProps { renderDepth: number; @@ -137,7 +140,7 @@ export interface DocumentViewProps extends DocumentViewSharedProps { hideDecorationTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings treeViewDoc?: Doc; isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events - isContentActive: () => boolean | undefined; // whether a document should handle pointer events + isContentActive: () => boolean | undefined; // whether document contents should handle pointer events contentPointerEvents?: string; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents radialMenu?: String[]; LayoutTemplateString?: string; @@ -180,9 +183,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps private _dropDisposer?: DragManager.DragDropDisposer; private _holdDisposer?: InteractionUtils.MultiTouchEventDisposer; protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; - _componentView: Opt<DocComponentView>; // needs to be accessed from DocumentView wrapper class + @observable _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); } @@ -420,8 +423,10 @@ 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 @@ -662,7 +667,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" })); @@ -746,7 +751,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" }); } } @@ -754,16 +759,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. @@ -775,8 +779,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps contentScaling = () => this.ContentScale; 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; + setContentView = action((view: { getAnchor?: () => Doc, forward?: () => boolean, back?: () => boolean }) => this._componentView = view); + isContentActive = (outsideReaction?: boolean) => { + return CurrentUserUtils.SelectedTool !== InkTool.None || + this.props.Document.forceActive || + this.props.isSelected(outsideReaction) || + this._componentView?.isAnyChildContentActive?.() || + this.props.isContentActive() ? true : false; + } @computed get contents() { TraceMobx(); const audioView = !this.layoutDoc._showAudio ? (null) : @@ -791,7 +801,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) || (this.isContentActive() ? "all" : "none"), height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined, }}> <DocumentContentsView key={1} {...this.props} @@ -808,7 +818,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>; @@ -838,7 +850,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} @@ -846,6 +858,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps PanelWidth={this.anchorPanelWidth} PanelHeight={this.anchorPanelHeight} dontRegisterView={false} + fitWidth={returnTrue} styleProvider={this.anchorStyleProvider} removeDocument={this.hideLinkAnchor} LayoutTemplate={undefined} @@ -884,7 +897,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) { @@ -970,7 +983,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps 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 ? ["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; @@ -978,7 +991,7 @@ 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 <div className={DocumentView.ROOT_DIV} ref={this._mainCont} onContextMenu={this.onContextMenu} @@ -1042,7 +1055,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.DataDoc, this.props.freezeDimensions)); } - @computed get shouldNotScale() { return (this.fitWidth && !this.nativeWidth) || [CollectionViewType.Docking, CollectionViewType.Tree].includes(this.Document._viewType as any); } + @computed get shouldNotScale() { return (this.fitWidth && !this.nativeWidth) || this.props.treeViewDoc || [CollectionViewType.Docking].includes(this.Document._viewType as any); } @computed get effectiveNativeWidth() { return this.shouldNotScale ? 0 : (this.nativeWidth || NumCast(this.layoutDoc.width)); } @computed get effectiveNativeHeight() { return this.shouldNotScale ? 0 : (this.nativeHeight || NumCast(this.layoutDoc.height)); } @computed get nativeScaling() { 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..7ad03e055 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); @@ -409,6 +417,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc renderDepth={1} dropAction={this.props.dropAction} ScreenToLocalTransform={this.props.ScreenToLocalTransform} + isAnyChildContentActive={returnFalse} addDocTab={returnFalse} pinToPres={returnFalse} isSelected={returnFalse} @@ -479,10 +488,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 index 33ac85a0e..718af2c16 100644 --- a/src/client/views/nodes/FontIconBox.scss +++ b/src/client/views/nodes/FontIconBox.scss @@ -1,5 +1,7 @@ +@import "../global/globalCssVariables"; + .fontIconBox-label { - color: white; + color: $white; margin-right: 4px; margin-top: 1px; position: relative; @@ -22,8 +24,8 @@ position: absolute; top: -10px; right: -10px; - color: white; - background: #f44b42; + color: $white; + background: $pink; font-weight: 300; border-radius: 100%; width: 25px; @@ -37,7 +39,7 @@ .menuButton-circle, .menuButton-round { border-radius: 100%; - background-color: black; + background-color: $dark-gray; padding: 0; .fontIconBox-label { @@ -47,13 +49,14 @@ } &:hover { - background-color: #aaaaa3; + background-color: $light-gray; } } .menuButton-square { padding-top: 3px; padding-bottom: 3px; + background-color: $dark-gray; .fontIconBox-label { border-radius: 0px; diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx index 6ae4b9726..0d415e238 100644 --- a/src/client/views/nodes/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox.tsx @@ -14,6 +14,7 @@ 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", }); @@ -47,7 +48,7 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>( 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 === "white" ? "100%" : "0%"})`, marginBottom: "5px" }} />; + 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"> diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index e2e08a0e6..38deb4a73 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,8 +1,9 @@ -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'; @@ -10,10 +11,11 @@ import { ComputedField } from '../../../fields/ScriptField'; import { Cast, NumCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, OmitKeys, returnOne, Utils } 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,11 +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 { SnappingManager } from '../../util/SnappingManager'; const path = require('path'); export const pageSchema = createSchema({ @@ -60,6 +64,13 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document + + getAnchor = () => { + const anchor = AnchorMenu.Instance?.GetAnchor(this._savedAnnotations); + anchor && this.addDocument(anchor); + return anchor ?? this.rootDoc; + } + componentDidMount() { this.props.setContentView?.(this); // bcz: do not remove this. without it, stepping into an image in the lightbox causes an infinite loop.... this._disposers.sizer = reaction(() => ( @@ -72,8 +83,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp { fireImmediately: true, delay: 1000 }); 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(); + // Array.from(this._savedAnnotations.values()).forEach(v => v.forEach(a => a.remove())); + // this._savedAnnotations.clear(); })); this._disposers.path = reaction(() => ({ nativeSize: this.nativeSize, width: this.layoutDoc[WidthSym]() }), ({ nativeSize, width }) => { @@ -227,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; } @@ -283,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} @@ -319,14 +331,19 @@ 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)) this._marqueeing = [e.clientX, e.clientY]; + if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.props.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); + } } @action finishMarquee = () => { this._marqueeing = undefined; - this.props.select(true); + this.props.select(false); } render() { @@ -342,16 +359,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp }} > <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} renderDepth={this.props.renderDepth + 1} + isAnnotationOverlay={true} fieldKey={this.annotationKey} CollectionView={undefined} - isAnnotationOverlay={true} annotationLayerHostsContent={true} PanelWidth={this.props.PanelWidth} PanelHeight={this.props.PanelHeight} ScreenToLocalTransform={this.screenToLocalTransform} - select={emptyFunction} - isContentActive={this.isContentActive} scaling={returnOne} + select={emptyFunction} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} removeDocument={this.removeDocument} moveDocument={this.moveDocument} diff --git a/src/client/views/nodes/KeyValueBox.scss b/src/client/views/nodes/KeyValueBox.scss index eb7c2f32b..ffcba4981 100644 --- a/src/client/views/nodes/KeyValueBox.scss +++ b/src/client/views/nodes/KeyValueBox.scss @@ -1,10 +1,10 @@ -@import "../globalCssVariables"; +@import "../global/globalCssVariables"; .keyValueBox-cont { overflow-y: scroll; width:100%; height: 100%; - background-color: $light-color; - border: 1px solid $intermediate-color; + background-color: $white; + border: 1px solid $medium-gray; border-radius: $border-radius; box-sizing: border-box; display: inline-block; @@ -56,8 +56,8 @@ $header-height: 30px; width:100%; position: relative; display: inline-block; - background: $intermediate-color; - color: $light-color; + background: $medium-gray; + color: $white; text-transform: uppercase; letter-spacing: 2px; font-size: 12px; @@ -66,7 +66,7 @@ $header-height: 30px; th { font-weight: normal; &:first-child { - border-right: 1px solid $light-color; + border-right: 1px solid $white; } } } @@ -76,9 +76,9 @@ $header-height: 30px; display: flex; width:100%; height:$header-height; - background: $light-color; + background: $white; .formattedTextBox-cont { - background: $light-color; + background: $white; } } .keyValueBox-cont { @@ -116,8 +116,8 @@ $header-height: 30px; display: flex; width:100%; height:30px; - background: $light-color-secondary; + background: $light-gray; .formattedTextBox-cont { - background: $light-color-secondary; + background: $light-gray; } }
\ No newline at end of file diff --git a/src/client/views/nodes/KeyValuePair.scss b/src/client/views/nodes/KeyValuePair.scss index f78767234..5b660e582 100644 --- a/src/client/views/nodes/KeyValuePair.scss +++ b/src/client/views/nodes/KeyValuePair.scss @@ -1,4 +1,4 @@ -@import "../globalCssVariables"; +@import "../global/globalCssVariables"; .keyValuePair-td-key { diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index c65ba9c69..55ea45bb8 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -30,6 +30,7 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps, LinkDocument>( dontRegisterView={true} renderDepth={this.props.renderDepth + 1} CollectionView={undefined} + isAnyChildContentActive={returnFalse} isContentActive={this.isContentActiveFunc} addDocument={returnFalse} removeDocument={returnFalse} 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.scss b/src/client/views/nodes/PDFBox.scss index 0f46da294..72dec6e4c 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -7,7 +7,7 @@ overflow: hidden; cursor: auto; transform-origin: top left; - z-index: 0; + //z-index: 0; .pdfBox-ui { position: absolute; @@ -30,6 +30,7 @@ justify-content: center; border-radius: 3px; pointer-events: all; + z-index: 1; // so it appears on top of the document's title, if shown } .pdfBox-pageNums { @@ -223,7 +224,7 @@ .pdfBox { width: 100%; height: 100%; - pointer-events: none; + //pointer-events: none; .pdfViewerDash-text { .textLayer { display: none; diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index feaeb9e21..5e07229c1 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'; @@ -23,6 +23,7 @@ import { FieldView, FieldViewProps } from './FieldView'; import { pageSchema } from "./ImageBox"; import "./PDFBox.scss"; import React = require("react"); +import { AnchorMenu } from '../pdf/AnchorMenu'; type PdfDocument = makeInterface<[typeof documentSchema, typeof panZoomSchema, typeof pageSchema]>; const PdfDocument = makeInterface(documentSchema, panZoomSchema, pageSchema); @@ -30,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; @@ -52,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?.(); } @@ -89,16 +67,21 @@ 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); } getAnchor = () => { - const anchor = Docs.Create.TextanchorDocument({ - title: StrCast(this.rootDoc.title + " " + this.layoutDoc._scrollTop), - annotationOn: this.rootDoc, - y: NumCast(this.layoutDoc._scrollTop), - }); + const anchor = + AnchorMenu.Instance?.GetAnchor() ?? + Docs.Create.TextanchorDocument({ + title: StrCast(this.rootDoc.title + " " + this.layoutDoc._scrollTop), + annotationOn: this.rootDoc, + y: NumCast(this.layoutDoc._scrollTop), + }); this.addDocument(anchor); return anchor; } @@ -162,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 = <> @@ -185,9 +177,9 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps </>; const searchTitle = `${!this._searching ? "Open" : "Close"} Search Bar`; const curPage = this.Document._curPage || 1; - return !this.isContentActive() ? (null) : + return !this.props.isContentActive() ? (null) : <div className="pdfBox-ui" onKeyDown={e => [KeyCodes.BACKSPACE, KeyCodes.DELETE].includes(e.keyCode) ? e.stopPropagation() : true} - onPointerDown={e => e.stopPropagation()} style={{ display: this.isContentActive() ? "flex" : "none" }}> + onPointerDown={e => e.stopPropagation()} style={{ display: this.props.isContentActive() ? "flex" : "none" }}> <div className="pdfBox-overlayCont" onPointerDown={(e) => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}> <button className="pdfBox-overlayButton" title={searchTitle} /> <input className="pdfBox-searchBar" placeholder="Search" ref={this._searchRef} onChange={this.searchStringChanged} @@ -217,13 +209,15 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps {this._pageControls ? pageBtns : (null)} </div> <button className="pdfBox-sidebarBtn" title="Toggle Sidebar" - style={{ display: !this.isContentActive() ? "none" : undefined }} + style={{ display: !this.props.isContentActive() ? "none" : undefined }} onPointerDown={this.sidebarBtnDown} > <FontAwesomeIcon icon={"chevron-left"} size="sm" /> </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[] = []; @@ -235,7 +229,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } @computed get renderTitleBox() { - const classname = "pdfBox" + (this.isContentActive() ? "-interactive" : ""); + const classname = "pdfBox" + (this.props.isContentActive() ? "-interactive" : ""); return <div className={classname} > <div className="pdfBox-title-outer"> <strong className="pdfBox-title" >{this.props.Document.title}</strong> @@ -244,43 +238,60 @@ 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.props.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} + setHeight={emptyFunction} + nativeWidth={this._previewNativeWidth ?? NumCast(this.layoutDoc._nativeWidth)} + showSidebar={this.SidebarShown} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} sidebarAddDocument={this.sidebarAddDocument} moveDocument={this.moveDocument} removeDocument={this.removeDocument} - isContentActive={this.isContentActive} /> {this.settingsPanel()} </div>; diff --git a/src/client/views/nodes/RadialMenu.scss b/src/client/views/nodes/RadialMenu.scss index daa620d12..312b51013 100644 --- a/src/client/views/nodes/RadialMenu.scss +++ b/src/client/views/nodes/RadialMenu.scss @@ -1,4 +1,4 @@ -@import "../globalCssVariables"; +@import "../global/globalCssVariables"; .radialMenu-cont { position: absolute; @@ -53,7 +53,7 @@ s transition: all .1s; border-width: .11px; border-style: none; - border-color: $intermediate-color; // rgb(187, 186, 186); + border-color: $medium-gray; // rgb(187, 186, 186); // padding: 10px 0px 10px 0px; white-space: nowrap; font-size: 13px; diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index 252c029e4..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"; + this._audioRec?.stop(); + this._videoRec?.stop(); + 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 263fd5a19..484dec7e2 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" }); } } @@ -314,12 +307,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp const interactive = CurrentUserUtils.SelectedTool !== InkTool.None || !this.props.isSelected() ? "" : "-interactive"; const style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive; return !field ? <div key="loading">Loading</div> : - <div className="container" key="container" style={{ pointerEvents: this._isAnyChildContentActive || this.isContentActive() ? "all" : "none" }}> + <div className="container" key="container" style={{ mixBlendMode: "multiply", pointerEvents: this.props.isContentActive() ? "all" : "none" }}> <div className={`${style}`} style={{ width: "100%", height: "100%", left: "0px" }}> <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()} @@ -328,7 +321,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp Not supported. </video> {!this.audiopath || this.audiopath === field.url.href ? (null) : - <audio ref={this.setAudioRef} className={`audiobox-control${this.isContentActive() ? "-interactive" : ""}`}> + <audio ref={this.setAudioRef} className={`audiobox-control${this.props.isContentActive() ? "-interactive" : ""}`}> <source src={this.audiopath} type="audio/mpeg" /> Not supported. </audio>} @@ -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>} key="play" placement="bottom"> + <div className="videoBox-play" onPointerDown={this.onPlayDown} > + <FontAwesomeIcon icon={this._playing ? "pause" : "play"} size="lg" /> + </div> + </Tooltip>, + <Tooltip title={<div className="dash-tooltip">{"timecode"}</div>} key="time" 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>} key="full" 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>} key="snap" 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>} key="timeline" placement="bottom"> + <div className="videoBox-timelineButton" onPointerDown={this.onTimelineHdlDown}> + <FontAwesomeIcon icon="eye" size="lg" /> + </div> + </Tooltip>,]} + </div>; } onPlayDown = () => this._playing ? this.Pause() : this.Play(); @@ -426,7 +429,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp setupMoveUpEvents(this, e, action((e: PointerEvent) => { this._clicking = false; - if (this.isContentActive()) { + if (this.props.isContentActive()) { const local = this.props.ScreenToLocalTransform().scale(this.props.scaling?.() || 1).transformPoint(e.clientX, e.clientY); this.layoutDoc._timelineHeightPercent = Math.max(0, Math.min(100, local[1] / this.props.PanelHeight() * 100)); } @@ -435,7 +438,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp () => { this.layoutDoc._timelineHeightPercent = this.heightPercent !== 100 ? 100 : VideoBox.heightPercent; setTimeout(action(() => this._clicking = false), 500); - }, this.isContentActive(), this.isContentActive()); + }, this.props.isContentActive(), this.props.isContentActive()); }); onResetDown = (e: React.PointerEvent) => { @@ -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 @@ -526,12 +529,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp playFrom={this.playFrom} setTime={this.setAnchorTime} playing={this.playing} + isAnyChildContentActive={this.isAnyChildContentActive} whenChildContentsActiveChanged={this.timelineWhenChildContentsActiveChanged} removeDocument={this.removeDocument} ScreenToLocalTransform={this.timelineScreenToLocal} Play={this.Play} Pause={this.Pause} - isContentActive={this.isContentActive} playLink={this.playLink} PanelHeight={this.timelineHeight} /> @@ -542,9 +545,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)) this._marqueeing = [e.clientX, e.clientY]; - }); + marqueeDown = (e: React.PointerEvent) => { + if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.props.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; @@ -561,7 +570,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } marqueeFitScaling = () => (this.props.scaling?.() || 1) * this.heightPercent / 100; marqueeOffset = () => [this.panelWidth() / 2 * (1 - this.heightPercent / 100) / (this.heightPercent / 100), 0]; - timelineDocFilter = () => ["_timelineLabel:true:x"]; + timelineDocFilter = () => [`_timelineLabel:true,${Utils.noRecursionHack}:x`]; render() { const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); const borderRadius = borderRad?.includes("px") ? `${Number(borderRad.split("px")[0]) / this.scaling()}px` : borderRad; @@ -583,7 +592,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp ScreenToLocalTransform={this.screenToLocalTransform} docFilters={this.timelineDocFilter} select={emptyFunction} - isContentActive={this.isContentActive} scaling={returnOne} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} removeDocument={this.removeDocument} @@ -614,4 +622,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.scss b/src/client/views/nodes/WebBox.scss index ca82c049c..19b69ff5a 100644 --- a/src/client/views/nodes/WebBox.scss +++ b/src/client/views/nodes/WebBox.scss @@ -1,4 +1,4 @@ -@import "../globalCssVariables.scss"; +@import "../global/globalCssVariables.scss"; .webBox { @@ -17,6 +17,7 @@ justify-content: center; border-radius: 3px; pointer-events: all; + z-index: 1; // so it appears on top of the document's title, if shown } .pdfViewerDash-dragAnnotationBox { diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index cb7e58559..e4d4557af 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,17 +82,19 @@ 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(), - selected => !selected && setTimeout(() => { - Array.from(this._savedAnnotations.values()).forEach(v => v.forEach(a => a.remove())); - this._savedAnnotations.clear(); - })); + this._disposers.autoHeight = reaction(() => this.layoutDoc._autoHeight, + autoHeight => { + if (autoHeight) { + this.layoutDoc._nativeHeight = NumCast(this.props.Document[this.props.fieldKey + "-nativeHeight"]); + this.props.setHeight(NumCast(this.props.Document[this.props.fieldKey + "-nativeHeight"]) * (this.props.scaling?.() || 1)); + } + }); if (this.webField?.href.indexOf("youtube") !== -1) { const youtubeaspect = 400 / 315; @@ -159,9 +163,14 @@ 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 scrollTo = doc.type === DocumentType.TEXTANCHOR ? NumCast(doc.y) : Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), this.props.PanelHeight() / (this.props.scaling?.() || 1)); + const windowHeight = this.props.PanelHeight() / (this.props.scaling?.() || 1); + const scrollTo = Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), windowHeight, windowHeight * .1); if (scrollTo !== undefined) { const focusSpeed = smooth ? 500 : 0; this._initialScroll !== undefined && (this._initialScroll = scrollTo); @@ -169,17 +178,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 = Docs.Create.TextanchorDocument({ - title: StrCast(this.rootDoc.title + " " + this.layoutDoc._scrollTop), - annotationOn: this.rootDoc, - y: NumCast(this.layoutDoc._scrollTop), - }); - this.addDocument(anchor); + const anchor = + AnchorMenu.Instance?.GetAnchor(this._savedAnnotations) ?? + Docs.Create.WebanchorDocument(this._url, { + title: StrCast(this.rootDoc.title + " " + this.layoutDoc._scrollTop), + annotationOn: this.rootDoc, + y: NumCast(this.layoutDoc._scrollTop) + }); + this.addDocumentWrapper(anchor); return anchor; } @@ -202,6 +213,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) { @@ -229,8 +242,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"); @@ -291,9 +306,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; @@ -305,9 +319,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; @@ -318,25 +331,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); } @@ -395,16 +402,19 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @action onMarqueeDown = (e: React.PointerEvent) => { - if (!e.altKey && e.button === 0 && this.isContentActive(true)) { - this._marqueeing = [e.clientX, e.clientY]; - this.props.select(false); + if (!e.altKey && e.button === 0 && this.props.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); } } @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 @@ -414,7 +424,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} @@ -429,9 +439,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) => { @@ -445,20 +460,32 @@ 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" : "")} + return <div className={"webBox-cont" + (!this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && this.props.isContentActive() && CurrentUserUtils.SelectedTool === InkTool.None && !DocumentDecorations.Instance?.Interacting ? "-interactive" : "")} style={{ width: NumCast(this.layoutDoc[this.fieldKey + "-contentWidth"]) || `${100 / (this.props.scaling?.() || 1)}%`, }}> {this.urlContent} </div>; @@ -471,60 +498,68 @@ 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} + 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" ref={this._mainCont} style={{ pointerEvents: this.props.isContentActive() ? "all" : this.props.isContentActive() || SnappingManager.GetIsDragging() ? undefined : "none" }} > + <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} - isAnnotationOverlay={true} - fieldKey={this.annotationKey} - setPreviewCursor={this.setPreviewCursor} - PanelWidth={this.panelWidth} - PanelHeight={this.panelHeight} - dropAction={"alias"} - select={emptyFunction} - isContentActive={returnFalse} - ContentScaling={returnOne} - bringToFront={emptyFunction} - whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - removeDocument={this.removeDocument} - moveDocument={this.moveDocument} - addDocument={this.sidebarAddDocument} - CollectionView={undefined} - ScreenToLocalTransform={this.scrollXf} - renderDepth={this.props.renderDepth + 1} - scaling={returnOne} - childPointerEvents={true} - pointerEvents={this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} /> + <div style={{ mixBlendMode: "multiply" }}> + {renderAnnotations(this.transparentFilter)} + </div> + {renderAnnotations(this.opaqueFilter)} + {SnappingManager.GetIsDragging() ? (null) : renderAnnotations()} {this.annotationLayer} </div> </div> @@ -535,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} @@ -544,17 +579,19 @@ 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} + setHeight={emptyFunction} + nativeWidth={this._previewNativeWidth ?? NumCast(this.layoutDoc._nativeWidth)} + showSidebar={this.SidebarShown} sidebarAddDocument={this.sidebarAddDocument} moveDocument={this.moveDocument} removeDocument={this.removeDocument} - isContentActive={this.isContentActive} /> <button className="webBox-overlayButton-sidebar" key="sidebar" title="Toggle Sidebar" - style={{ display: !this.isContentActive() ? "none" : undefined }} + style={{ display: !this.props.isContentActive() ? "none" : undefined }} onPointerDown={this.sidebarBtnDown} > <FontAwesomeIcon style={{ color: "white" }} icon={"chevron-left"} size="sm" /> </button> diff --git a/src/client/views/nodes/formattedText/DashFieldView.scss b/src/client/views/nodes/formattedText/DashFieldView.scss index e16036000..e7dd286a5 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.scss +++ b/src/client/views/nodes/formattedText/DashFieldView.scss @@ -1,6 +1,7 @@ .dashFieldView { position: relative; - display: inline-block; + display: inline-flex; + align-items: center; .dashFieldView-enumerables { width: 10px; @@ -13,6 +14,8 @@ min-width: 12px; position: relative; display: inline-block; + margin: 0; + transform: scale(0.7); background-color: rgba(155, 155, 155, 0.24); } .dashFieldView-labelSpan { @@ -22,11 +25,11 @@ background: rgba(0,0,0,0.1); } .dashFieldView-fieldSpan { - min-width: 20px; + min-width: 8px; margin-left: 2px; margin-right: 5px; - position: relative; - display: inline; + padding-left: 2px; + display: inline-block; background-color: rgba(155, 155, 155, 0.24); font-weight: bold; span { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index 53aceb533..3cedab1a4 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -1,4 +1,4 @@ -@import "../../globalCssVariables"; +@import "../../global/globalCssVariables"; .ProseMirror { width: 100%; @@ -31,7 +31,7 @@ audiotag:hover { padding: 0; border-width: 0px; border-radius: inherit; - border-color: $intermediate-color; + border-color: $medium-gray; box-sizing: border-box; background-color: inherit; border-style: solid; @@ -363,7 +363,7 @@ footnote::after { @media only screen and (max-width: 1000px) { - @import "../../globalCssVariables"; + @import "../../global/globalCssVariables"; .ProseMirror { width: 100%; @@ -381,7 +381,7 @@ footnote::after { padding: 0; border-width: 0px; border-radius: inherit; - border-color: $intermediate-color; + border-color: $medium-gray; box-sizing: border-box; background-color: inherit; border-style: solid; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 911ec1560..4b1d76d00 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'; @@ -71,7 +71,8 @@ export interface FormattedTextBoxProps { xPadding?: number; // used to override document's settings for xMargin --- see CollectionCarouselView yPadding?: number; noSidebar?: boolean; - dontSelectOnLoad?: boolean; // suppress selecting the text box when loaded + dontScale?: boolean; + dontSelectOnLoad?: boolean; // suppress selecting the text box when loaded (and mark as not being associated with scrollTop document field) } export const GoogleRef = "googleDocId"; @@ -119,13 +120,14 @@ 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"]); } @computed get scrollHeight() { return NumCast(this.rootDoc[this.fieldKey + "-scrollHeight"]); } - @computed get sidebarHeight() { return NumCast(this.rootDoc[this.SidebarKey + "-height"]); } + @computed get sidebarHeight() { return !this.sidebarWidth() ? 0 : NumCast(this.rootDoc[this.SidebarKey + "-height"]); } @computed get titleHeight() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin) || 0; } + @computed get autoHeightMargins() { return this.titleHeight + NumCast(this.layoutDoc._autoHeightMargins); } @computed get _recording() { return this.dataDoc?.mediaState === "recording"; } set _recording(value) { !this.dataDoc.recordingSource && (this.dataDoc.mediaState = value ? "recording" : undefined); @@ -215,6 +217,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this._editorView?.state && RichTextMenu.Instance.insertHighlight(color, this._editorView.state, this._editorView?.dispatch); 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. @@ -251,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("#")) { @@ -368,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); }); @@ -429,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 @@ -532,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" }); @@ -541,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); @@ -702,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)) { @@ -723,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[] = []; @@ -780,12 +792,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp // Since we also monitor all component height changes, this will update the document's height. resetNativeHeight = (scrollHeight: number) => { const nh = this.layoutDoc.isTemplateForField ? 0 : NumCast(this.layoutDoc._nativeHeight); - this.rootDoc[this.fieldKey + "-height"] = scrollHeight + this.titleHeight; + this.rootDoc[this.fieldKey + "-height"] = scrollHeight; if (nh) this.layoutDoc._nativeHeight = scrollHeight; } 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.props.dontSelectOnLoad && 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._cachedLinks = DocListCast(this.Document.links); this._disposers.breakupDictation = reaction(() => DocumentManager.Instance.RecordingEvent, this.breakupDictation); this._disposers.autoHeight = reaction(() => this.autoHeight, autoHeight => autoHeight && this.tryUpdateScrollHeight()); @@ -793,8 +805,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp ({ width, scrollHeight, autoHeight }) => width && autoHeight && this.resetNativeHeight(scrollHeight) ); this._disposers.componentHeights = reaction( // set the document height when one of the component heights changes and autoHeight is on - () => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, autoHeight: this.autoHeight }), - ({ sidebarHeight, textHeight, autoHeight }) => autoHeight && this.props.setHeight(Math.max(sidebarHeight, textHeight))); + () => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, autoHeight: this.autoHeight, marginsHeight: this.autoHeightMargins }), + ({ sidebarHeight, textHeight, autoHeight, marginsHeight }) => autoHeight && this.props.setHeight(marginsHeight + Math.max(sidebarHeight, textHeight))); this._disposers.links = reaction(() => DocListCast(this.Document.links), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks newLinks => { this._cachedLinks.forEach(l => !newLinks.includes(l) && this.RemoveLinkFromDoc(l)); @@ -871,12 +883,12 @@ 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), pos => { - if (!this._ignoreScroll && this._scrollRef.current) { + if (!this._ignoreScroll && this._scrollRef.current && !this.props.dontSelectOnLoad) { const viewTrans = quickScroll ?? StrCast(this.Document._viewTransition); const durationMiliStr = viewTrans.match(/([0-9]*)ms/); const durationSecStr = viewTrans.match(/([0-9.]*)s/); @@ -1039,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); @@ -1199,7 +1211,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if ((e.nativeEvent as any).formattedHandled) { console.log("handled"); } - if (!(e.nativeEvent as any).formattedHandled && this.isContentActive(true)) { + if (!(e.nativeEvent as any).formattedHandled && this.props.isContentActive(true)) { const editor = this._editorView!; const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY }); !this.props.isSelected(true) && editor.dispatch(editor.state.tr.setSelection(new TextSelection(editor.state.doc.resolve(pcords?.pos || 0)))); @@ -1391,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))); @@ -1413,23 +1433,32 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } onScroll = (e: React.UIEvent) => { if (!LinkDocPreview.LinkInfo && this._scrollRef.current) { - this._ignoreScroll = true; - this.layoutDoc._scrollTop = this._scrollRef.current.scrollTop; - this._ignoreScroll = false; + if (!this.props.dontSelectOnLoad) { + this._ignoreScroll = true; + this.layoutDoc._scrollTop = this._scrollRef.current.scrollTop; + this._ignoreScroll = false; + } } } - tryUpdateScrollHeight() { + tryUpdateScrollHeight = () => { if (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath())) { - setTimeout(() => { // bcz: don't know why this is needed, but without it, the size of the textbox is too big as it includes the size of the title header. after the timeout, the size seems to get computed correctly. - const proseHeight = this.ProseRef?.scrollHeight || 0; - const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight); + const margins = 2 * NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0); + 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... } - }); + } } } fitToBox = () => this.props.Document._fitToBox; @@ -1452,7 +1481,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp @computed get sidebarHandle() { TraceMobx(); const annotated = DocListCast(this.dataDoc[this.SidebarKey]).filter(d => d?.author).length; - return (!annotated && !this.isContentActive()) ? (null) : <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} + return (!annotated && !this.props.isContentActive()) ? (null) : <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} style={{ left: `max(0px, calc(100% - ${this.sidebarWidthPercent} ${this.sidebarWidth() ? "- 5px" : "- 10px"}))`, background: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.WidgetColor + (annotated ? ":annotated" : "")) @@ -1464,15 +1493,17 @@ 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} moveDocument={this.moveDocument} removeDocument={this.removeDocument} - isContentActive={this.isContentActive} /> : <ComponentTag {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} @@ -1485,7 +1516,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp scaleField={this.SidebarKey + "-scale"} isAnnotationOverlay={false} select={emptyFunction} - isContentActive={this.isContentActive} scaling={this.sidebarContentScaling} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} removeDocument={this.sidebarRemDocument} @@ -1507,7 +1537,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp render() { TraceMobx(); const selected = this.props.isSelected(); - const active = this.isContentActive(); + const active = this.props.isContentActive(); const scale = this.props.hideOnLeave ? 1 : (this.props.scaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1); const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : ""; const interactive = (CurrentUserUtils.SelectedTool === InkTool.None || SnappingManager.GetIsDragging()) && (this.layoutDoc.z || this.props.layerProvider?.(this.layoutDoc) !== false); @@ -1517,16 +1547,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const selPad = Math.min(margins, 10); const padding = Math.max(margins + ((selected && !this.layoutDoc._singleLine) || minimal ? -selPad : 0), 0); const selPaddingClass = selected && !this.layoutDoc._singleLine && margins >= 10 ? "-selected" : ""; - const col = this.props.color ? this.props.color : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color); - const back = this.props.background ? this.props.background : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor); return ( <div className="formattedTextBox-cont" - onWheel={e => this.isContentActive() && e.stopPropagation()} + onWheel={e => this.props.isContentActive() && e.stopPropagation()} style={{ - transform: `scale(${scale})`, - transformOrigin: "top left", - width: `${100 / scale}%`, - height: `${100 / scale}%`, + transform: this.props.dontScale ? undefined : `scale(${scale})`, + transformOrigin: this.props.dontScale ? undefined : "top left", + width: this.props.dontScale ? undefined : `${100 / scale}%`, + height: this.props.dontScale ? undefined : `${100 / scale}%`, // overflowY: this.layoutDoc._autoHeight ? "hidden" : undefined, ...this.styleFromLayoutString(scale) // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' > }}> @@ -1554,7 +1582,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp > <div className={`formattedTextBox-outer${selected ? "-selected" : ""}`} ref={this._scrollRef} style={{ - width: `calc(100% - ${this.sidebarWidthPercent})`, + width: this.props.dontSelectOnLoad ? "100%" : `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: !active && !SnappingManager.GetIsDragging() ? "none" : undefined, overflow: this.layoutDoc._singleLine ? "hidden" : undefined, }} @@ -1566,8 +1594,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp }} /> </div> - {(this.props.noSidebar || this.Document._noSidebar) || !this.layoutDoc._showSidebar || this.sidebarWidthPercent === "0%" ? (null) : this.sidebarCollection} - {(this.props.noSidebar || this.Document._noSidebar) || this.Document._singleLine ? (null) : this.sidebarHandle} + {(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> </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.scss b/src/client/views/nodes/formattedText/RichTextMenu.scss index 1d24d6833..c94e93541 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.scss +++ b/src/client/views/nodes/formattedText/RichTextMenu.scss @@ -1,4 +1,4 @@ -@import "../../globalCssVariables"; +@import "../../global/globalCssVariables"; .button-dropdown-wrapper { position: relative; @@ -24,7 +24,7 @@ top: 35px; left: 0; background-color: #323232; - color: $light-color-secondary; + color: $light-gray; border: 1px solid #4d4d4d; border-radius: 0 6px 6px 6px; box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25); diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 071491463..82ad2b7db 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -352,7 +352,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { function onClick(e: React.PointerEvent) { e.preventDefault(); e.stopPropagation(); - self.TextView.endUndoTypingBatch(); + self.TextView?.endUndoTypingBatch(); UndoManager.RunInBatch(() => { self.view && command && command(self.view.state, self.view.dispatch, self.view); self.view && onclick && onclick(self.view.state, self.view.dispatch, self.view); @@ -821,8 +821,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) { @@ -852,6 +852,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { @undoBatch makeLinkToURL = (target: string, lcoation: string) => { ((this.view as any)?.TextView as FormattedTextBox).makeLinkAnchor(undefined, "onRadd:rightight", target, target); + console.log((this.view as any)?.TextView); } @undoBatch @@ -863,8 +864,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)); }); } @@ -963,7 +964,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.createHighlighterButton(), this.createLinkButton(), this.createBrushButton(), - <div className="richTextMenu-divider" key="divider 2" />, + <div className="collectionMenu-divider" key="divider 2" />, this.createButton("align-left", "Align Left", this.activeAlignment === "left", this.alignLeft), this.createButton("align-center", "Align Center", this.activeAlignment === "center", this.alignCenter), this.createButton("align-right", "Align Right", this.activeAlignment === "right", this.alignRight), @@ -976,7 +977,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const row2 = <div className="antimodeMenu-row row-2" key="row2"> {this.collapsed ? this.getDragger() : (null)} <div key="row 2" style={{ display: this.collapsed ? "none" : undefined }}> - <div className="richTextMenu-divider" key="divider 3" /> + <div className="collectionMenu-divider" key="divider 3" /> {[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions, "font size", action((val: string) => { this.activeFontSize = val; SelectionManager.Views().map(dv => dv.props.Document._fontSize = val); @@ -985,12 +986,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.activeFontFamily = val; SelectionManager.Views().map(dv => dv.props.Document._fontFamily = val); })), - <div className="richTextMenu-divider" key="divider 4" />, + <div className="collectionMenu-divider" key="divider 4" />, this.createNodesDropdown(this.activeListType, this.listTypeOptions, "list type", () => ({})), this.createButton("sort-amount-down", "Summarize", undefined, this.insertSummarizer), this.createButton("quote-left", "Blockquote", undefined, this.insertBlockquote), - this.createButton("minus", "Horizontal Rule", undefined, this.insertHorizontalRule), - <div className="richTextMenu-divider" key="divider 5" />,]} + this.createButton("minus", "Horizontal Rule", undefined, this.insertHorizontalRule) + ]} </div> {/* <div key="collapser"> {<div key="collapser"> diff --git a/src/client/views/nodes/formattedText/TooltipTextMenu.scss b/src/client/views/nodes/formattedText/TooltipTextMenu.scss index 0e4b752ac..8c4d77da9 100644 --- a/src/client/views/nodes/formattedText/TooltipTextMenu.scss +++ b/src/client/views/nodes/formattedText/TooltipTextMenu.scss @@ -1,4 +1,4 @@ -@import "../views/globalCssVariables"; +@import "../views/global/globalCssVariables"; .ProseMirror-menu-dropdown-wrap { display: inline-block; position: relative; @@ -50,7 +50,7 @@ padding: 3px; &:hover { - background-color: $light-color-secondary; + background-color: $light-gray; } } } @@ -294,9 +294,9 @@ top: 31px; background-color: #323232; border: 1px solid #4d4d4d; - color: $light-color-secondary; + color: $light-gray; // border: none; - // border: 1px solid $light-color-secondary; + // border: 1px solid $light-gray; border-radius: 0 6px 6px 6px; padding: 3px; box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25); @@ -323,7 +323,7 @@ } .separated-button { - border-top: 1px solid $light-color-secondary; + border-top: 1px solid $light-gray; padding-top: 6px; } diff --git a/src/client/views/nodes/PresBox.scss b/src/client/views/nodes/trails/PresBox.scss index 1ba86232b..06932d145 100644 --- a/src/client/views/nodes/PresBox.scss +++ b/src/client/views/nodes/trails/PresBox.scss @@ -1,7 +1,4 @@ -$light-blue: #AEDDF8; -$dark-blue: #5B9FDD; -$light-background: #ececec; -$dark-grey: #656565; +@import "../../global/globalCssVariables"; .presBox-cont { cursor: auto; @@ -10,7 +7,6 @@ $dark-grey: #656565; pointer-events: inherit; z-index: 2; font-family: Roboto; - box-shadow: #AAAAAA .2vw .2vw .4vw; width: 100%; min-width: 20px; height: 100%; @@ -47,8 +43,8 @@ $dark-grey: #656565; align-items: center; height: 30px; width: 100%; - color: white; - background-color: #323232; + color: $white; + background-color: $dark-gray; .toolbar-button { cursor: pointer; @@ -110,7 +106,7 @@ $dark-grey: #656565; } .toolbar-divider { - border-left: solid #ffffff70 0.5px; + border-left: solid $medium-gray 0.5px; height: 20px; } } @@ -118,7 +114,7 @@ $dark-grey: #656565; .dropdown { font-size: 10; margin-left: 5px; - color: darkgrey; + color: $medium-gray; transition: 0.5s ease; } @@ -174,7 +170,7 @@ $dark-grey: #656565; .ribbon-colorBox { cursor: pointer; - border: solid 1px black; + border: solid 1px $black; display: flex; margin-left: 5px; margin-top: 5px; @@ -191,9 +187,9 @@ $dark-grey: #656565; font-size: 11; font-weight: 200; height: 20; - background-color: #ececec; - color: black; - border: solid 1px black; + background-color: $white; + color: $black; + border: solid 1px $black; display: flex; margin-left: 5px; margin-top: 5px; @@ -220,11 +216,11 @@ $dark-grey: #656565; align-items: center; height: 100%; width: 100%; - background: black; + background: $black; } .ribbon-propertyUpDownItem:hover { - background: darkgrey; + background: $medium-gray; transform: scale(1.05); } } @@ -239,7 +235,7 @@ $dark-grey: #656565; .multiThumb-slider { display: grid; - background-color: $light-background; + background-color: $white; height: 10px; border-radius: 10px; overflow: hidden; @@ -257,8 +253,8 @@ $dark-grey: #656565; -webkit-appearance: none; height: 10px; cursor: ew-resize; - background: $dark-blue; - box-shadow: -100vw 0 0 100vw $light-background; + background: $medium-blue; + box-shadow: -100vw 0 0 100vw $white; } .toolbar-slider.end::-webkit-slider-thumb { @@ -267,7 +263,7 @@ $dark-grey: #656565; -webkit-appearance: none; height: 10px; cursor: ew-resize; - background: $dark-blue; + background: $medium-blue; box-shadow: -100vw 0 0 100vw $light-blue; } } @@ -282,7 +278,7 @@ $dark-grey: #656565; height: 10px; border-radius: 10px; -webkit-appearance: none; - background-color: $light-background; + background-color: $white; } .toolbar-slider:focus { @@ -301,7 +297,7 @@ $dark-grey: #656565; -webkit-appearance: none; height: 10px; cursor: ew-resize; - background: $dark-blue; + background: $medium-blue; box-shadow: -100vw 0 0 100vw $light-blue; } @@ -318,7 +314,7 @@ $dark-grey: #656565; width: 15px; min-width: 15px; cursor: pointer; - background: $light-background; + background: $white; } .presBox-checkbox:focus { @@ -326,7 +322,7 @@ $dark-grey: #656565; } .presBox-checkbox:hover { - background: #c0c0c0; + background: $light-gray; } .presBox-checkbox:checked { @@ -381,9 +377,9 @@ $dark-grey: #656565; text-align: center; font-size: 16; width: 90%; - color: black; + color: $black; transform: translate(5%, 0px); - border-bottom: solid 2px darkgrey; + border-bottom: solid 2px $medium-gray; } @@ -396,8 +392,8 @@ $dark-grey: #656565; justify-self: left; margin-top: 5px; padding-left: 10px; - background-color: $light-background; - border: solid 1px black; + background-color: $white; + border: solid 1px $black; min-width: 80px; max-width: 200px; width: 100%; @@ -416,7 +412,7 @@ $dark-grey: #656565; } .ribbon-frameSelector { - border: black solid 1px; + border: $black solid 1px; width: 60px; height: 20px; margin-top: 5px; @@ -433,12 +429,12 @@ $dark-grey: #656565; cursor: pointer; position: relative; height: 100%; - background: $light-background; + background: $white; display: flex; align-items: center; justify-content: center; text-align: center; - color: black; + color: $black; } .numKeyframe { @@ -446,7 +442,7 @@ $dark-grey: #656565; font-size: 10; font-weight: 600; position: relative; - color: black; + color: $black; display: flex; width: 100%; height: 100%; @@ -489,7 +485,7 @@ $dark-grey: #656565; padding-left: 10; padding-right: 10; border-radius: 10px; - background-color: #979797; + background-color: $medium-gray; } .ribbon-final-button:hover { @@ -508,13 +504,13 @@ $dark-grey: #656565; align-items: center; margin-bottom: 5px; height: 25px; - color: lightgrey; + color: $light-gray; width: 100%; max-width: 120; padding-left: 10; padding-right: 10; border-radius: 10px; - background-color: black; + background-color: $black; } .ribbon-final-button-hidden:hover { @@ -525,15 +521,15 @@ $dark-grey: #656565; .ribbon-frameList { width: calc(100% - 5px); height: 50px; - background-color: #ececec; - border: 1px solid #9f9f9f; + background-color: $white; + border: 1px solid $medium-gray; grid-template-rows: max-content; .frameList-header { display: grid; width: 100%; height: 20px; - background-color: #9f9f9f; + background-color: $medium-gray; .frameList-headerButtons { display: flex; @@ -588,7 +584,7 @@ $dark-grey: #656565; font-size: 10.5; font-weight: 300; height: 20; - background-color: #979797; + background-color: $medium-gray; color: white; display: flex; margin-top: 5px; @@ -607,8 +603,8 @@ $dark-grey: #656565; transition: all 0.4s; font-weight: 400; opacity: 1; - color: white; - background-color: black; + color: $white; + background-color: $black; } .ribbon-toggle { @@ -616,10 +612,10 @@ $dark-grey: #656565; font-size: 10.5; font-weight: 200; height: 20; - background-color: $light-background; + background-color: $white; border: solid 1px rgba(0, 0, 0, 0.5); display: flex; - color: black; + color: $black; margin-top: 5px; margin-bottom: 5px; border-radius: 5px; @@ -660,13 +656,13 @@ $dark-grey: #656565; position: relative; font-size: 13; padding-bottom: 10px; - border-bottom: solid 1px $dark-grey; + border-bottom: solid 1px $dark-gray; .presBox-dropdown:hover { - border: solid 1px $dark-blue; + border: solid 1px $medium-blue; .presBox-dropdownIcon { - color: $dark-blue; + color: $medium-blue; } } @@ -675,12 +671,12 @@ $dark-grey: #656565; display: grid; grid-template-columns: auto 20%; position: relative; - border: solid 1px black; - background-color: $light-background; + border: solid 1px $black; + background-color: $light-gray; border-radius: 5px; font-size: 10; height: 25; - color: black; + color: $black; padding-left: 5px; align-items: center; margin-top: 5px; @@ -744,7 +740,7 @@ $dark-grey: #656565; height: 100px; padding-top: 5px; padding-bottom: 5px; - border: solid 1px black; + border: solid 1px $black; // overflow: auto; ::-webkit-scrollbar { @@ -794,7 +790,7 @@ $dark-grey: #656565; cursor: pointer; position: relative; text-align: center; - border-left: solid 1px darkgrey; + border-left: solid 1px $medium-gray; width: 20%; height: 100%; display: flex; @@ -825,7 +821,7 @@ $dark-grey: #656565; box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.8); z-index: 200; background-color: white; - color: black; + color: $black; position: absolute; overflow: hidden; } @@ -841,12 +837,12 @@ $dark-grey: #656565; align-items: center; justify-content: center; transform: translate(0px, -1px); - background-color: $light-background; + background-color: $white; width: 40px; height: 15px; align-self: center; justify-self: center; - border: solid 1px black; + border: solid 1px $black; border-top: 0px; border-bottom-right-radius: 7px; border-bottom-left-radius: 7px; @@ -855,15 +851,15 @@ $dark-grey: #656565; .layout-container { padding: 5px; display: grid; - background-color: $light-background; + background-color: $white; grid-template-columns: repeat(auto-fit, minmax(90px, 100px)); width: 100%; - border: solid 1px black; + border: solid 1px $black; min-width: 100px; overflow: hidden; .layout:hover { - border: solid 2px #5c9edd; + border: solid 2px $medium-blue; } .layout { @@ -878,7 +874,7 @@ $dark-grey: #656565; width: 90px; overflow: hidden; background-color: white; - border: solid darkgrey 1px; + border: solid $medium-gray 1px; display: grid; grid-template-rows: auto; align-items: center; @@ -893,7 +889,7 @@ $dark-grey: #656565; height: 13; font-size: 12; display: flex; - background-color: #f1efec; + background-color: $white; } .subtitle { @@ -906,7 +902,7 @@ $dark-grey: #656565; height: 13; font-size: 9; display: flex; - background-color: #f1efec; + background-color: $white; } .content { @@ -919,7 +915,7 @@ $dark-grey: #656565; height: 13; font-size: 10; display: flex; - background-color: #f1efec; + background-color: $white; height: 33; text-align: left; font-size: 8px; @@ -930,7 +926,7 @@ $dark-grey: #656565; .presBox-buttons { position: relative; width: 100%; - background: gray; + background: $medium-gray; min-height: 35px; padding-top: 5px; padding-bottom: 5px; @@ -960,8 +956,8 @@ $dark-grey: #656565; select { - background: #323232; - color: white; + background: $dark-gray; + color: $white; } .presBox-button { @@ -975,8 +971,8 @@ $dark-grey: #656565; text-align: center; letter-spacing: normal; width: inherit; - background: #323232; - color: white; + background: $dark-gray; + color: $white; } .presBox-button.active { @@ -984,7 +980,7 @@ $dark-grey: #656565; } .presBox-button.active:hover { - background-color: #233163; + background-color: $medium-blue; } .presBox-button.edit { @@ -1053,8 +1049,8 @@ $dark-grey: #656565; font-size: 100; display: flex; align-items: center; - background: #323232; - color: white; + background: $dark-gray; + color: $white; } .presBox-viewPicker { @@ -1086,7 +1082,7 @@ $dark-grey: #656565; top: 10; opacity: 0.1; transition: all 0.4s; - color: white; + color: $white; } .miniPres:hover { @@ -1094,8 +1090,8 @@ $dark-grey: #656565; } .presPanelOverlay { - background-color: #323232; - color: white; + background-color: $dark-gray; + color: $white; border-radius: 5px; grid-template-rows: 100%; height: 25; @@ -1129,7 +1125,7 @@ $dark-grey: #656565; .presPanel-divider { width: 0.5px; height: 80%; - border-right: solid 1px #5a5a5a; + border-right: solid 1px $medium-gray; } .presPanel-button-frame { @@ -1161,12 +1157,12 @@ $dark-grey: #656565; } .presPanel-button:hover { - background-color: #5a5a5a; + background-color: $medium-gray; transform: scale(1.2); } .presPanel-button-text:hover { - background-color: #5a5a5a; + background-color: $medium-gray; } diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index f3fb6ff17..cfc3e75cc 100644 --- a/src/client/views/nodes/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -5,67 +5,33 @@ import { action, computed, IReactionDisposer, observable, ObservableMap, reactio import { observer } from "mobx-react"; import { ColorState, SketchPicker } from "react-color"; import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal'; -import { Doc, DocListCast, DocListCastAsync, FieldResult } from "../../../fields/Doc"; -import { documentSchema } from "../../../fields/documentSchemas"; -import { InkTool } from "../../../fields/InkField"; -import { List } from "../../../fields/List"; -import { PrefetchProxy } from "../../../fields/Proxy"; -import { listSpec, makeInterface } from "../../../fields/Schema"; -import { ScriptField } from "../../../fields/ScriptField"; -import { BoolCast, Cast, NumCast, StrCast } from "../../../fields/Types"; -import { returnFalse, returnOne, returnTrue, emptyFunction } from '../../../Utils'; -import { Docs } from "../../documents/Documents"; -import { DocumentType } from "../../documents/DocumentTypes"; -import { CurrentUserUtils } from "../../util/CurrentUserUtils"; -import { DocumentManager } from "../../util/DocumentManager"; -import { Scripting } from "../../util/Scripting"; -import { SelectionManager } from "../../util/SelectionManager"; -import { undoBatch, UndoManager } from "../../util/UndoManager"; -import { CollectionDockingView } from "../collections/CollectionDockingView"; -import { CollectionView, CollectionViewType } from "../collections/CollectionView"; -import { TabDocView } from "../collections/TabDocView"; -import { ViewBoxBaseComponent } from "../DocComponent"; -import { CollectionFreeFormDocumentView } from "./CollectionFreeFormDocumentView"; -import { FieldView, FieldViewProps } from './FieldView'; +import { Doc, DocListCast, DocListCastAsync, FieldResult } from "../../../../fields/Doc"; +import { documentSchema } from "../../../../fields/documentSchemas"; +import { InkTool } from "../../../../fields/InkField"; +import { List } from "../../../../fields/List"; +import { PrefetchProxy } from "../../../../fields/Proxy"; +import { listSpec, makeInterface } from "../../../../fields/Schema"; +import { ScriptField } from "../../../../fields/ScriptField"; +import { BoolCast, Cast, NumCast, StrCast } from "../../../../fields/Types"; +import { emptyFunction, returnFalse, returnOne, returnTrue } from '../../../../Utils'; +import { Docs } from "../../../documents/Documents"; +import { DocumentType } from "../../../documents/DocumentTypes"; +import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; +import { DocumentManager } from "../../../util/DocumentManager"; +import { Scripting } from "../../../util/Scripting"; +import { SelectionManager } from "../../../util/SelectionManager"; +import { undoBatch, UndoManager } from "../../../util/UndoManager"; +import { CollectionDockingView } from "../../collections/CollectionDockingView"; +import { CollectionView, CollectionViewType } from "../../collections/CollectionView"; +import { TabDocView } from "../../collections/TabDocView"; +import { ViewBoxBaseComponent } from "../../DocComponent"; +import { Colors } from "../../global/globalEnums"; +import { LightboxView } from "../../LightboxView"; +import { CollectionFreeFormDocumentView } from "../CollectionFreeFormDocumentView"; +import { FieldView, FieldViewProps } from '../FieldView'; import "./PresBox.scss"; import Color = require("color"); -import { LightboxView } from "../LightboxView"; - -export enum PresMovement { - Zoom = "zoom", - Pan = "pan", - Jump = "jump", - None = "none", -} - -export enum PresEffect { - Zoom = "Zoom", - Lightspeed = "Lightspeed", - Fade = "Fade in", - Flip = "Flip", - Rotate = "Rotate", - Bounce = "Bounce", - Roll = "Roll", - None = "None", - Left = "left", - Right = "right", - Center = "center", - Top = "top", - Bottom = "bottom" -} - -enum PresStatus { - Autoplay = "auto", - Manual = "manual", - Edit = "edit" -} - -export enum PresColor { - LightBlue = "#AEDDF8", - DarkBlue = "#5B9FDD", - LightBackground = "#ececec", - SlideBackground = "#d5dce2", -} +import { PresEffect, PresStatus, PresMovement } from "./PresEnums"; export class PinProps { audioRange?: boolean; @@ -639,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); @@ -1198,9 +1164,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> {this.scrollable ? "Scroll to pinned view" : !isPinWithView ? "No movement" : "Pan & Zoom to pinned view"} </div> : - <div className="presBox-dropdown" onClick={action(e => { e.stopPropagation(); this.openMovementDropdown = !this.openMovementDropdown; })} style={{ borderBottomLeftRadius: this.openMovementDropdown ? 0 : 5, border: this.openMovementDropdown ? `solid 2px ${PresColor.DarkBlue}` : 'solid 1px black' }}> + <div className="presBox-dropdown" onClick={action(e => { e.stopPropagation(); this.openMovementDropdown = !this.openMovementDropdown; })} style={{ borderBottomLeftRadius: this.openMovementDropdown ? 0 : 5, border: this.openMovementDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}> {this.setMovementName(activeItem.presMovement, activeItem)} - <FontAwesomeIcon className='presBox-dropdownIcon' style={{ gridColumn: 2, color: this.openMovementDropdown ? PresColor.DarkBlue : 'black' }} icon={"angle-down"} /> + <FontAwesomeIcon className='presBox-dropdownIcon' style={{ gridColumn: 2, color: this.openMovementDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={"angle-down"} /> <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} onPointerDown={e => e.stopPropagation()} style={{ display: this.openMovementDropdown ? "grid" : "none" }}> <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.None ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.None)}>None</div> <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.Zoom ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.Zoom)}>Pan {"&"} Zoom</div> @@ -1245,7 +1211,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <div className="ribbon-doubleButton"> {isPresCollection ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Hide before presented"}</div></>}><div className={`ribbon-toggle ${activeItem.presHideBefore ? "active" : ""}`} onClick={() => this.updateHideBefore(activeItem)}>Hide before</div></Tooltip>} {isPresCollection ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Hide after presented"}</div></>}><div className={`ribbon-toggle ${activeItem.presHideAfter ? "active" : ""}`} onClick={() => this.updateHideAfter(activeItem)}>Hide after</div></Tooltip>} - <Tooltip title={<><div className="dash-tooltip">{"Open in lightbox view"}</div></>}><div className="ribbon-toggle" style={{ backgroundColor: activeItem.openDocument ? PresColor.LightBlue : "" }} onClick={() => this.updateOpenDoc(activeItem)}>Lightbox</div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Open in lightbox view"}</div></>}><div className="ribbon-toggle" style={{ backgroundColor: activeItem.openDocument ? Colors.LIGHT_BLUE : "" }} onClick={() => this.updateOpenDoc(activeItem)}>Lightbox</div></Tooltip> </div> {(type === DocumentType.AUDIO || type === DocumentType.VID) ? (null) : <> <div className="ribbon-doubleButton" > @@ -1280,9 +1246,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> </div> {isPresCollection ? (null) : <div className="ribbon-box"> Effects - <div className="presBox-dropdown" onClick={action(e => { e.stopPropagation(); this.openEffectDropdown = !this.openEffectDropdown; })} style={{ borderBottomLeftRadius: this.openEffectDropdown ? 0 : 5, border: this.openEffectDropdown ? `solid 2px ${PresColor.DarkBlue}` : 'solid 1px black' }}> + <div className="presBox-dropdown" onClick={action(e => { e.stopPropagation(); this.openEffectDropdown = !this.openEffectDropdown; })} style={{ borderBottomLeftRadius: this.openEffectDropdown ? 0 : 5, border: this.openEffectDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}> {effect} - <FontAwesomeIcon className='presBox-dropdownIcon' style={{ gridColumn: 2, color: this.openEffectDropdown ? PresColor.DarkBlue : 'black' }} icon={"angle-down"} /> + <FontAwesomeIcon className='presBox-dropdownIcon' style={{ gridColumn: 2, color: this.openEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={"angle-down"} /> <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} style={{ display: this.openEffectDropdown ? "grid" : "none" }} onPointerDown={e => e.stopPropagation()}> <div className={`presBox-dropdownOption ${targetDoc.presEffect === PresEffect.None || !targetDoc.presEffect ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.None)}>None</div> <div className={`presBox-dropdownOption ${targetDoc.presEffect === PresEffect.Fade ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Fade)}>Fade In</div> @@ -1299,11 +1265,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> </div> </div> <div className="effectDirection" style={{ display: effect === 'None' ? "none" : "grid", width: 40 }}> - <Tooltip title={<><div className="dash-tooltip">{"Enter from left"}</div></>}><div style={{ gridColumn: 1, gridRow: 2, justifySelf: 'center', color: targetDoc.presEffectDirection === PresEffect.Left ? PresColor.LightBlue : "black", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Left)}><FontAwesomeIcon icon={"angle-right"} /></div></Tooltip> - <Tooltip title={<><div className="dash-tooltip">{"Enter from right"}</div></>}><div style={{ gridColumn: 3, gridRow: 2, justifySelf: 'center', color: targetDoc.presEffectDirection === PresEffect.Right ? PresColor.LightBlue : "black", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Right)}><FontAwesomeIcon icon={"angle-left"} /></div></Tooltip> - <Tooltip title={<><div className="dash-tooltip">{"Enter from top"}</div></>}><div style={{ gridColumn: 2, gridRow: 1, justifySelf: 'center', color: targetDoc.presEffectDirection === PresEffect.Top ? PresColor.LightBlue : "black", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Top)}><FontAwesomeIcon icon={"angle-down"} /></div></Tooltip> - <Tooltip title={<><div className="dash-tooltip">{"Enter from bottom"}</div></>}><div style={{ gridColumn: 2, gridRow: 3, justifySelf: 'center', color: targetDoc.presEffectDirection === PresEffect.Bottom ? PresColor.LightBlue : "black", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Bottom)}><FontAwesomeIcon icon={"angle-up"} /></div></Tooltip> - <Tooltip title={<><div className="dash-tooltip">{"Enter from center"}</div></>}><div style={{ gridColumn: 2, gridRow: 2, width: 10, height: 10, alignSelf: 'center', justifySelf: 'center', border: targetDoc.presEffectDirection === PresEffect.Center || !targetDoc.presEffectDirection ? `solid 2px ${PresColor.LightBlue}` : "solid 2px black", borderRadius: "100%", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Center)}></div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Enter from left"}</div></>}><div style={{ gridColumn: 1, gridRow: 2, justifySelf: 'center', color: targetDoc.presEffectDirection === PresEffect.Left ? Colors.LIGHT_BLUE : "black", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Left)}><FontAwesomeIcon icon={"angle-right"} /></div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Enter from right"}</div></>}><div style={{ gridColumn: 3, gridRow: 2, justifySelf: 'center', color: targetDoc.presEffectDirection === PresEffect.Right ? Colors.LIGHT_BLUE : "black", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Right)}><FontAwesomeIcon icon={"angle-left"} /></div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Enter from top"}</div></>}><div style={{ gridColumn: 2, gridRow: 1, justifySelf: 'center', color: targetDoc.presEffectDirection === PresEffect.Top ? Colors.LIGHT_BLUE : "black", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Top)}><FontAwesomeIcon icon={"angle-down"} /></div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Enter from bottom"}</div></>}><div style={{ gridColumn: 2, gridRow: 3, justifySelf: 'center', color: targetDoc.presEffectDirection === PresEffect.Bottom ? Colors.LIGHT_BLUE : "black", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Bottom)}><FontAwesomeIcon icon={"angle-up"} /></div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Enter from center"}</div></>}><div style={{ gridColumn: 2, gridRow: 2, width: 10, height: 10, alignSelf: 'center', justifySelf: 'center', border: targetDoc.presEffectDirection === PresEffect.Center || !targetDoc.presEffectDirection ? `solid 2px ${Colors.LIGHT_BLUE}` : "solid 2px black", borderRadius: "100%", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Center)}></div></Tooltip> </div> </div>} <div className="ribbon-final-box"> @@ -1356,7 +1322,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <> {this.panable || this.scrollable || this.targetDoc.type === DocumentType.COMPARISON ? 'Pinned view' : (null)} <div className="ribbon-doubleButton"> - <Tooltip title={<><div className="dash-tooltip">{activeItem.presPinView ? "Turn off pin with view" : "Turn on pin with view"}</div></>}><div className="ribbon-toggle" style={{ width: 20, padding: 0, backgroundColor: activeItem.presPinView ? PresColor.LightBlue : "" }} + <Tooltip title={<><div className="dash-tooltip">{activeItem.presPinView ? "Turn off pin with view" : "Turn on pin with view"}</div></>}><div className="ribbon-toggle" style={{ width: 20, padding: 0, backgroundColor: activeItem.presPinView ? Colors.LIGHT_BLUE : "" }} onClick={() => { activeItem.presPinView = !activeItem.presPinView; targetDoc.presPinView = activeItem.presPinView; @@ -1496,7 +1462,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <div className="slider-text" style={{ fontWeight: 500 }}> Start time (s) </div> - <div id={"startTime"} className="slider-number" style={{ backgroundColor: PresColor.LightBackground }}> + <div id={"startTime"} className="slider-number" style={{ backgroundColor: Colors.LIGHT_GRAY }}> <input className="presBox-input" style={{ textAlign: 'center', width: 30, height: 15, fontSize: 10 }} type="number" value={NumCast(activeItem.presStartTime)} @@ -1508,7 +1474,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <div className="slider-text" style={{ fontWeight: 500 }}> Duration (s) </div> - <div className="slider-number" style={{ backgroundColor: PresColor.LightBlue }}> + <div className="slider-number" style={{ backgroundColor: Colors.LIGHT_BLUE }}> {Math.round((NumCast(activeItem.presEndTime) - NumCast(activeItem.presStartTime)) * 10) / 10} </div> </div> @@ -1516,7 +1482,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <div className="slider-text" style={{ fontWeight: 500 }}> End time (s) </div> - <div id={"endTime"} className="slider-number" style={{ backgroundColor: PresColor.LightBackground }}> + <div id={"endTime"} className="slider-number" style={{ backgroundColor: Colors.LIGHT_GRAY }}> <input className="presBox-input" style={{ textAlign: 'center', width: 30, height: 15, fontSize: 10 }} type="number" value={NumCast(activeItem.presEndTime)} @@ -1534,16 +1500,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> this._batch = UndoManager.StartBatch("presEndTime"); const endBlock = document.getElementById("endTime"); if (endBlock) { - endBlock.style.color = PresColor.LightBackground; - endBlock.style.backgroundColor = PresColor.DarkBlue; + endBlock.style.color = Colors.LIGHT_GRAY; + endBlock.style.backgroundColor = Colors.MEDIUM_BLUE; } }} onPointerUp={() => { this._batch?.end(); const endBlock = document.getElementById("endTime"); if (endBlock) { - endBlock.style.color = "black"; - endBlock.style.backgroundColor = PresColor.LightBackground; + endBlock.style.color = Colors.BLACK; + endBlock.style.backgroundColor = Colors.LIGHT_GRAY; } }} onChange={(e: React.ChangeEvent<HTMLInputElement>) => { @@ -1558,16 +1524,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> this._batch = UndoManager.StartBatch("presStartTime"); const startBlock = document.getElementById("startTime"); if (startBlock) { - startBlock.style.color = PresColor.LightBackground; - startBlock.style.backgroundColor = PresColor.DarkBlue; + startBlock.style.color = Colors.LIGHT_GRAY; + startBlock.style.backgroundColor = Colors.MEDIUM_BLUE; } }} onPointerUp={() => { this._batch?.end(); const startBlock = document.getElementById("startTime"); if (startBlock) { - startBlock.style.color = "black"; - startBlock.style.backgroundColor = PresColor.LightBackground; + startBlock.style.color = Colors.BLACK; + startBlock.style.backgroundColor = Colors.LIGHT_GRAY; } }} onChange={(e: React.ChangeEvent<HTMLInputElement>) => { @@ -1651,15 +1617,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <div> <div className={'presBox-toolbar-dropdown'} style={{ display: this.newDocumentTools && this.layoutDoc.presStatus === "edit" ? "inline-flex" : "none" }} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> <div className="layout-container" style={{ height: 'max-content' }}> - <div className="layout" style={{ border: this.layout === 'blank' ? `solid 2px ${PresColor.DarkBlue}` : '' }} onClick={action(() => { this.layout = 'blank'; this.createNewSlide(this.layout); })} /> - <div className="layout" style={{ border: this.layout === 'title' ? `solid 2px ${PresColor.DarkBlue}` : '' }} onClick={action(() => { this.layout = 'title'; this.createNewSlide(this.layout); })}> + <div className="layout" style={{ border: this.layout === 'blank' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => { this.layout = 'blank'; this.createNewSlide(this.layout); })} /> + <div className="layout" style={{ border: this.layout === 'title' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => { this.layout = 'title'; this.createNewSlide(this.layout); })}> <div className="title">Title</div> <div className="subtitle">Subtitle</div> </div> - <div className="layout" style={{ border: this.layout === 'header' ? `solid 2px ${PresColor.DarkBlue}` : '' }} onClick={action(() => { this.layout = 'header'; this.createNewSlide(this.layout); })}> + <div className="layout" style={{ border: this.layout === 'header' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => { this.layout = 'header'; this.createNewSlide(this.layout); })}> <div className="title" style={{ alignSelf: 'center', fontSize: 10 }}>Section header</div> </div> - <div className="layout" style={{ border: this.layout === 'content' ? `solid 2px ${PresColor.DarkBlue}` : '' }} onClick={action(() => { this.layout = 'content'; this.createNewSlide(this.layout); })}> + <div className="layout" style={{ border: this.layout === 'content' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => { this.layout = 'content'; this.createNewSlide(this.layout); })}> <div className="title" style={{ alignSelf: 'center' }}>Title</div> <div className="content">Text goes here</div> </div> @@ -1691,26 +1657,26 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <div className="ribbon-box"> Choose type: <div className="ribbon-doubleButton"> - <div title="Text" className={'ribbon-toggle'} style={{ background: this.addFreeform ? "" : PresColor.LightBlue }} onClick={action(() => this.addFreeform = !this.addFreeform)}>Text</div> - <div title="Freeform" className={'ribbon-toggle'} style={{ background: this.addFreeform ? PresColor.LightBlue : "" }} onClick={action(() => this.addFreeform = !this.addFreeform)}>Freeform</div> + <div title="Text" className={'ribbon-toggle'} style={{ background: this.addFreeform ? "" : Colors.LIGHT_BLUE }} onClick={action(() => this.addFreeform = !this.addFreeform)}>Text</div> + <div title="Freeform" className={'ribbon-toggle'} style={{ background: this.addFreeform ? Colors.LIGHT_BLUE : "" }} onClick={action(() => this.addFreeform = !this.addFreeform)}>Freeform</div> </div> </div> <div className="ribbon-box" style={{ display: this.addFreeform ? "grid" : "none" }}> Preset layouts: <div className="layout-container" style={{ height: this.openLayouts ? 'max-content' : '75px' }}> - <div className="layout" style={{ border: this.layout === 'blank' ? `solid 2px ${PresColor.DarkBlue}` : '' }} onClick={action(() => this.layout = 'blank')} /> - <div className="layout" style={{ border: this.layout === 'title' ? `solid 2px ${PresColor.DarkBlue}` : '' }} onClick={action(() => this.layout = 'title')}> + <div className="layout" style={{ border: this.layout === 'blank' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => this.layout = 'blank')} /> + <div className="layout" style={{ border: this.layout === 'title' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => this.layout = 'title')}> <div className="title">Title</div> <div className="subtitle">Subtitle</div> </div> - <div className="layout" style={{ border: this.layout === 'header' ? `solid 2px ${PresColor.DarkBlue}` : '' }} onClick={action(() => this.layout = 'header')}> + <div className="layout" style={{ border: this.layout === 'header' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => this.layout = 'header')}> <div className="title" style={{ alignSelf: 'center', fontSize: 10 }}>Section header</div> </div> - <div className="layout" style={{ border: this.layout === 'content' ? `solid 2px ${PresColor.DarkBlue}` : '' }} onClick={action(() => this.layout = 'content')}> + <div className="layout" style={{ border: this.layout === 'content' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => this.layout = 'content')}> <div className="title" style={{ alignSelf: 'center' }}>Title</div> <div className="content">Text goes here</div> </div> - <div className="layout" style={{ border: this.layout === 'twoColumns' ? `solid 2px ${PresColor.DarkBlue}` : '' }} onClick={action(() => this.layout = 'twoColumns')}> + <div className="layout" style={{ border: this.layout === 'twoColumns' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => this.layout = 'twoColumns')}> <div className="title" style={{ alignSelf: 'center', gridColumn: '1/3' }}>Title</div> <div className="content" style={{ gridColumn: 1, gridRow: 2 }}>Column one text</div> <div className="content" style={{ gridColumn: 2, gridRow: 2 }}>Column two text</div> @@ -1869,8 +1835,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <div className="ribbon-box"> {this.stringType} selected <div className="ribbon-doubleButton" style={{ borderTop: 'solid 1px darkgrey', display: (targetDoc.type === DocumentType.COL && targetDoc._viewType === 'freeform') || targetDoc.type === DocumentType.IMG || targetDoc.type === DocumentType.RTF ? "inline-flex" : "none" }}> - <div className="ribbon-toggle" style={{ backgroundColor: activeItem.presProgressivize ? PresColor.LightBlue : "" }} onClick={this.progressivizeChild}>Contents</div> - <div className="ribbon-toggle" style={{ opacity: activeItem.presProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editProgressivize ? PresColor.LightBlue : "" }} onClick={this.editProgressivize}>Edit</div> + <div className="ribbon-toggle" style={{ backgroundColor: activeItem.presProgressivize ? Colors.LIGHT_BLUE : "" }} onClick={this.progressivizeChild}>Contents</div> + <div className="ribbon-toggle" style={{ opacity: activeItem.presProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editProgressivize ? Colors.LIGHT_BLUE : "" }} onClick={this.editProgressivize}>Edit</div> </div> <div className="ribbon-doubleButton" style={{ display: activeItem.presProgressivize ? "inline-flex" : "none" }}> <div className="presBox-subheading">Active text color</div> @@ -1885,12 +1851,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> </div> {this.viewedColorPicker} <div className="ribbon-doubleButton" style={{ borderTop: 'solid 1px darkgrey', display: (targetDoc.type === DocumentType.COL && targetDoc._viewType === 'freeform') || targetDoc.type === DocumentType.IMG ? "inline-flex" : "none" }}> - <div className="ribbon-toggle" style={{ backgroundColor: activeItem.zoomProgressivize ? PresColor.LightBlue : "" }} onClick={this.progressivizeZoom}>Zoom</div> - <div className="ribbon-toggle" style={{ opacity: activeItem.zoomProgressivize ? 1 : 0.4, backgroundColor: activeItem.editZoomProgressivize ? PresColor.LightBlue : "" }} onClick={this.editZoomProgressivize}>Edit</div> + <div className="ribbon-toggle" style={{ backgroundColor: activeItem.zoomProgressivize ? Colors.LIGHT_BLUE : "" }} onClick={this.progressivizeZoom}>Zoom</div> + <div className="ribbon-toggle" style={{ opacity: activeItem.zoomProgressivize ? 1 : 0.4, backgroundColor: activeItem.editZoomProgressivize ? Colors.LIGHT_BLUE : "" }} onClick={this.editZoomProgressivize}>Edit</div> </div> <div className="ribbon-doubleButton" style={{ borderTop: 'solid 1px darkgrey', display: targetDoc._viewType === "stacking" || targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF ? "inline-flex" : "none" }}> - <div className="ribbon-toggle" style={{ backgroundColor: activeItem.scrollProgressivize ? PresColor.LightBlue : "" }} onClick={this.progressivizeScroll}>Scroll</div> - <div className="ribbon-toggle" style={{ opacity: activeItem.scrollProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editScrollProgressivize ? PresColor.LightBlue : "" }} onClick={this.editScrollProgressivize}>Edit</div> + <div className="ribbon-toggle" style={{ backgroundColor: activeItem.scrollProgressivize ? Colors.LIGHT_BLUE : "" }} onClick={this.progressivizeScroll}>Scroll</div> + <div className="ribbon-toggle" style={{ opacity: activeItem.scrollProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editScrollProgressivize ? Colors.LIGHT_BLUE : "" }} onClick={this.editScrollProgressivize}>Edit</div> </div> </div> <div className="ribbon-final-box"> @@ -1900,7 +1866,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <div key="back" title="back frame" className="backKeyframe" onClick={e => { e.stopPropagation(); this.prevKeyframe(targetDoc, activeItem); }}> <FontAwesomeIcon icon={"caret-left"} size={"lg"} /> </div> - <div key="num" title="toggle view all" className="numKeyframe" style={{ color: targetDoc.keyFrameEditing ? "white" : "black", backgroundColor: targetDoc.keyFrameEditing ? PresColor.DarkBlue : PresColor.LightBlue }} + <div key="num" title="toggle view all" className="numKeyframe" style={{ color: targetDoc.keyFrameEditing ? "white" : "black", backgroundColor: targetDoc.keyFrameEditing ? Colors.MEDIUM_BLUE : Colors.LIGHT_BLUE }} onClick={action(() => targetDoc.keyFrameEditing = !targetDoc.keyFrameEditing)} > {NumCast(targetDoc._currentFrame)} </div> @@ -1914,7 +1880,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> {this.frameListHeader} {this.frameList} </div> - <div className="ribbon-toggle" style={{ height: 20, backgroundColor: PresColor.LightBlue }} onClick={() => console.log(" TODO: play frames")}>Play</div> + <div className="ribbon-toggle" style={{ height: 20, backgroundColor: Colors.LIGHT_BLUE }} onClick={() => console.log(" TODO: play frames")}>Play</div> </div> </div> </div> @@ -2130,7 +2096,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> tags.push(<div style={{ position: 'absolute', display: doc.displayMovement ? "block" : "none" }}>{this.checkMovementLists(doc, doc["x-indexed"], doc["y-indexed"])}</div>); } tags.push( - <div className="progressivizeButton" key={index} onPointerLeave={() => { if (NumCast(targetDoc._currentFrame) < NumCast(doc.appearFrame)) doc.opacity = 0; }} onPointerOver={() => { if (NumCast(targetDoc._currentFrame) < NumCast(doc.appearFrame)) doc.opacity = 0.5; }} onClick={e => { this.toggleDisplayMovement(doc); e.stopPropagation(); }} style={{ backgroundColor: doc.displayMovement ? PresColor.LightBlue : "#c8c8c8", top: NumCast(doc.y), left: NumCast(doc.x) }}> + <div className="progressivizeButton" key={index} onPointerLeave={() => { if (NumCast(targetDoc._currentFrame) < NumCast(doc.appearFrame)) doc.opacity = 0; }} onPointerOver={() => { if (NumCast(targetDoc._currentFrame) < NumCast(doc.appearFrame)) doc.opacity = 0.5; }} onClick={e => { this.toggleDisplayMovement(doc); e.stopPropagation(); }} style={{ backgroundColor: doc.displayMovement ? Colors.LIGHT_BLUE : "#c8c8c8", top: NumCast(doc.y), left: NumCast(doc.x) }}> <div className="progressivizeButton-prev"><FontAwesomeIcon icon={"caret-left"} size={"lg"} onClick={e => { e.stopPropagation(); this.prevAppearFrame(doc, index); }} /></div> <div className="progressivizeButton-frame">{doc.appearFrame}</div> <div className="progressivizeButton-next"><FontAwesomeIcon icon={"caret-right"} size={"lg"} onClick={e => { e.stopPropagation(); this.nextAppearFrame(doc, index); }} /></div> @@ -2213,6 +2179,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> const mode = StrCast(this.rootDoc._viewType) as CollectionViewType; const isMini: boolean = this.toolbarWidth <= 100; const presKeyEvents: boolean = (this.isPres && this._presKeyEventsActive && this.rootDoc === Doc.UserDoc().activePresentation); + const activeColor = Colors.LIGHT_BLUE; + const inactiveColor = Colors.WHITE; return (mode === CollectionViewType.Carousel3D) ? (null) : ( <div id="toolbarContainer" className={'presBox-toolbar'}> {/* <Tooltip title={<><div className="dash-tooltip">{"Add new slide"}</div></>}><div className={`toolbar-button ${this.newDocumentTools ? "active" : ""}`} onClick={action(() => this.newDocumentTools = !this.newDocumentTools)}> @@ -2220,7 +2188,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <FontAwesomeIcon className={`dropdown ${this.newDocumentTools ? "active" : ""}`} icon={"angle-down"} /> </div></Tooltip> */} <Tooltip title={<><div className="dash-tooltip">{"View paths"}</div></>}> - <div style={{ opacity: this.childDocs.length > 1 && this.layoutDoc.presCollection ? 1 : 0.3, color: this._pathBoolean ? PresColor.DarkBlue : 'white', width: isMini ? "100%" : undefined }} className={"toolbar-button"} onClick={this.childDocs.length > 1 && this.layoutDoc.presCollection ? this.viewPaths : undefined}> + <div style={{ opacity: this.childDocs.length > 1 && this.layoutDoc.presCollection ? 1 : 0.3, color: this._pathBoolean ? Colors.MEDIUM_BLUE : 'white', width: isMini ? "100%" : undefined }} className={"toolbar-button"} onClick={this.childDocs.length > 1 && this.layoutDoc.presCollection ? this.viewPaths : undefined}> <FontAwesomeIcon icon={"exchange-alt"} /> </div> </Tooltip> @@ -2229,7 +2197,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <div className="toolbar-divider" /> {/* <Tooltip title={<><div className="dash-tooltip">{this._expandBoolean ? "Minimize all" : "Expand all"}</div></>}> <div className={"toolbar-button"} - style={{ color: this._expandBoolean ? PresColors.DarkBlue : 'white' }} + style={{ color: this._expandBoolean ? Colors.MEDIUM_BLUE : 'white' }} onClick={this.toggleExpandMode}> <FontAwesomeIcon icon={"eye"} /> </div> @@ -2237,12 +2205,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <div className="toolbar-divider" /> */} <Tooltip title={<><div className="dash-tooltip">{presKeyEvents ? "Keys are active" : "Keys are not active - click anywhere on the presentation trail to activate keys"}</div></>}> <div className="toolbar-button" style={{ cursor: presKeyEvents ? 'default' : 'pointer', position: 'absolute', right: 30, fontSize: 16 }}> - <FontAwesomeIcon className={"toolbar-thumbtack"} icon={"keyboard"} style={{ color: presKeyEvents ? PresColor.DarkBlue : 'white' }} /> + <FontAwesomeIcon className={"toolbar-thumbtack"} icon={"keyboard"} style={{ color: presKeyEvents ? activeColor : inactiveColor }} /> </div> </Tooltip> <Tooltip title={<><div className="dash-tooltip">{propTitle}</div></>}> <div className="toolbar-button" style={{ position: 'absolute', right: 4, fontSize: 16 }} onClick={this.toggleProperties}> - <FontAwesomeIcon className={"toolbar-thumbtack"} icon={propIcon} style={{ color: CurrentUserUtils.propertiesWidth > 0 ? PresColor.DarkBlue : 'white' }} /> + <FontAwesomeIcon className={"toolbar-thumbtack"} icon={propIcon} style={{ color: CurrentUserUtils.propertiesWidth > 0 ? activeColor : inactiveColor }} /> </div> </Tooltip> </> @@ -2379,7 +2347,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> const presStart: boolean = !this.layoutDoc.presLoop && (this.itemIndex === 0); // Case 1: There are still other frames and should go through all frames before going to next slide return (<div className="presPanelOverlay" style={{ display: this.layoutDoc.presStatus !== "edit" ? "inline-flex" : "none" }}> - <Tooltip title={<><div className="dash-tooltip">{"Loop"}</div></>}><div className="presPanel-button" style={{ color: this.layoutDoc.presLoop ? PresColor.DarkBlue : 'white' }} onClick={() => this.layoutDoc.presLoop = !this.layoutDoc.presLoop}><FontAwesomeIcon icon={"redo-alt"} /></div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Loop"}</div></>}><div className="presPanel-button" style={{ color: this.layoutDoc.presLoop ? Colors.MEDIUM_BLUE : 'white' }} onClick={() => this.layoutDoc.presLoop = !this.layoutDoc.presLoop}><FontAwesomeIcon icon={"redo-alt"} /></div></Tooltip> <div className="presPanel-divider"></div> <div className="presPanel-button" style={{ opacity: presStart ? 0.4 : 1 }} onClick={() => { this.back(); if (this._presTimer) { clearTimeout(this._presTimer); this.layoutDoc.presStatus = PresStatus.Manual; } }}><FontAwesomeIcon icon={"arrow-left"} /></div> <Tooltip title={<><div className="dash-tooltip">{this.layoutDoc.presStatus === PresStatus.Autoplay ? "Pause" : "Autoplay"}</div></>}><div className="presPanel-button" onClick={this.startOrPause}><FontAwesomeIcon icon={this.layoutDoc.presStatus === PresStatus.Autoplay ? "pause" : "play"} /></div></Tooltip> @@ -2418,8 +2386,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> const presStart: boolean = !this.layoutDoc.presLoop && (this.itemIndex === 0); return CurrentUserUtils.OverlayDocs.includes(this.rootDoc) ? <div className="miniPres"> - <div className="presPanelOverlay" style={{ display: "inline-flex", height: 30, background: '#323232', top: 0, zIndex: 3000000, boxShadow: presKeyEvents ? '0 0 0px 3px ' + PresColor.DarkBlue : undefined }}> - <Tooltip title={<><div className="dash-tooltip">{"Loop"}</div></>}><div className="presPanel-button" style={{ color: this.layoutDoc.presLoop ? PresColor.DarkBlue : undefined }} onClick={() => this.layoutDoc.presLoop = !this.layoutDoc.presLoop}><FontAwesomeIcon icon={"redo-alt"} /></div></Tooltip> + <div className="presPanelOverlay" style={{ display: "inline-flex", height: 30, background: '#323232', top: 0, zIndex: 3000000, boxShadow: presKeyEvents ? '0 0 0px 3px ' + Colors.MEDIUM_BLUE : undefined }}> + <Tooltip title={<><div className="dash-tooltip">{"Loop"}</div></>}><div className="presPanel-button" style={{ color: this.layoutDoc.presLoop ? Colors.MEDIUM_BLUE : undefined }} onClick={() => this.layoutDoc.presLoop = !this.layoutDoc.presLoop}><FontAwesomeIcon icon={"redo-alt"} /></div></Tooltip> <div className="presPanel-divider"></div> <div className="presPanel-button" style={{ opacity: presStart ? 0.4 : 1 }} onClick={() => { this.back(); if (this._presTimer) { clearTimeout(this._presTimer); this.layoutDoc.presStatus = PresStatus.Manual; } }}><FontAwesomeIcon icon={"arrow-left"} /></div> <Tooltip title={<><div className="dash-tooltip">{this.layoutDoc.presStatus === PresStatus.Autoplay ? "Pause" : "Autoplay"}</div></>}><div className="presPanel-button" onClick={this.startOrPause}><FontAwesomeIcon icon={this.layoutDoc.presStatus === "auto" ? "pause" : "play"} /></div></Tooltip> diff --git a/src/client/views/presentationview/PresElementBox.scss b/src/client/views/nodes/trails/PresElementBox.scss index 1ad4b820e..1ad4b820e 100644 --- a/src/client/views/presentationview/PresElementBox.scss +++ b/src/client/views/nodes/trails/PresElementBox.scss diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index f15d51764..5e713c3cf 100644 --- a/src/client/views/presentationview/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -2,27 +2,29 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Tooltip } from "@material-ui/core"; import { action, computed, IReactionDisposer, observable, reaction } from "mobx"; import { observer } from "mobx-react"; -import { DataSym, Doc, Opt } from "../../../fields/Doc"; -import { documentSchema } from '../../../fields/documentSchemas'; -import { Id } from "../../../fields/FieldSymbols"; -import { createSchema, makeInterface } from '../../../fields/Schema'; -import { Cast, NumCast, StrCast } from "../../../fields/Types"; -import { emptyFunction, returnFalse, returnTrue, setupMoveUpEvents, emptyPath, returnEmptyDoclist } from "../../../Utils"; -import { DocumentType } from "../../documents/DocumentTypes"; -import { CurrentUserUtils } from "../../util/CurrentUserUtils"; -import { DocumentManager } from "../../util/DocumentManager"; -import { DragManager } from "../../util/DragManager"; -import { Transform } from "../../util/Transform"; -import { undoBatch } from "../../util/UndoManager"; -import { ViewBoxBaseComponent } from '../DocComponent'; -import { EditableView } from "../EditableView"; -import { DocumentView, DocumentViewProps } from "../nodes/DocumentView"; -import { FieldView, FieldViewProps } from '../nodes/FieldView'; -import { PresBox, PresColor, PresMovement } from "../nodes/PresBox"; -import { StyleProp } from "../StyleProvider"; +import { DataSym, Doc, Opt } from "../../../../fields/Doc"; +import { documentSchema } from '../../../../fields/documentSchemas'; +import { Id } from "../../../../fields/FieldSymbols"; +import { createSchema, makeInterface } from '../../../../fields/Schema'; +import { Cast, NumCast, StrCast } from "../../../../fields/Types"; +import { emptyFunction, returnFalse, returnTrue, setupMoveUpEvents, emptyPath, returnEmptyDoclist } from "../../../../Utils"; +import { DocumentType } from "../../../documents/DocumentTypes"; +import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; +import { DocumentManager } from "../../../util/DocumentManager"; +import { DragManager } from "../../../util/DragManager"; +import { Transform } from "../../../util/Transform"; +import { undoBatch } from "../../../util/UndoManager"; +import { ViewBoxBaseComponent } from '../../DocComponent'; +import { EditableView } from "../../EditableView"; +import { DocumentView, DocumentViewProps } from "../../nodes/DocumentView"; +import { FieldView, FieldViewProps } from '../../nodes/FieldView'; +import { PresBox } from "./PresBox"; +import { Colors } from "../../global/globalEnums"; +import { StyleProp } from "../../StyleProvider"; import "./PresElementBox.scss"; import React = require("react"); -import { DocUtils } from "../../documents/Documents"; +import { DocUtils } from "../../../documents/Documents"; +import { PresMovement } from "./PresEnums"; export const presSchema = createSchema({ presentationTargetDoc: Doc, @@ -210,11 +212,11 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc const height = slide.clientHeight; const halfLine = height / 2; if (y <= halfLine) { - slide.style.borderTop = "solid 2px #5B9FDD"; + slide.style.borderTop = `solid 2px ${Colors.MEDIUM_BLUE}`; slide.style.borderBottom = "0px"; } else if (y > halfLine) { slide.style.borderTop = "0px"; - slide.style.borderBottom = "solid 2px #5B9FDD"; + slide.style.borderBottom = `solid 2px ${Colors.MEDIUM_BLUE}`; } } document.removeEventListener("pointermove", this.onPointerMove); @@ -292,7 +294,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc const miniView: boolean = this.toolbarWidth <= 110; const presBox: Doc = this.presBox; //presBox const presBoxColor: string = StrCast(presBox._backgroundColor); - const presColorBool: boolean = presBoxColor ? (presBoxColor !== "white" && presBoxColor !== "transparent") : false; + const presColorBool: boolean = presBoxColor ? (presBoxColor !== Colors.WHITE && presBoxColor !== "transparent") : false; const targetDoc: Doc = this.targetDoc; const activeItem: Doc = this.rootDoc; return ( @@ -300,7 +302,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc key={this.props.Document[Id] + this.indexInPres} ref={this._itemRef} style={{ - backgroundColor: presColorBool ? isSelected ? "rgba(250,250,250,0.3)" : "transparent" : isSelected ? "#AEDDF8" : "transparent", + backgroundColor: presColorBool ? isSelected ? "rgba(250,250,250,0.3)" : "transparent" : isSelected ? Colors.LIGHT_BLUE : "transparent", opacity: this._dragging ? 0.3 : 1 }} onClick={e => { @@ -356,7 +358,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc style={{ zIndex: 1000 - this.indexInPres, fontWeight: 700, - backgroundColor: activeItem.groupWithUp ? presColorBool ? presBoxColor : PresColor.DarkBlue : undefined, + backgroundColor: activeItem.groupWithUp ? presColorBool ? presBoxColor : Colors.MEDIUM_BLUE : undefined, height: activeItem.groupWithUp ? 53 : 18, transform: activeItem.groupWithUp ? "translate(0, -17px)" : undefined }}> diff --git a/src/client/views/nodes/trails/PresEnums.ts b/src/client/views/nodes/trails/PresEnums.ts new file mode 100644 index 000000000..93ab323fb --- /dev/null +++ b/src/client/views/nodes/trails/PresEnums.ts @@ -0,0 +1,28 @@ +export enum PresMovement { + Zoom = "zoom", + Pan = "pan", + Jump = "jump", + None = "none", +} + +export enum PresEffect { + Zoom = "Zoom", + Lightspeed = "Lightspeed", + Fade = "Fade in", + Flip = "Flip", + Rotate = "Rotate", + Bounce = "Bounce", + Roll = "Roll", + None = "None", + Left = "left", + Right = "right", + Center = "center", + Top = "top", + Bottom = "bottom" +} + +export enum PresStatus { + Autoplay = "auto", + Manual = "manual", + Edit = "edit" +}
\ No newline at end of file diff --git a/src/client/views/nodes/trails/index.ts b/src/client/views/nodes/trails/index.ts new file mode 100644 index 000000000..8f3f7b03a --- /dev/null +++ b/src/client/views/nodes/trails/index.ts @@ -0,0 +1,3 @@ +export * from "./PresBox"; +export * from "./PresElementBox"; +export * from "./PresEnums";
\ No newline at end of file diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx index 1e2d72254..17979ef4b 100644 --- a/src/client/views/pdf/AnchorMenu.tsx +++ b/src/client/views/pdf/AnchorMenu.tsx @@ -1,7 +1,7 @@ import React = require("react"); import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Tooltip } from "@material-ui/core"; -import { action, computed, observable, IReactionDisposer, reaction } from "mobx"; +import { action, computed, observable, IReactionDisposer, reaction, ObservableMap } from "mobx"; import { observer } from "mobx-react"; import { ColorState } from "react-color"; import { Doc, Opt } from "../../../fields/Doc"; @@ -10,6 +10,7 @@ import { AntimodeMenu, AntimodeMenuProps } from "../AntimodeMenu"; import { ButtonDropdown } from "../nodes/formattedText/RichTextMenu"; import "./AnchorMenu.scss"; import { SelectionManager } from "../../util/SelectionManager"; +import { LinkPopup } from "../linking/LinkPopup"; @observer export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { @@ -38,14 +39,18 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { @observable private _valueValue: string = ""; @observable private _added: boolean = false; @observable private highlightColor: string = "rgba(245, 230, 95, 0.616)"; + @observable private _showLinkPopup: boolean = false; @observable public _colorBtn = false; @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; + public GetAnchor: (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => Opt<Doc> = () => undefined; public Delete: () => void = unimplementedFunction; public AddTag: (key: string, value: string) => boolean = returnFalse; public PinToPres: () => void = unimplementedFunction; @@ -62,7 +67,10 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { componentDidMount() { this._disposer = reaction(() => SelectionManager.Views(), - selected => AnchorMenu.Instance.fadeOut(true)); + selected => { + this._showLinkPopup = false; + AnchorMenu.Instance.fadeOut(true); + }); } pointerDown = (e: React.PointerEvent) => { @@ -79,6 +87,13 @@ 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 + //change popup visibility field to visible + this._showLinkPopup = !this._showLinkPopup; + } + @computed get highlighter() { const button = <button className="antimodeMenu-button color-preview-button" title="" key="highlighter-button" onClick={this.highlightClicked}> @@ -135,6 +150,13 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { <FontAwesomeIcon icon="comment-alt" size="lg" /> </button> </Tooltip>, + //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 key="popup" 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 85cf5abd7..02010e123 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(); @@ -120,9 +119,11 @@ export class PDFViewer extends React.Component<IViewerProps> { this._mainCont.current?.addEventListener("scroll", e => (e.target as any).scrollLeft = 0); this._disposers.autoHeight = reaction(() => this.props.layoutDoc._autoHeight, - () => { - this.props.layoutDoc._nativeHeight = NumCast(this.props.Document[this.props.fieldKey + "-nativeHeight"]); - this.props.setHeight(NumCast(this.props.Document[this.props.fieldKey + "-nativeHeight"]) * (this.props.scaling?.() || 1)); + autoHeight => { + if (autoHeight) { + this.props.layoutDoc._nativeHeight = NumCast(this.props.Document[this.props.fieldKey + "-nativeHeight"]); + this.props.setHeight(NumCast(this.props.Document[this.props.fieldKey + "-nativeHeight"]) * (this.props.scaling?.() || 1)); + } }); this._disposers.searchMatch = reaction(() => Doc.IsSearchMatch(this.props.rootDoc), @@ -133,10 +134,10 @@ export class PDFViewer extends React.Component<IViewerProps> { this._disposers.selected = reaction(() => this.props.isSelected(), selected => { - if (!selected) { - Array.from(this._savedAnnotations.values()).forEach(v => v.forEach(a => a.remove())); - Array.from(this._savedAnnotations.keys()).forEach(k => this._savedAnnotations.set(k, [])); - } + // if (!selected) { + // Array.from(this._savedAnnotations.values()).forEach(v => v.forEach(a => a.remove())); + // Array.from(this._savedAnnotations.keys()).forEach(k => this._savedAnnotations.set(k, [])); + // } (SelectionManager.Views().length === 1) && this.setupPdfJsViewer(); }, { fireImmediately: true }); @@ -183,16 +184,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) { - const scrollTo = Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.props.layoutDoc._scrollTop), this.props.PanelHeight() / (this.props.scaling?.() || 1)); + 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; } @@ -370,10 +373,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)) { + 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; @@ -381,10 +385,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); @@ -453,12 +454,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)) { @@ -507,16 +508,12 @@ 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} fieldKey={this.props.fieldKey + "-annotations"} setPreviewCursor={this.setPreviewCursor} PanelHeight={this.panelHeight} @@ -525,10 +522,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} /> + 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)} + {SnappingManager.GetIsDragging() ? (null) : renderAnnotations()} + </div> </div>; } @computed get pdfViewerDiv() { @@ -549,7 +566,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/CheckBox.scss b/src/client/views/search/CheckBox.scss index cc858bec6..2a0085ade 100644 --- a/src/client/views/search/CheckBox.scss +++ b/src/client/views/search/CheckBox.scss @@ -1,4 +1,4 @@ -@import "../globalCssVariables"; +@import "../global/globalCssVariables"; .checkboxfilter { display: flex; @@ -13,7 +13,7 @@ margin-top: 0px; .check-container:hover~.check-box { - background-color: $darker-alt-accent; + background-color: $medium-blue; } .check-container { @@ -40,7 +40,7 @@ overflow: visible; background-color: transparent; border-style: solid; - border-color: $alt-accent; + border-color: $medium-gray; border-width: 2px; -webkit-transition: all 0.2s ease-in-out; -moz-transition: all 0.2s ease-in-out; diff --git a/src/client/views/search/CollectionFilters.scss b/src/client/views/search/CollectionFilters.scss index b54cdcbd1..845b16f67 100644 --- a/src/client/views/search/CollectionFilters.scss +++ b/src/client/views/search/CollectionFilters.scss @@ -1,4 +1,4 @@ -@import "../globalCssVariables"; +@import "../global/globalCssVariables"; .collection-filters { display: flex; diff --git a/src/client/views/search/IconBar.scss b/src/client/views/search/IconBar.scss index 013dcd57e..6aaf7918d 100644 --- a/src/client/views/search/IconBar.scss +++ b/src/client/views/search/IconBar.scss @@ -1,4 +1,4 @@ -@import "../globalCssVariables"; +@import "../global/globalCssVariables"; .icon-bar { display: flex; diff --git a/src/client/views/search/IconButton.scss b/src/client/views/search/IconButton.scss index 4ec03c7c9..3cb08d756 100644 --- a/src/client/views/search/IconButton.scss +++ b/src/client/views/search/IconButton.scss @@ -1,4 +1,4 @@ -@import "../globalCssVariables"; +@import "../global/globalCssVariables"; .type-outer { display: flex; @@ -9,7 +9,7 @@ .type-icon { height: 30px; width: 30px; - color: $light-color; + color: $white; // background-color: rgb(194, 194, 197); background-color: gray; border-radius: 50%; @@ -43,7 +43,7 @@ .type-icon:hover { transform: scale(1.1); - background-color: $darker-alt-accent; + background-color: $medium-blue; opacity: 1; +.filter-description { diff --git a/src/client/views/search/IconButton.tsx b/src/client/views/search/IconButton.tsx index 349690b20..2dd6b1b79 100644 --- a/src/client/views/search/IconButton.tsx +++ b/src/client/views/search/IconButton.tsx @@ -4,7 +4,7 @@ import { action, IReactionDisposer, observable, reaction, runInAction } from 'mo import { observer } from 'mobx-react'; import * as React from 'react'; import { DocumentType } from "../../documents/DocumentTypes"; -import '../globalCssVariables.scss'; +import '../global/globalCssVariables.scss'; import { IconBar } from './IconBar'; import "./IconButton.scss"; import "./SearchBox.scss"; @@ -104,7 +104,7 @@ export class IconButton extends React.Component<IconButtonProps>{ hoverStyle = { opacity: 1, backgroundColor: "rgb(128, 128, 128)" - //backgroundColor: "rgb(178, 206, 248)" //$darker-alt-accent + //backgroundColor: "rgb(178, 206, 248)" //$medium-blue }; render() { diff --git a/src/client/views/search/NaviconButton.scss b/src/client/views/search/NaviconButton.scss index c23bab461..8a70b29de 100644 --- a/src/client/views/search/NaviconButton.scss +++ b/src/client/views/search/NaviconButton.scss @@ -1,4 +1,4 @@ -@import "../globalCssVariables"; +@import "../global/globalCssVariables"; $height-icon: 15px; $width-line: 30px; @@ -20,7 +20,7 @@ $translateX: 0; .line { display: block; - background: $alt-accent; + background: $medium-gray; width: $width-line; height: $height-line; position: absolute; diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss index 4f5b7e41a..2586ef2ee 100644 --- a/src/client/views/search/SearchBox.scss +++ b/src/client/views/search/SearchBox.scss @@ -1,142 +1,110 @@ -@import "../globalCssVariables"; +@import "../global/globalCssVariables"; @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: black; + 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:black; - 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: $light-color; - 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-color-secondary; - 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..260ddfc90 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -1,601 +1,342 @@ -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(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[] = []; + 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); + const 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; - } + /** + * 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"]; - // 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; - } + return selectValues.map(value => <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/search/SelectorContextMenu.scss b/src/client/views/search/SelectorContextMenu.scss index 48cacc608..a114f679c 100644 --- a/src/client/views/search/SelectorContextMenu.scss +++ b/src/client/views/search/SelectorContextMenu.scss @@ -1,7 +1,7 @@ -@import "../globalCssVariables"; +@import "../global/globalCssVariables"; .parents { - background: $lighter-alt-accent; + background: $light-blue; padding: 10px; // width: 300px; @@ -10,7 +10,7 @@ } .collection { - border-color: $darker-alt-accent; + border-color: $medium-blue; border-bottom-style: solid; } }
\ No newline at end of file diff --git a/src/client/views/search/ToggleBar.scss b/src/client/views/search/ToggleBar.scss index 79f866acb..3a164f133 100644 --- a/src/client/views/search/ToggleBar.scss +++ b/src/client/views/search/ToggleBar.scss @@ -1,9 +1,9 @@ -@import "../globalCssVariables"; +@import "../global/globalCssVariables"; .toggle-title { display: flex; align-items: center; - color: $light-color; + color: $white; text-transform: uppercase; flex-direction: row; justify-content: space-around; @@ -25,7 +25,7 @@ // height: 50px; height: 30px; width: 100px; - background-color: $alt-accent; + background-color: $medium-gray; border-radius: 10px; padding: 5px; display: flex; @@ -36,6 +36,6 @@ width: 40px; height: 100%; border-radius: 10px; - background-color: $light-color; + background-color: $white; } }
\ No newline at end of file diff --git a/src/client/views/topbar/TopBar.scss b/src/client/views/topbar/TopBar.scss new file mode 100644 index 000000000..d04ae8a80 --- /dev/null +++ b/src/client/views/topbar/TopBar.scss @@ -0,0 +1,218 @@ +@import "../global/globalCssVariables"; + +.topbar-container { + display: flex; + flex-direction: column; + width: 100%; + position: relative; + font-size: 10px; + line-height: 1; + overflow-y: auto; + overflow-x: visible; + background: $dark-gray; + overflow: visible; + z-index: 1000; + + .topbar-bar { + height: $topbar-height; + display: grid; + grid-auto-columns: 33.3% 33.3% 33.3%; + align-items: center; + background-color: $dark-gray; + + .topBar-icon { + cursor: pointer; + font-size: 12px; + font-family: 'Roboto'; + width: fit-content; + display: flex; + justify-content: center; + gap: 4px; + align-items: center; + justify-self: center; + align-self: center; + border-radius: 5px; + padding: 5px; + transition: linear 0.1s; + color: $black; + background-color: $light-gray; + } + + .topBar-icon:hover { + background-color: $light-blue; + } + + + .topbar-center { + grid-column: 2; + display: inline-flex; + justify-content: center; + align-items: center; + gap: 5px; + + .topbar-dashboards { + display: flex; + flex-direction: row; + gap: 5px; + } + + .topbar-lozenge-dashboard { + display: flex; + + + + .topbar-dashSelect { + border: none; + background-color: $dark-gray; + color: $white; + font-family: 'Roboto'; + font-size: 17; + font-weight: 500; + + &:hover { + cursor: pointer; + } + } + } + } + + + .topbar-right { + grid-column: 3; + position: relative; + display: flex; + justify-content: flex-end; + gap: 5px; + margin-right: 5px; + } + + .topbar-left { + grid-column: 1; + color: black; + font-family: 'Roboto'; + position: relative; + display: flex; + width: fit-content; + gap: 5px; + + .topBar-icon:hover { + background-color: $close-red; + } + + .topbar-lozenge-user, + .topbar-lozenge { + height: 23; + font-size: 12; + color: white; + font-family: 'Roboto'; + font-weight: 400; + padding: 4px; + align-self: center; + margin-left: 7px; + display: flex; + align-items: center; + + .topbar-dashSelect { + border: none; + background-color: transparent; + color: black; + font-family: 'Roboto'; + font-size: 17; + font-weight: 500; + + &:hover { + cursor: pointer; + } + } + } + + .topbar-logoff { + border-radius: 3px; + background: olivedrab; + color: white; + display: none; + margin-left: 5px; + padding: 1px 2px 1px 2px; + cursor: pointer; + } + + .topbar-logoff { + background: red; + } + + .topbar-lozenge-user:hover { + .topbar-logoff { + display: inline-block; + } + } + } + + .topbar-barChild { + + &.topbar-collection { + flex: 0 1 auto; + margin-left: 2px; + margin-right: 2px + } + + &.topbar-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; + } + } + + &.topbar-filter { + align-self: stretch; + + button { + transform: none; + + &:hover { + transform: none; + } + } + } + + &.topbar-submit { + margin-left: 2px; + margin-right: 2px + } + + &.topbar-close { + color: $white; + max-height: $topbar-height; + } + } + } +} + +.topbar-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/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx new file mode 100644 index 000000000..05edb975c --- /dev/null +++ b/src/client/views/topbar/TopBar.tsx @@ -0,0 +1,66 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { observer } from "mobx-react"; +import * as React from 'react'; +import { Doc, DocListCast } from '../../../fields/Doc'; +import { Id } from '../../../fields/FieldSymbols'; +import { StrCast } from '../../../fields/Types'; +import { Utils } from '../../../Utils'; +import { CurrentUserUtils } from "../../util/CurrentUserUtils"; +import { SettingsManager } from "../../util/SettingsManager"; +import { undoBatch } from "../../util/UndoManager"; +import { Borders, Colors } from "../global/globalEnums"; +import "./TopBar.scss"; + +/** + * ABOUT: This is the topbar in Dash, which included the current Dashboard as well as access to information on the user + * and settings and help buttons. Future scope for this bar is to include the collaborators that are on the same Dashboard. + */ +@observer +export class TopBar extends React.Component { + render() { + const myDashboards = DocListCast(CurrentUserUtils.MyDashboards.data); + return ( + //TODO:glr Add support for light / dark mode + <div style={{ pointerEvents: "all" }} className="topbar-container"> + <div className="topbar-bar" style={{ background: Colors.DARK_GRAY, borderBottom: Borders.STANDARD }}> + <div className="topbar-left"> + <div className="topbar-lozenge-user"> + {`${Doc.CurrentUserEmail}`} + </div> + <div className="topbar-icon" onClick={() => window.location.assign(Utils.prepend("/logout"))}> + {"Sign out"} + </div> + </div> + <div className="topbar-center" > + <div className="topbar-lozenge-dashboard"> + <select className="topbar-dashSelect" onChange={e => CurrentUserUtils.openDashboard(Doc.UserDoc(), myDashboards[Number(e.target.value)])} + value={myDashboards.indexOf(CurrentUserUtils.ActiveDashboard)} + style={{ color: Colors.WHITE }}> + {myDashboards.map((dash, i) => <option key={dash[Id]} value={i}> {StrCast(dash.title)} </option>)} + </select> + </div> + <div className="topbar-dashboards"> + <div className="topbar-icon" onClick={undoBatch(() => CurrentUserUtils.createNewDashboard(Doc.UserDoc()))} + > + {"New"}<FontAwesomeIcon icon="plus"></FontAwesomeIcon> + </div> + {Doc.UserDoc().noviceMode ? (null) : <div className="topbar-icon" onClick={undoBatch(() => CurrentUserUtils.snapshotDashboard(Doc.UserDoc()))} + > + {"Snapshot"}<FontAwesomeIcon icon="camera"></FontAwesomeIcon> + </div>} + </div> + </div> + <div className="topbar-right" > + <div className="topbar-icon"> + {"Help"}<FontAwesomeIcon icon="question-circle"></FontAwesomeIcon> + </div> + <div className="topbar-icon" onClick={() => SettingsManager.Instance.open()}> + {"Settings"}<FontAwesomeIcon icon="cog"></FontAwesomeIcon> + </div> + + </div> + </div> + </div > + ); + } +}
\ No newline at end of file diff --git a/src/client/views/webcam/DashWebRTCVideo.scss b/src/client/views/webcam/DashWebRTCVideo.scss index 41307a808..249aee9d6 100644 --- a/src/client/views/webcam/DashWebRTCVideo.scss +++ b/src/client/views/webcam/DashWebRTCVideo.scss @@ -1,4 +1,4 @@ -@import "../globalCssVariables"; +@import "../global/globalCssVariables"; .webcam-cont { background: whitesmoke; diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index e51eb44db..d24175ecd 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -21,8 +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 { @@ -53,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; @@ -60,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[]>; @@ -88,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"); @@ -100,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] ]); @@ -250,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 { @@ -364,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); @@ -397,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) { @@ -423,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); @@ -516,30 +528,30 @@ export namespace Doc { return alias; } - export async function makeClone(doc: Doc, cloneMap: Map<string, Doc>, rtfs: { copy: Doc, key: string, field: RichTextField }[], exclusions: string[], dontCreate: boolean, asBranch: boolean): Promise<Doc> { + export async function makeClone(doc: Doc, cloneMap: Map<string, Doc>, linkMap: Map<Doc, Doc>, rtfs: { copy: Doc, key: string, field: RichTextField }[], exclusions: string[], dontCreate: boolean, asBranch: boolean): Promise<Doc> { if (Doc.IsBaseProto(doc)) return doc; if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!; const copy = dontCreate ? asBranch ? (Cast(doc.branchMaster, Doc, null) || doc) : doc : new Doc(undefined, true); cloneMap.set(doc[Id], copy); - if (LinkManager.Instance.getAllLinks().includes(doc) && LinkManager.Instance.getAllLinks().indexOf(copy) === -1) LinkManager.Instance.addLink(copy); - const filter = [...exclusions, ...Cast(doc.cloneFieldFilter, listSpec("string"), [])]; - await Promise.all([...Object.keys(doc), "links"].map(async key => { + const fieldExclusions = doc.type === DocumentType.MARKER ? exclusions.filter(ex => ex !== "annotationOn") : exclusions; + const filter = [...fieldExclusions, ...Cast(doc.cloneFieldFilter, listSpec("string"), [])]; + await Promise.all(Object.keys(doc).map(async key => { if (filter.includes(key)) return; const assignKey = (val: any) => !dontCreate && (copy[key] = val); const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); - const field = key === "links" && Doc.IsPrototype(doc) ? doc[key] : ProxyField.WithoutProxy(() => 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, rtfs, exclusions, dontCreate, asBranch))); + 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, 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) { - if (field.Data.includes('"docid":') || field.Data.includes('"targetId":') || field.Data.includes('"linkId":')) { + if (field.Data.includes('"audioId":') || field.Data.includes('"textId":') || field.Data.includes('"anchorId":')) { rtfs.push({ copy, key, field }); } } @@ -547,23 +559,30 @@ export namespace Doc { }; if (key === "proto") { if (doc[key] instanceof Doc) { - assignKey(await Doc.makeClone(doc[key]!, cloneMap, rtfs, exclusions, dontCreate, asBranch)); + assignKey(await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch)); + } + } else if (key === "anchor1" || key === "anchor2") { + if (doc[key] instanceof Doc) { + assignKey(await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, true, asBranch)); } } else { if (field instanceof RefField) { assignKey(field); } else if (cfield instanceof ComputedField) { !dontCreate && assignKey(ComputedField.MakeFunction(cfield.script.originalScript)); - (key === "links" && field instanceof ObjectField) && await copyObjectField(field); } 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); } } })); + for (const link of Array.from(doc[DirectLinksSym])) { + const linkClone = await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch); + linkMap.set(link, linkClone); + } if (!dontCreate) { Doc.SetInPlace(copy, "title", (asBranch ? "BRANCH: " : "CLONE: ") + doc.title, true); asBranch ? (copy.branchOf = doc) : (copy.cloneOf = doc); @@ -574,10 +593,11 @@ 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, 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) => { const mapped = cloneMap.get(id); @@ -587,9 +607,9 @@ 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(/("docid":|"targetId":|"linkId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text); + copy[key] = new RichTextField(field.Data.replace(/("textId":|"audioId":|"anchorId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text); }); return { clone: copy, map: cloneMap }; } @@ -657,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); @@ -765,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... @@ -803,6 +823,27 @@ export namespace Doc { return undefined; } + // Makes a delegate of a document by first creating a delegate where data should be stored + // (ie, the 'data' doc), and then creates another delegate of that (ie, the 'layout' doc). + // This is appropriate if you're trying to create a document that behaves like all + // regularly created documents (e.g, text docs, pdfs, etc which all have data/layout docs) + export function MakeDelegateWithProto(doc: Doc, id?: string, title?: string): Doc { + const delegateProto = new Doc(); + delegateProto[Initializing] = true; + delegateProto.proto = doc; + delegateProto.author = Doc.CurrentUserEmail; + delegateProto.isPrototype = true; + title && (delegateProto.title = title); + const delegate = new Doc(id, true); + delegate[Initializing] = true; + delegate.proto = delegateProto; + delegate.author = Doc.CurrentUserEmail; + Doc.AddDocToList(delegateProto[DataSym], "aliases", delegate); + delegate[Initializing] = false; + delegateProto[Initializing] = false; + return delegate; + } + let _applyCount: number = 0; export function ApplyTemplate(templateDoc: Doc) { if (templateDoc) { @@ -866,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); @@ -897,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; @@ -1037,6 +1088,13 @@ 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])); + } + if (typeof value === "string") { + value = value.replace(`,${Utils.noRecursionHack}`, ""); + } const fieldVal = doc[key]; if (Cast(fieldVal, listSpec("string"), []).length) { const vals = Cast(fieldVal, listSpec("string"), []); @@ -1076,7 +1134,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; @@ -1147,11 +1205,13 @@ 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) { - const ndoc = Doc.MakeDelegate(dragFactory); - ndoc.isPrototype = true; + const ndoc = Doc.MakeDelegateWithProto(dragFactory); if (ndoc && dragFactory["dragFactory-count"] !== undefined) { dragFactory["dragFactory-count"] = NumCast(dragFactory["dragFactory-count"]) + 1; Doc.GetProto(ndoc).title = ndoc.title + " " + NumCast(dragFactory["dragFactory-count"]).toString(); @@ -1164,7 +1224,10 @@ export namespace Doc { case DocumentType.IMG: return "image"; case DocumentType.COMPARISON: return "columns"; case DocumentType.RTF: return "sticky-note"; - case DocumentType.COL: return !doc?.isFolder ? "folder" + (isOpen ? "-open" : "") : "chevron-" + (isOpen ? "down" : "right"); + case DocumentType.COL: + const folder: IconProp = isOpen ? "folder-open" : "folder"; + 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"; case DocumentType.WEBCAM: return "video"; @@ -1198,39 +1261,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. */ @@ -1267,7 +1330,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> => { @@ -1291,10 +1354,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>> => { @@ -1333,7 +1396,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 dbe51b24a..1270a2dab 100644 --- a/src/fields/InkField.ts +++ b/src/fields/InkField.ts @@ -4,6 +4,7 @@ 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 { None = "none", Pen = "pen", @@ -12,13 +13,41 @@ export enum InkTool { Stamp = "stamp" } + +// Defines a point in an ink as a pair of x- and y-coordinates. export interface PointData { X: number; Y: number; } +// Defines an ink as an array of points. export type InkData = Array<PointData>; +export interface ControlPoint { + X: number; + Y: number; + I: number; +} + +export interface HandlePoint { + X: number; + Y: number; + I: number; + dot1: number; + dot2: number; +} + +export interface HandleLine { + X1: number; + Y1: number; + X2: number; + Y2: number; + X3: number; + Y3: number; + dot1: number; + dot2: number; +} + const pointSchema = createSimpleSchema({ X: true, Y: true }); @@ -32,8 +61,6 @@ const strokeDataSchema = createSimpleSchema({ export class InkField extends ObjectField { @serializable(list(object(strokeDataSchema))) readonly inkData: InkData; - // inkData: InkData; - constructor(data: InkData) { super(); 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/AudioUpload.scss b/src/mobile/AudioUpload.scss index 6e64d9e2e..dce0c724f 100644 --- a/src/mobile/AudioUpload.scss +++ b/src/mobile/AudioUpload.scss @@ -1,4 +1,4 @@ -@import "../client/views/globalCssVariables.scss"; +@import "../client/views/global/globalCssVariables.scss"; .audioUpload_cont { display: flex; diff --git a/src/mobile/ImageUpload.scss b/src/mobile/ImageUpload.scss index 890258918..6669a3d21 100644 --- a/src/mobile/ImageUpload.scss +++ b/src/mobile/ImageUpload.scss @@ -1,4 +1,4 @@ -@import "../client/views/globalCssVariables.scss"; +@import "../client/views/global/globalCssVariables.scss"; .imgupload_cont { display: flex; diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx index 2183d2172..f910d765e 100644 --- a/src/mobile/ImageUpload.tsx +++ b/src/mobile/ImageUpload.tsx @@ -5,7 +5,7 @@ import * as rp from 'request-promise'; import { DocServer } from '../client/DocServer'; import { Docs } from '../client/documents/Documents'; import { Networking } from '../client/Network'; -import { DFLT_IMAGE_NATIVE_DIM } from '../client/views/globalCssVariables.scss'; +import { DFLT_IMAGE_NATIVE_DIM } from '../client/views/global/globalCssVariables.scss'; import { MainViewModal } from '../client/views/MainViewModal'; import { Doc, Opt } from '../fields/Doc'; import { List } from '../fields/List'; @@ -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/pen-gestures/GestureUtils.ts b/src/pen-gestures/GestureUtils.ts index e7cc89697..65f2bf80c 100644 --- a/src/pen-gestures/GestureUtils.ts +++ b/src/pen-gestures/GestureUtils.ts @@ -20,9 +20,7 @@ export namespace GestureUtils { ): GestureEventDisposer { const handler = (e: Event) => func(e, (e as CustomEvent<GestureEvent>).detail); element.addEventListener("dashOnGesture", handler); - return () => { - element.removeEventListener("dashOnGesture", handler); - }; + return () => element.removeEventListener("dashOnGesture", handler); } export enum Gestures { diff --git a/src/server/DashSession/Session/agents/server_worker.ts b/src/server/DashSession/Session/agents/server_worker.ts index 6a19bfa5d..84d35b40e 100644 --- a/src/server/DashSession/Session/agents/server_worker.ts +++ b/src/server/DashSession/Session/agents/server_worker.ts @@ -1,10 +1,10 @@ -import { ExitHandler } from "./applied_session_agent"; import { isMaster } from "cluster"; -import { manage, ErrorLike } from "./promisified_ipc_manager"; -import IPCMessageReceiver from "./process_message_router"; -import { red, green, white, yellow } from "colors"; +import { green, red, white, yellow } from "colors"; import { get } from "request-promise"; +import { ExitHandler } from "./applied_session_agent"; import { Monitor } from "./monitor"; +import IPCMessageReceiver from "./process_message_router"; +import { ErrorLike, manage } from "./promisified_ipc_manager"; /** * Effectively, each worker repairs the connection to the server by reintroducing a consistent state @@ -21,7 +21,6 @@ export class ServerWorker extends IPCMessageReceiver { private pollTarget: string; private serverPort: number; private isInitialized = false; - public static Create(work: Function) { if (isMaster) { console.error(red("cannot create a worker on the monitor process.")); diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index 555e3bf3b..a617571ae 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -1,4 +1,4 @@ -import { red, green } from 'colors'; +import { green, red } from 'colors'; import { ExifImage } from 'exif'; import { File } from 'formidable'; import { createWriteStream, existsSync, readFileSync, rename, unlinkSync, writeFile } from 'fs'; @@ -33,7 +33,7 @@ export function InjectSize(filename: string, size: SizeSuffix) { } function isLocal() { - return /Dash-Web[\\\/]src[\\\/]server[\\\/]public[\\\/](.*)/; + return /Dash-Web[0-9]*[\\\/]src[\\\/]server[\\\/]public[\\\/](.*)/; } export namespace DashUploadUtils { 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, |