diff options
Diffstat (limited to 'src')
159 files changed, 7852 insertions, 4753 deletions
diff --git a/src/Utils.ts b/src/Utils.ts index de3b13f63..53182cc9c 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -3,6 +3,8 @@ import v5 = require("uuid/v5"); import { ColorState } from 'react-color'; import { Socket } from 'socket.io'; import { Message } from './server/Message'; +import { Colors } from './client/views/global/globalEnums'; +import Color = require('color'); export namespace Utils { export let DRAG_THRESHOLD = 4; @@ -80,15 +82,7 @@ export namespace Utils { } export function CopyText(text: string) { - const textArea = document.createElement("textarea"); - textArea.value = text; - document.body.appendChild(textArea); - textArea.focus(); - textArea.select(); - - try { document.execCommand('copy'); } catch (err) { } - - document.body.removeChild(textArea); + navigator.clipboard.writeText(text); } export function decimalToHexString(number: number) { @@ -131,7 +125,9 @@ export namespace Utils { // 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 PropUnsetFilter(prop: string) { + return `${prop}:any,${noRecursionHack}:unset`; + } 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 : "") + ")"; @@ -558,66 +554,46 @@ export function simulateMouseClick(element: Element | null | undefined, x: numbe screenY: sy, }))); - rightClick && element.dispatchEvent( - new MouseEvent("contextmenu", { - view: window, - bubbles: true, - cancelable: true, - button: 2, - clientX: x, - clientY: y, - movementX: 0, - movementY: 0, - screenX: sx, - screenY: sy, - })); -} - -export function lightOrDark(color: any) { - - // Variables for red, green, blue values - var r, g, b, hsp; - - // Check the format of the color, HEX or RGB? - if (color.match(/^rgb/)) { - - // If RGB --> store the red, green, blue values in separate variables - color = color.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/); - - r = color[1]; - g = color[2]; - b = color[3]; + if (rightClick) { + const me = + new MouseEvent("contextmenu", { + view: window, + bubbles: true, + cancelable: true, + button: 2, + clientX: x, + clientY: y, + movementX: 0, + movementY: 0, + screenX: sx, + screenY: sy, + }); + (me as any).dash = true; + element.dispatchEvent(me); } - else { - - // If hex --> Convert it to RGB: http://gist.github.com/983661 - color = +("0x" + color.slice(1).replace( - color.length < 5 && /./g, '$&$&')); - - r = color >> 16; - g = color >> 8 & 255; - b = color & 255; - } - - // HSP (Highly Sensitive Poo) equation from http://alienryderflex.com/hsp.html - hsp = Math.sqrt( - 0.299 * (r * r) + - 0.587 * (g * g) + - 0.114 * (b * b) - ); +} - // Using the HSP value, determine whether the color is light or dark - if (hsp > 127.5) { - return 'light'; +export function DashColor(color: string) { + try { + return color ? Color(color.toLowerCase()) : Color("transparent"); + } catch (e) { + console.log("COLOR error:", e); + return Color("red"); } - else { +} - return 'dark'; - } +export function lightOrDark(color: any) { + const nonAlphaColor = color.startsWith("#") ? (color as string).substring(0, 7) : + color.startsWith("rgba") ? color.replace(/,.[^,]*\)/, ")").replace("rgba", "rgb") : color; + const col = DashColor(nonAlphaColor).rgb(); + const colsum = (col.red() + col.green() + col.blue()); + if (colsum / col.alpha() > 400 || col.alpha() < 0.25) return Colors.DARK_GRAY; + else return Colors.WHITE; } export function getWordAtPoint(elem: any, x: number, y: number): string | undefined { + if (elem.tagName === "INPUT") return "input"; if (elem.nodeType === elem.TEXT_NODE) { const range = elem.ownerDocument.createRange(); range.selectNodeContents(elem); diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index a1ffddb98..da5c8efa9 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -39,7 +39,7 @@ import { DocFocusOptions } from "../views/nodes/DocumentView"; import { EquationBox } from "../views/nodes/EquationBox"; import { FieldViewProps } from "../views/nodes/FieldView"; import { FilterBox } from "../views/nodes/FilterBox"; -import { FontIconBox } from "../views/nodes/FontIconBox"; +import { FontIconBox } from "../views/nodes/button/FontIconBox"; import { FormattedTextBox } from "../views/nodes/formattedText/FormattedTextBox"; import { FunctionPlotBox } from "../views/nodes/FunctionPlotBox"; import { ImageBox } from "../views/nodes/ImageBox"; @@ -59,6 +59,7 @@ import { WebBox } from "../views/nodes/WebBox"; import { SearchBox } from "../views/search/SearchBox"; import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo"; import { DocumentType } from "./DocumentTypes"; +import { IconProp } from "@fortawesome/fontawesome-svg-core"; const path = require('path'); const defaultNativeImageDim = Number(DFLT_IMAGE_NATIVE_DIM.replace("px", "")); @@ -140,8 +141,8 @@ export class DocumentOptions { _columnWidth?: number; _columnsHideIfEmpty?: boolean; // whether stacking view column headings should be hidden _fontSize?: string; - _fontWeight?: number; _fontFamily?: string; + _fontWeight?: string; _pivotField?: string; // field key used to determine headings for sections in stacking, masonry, pivot views _curPage?: number; // current page of a PDF or other? paginated document _currentTimecode?: number; // the current timecode of a time-based document (e.g., current time of a video) value is in seconds @@ -156,6 +157,7 @@ export class DocumentOptions { z?: number; // whether document is in overlay (1) or not (0 or undefined) author?: string; _layoutKey?: string; + unrendered?: boolean; // denotes an annotation that is not rendered with a DocumentView (e.g, rtf/pdf text selections and links to scroll locations in web/pdf) type?: string; title?: string; "acl-Public"?: string; // public permissions @@ -163,6 +165,7 @@ export class DocumentOptions { version?: string; // version identifier for a document label?: string; hidden?: boolean; + _hidden?: boolean; mediaState?: string; // status of media document: "pendingRecording", "recording", "paused", "playing" autoPlayAnchors?: boolean; // whether to play audio/video when an anchor is clicked in a stackedTimeline. dontPlayLinkOnSelect?: boolean; // whether an audio/video should start playing when a link is followed to it. @@ -188,6 +191,7 @@ export class DocumentOptions { opacity?: number; defaultBackgroundColor?: string; _isLinkButton?: boolean; // marks a document as a button that will follow its primary link when clicked + _linkAutoMove?: boolean; // whether link endpoint should move around the edges of a document to make shortest path to other link endpoint isFolder?: boolean; lastFrame?: number; // the last frame of a frame-based collection (e.g., progressive slide) activeFrame?: number; // the active frame of a document in a frame base collection @@ -214,7 +218,33 @@ export class DocumentOptions { annotationOn?: Doc; isPushpin?: boolean; _removeDropProperties?: List<string>; // list of properties that should be removed from a document when it is dropped. e.g., a creator button may be forceActive to allow it be dragged, but the forceActive property can be removed from the dropped document + + // BACKGROUND GRID + _backgroundGridShow?: boolean; + + //BUTTONS + buttonText?: string; iconShape?: string; // shapes of the fonticon border + btnType?: string; + btnList?: List<string>; + docColorBtn?: string; + userColorBtn?: string; + canClick?: string; + script?: string; + numBtnType?: string; + numBtnMax?: number; + numBtnMin?: number; + switchToggle?: boolean; + + //LINEAR VIEW + linearViewIsExpanded?: boolean; // is linear view expanded + linearViewExpandable?: boolean; // can linear view be expanded + linearViewToggleButton?: string; // button to open close linear view group + linearViewSubMenu?: boolean; + linearViewFloating?: boolean; + flexGap?: number; // Linear view flex gap + flexDirection?: "unset" | "row" | "column" | "row-reverse" | "column-reverse"; + layout_linkView?: Doc; // view template for a link document layout_keyValue?: string; // view tempalte for key value docs linkRelationship?: string; // type of relatinoship a link represents @@ -246,7 +276,12 @@ export class DocumentOptions { treeViewHideTitle?: boolean; // whether to hide the top document title of a tree view treeViewHideHeader?: boolean; // whether to hide the header for a document in a tree view treeViewHideHeaderFields?: boolean; // whether to hide the drop down options for tree view items. - treeViewShowClearButton?: boolean; // whether a clear button should be displayed + + // Action Button + buttonMenu?: boolean; // whether a action button should be displayed + buttonMenuDoc?: Doc; + explainer?: string; + treeViewOpenIsTransient?: boolean; // ignores the treeViewOpen Doc flag, allowing a treeViewItem's expand/collapse state to be independent of other views of the same document in the same or any other tree view _treeViewOpen?: boolean; // whether this document is expanded in a tree view (note: need _ and regular versions since this can be specified for both proto and layout docs) treeViewOpen?: boolean; // whether this document is expanded in a tree view @@ -261,15 +296,14 @@ export class DocumentOptions { text?: string; textTransform?: string; // is linear view expanded letterSpacing?: string; // is linear view expanded - flexDirection?: "unset" | "row" | "column" | "row-reverse" | "column-reverse"; selectedIndex?: number; // which item in a linear view has been selected using the "thumb doc" ui clipboard?: Doc; searchQuery?: string; // for quersyBox - linearViewIsExpanded?: boolean; // is linear view expanded useLinkSmallAnchor?: boolean; // whether links to this document should use a miniature linkAnchorBox border?: string; //for searchbox hoverBackgroundColor?: string; // background color of a label when hovered linkRelationshipList?: List<string>; // for storing different link relationships (when set by user in the link editor) + linkRelationshipSizes?: List<number>; //stores number of links contained in each relationship linkColorList?: List<string>; // colors of links corresponding to specific link relationships } export namespace Docs { @@ -321,7 +355,7 @@ export namespace Docs { const TemplateMap: TemplateMap = new Map([ [DocumentType.RTF, { layout: { view: FormattedTextBox, dataField: "text" }, - options: { _height: 150, _xMargin: 10, _yMargin: 10, links: ComputedField.MakeFunction("links(self)") as any } + options: { _height: 150, _xMargin: 10, _yMargin: 10, nativeDimModifiable: true, nativeHeightUnfrozen: true, links: ComputedField.MakeFunction("links(self)") as any } }], [DocumentType.SEARCH, { layout: { view: SearchBox, dataField: defaultDataKey }, @@ -341,7 +375,7 @@ export namespace Docs { }], [DocumentType.WEB, { layout: { view: WebBox, dataField: defaultDataKey }, - options: { _height: 300, _fitWidth: true, links: ComputedField.MakeFunction("links(self)") as any } + options: { _height: 300, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true, links: ComputedField.MakeFunction("links(self)") as any } }], [DocumentType.COL, { layout: { view: CollectionView, dataField: defaultDataKey }, @@ -357,11 +391,11 @@ export namespace Docs { }], [DocumentType.AUDIO, { layout: { view: AudioBox, dataField: defaultDataKey }, - options: { _height: 35, backgroundColor: "lightGray", links: ComputedField.MakeFunction("links(self)") as any } + options: { _height: 100, backgroundColor: "lightGray", links: ComputedField.MakeFunction("links(self)") as any } }], [DocumentType.PDF, { layout: { view: PDFBox, dataField: defaultDataKey }, - options: { _curPage: 1, _fitWidth: true, links: ComputedField.MakeFunction("links(self)") as any } + options: { _curPage: 1, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true, links: ComputedField.MakeFunction("links(self)") as any } }], [DocumentType.IMPORT, { layout: { view: DirectoryImportBox, dataField: defaultDataKey }, @@ -370,7 +404,7 @@ export namespace Docs { [DocumentType.LINK, { layout: { view: LinkBox, dataField: defaultDataKey }, options: { - childDontRegisterViews: true, _isLinkButton: true, _height: 150, description: "", + childDontRegisterViews: true, _isLinkButton: true, _height: 150, description: "", showCaption: "description", backgroundColor: "lightblue", // lightblue is default color for linking dot and link documents text comment area links: ComputedField.MakeFunction("links(self)") as any, _removeDropProperties: new List(["_layerTags", "isLinkButton"]), @@ -399,7 +433,7 @@ export namespace Docs { }], [DocumentType.EQUATION, { layout: { view: EquationBox, dataField: defaultDataKey }, - options: { links: ComputedField.MakeFunction("links(self)") as any } + options: { links: ComputedField.MakeFunction("links(self)") as any, hideResizeHandles: true, hideDecorationTitle: true } }], [DocumentType.FUNCPLOT, { layout: { view: FunctionPlotBox, dataField: defaultDataKey }, @@ -432,9 +466,9 @@ export namespace Docs { layout: { view: CollectionView, dataField: defaultDataKey }, options: { links: ComputedField.MakeFunction("links(self)") as any, hideLinkButton: true } }], - [DocumentType.INK, { + [DocumentType.INK, { // NOTE: this is unused!! ink fields are filled in directly within the InkDocument() method layout: { view: InkingStroke, dataField: defaultDataKey }, - options: { _fontFamily: "cursive", backgroundColor: "transparent", links: ComputedField.MakeFunction("links(self)") as any } + options: { links: ComputedField.MakeFunction("links(self)") as any } }], [DocumentType.SCREENSHOT, { layout: { view: ScreenshotBox, dataField: defaultDataKey }, @@ -475,6 +509,14 @@ export namespace Docs { const prototypeIds = Object.values(DocumentType).filter(type => type !== DocumentType.NONE).map(type => type + suffix); // fetch the actual prototype documents from the server const actualProtos = Docs.newAccount ? {} : await DocServer.GetRefFields(prototypeIds); + if (!Docs.newAccount) { + Cast(actualProtos[DocumentType.WEB + suffix], Doc, null).nativeHeightUnfrozen = true; // to avoid having to recreate the DB + Cast(actualProtos[DocumentType.PDF + suffix], Doc, null).nativeHeightUnfrozen = true; + Cast(actualProtos[DocumentType.RTF + suffix], Doc, null).nativeHeightUnfrozen = true; + Cast(actualProtos[DocumentType.WEB + suffix], Doc, null).nativeDimModifiable = true; // to avoid having to recreate the DB + Cast(actualProtos[DocumentType.PDF + suffix], Doc, null).nativeDimModifiable = true; + Cast(actualProtos[DocumentType.RTF + suffix], Doc, null).nativeDimModifiable = true; + } // update this object to include any default values: DocumentOptions for all prototypes prototypeIds.map(id => { @@ -679,14 +721,16 @@ export namespace Docs { return linkDoc; } - export function InkDocument(color: string, tool: string, strokeWidth: string, strokeBezier: string, fillColor: string, arrowStart: string, arrowEnd: string, dash: string, points: { X: number, Y: number }[], options: DocumentOptions = {}) { + export function InkDocument(color: string, tool: string, strokeWidth: number, strokeBezier: string, fillColor: string, arrowStart: string, arrowEnd: string, dash: string, points: { X: number, Y: number }[], options: DocumentOptions = {}) { const I = new Doc(); I[Initializing] = true; I.type = DocumentType.INK; I.layout = InkingStroke.LayoutString("data"); I.color = color; + I.hideDecorationTitle = true; // don't show title when selected + I.hideOpenButton = true; // don't show open full screen button when selected I.fillColor = fillColor; - I.strokeWidth = Number(strokeWidth); + I.strokeWidth = strokeWidth; I.strokeBezier = strokeBezier; I.strokeStartMarker = arrowStart; I.strokeEndMarker = arrowEnd; @@ -829,7 +873,7 @@ 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, 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); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List([tabs, all]), { freezeChildren: "remove|add", ...options, _viewType: CollectionViewType.Docking, dockingConfig: config }, id); } export function DirectoryImportDocument(options: DocumentOptions = {}) { @@ -928,12 +972,20 @@ export namespace DocUtils { // facets that have a check next to them const checks = Object.keys(facet).filter(value => facet[value] === "check"); + // metadata facets that exist + const exists = Object.keys(facet).filter(value => facet[value] === "exists"); + + // metadata facets that exist + const unsets = Object.keys(facet).filter(value => facet[value] === "unset"); + // facets that have an x next to them const xs = Object.keys(facet).filter(value => facet[value] === "x"); - if (!xs.length && !checks.length && !matches.length) return true; + if (!unsets.length && !exists.length && !xs.length && !checks.length && !matches.length) return true; const failsNotEqualFacets = !xs.length ? false : xs.some(value => Doc.matchFieldValue(d, facetKey, value)); const satisfiesCheckFacets = !checks.length ? true : checks.some(value => Doc.matchFieldValue(d, facetKey, value)); + const satisfiesExistsFacets = !exists.length ? true : exists.some(value => d[facetKey] !== undefined); + const satisfiesUnsetsFacets = !unsets.length ? true : unsets.some(value => d[facetKey] === undefined); const satisfiesMatchFacets = !matches.length ? true : matches.some(value => { if (facetKey.startsWith("*")) { // fields starting with a '*' are used to match families of related fields. ie, *lastModified will match text-lastModified, data-lastModified, etc const allKeys = Array.from(Object.keys(d)); @@ -945,11 +997,11 @@ export namespace DocUtils { }); // if we're ORing them together, the default return is false, and we return true for a doc if it satisfies any one set of criteria if ((parentCollection?.currentFilter as Doc)?.filterBoolean === "OR") { - if (satisfiesCheckFacets && !failsNotEqualFacets && satisfiesMatchFacets) return true; + if (satisfiesUnsetsFacets && satisfiesExistsFacets && satisfiesCheckFacets && !failsNotEqualFacets && satisfiesMatchFacets) return true; } // if we're ANDing them together, the default return is true, and we return false for a doc if it doesn't satisfy any set of criteria else { - if (!satisfiesCheckFacets || failsNotEqualFacets || (matches.length && !satisfiesMatchFacets)) return false; + if (!satisfiesUnsetsFacets || !satisfiesExistsFacets || !satisfiesCheckFacets || failsNotEqualFacets || (matches.length && !satisfiesMatchFacets)) return false; } } @@ -1053,10 +1105,12 @@ export namespace DocUtils { "anchor2-useLinkSmallAnchor": target.doc.useLinkSmallAnchor ? true : undefined, "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, + linkDisplay: true, + _hidden: true, + _linkAutoMove: true, linkRelationship, - _layoutKey: "layout_linkView", + _showCaption: "description", + _showTitle: "linkRelationship", description }, id), showPopup); } @@ -1141,7 +1195,7 @@ export namespace DocUtils { export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number, simpleMenu: boolean = false): void { !simpleMenu && ContextMenu.Instance.addItem({ - description: "Add Note ...", + description: "Quick Notes", subitems: DocListCast((Doc.UserDoc()["template-notes"] as Doc).data).map((note, i) => ({ description: ":" + StrCast(note.title), event: undoBatch((args: { x: number, y: number }) => { @@ -1153,12 +1207,12 @@ export namespace DocUtils { textDoc[textDoc.layoutKey] = note; docTextAdder(textDoc); }), - icon: "eye" + icon: StrCast(note.icon) as IconProp })) as ContextMenuProps[], - icon: "eye" + icon: "sticky-note" }); - ContextMenu.Instance.addItem({ - description: ":=math", event: () => { + const math: ContextMenuProps = ({ + description: ":Math", event: () => { const created = Docs.Create.EquationDocument(); if (created) { created.author = Doc.CurrentUserEmail; @@ -1169,25 +1223,27 @@ export namespace DocUtils { EquationBox.SelectOnLoad = created[Id]; docAdder?.(created); } - }, icon: "compress-arrows-alt" + }, icon: "calculator" }); + const documentList: ContextMenuProps[] = DocListCast(Cast(Doc.UserDoc().myItemCreators, Doc, null)?.data).filter(btnDoc => !btnDoc.hidden).map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null)).filter(doc => doc && doc !== Doc.UserDoc().emptyPresentation).map((dragDoc, i) => ({ + description: ":" + StrCast(dragDoc.title), + event: undoBatch((args: { x: number, y: number }) => { + const newDoc = Doc.copyDragFactory(dragDoc); + if (newDoc) { + newDoc.author = Doc.CurrentUserEmail; + newDoc.x = x; + newDoc.y = y; + if (newDoc.type === DocumentType.RTF) FormattedTextBox.SelectOnLoad = newDoc[Id]; + docAdder?.(newDoc); + } + }), + icon: Doc.toIcon(dragDoc), + })) as ContextMenuProps[]; + documentList.push(math); ContextMenu.Instance.addItem({ - description: "Add Template Doc ...", - subitems: DocListCast(Cast(Doc.UserDoc().myItemCreators, Doc, null)?.data).filter(btnDoc => !btnDoc.hidden).map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null)).filter(doc => doc && doc !== Doc.UserDoc().emptyPresentation).map((dragDoc, i) => ({ - description: ":" + StrCast(dragDoc.title), - event: undoBatch((args: { x: number, y: number }) => { - const newDoc = Doc.copyDragFactory(dragDoc); - if (newDoc) { - newDoc.author = Doc.CurrentUserEmail; - newDoc.x = x; - newDoc.y = y; - if (newDoc.type === DocumentType.RTF) FormattedTextBox.SelectOnLoad = newDoc[Id]; - docAdder?.(newDoc); - } - }), - icon: "eye" - })) as ContextMenuProps[], - icon: "eye" + description: "Create document", + subitems: documentList, + icon: "file" }); }// applies a custom template to a document. the template is identified by it's short name (e.g, slideView not layout_slideView) export function makeCustomViewClicked(doc: Doc, creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = "custom", docLayoutTemplate?: Doc) { diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 185b8ac51..3c32c2359 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -20,8 +20,10 @@ 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 { TreeView } from "../views/collections/TreeView"; import { Colors } from "../views/global/globalEnums"; import { MainView } from "../views/MainView"; +import { ButtonType, NumButtonType } from "../views/nodes/button/FontIconBox"; import { FormattedTextBox } from "../views/nodes/formattedText/FormattedTextBox"; import { LabelBox } from "../views/nodes/LabelBox"; import { OverlayView } from "../views/OverlayView"; @@ -38,6 +40,23 @@ import { SharingManager } from "./SharingManager"; import { SnappingManager } from "./SnappingManager"; import { UndoManager } from "./UndoManager"; +interface Button { + title?: string; + toolTip?: string; + icon?: string; + btnType?: ButtonType; + click?: string; + numBtnType?: NumButtonType; + numBtnMin?: number; + numBtnMax?: number; + switchToggle?: boolean; + script?: string; + checkResult?: string; + width?: number; + list?: string[]; + ignoreClick?: boolean; + buttonText?: string; +} export let resolvedPorts: { server: number, socket: number }; const headerViewVersion = "0.1"; @@ -66,16 +85,17 @@ export class CurrentUserUtils { title: "NEW MOBILE BUTTON", onClick: undefined, }, - [this.ficon({ + [this.createToolButton({ ignoreClick: true, icon: "mobile", + btnType: ButtonType.ToolButton, backgroundColor: "transparent" }), this.mobileTextContainer({}, [this.mobileButtonText({}, "NEW MOBILE BUTTON"), this.mobileButtonInfo({}, "You can customize this button and make it your own.")])]); - doc["template-mobile-button"] = CurrentUserUtils.ficon({ + doc["template-mobile-button"] = CurrentUserUtils.createToolButton({ onDragStart: ScriptField.MakeFunction('copyDragFactory(this.dragFactory)'), - dragFactory: new PrefetchProxy(queryTemplate) as any as Doc, title: "mobile button", icon: "mobile" + dragFactory: new PrefetchProxy(queryTemplate) as any as Doc, title: "mobile button", icon: "mobile", btnType: ButtonType.ToolButton, }); } @@ -83,14 +103,15 @@ export class CurrentUserUtils { const slideTemplate = Docs.Create.MultirowDocument( [ Docs.Create.MulticolumnDocument([], { title: "data", _height: 200, system: true }), - Docs.Create.TextDocument("", { title: "text", _height: 100, system: true }) + Docs.Create.TextDocument("", { title: "text", _height: 100, system: true, _fontFamily: StrCast(Doc.UserDoc()._fontFamily), _fontSize: StrCast(Doc.UserDoc()._fontSize) }) ], { _width: 400, _height: 300, title: "slideView", _xMargin: 3, _yMargin: 3, system: true } ); slideTemplate.isTemplateDoc = makeTemplate(slideTemplate); - doc["template-button-slides"] = CurrentUserUtils.ficon({ + doc["template-button-slides"] = CurrentUserUtils.createToolButton({ onDragStart: ScriptField.MakeFunction('copyDragFactory(this.dragFactory)'), - dragFactory: new PrefetchProxy(slideTemplate) as any as Doc, title: "presentation slide", icon: "address-card" + dragFactory: new PrefetchProxy(slideTemplate) as any as Doc, title: "presentation slide", icon: "address-card", + btnType: ButtonType.ToolButton }); } @@ -134,9 +155,10 @@ export class CurrentUserUtils { }; linkTemplate.header = new RichTextField(JSON.stringify(rtf2), ""); - doc["template-button-link"] = CurrentUserUtils.ficon({ + doc["template-button-link"] = CurrentUserUtils.createToolButton({ onDragStart: ScriptField.MakeFunction('copyDragFactory(this.dragFactory)'), - dragFactory: new PrefetchProxy(linkTemplate) as any as Doc, title: "link view", icon: "window-maximize", system: true + dragFactory: new PrefetchProxy(linkTemplate) as any as Doc, title: "link view", icon: "window-maximize", system: true, + btnType: ButtonType.ToolButton }); } @@ -165,9 +187,10 @@ export class CurrentUserUtils { const box = MulticolumnDocument([/*no, */ yes, name], { title: "value", _width: 120, _height: 35, system: true }); box.isTemplateDoc = makeTemplate(box, true, "switch"); - doc["template-button-switch"] = CurrentUserUtils.ficon({ + doc["template-button-switch"] = CurrentUserUtils.createToolButton({ onDragStart: ScriptField.MakeFunction('copyDragFactory(this.dragFactory)'), - dragFactory: new PrefetchProxy(box) as any as Doc, title: "data switch", icon: "toggle-on", system: true + dragFactory: new PrefetchProxy(box) as any as Doc, title: "data switch", icon: "toggle-on", system: true, + btnType: ButtonType.ToolButton }); } @@ -203,9 +226,9 @@ export class CurrentUserUtils { const descriptionWrapper = MasonryDocument([details, short, long], { ...shared, ...descriptionWrapperOpts }); descriptionWrapper._columnHeaders = new List<SchemaHeaderField>([ - new SchemaHeaderField("[A Short Description]", "dimGray", undefined, undefined, undefined, false), - new SchemaHeaderField("[Long Description]", "dimGray", undefined, undefined, undefined, true), - new SchemaHeaderField("[Details]", "dimGray", undefined, undefined, undefined, true), + new SchemaHeaderField("[A Short Description]", "dimgray", undefined, undefined, undefined, false), + new SchemaHeaderField("[Long Description]", "dimgray", undefined, undefined, undefined, true), + new SchemaHeaderField("[Details]", "dimgray", undefined, undefined, undefined, true), ]); const detailView = Docs.Create.StackingDocument([carousel, descriptionWrapper], { ...shared, ...detailViewOpts, _chromeHidden: true, system: true }); detailView.isTemplateDoc = makeTemplate(detailView); @@ -214,9 +237,13 @@ export class CurrentUserUtils { short.title = "A Short Description"; long.title = "Long Description"; - doc["template-button-detail"] = CurrentUserUtils.ficon({ + doc["template-button-detail"] = CurrentUserUtils.createToolButton({ onDragStart: ScriptField.MakeFunction('copyDragFactory(this.dragFactory)'), - dragFactory: new PrefetchProxy(detailView) as any as Doc, title: "detailView", icon: "window-maximize", system: true + dragFactory: new PrefetchProxy(detailView) as any as Doc, + title: "detailView", + icon: "window-maximize", + system: true, + btnType: ButtonType.ToolButton, }); } @@ -231,7 +258,7 @@ export class CurrentUserUtils { doc["template-buttons"] = new PrefetchProxy(Docs.Create.MasonryDocument(requiredTypes, { title: "Advanced Item Prototypes", _xMargin: 0, _showTitle: "title", _chromeHidden: true, hidden: ComputedField.MakeFunction("IsNoviceMode()") as any, - _stayInCollection: true, _hideContextMenu: true, + _stayInCollection: true, _hideContextMenu: true, _forceActive: true, _autoHeight: true, _width: 500, _height: 300, _fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true, dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), system: true })); @@ -248,37 +275,47 @@ export class CurrentUserUtils { // setup the different note type skins static setupNoteTemplates(doc: Doc) { if (doc["template-note-Note"] === undefined) { - const noteView = Docs.Create.TextDocument("", { title: "text", isTemplateDoc: true, backgroundColor: "yellow", system: true }); + const noteView = Docs.Create.TextDocument("", { + title: "text", isTemplateDoc: true, backgroundColor: "yellow", system: true, icon: "sticky-note", + _fontFamily: StrCast(Doc.UserDoc()._fontFamily), _fontSize: StrCast(Doc.UserDoc()._fontSize), + }); noteView.isTemplateDoc = makeTemplate(noteView, true, "Note"); doc["template-note-Note"] = new PrefetchProxy(noteView); } if (doc["template-note-Idea"] === undefined) { - const noteView = Docs.Create.TextDocument("", { title: "text", backgroundColor: "pink", system: true }); + const noteView = Docs.Create.TextDocument("", { + title: "text", backgroundColor: "pink", system: true, icon: "lightbulb", + _fontFamily: StrCast(Doc.UserDoc()._fontFamily), _fontSize: StrCast(Doc.UserDoc()._fontSize), + }); noteView.isTemplateDoc = makeTemplate(noteView, true, "Idea"); doc["template-note-Idea"] = new PrefetchProxy(noteView); } if (doc["template-note-Topic"] === undefined) { - const noteView = Docs.Create.TextDocument("", { title: "text", backgroundColor: "lightblue", system: true }); - noteView.isTemplateDoc = makeTemplate(noteView, true, "Topic"); - doc["template-note-Topic"] = new PrefetchProxy(noteView); - } - if (doc["template-note-Todo"] === undefined) { const noteView = Docs.Create.TextDocument("", { - title: "text", backgroundColor: "orange", _autoHeight: false, _height: 100, _showCaption: "caption", - layout: FormattedTextBox.LayoutString("Todo"), caption: RichTextField.DashField("taskStatus"), system: true + title: "text", backgroundColor: "lightblue", system: true, icon: "book-open", + _fontFamily: StrCast(Doc.UserDoc()._fontFamily), _fontSize: StrCast(Doc.UserDoc()._fontSize), }); - noteView.isTemplateDoc = makeTemplate(noteView, true, "Todo"); - doc["template-note-Todo"] = new PrefetchProxy(noteView); - } - const taskStatusValues = [ - { title: "todo", _backgroundColor: "blue", color: "white", system: true }, - { title: "in progress", _backgroundColor: "yellow", color: "black", system: true }, - { title: "completed", _backgroundColor: "green", color: "white", system: true } - ]; - if (doc.fieldTypes === undefined) { - doc.fieldTypes = Docs.Create.TreeDocument([], { title: "field enumerations", system: true }); - DocUtils.addFieldEnumerations(Doc.GetProto(doc["template-note-Todo"] as any as Doc), "taskStatus", taskStatusValues); + noteView.isTemplateDoc = makeTemplate(noteView, true, "Topic"); + doc["template-note-Topic"] = new PrefetchProxy(noteView); } + // if (doc["template-note-Todo"] === undefined) { + // const noteView = Docs.Create.TextDocument("", { + // title: "text", backgroundColor: "orange", _autoHeight: false, _height: 100, _showCaption: "caption", + // layout: FormattedTextBox.LayoutString("Todo"), caption: RichTextField.DashField("taskStatus"), system: true, + // _fontFamily: StrCast(Doc.UserDoc()._fontFamily), _fontSize: StrCast(Doc.UserDoc()._fontSize), + // }); + // noteView.isTemplateDoc = makeTemplate(noteView, true, "Todo"); + // doc["template-note-Todo"] = new PrefetchProxy(noteView); + // } + // const taskStatusValues = [ + // { title: "todo", _backgroundColor: "blue", color: "white", system: true }, + // { title: "in progress", _backgroundColor: "yellow", color: "black", system: true }, + // { title: "completed", _backgroundColor: "green", color: "white", system: true } + // ]; + // if (doc.fieldTypes === undefined) { + // doc.fieldTypes = Docs.Create.TreeDocument([], { title: "field enumerations", system: true }); + // DocUtils.addFieldEnumerations(Doc.GetProto(doc["template-note-Todo"] as any as Doc), "taskStatus", taskStatusValues); + // } if (doc["template-notes"] === undefined) { doc["template-notes"] = new PrefetchProxy(Docs.Create.TreeDocument([doc["template-note-Note"] as any as Doc, doc["template-note-Idea"] as any as Doc, doc["template-note-Topic"] as any as Doc], // doc["template-note-Todo"] as any as Doc], @@ -312,7 +349,7 @@ export class CurrentUserUtils { static setupDefaultIconTemplates(doc: Doc) { if (doc["template-icon-view"] === undefined) { const iconView = Docs.Create.LabelDocument({ - title: "icon", textTransform: "unset", letterSpacing: "unset", layout: LabelBox.LayoutString("title"), _backgroundColor: "dimGray", + title: "icon", textTransform: "unset", letterSpacing: "unset", layout: LabelBox.LayoutString("title"), _backgroundColor: "dimgray", _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)"), system: true }); // Docs.Create.TextDocument("", { @@ -380,7 +417,7 @@ export class CurrentUserUtils { ((doc.emptyCollection as Doc).proto as Doc)["dragFactory-count"] = 0; } if (doc.emptyPane === undefined) { - doc.emptyPane = Docs.Create.FreeformDocument([], { _nativeWidth: undefined, _nativeHeight: undefined, _width: 500, _height: 800, title: "Untitled Tab", system: true, cloneFieldFilter: new List<string>(["system"]) }); + doc.emptyPane = Docs.Create.FreeformDocument([], { _nativeWidth: undefined, _backgroundGridShow: true, _nativeHeight: undefined, _width: 500, _height: 800, title: "Untitled Tab", system: true, cloneFieldFilter: new List<string>(["system"]) }); ((doc.emptyPane as Doc).proto as Doc)["dragFactory-count"] = 0; } if (doc.emptySlide === undefined) { @@ -410,16 +447,16 @@ export class CurrentUserUtils { storedMarks: [] }; const headerTemplate = Docs.Create.RTFDocument(new RichTextField(JSON.stringify(json), ""), { - title: "text", version: headerViewVersion, target: doc, _height: 70, _headerPointerEvents: "all", + title: "text", version: headerViewVersion, _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 = "<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>` + + ` <FormattedTextBox {...props} dontScale='true' fieldKey={'text'} height='calc(100% - ${headerBtnHgt}px - {this._headerHeight||0}px)'/>` + + " <FormattedTextBox {...props} dontScale='true' fieldKey={'header'} dontSelectOnLoad='true' ignoreAutoHeight='true' fontSize='{this._headerFontSize||9}px' height='{(this._headerHeight||0)}px' background='{this._headerColor || MySharedDocs().userColor||`lightGray`}' />" + + ` <HTMLdiv fontSize='${headerBtnHgt - 1}px' height='${headerBtnHgt}px' background='yellow' onClick={‘(this._headerHeight=scale*Math.min(Math.max(0,this._height-30),this._headerHeight===0?50:0)) + (this._autoHeightMargins=this._headerHeight ? this._headerHeight+${headerBtnHgt}:0)’} >Metadata</HTMLdiv>` + "</HTMLdiv>"; // "<div style={'height:100%'}>" + @@ -431,25 +468,29 @@ export class CurrentUserUtils { ((doc.emptyHeader as Doc).proto as Doc)["dragFactory-count"] = 0; } if (doc.emptyComparison === undefined) { - doc.emptyComparison = Docs.Create.ComparisonDocument({ title: "compare", _width: 300, _height: 300, system: true, cloneFieldFilter: new List<string>(["system"]) }); + doc.emptyComparison = Docs.Create.ComparisonDocument({ title: "comparison box", _width: 300, _height: 300, system: true, cloneFieldFilter: new List<string>(["system"]) }); } if (doc.emptyScript === undefined) { doc.emptyScript = Docs.Create.ScriptingDocument(undefined, { _width: 200, _height: 250, title: "script", system: true, cloneFieldFilter: new List<string>(["system"]) }); ((doc.emptyScript as Doc).proto as Doc)["dragFactory-count"] = 0; } if (doc.emptyScreenshot === undefined) { - doc.emptyScreenshot = Docs.Create.ScreenshotDocument("empty screenshot", { _fitWidth: true, _width: 400, _height: 200, system: true, cloneFieldFilter: new List<string>(["system"]) }); + doc.emptyScreenshot = Docs.Create.ScreenshotDocument("empty screenshot", { _fitWidth: true, title: "empty screenshot", _width: 400, _height: 200, system: true, cloneFieldFilter: new List<string>(["system"]) }); } if (doc.emptyWall === undefined) { doc.emptyWall = Docs.Create.ScreenshotDocument("", { _fitWidth: true, _width: 400, _height: 200, title: "screen snapshot", system: true, cloneFieldFilter: new List<string>(["system"]) }); (doc.emptyWall as Doc).videoWall = true; } if (doc.emptyAudio === undefined) { - doc.emptyAudio = Docs.Create.AudioDocument(nullAudio, { _width: 200, title: "audio recording", system: true, cloneFieldFilter: new List<string>(["system"]) }); + doc.emptyAudio = Docs.Create.AudioDocument(nullAudio, { _width: 200, _height: 100, title: "audio recording", system: true, cloneFieldFilter: new List<string>(["system"]) }); ((doc.emptyAudio as Doc).proto as Doc)["dragFactory-count"] = 0; } if (doc.emptyNote === undefined) { - doc.emptyNote = Docs.Create.TextDocument("", { _width: 200, title: "text note", _autoHeight: true, system: true, cloneFieldFilter: new List<string>(["system"]) }); + doc.emptyNote = Docs.Create.TextDocument("", { + _width: 200, title: "text note", _autoHeight: true, system: true, + _fontFamily: StrCast(Doc.UserDoc()._fontFamily), _fontSize: StrCast(Doc.UserDoc()._fontSize), + cloneFieldFilter: new List<string>(["system"]) + }); ((doc.emptyNote as Doc).proto as Doc)["dragFactory-count"] = 0; } if (doc.emptyImage === undefined) { @@ -460,7 +501,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, _height: 512, _width: 400, useCors: true, system: true, cloneFieldFilter: new List<string>(["system"]) }); + doc.emptyWebpage = Docs.Create.WebDocument("http://www.bing.com/", { title: "webpage", _nativeWidth: 850, _height: 512, _width: 400, useCors: true, system: true, cloneFieldFilter: new List<string>(["system"]) }); } if (doc.activeMobileMenu === undefined) { this.setupActiveMobileMenu(doc); @@ -469,10 +510,10 @@ export class CurrentUserUtils { { toolTip: "Tap to create a note in a new pane, drag for a note", title: "Note", icon: "sticky-note", click: 'openOnRight(copyDragFactory(this.clickFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyNote as Doc, noviceMode: true, clickFactory: doc.emptyNote as Doc, }, { toolTip: "Tap to create a collection in a new pane, drag for a collection", title: "Col", icon: "folder", click: 'openOnRight(copyDragFactory(this.clickFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyCollection as Doc, noviceMode: true, clickFactory: doc.emptyPane as Doc, }, { toolTip: "Tap to create a webpage in a new pane, drag for a webpage", title: "Web", icon: "globe-asia", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyWebpage as Doc, noviceMode: true }, - { toolTip: "Tap to create a progressive slide", title: "Slide", icon: "file", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptySlide as Doc, noviceMode: true }, + { toolTip: "Tap to create a progressive slide", title: "Slide", icon: "file", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptySlide as Doc }, { toolTip: "Tap to create a cat image in a new pane, drag for a cat image", title: "Image", icon: "cat", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyImage as Doc }, { toolTip: "Tap to create a comparison box in a new pane, drag for a comparison box", title: "Compare", icon: "columns", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyComparison as Doc, noviceMode: true }, - { toolTip: "Tap to create a screen grabber in a new pane, drag for a screen grabber", title: "Grab", icon: "photo-video", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyScreenshot as Doc, noviceMode: true }, + { toolTip: "Tap to create a screen grabber in a new pane, drag for a screen grabber", title: "Grab", icon: "photo-video", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyScreenshot as Doc }, { 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 }, @@ -502,11 +543,13 @@ export class CurrentUserUtils { icon, title, toolTip, + btnType: ButtonType.ToolButton, ignoreClick, _dropAction: "alias", onDragStart: drag ? ScriptField.MakeFunction(drag) : undefined, onClick: click ? ScriptField.MakeScript(click) : undefined, - backgroundColor, + backgroundColor: backgroundColor ? backgroundColor : Colors.DARK_GRAY, + color: Colors.WHITE, _hideContextMenu: true, _removeDropProperties: new List<string>(["_stayInCollection"]), _stayInCollection: true, @@ -519,7 +562,7 @@ export class CurrentUserUtils { if (dragCreatorSet === undefined) { doc.myItemCreators = new PrefetchProxy(Docs.Create.MasonryDocument(creatorBtns, { title: "Basic Item Creators", _showTitle: "title", _xMargin: 0, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true, - _autoHeight: true, _width: 500, _height: 300, _fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true, + _autoHeight: true, _width: 500, _height: 300, _fitWidth: true, _columnWidth: 40, ignoreClick: true, _lockedPosition: true, _forceActive: true, dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), system: true })); } else { @@ -532,31 +575,31 @@ export class CurrentUserUtils { 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: "Files", target: Cast(doc.myFilesystem, Doc, null), icon: "folder-open", click: 'selectMainMenu(self)' }, + { title: "Tools", target: Cast(doc.myTools, Doc, null), icon: "wrench", click: 'selectMainMenu(self)', hidden: "IsNoviceMode()" }, + { title: "Imports", target: Cast(doc.myImportDocs, 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: "Pres. Trails", target: Cast(doc.myPresentations, Doc, null), icon: "pres-trail", click: 'selectMainMenu(self)' }, - // { title: "Help", target: undefined as any, icon: "question-circle", click: 'selectMainMenu(self)' }, - // { title: "Settings", target: undefined as any, icon: "cog", click: 'selectMainMenu(self)' }, - { title: "User Doc", target: Cast(doc.myUserDoc, Doc, null), icon: "address-card", click: 'selectMainMenu(self)' }, + { title: "Shared with me", target: Cast(doc.mySharedDocs, Doc, null), icon: "users", click: 'selectMainMenu(self)', watchedDocuments: doc.mySharedDocs as Doc }, + { title: "Trails", target: Cast(doc.myTrails, Doc, null), icon: "pres-trail", click: 'selectMainMenu(self)' }, + { title: "User Doc", target: Cast(doc.myUserDoc, Doc, null), icon: "address-card", click: 'selectMainMenu(self)', hidden: "IsNoviceMode()" }, ]; } static async setupMenuPanel(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) { if (doc.menuStack === undefined) { await this.setupSharingSidebar(doc, sharingDocumentId, linkDatabaseId); // sets up the right sidebar collection for mobile upload documents and sharing - const menuBtns = (await CurrentUserUtils.menuBtnDescriptions(doc)).map(({ title, target, icon, click, watchedDocuments }) => + const menuBtns = (await CurrentUserUtils.menuBtnDescriptions(doc)).map(({ title, target, icon, click, watchedDocuments, hidden }) => Docs.Create.FontIconDocument({ icon, - iconShape: "square", + btnType: ButtonType.MenuButton, _stayInCollection: true, _hideContextMenu: true, + _chromeHidden: true, system: true, dontUndo: true, title, target, + hidden: hidden ? ComputedField.MakeFunction("IsNoviceMode()") as any : undefined, _dropAction: "alias", _removeDropProperties: new List<string>(["dropAction", "_stayInCollection"]), _width: 60, @@ -571,8 +614,6 @@ export class CurrentUserUtils { 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") { @@ -628,7 +669,7 @@ export class CurrentUserUtils { // SEts up mobile buttons for inside mobile menu static setupMobileButtons(doc?: Doc, buttons?: string[]) { - const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, activePen?: Doc, backgroundColor?: string, info: string, dragFactory?: Doc }[] = [ + const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, backgroundColor?: string, info: string, dragFactory?: Doc }[] = [ { title: "DASHBOARDS", icon: "bars", click: 'switchToMobileLibrary()', backgroundColor: "lightgrey", info: "Access your Dashboards from your mobile, and navigate through all of your documents. " }, { title: "UPLOAD", icon: "upload", click: 'openMobileUploads()', backgroundColor: "lightgrey", info: "Upload files from your mobile device so they can be accessed on Dash Web." }, { title: "MOBILE UPLOAD", icon: "mobile", click: 'switchToMobileUploadCollection()', backgroundColor: "lightgrey", info: "Access the collection of your mobile uploads." }, @@ -644,7 +685,7 @@ export class CurrentUserUtils { onClick: data.click ? ScriptField.MakeScript(data.click) : undefined, backgroundColor: data.backgroundColor, system: true }, - [this.ficon({ ignoreClick: true, icon: data.icon, backgroundColor: "rgba(0,0,0,0)", system: true }), this.mobileTextContainer({}, [this.mobileButtonText({}, data.title), this.mobileButtonInfo({}, data.info)])]) + [this.createToolButton({ ignoreClick: true, icon: data.icon, backgroundColor: "rgba(0,0,0,0)", system: true, btnType: ButtonType.ClickButton, }), this.mobileTextContainer({}, [this.mobileButtonText({}, data.title), this.mobileButtonInfo({}, data.info)])]) ); } @@ -753,8 +794,8 @@ export class CurrentUserUtils { } if (doc.myTools === undefined) { - const toolsStack = new PrefetchProxy(Docs.Create.StackingDocument([doc.myCreators as Doc, doc.myColorPicker as Doc], { - title: "My Tools", _width: 500, _yMargin: 20, ignoreClick: true, _lockedPosition: true, _forceActive: true, + const toolsStack = new PrefetchProxy(Docs.Create.StackingDocument([doc.myCreators as Doc], { + title: "My Tools", _showTitle: "title", _width: 500, _yMargin: 20, ignoreClick: true, _lockedPosition: true, _forceActive: true, system: true, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true, boxShadow: "0 0", })) as any as Doc; @@ -766,69 +807,93 @@ export class CurrentUserUtils { // setup dashboards library item await doc.myDashboards; if (doc.myDashboards === undefined) { + const newDashboard = ScriptField.MakeScript(`createNewDashboard(Doc.UserDoc())`); + const newDashboardButton: Doc = Docs.Create.FontIconDocument({ onClick: newDashboard, _forceActive: true, toolTip: "Create new dashboard", _stayInCollection: true, _hideContextMenu: true, title: "new dashboard", btnType: ButtonType.ClickButton, _width: 30, _height: 30, buttonText: "New trail", icon: "plus", system: true }); doc.myDashboards = new PrefetchProxy(Docs.Create.TreeDocument([], { 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", treeViewType: "fileSystem", isFolder: true, system: true + treeViewTruncateTitleWidth: 150, ignoreClick: true, buttonMenu: true, buttonMenuDoc: newDashboardButton, + _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", treeViewType: "fileSystem", isFolder: true, system: true, + explainer: "This is your collection of dashboards. A dashboard represents the tab configuration of your workspace. To manage documents as folders, go to the Files." })); - const newDashboard = ScriptField.MakeScript(`createNewDashboard(Doc.UserDoc())`); - const toggleTheme = ScriptField.MakeScript(`Doc.UserDoc().darkScheme = !Doc.UserDoc().darkScheme`); + const toggleDarkTheme = ScriptField.MakeScript(`this.colorScheme = this.colorScheme ? undefined : "${ColorScheme.Dark}"`); 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"]); + const developerFilter = ScriptField.MakeFunction('!IsNoviceMode()'); + // (doc.myDashboards as any as Doc).childContextMenuScripts = new List<ScriptField>([newDashboard!, shareDashboard!, removeDashboard!]); + // (doc.myDashboards as any as Doc).childContextMenuLabels = new List<string>(["Create New Dashboard", "Share Dashboard", "Remove Dashboard"]); + // (doc.myDashboards as any as Doc).childContextMenuIcons = new List<string>(["plus", "user-friends", "times"]); + (doc.myDashboards as any as Doc).childContextMenuScripts = new List<ScriptField>([newDashboard!, toggleDarkTheme!, toggleComic!, snapshotDashboard!, shareDashboard!, removeDashboard!]); + (doc.myDashboards as any as Doc).childContextMenuLabels = new List<string>(["Create New Dashboard", "Toggle Dark Theme", "Toggle Comic Mode", "Snapshot Dashboard", "Share Dashboard", "Remove Dashboard"]); + (doc.myDashboards as any as Doc).childContextMenuIcons = new List<string>(["plus", "chalkboard", "tv", "camera", "users", "times"]); + (doc.myDashboards as any as Doc).childContextMenuFilters = new List<ScriptField>([undefined as any, developerFilter, developerFilter, developerFilter, undefined as any, undefined as any]); } return doc.myDashboards as any as Doc; } static async setupPresentations(doc: Doc) { - await doc.myPresentations; - if (doc.myPresentations === undefined) { - doc.myPresentations = new PrefetchProxy(Docs.Create.TreeDocument([], { + await doc.myTrails; + if (doc.myTrails === undefined) { + const newTrail = ScriptField.MakeScript(`createNewPresentation()`); + const newTrailButton: Doc = Docs.Create.FontIconDocument({ onClick: newTrail, _forceActive: true, toolTip: "Create new trail", _stayInCollection: true, _hideContextMenu: true, title: "New trail", btnType: ButtonType.ClickButton, _width: 30, _height: 30, buttonText: "New trail", icon: "plus", system: true }); + doc.myTrails = new PrefetchProxy(Docs.Create.TreeDocument([], { 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 + treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _fitWidth: true, _gridGap: 5, _forceActive: true, childDropAction: "alias", + treeViewTruncateTitleWidth: 150, ignoreClick: true, buttonMenu: true, buttonMenuDoc: newTrailButton, + _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true, + explainer: "All of the trails that you have created will appear here." })); - const newPresentations = ScriptField.MakeScript(`createNewPresentation()`); - (doc.myPresentations as any as Doc).contextMenuScripts = new List<ScriptField>([newPresentations!]); - (doc.myPresentations as any as Doc).contextMenuLabels = new List<string>(["Create New Presentation"]); - const presentations = doc.myPresentations as any as Doc; + (doc.myTrails as any as Doc).contextMenuScripts = new List<ScriptField>([newTrail!]); + (doc.myTrails as any as Doc).contextMenuLabels = new List<string>(["Create New Trail"]); + (doc.myTrails as any as Doc).childContextMenuIcons = new List<string>(["plus"]); } - return doc.myPresentations as any as Doc; + return doc.myTrails as any as Doc; } static async setupFilesystem(doc: Doc) { await doc.myFilesystem; if (doc.myFilesystem === undefined) { 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", _showTitle: "title", _height: 100, + // doc.myFileRoot = Docs.Create.TreeDocument([], { title: "file root", _stayInCollection: true, system: true, isFolder: true }); + const newFolder = ScriptField.MakeFunction(`makeTopLevelFolder()`, { scriptContext: "any" })!; + const newFolderButton: Doc = Docs.Create.FontIconDocument({ + onClick: newFolder, _forceActive: true, toolTip: "Create new folder", + _stayInCollection: true, _hideContextMenu: true, title: "New folder", btnType: ButtonType.ClickButton, _width: 30, _height: 30, + buttonText: "New folder", icon: "folder-plus", system: true + }); + doc.myFilesystem = new PrefetchProxy(Docs.Create.TreeDocument([doc.myFileOrphans as Doc], { + title: "My Documents", _showTitle: "title", buttonMenu: true, buttonMenuDoc: newFolderButton, _height: 100, treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, childDropAction: "alias", treeViewTruncateTitleWidth: 150, ignoreClick: true, isFolder: true, treeViewType: "fileSystem", childHideLinkButton: true, - _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "proto", system: true + _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "proto", system: true, + explainer: "This is your file manager where you can create folders to keep track of documents independently of your dashboard." })); + (doc.myFilesystem as any as Doc).contextMenuScripts = new List<ScriptField>([newFolder]); + (doc.myFilesystem as any as Doc).contextMenuLabels = new List<string>(["Create new folder"]); + (doc.myFilesystem as any as Doc).childContextMenuIcons = new List<string>(["plus"]); } return doc.myFilesystem as any as Doc; } static setupRecentlyClosedDocs(doc: Doc) { - // setup Recently Closed library item if (doc.myRecentlyClosedDocs === undefined) { + const clearAll = ScriptField.MakeScript(`getProto(self).data = new List([])`); + const clearDocsButton: Doc = Docs.Create.FontIconDocument({ onClick: clearAll, _forceActive: true, toolTip: "Empty recently closed", _stayInCollection: true, _hideContextMenu: true, title: "Empty", btnType: ButtonType.ClickButton, _width: 30, _height: 30, buttonText: "Empty", icon: "trash", system: true }); doc.myRecentlyClosedDocs = new PrefetchProxy(Docs.Create.TreeDocument([], { - title: "Recently Closed", _showTitle: "title", treeViewShowClearButton: true, childHideLinkButton: true, + title: "My Recently Closed", _showTitle: "title", buttonMenu: true, buttonMenuDoc: clearDocsButton, 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", system: true, + explainer: "Recently closed documents appear in this menu. They will only be deleted if you explicity empty this list." + })); - const clearAll = ScriptField.MakeScript(`getProto(self).data = new List([])`); (doc.myRecentlyClosedDocs as any as Doc).contextMenuScripts = new List<ScriptField>([clearAll!]); - (doc.myRecentlyClosedDocs as any as Doc).contextMenuLabels = new List<string>(["Clear All"]); + (doc.myRecentlyClosedDocs as any as Doc).contextMenuLabels = new List<string>(["Empty recently closed"]); + (doc.myRecentlyClosedDocs as any as Doc).contextMenuIcons = new List<string>(["trash"]); + } } @@ -836,7 +901,7 @@ export class CurrentUserUtils { // setup Filter item if (doc.currentFilter === undefined) { doc.currentFilter = Docs.Create.FilterDocument({ - title: "unnamed filter", _height: 150, + title: "Unnamed Filter", _height: 150, treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, childDropAction: "none", treeViewTruncateTitleWidth: 150, ignoreClick: true, _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true, _autoHeight: true, _fitWidth: true @@ -881,31 +946,227 @@ export class CurrentUserUtils { CurrentUserUtils.setupUserDoc(doc); } - static blist = (opts: DocumentOptions, docs: Doc[]) => new PrefetchProxy(Docs.Create.LinearDocument(docs, { - ...opts, _gridGap: 5, _xMargin: 5, _yMargin: 5, _height: 42, _width: 100, boxShadow: "0 0", _forceActive: true, + static linearButtonList = (opts: DocumentOptions, docs: Doc[]) => new PrefetchProxy(Docs.Create.LinearDocument(docs, { + ...opts, _gridGap: 0, _xMargin: 5, _yMargin: 5, boxShadow: "0 0", _forceActive: true, dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), - backgroundColor: "black", _lockedPosition: true, linearViewIsExpanded: true, system: true + _lockedPosition: true, system: true, flexDirection: "row" })) as any as Doc - static ficon = (opts: DocumentOptions) => new PrefetchProxy(Docs.Create.FontIconDocument({ - ...opts, _dropAction: "alias", _removeDropProperties: new List<string>(["_dropAction", "stayInCollection"]), _nativeWidth: 40, _nativeHeight: 40, _width: 40, _height: 40, system: true + static createToolButton = (opts: DocumentOptions) => new PrefetchProxy(Docs.Create.FontIconDocument({ + ...opts, btnType: ButtonType.ToolButton, _forceActive: true, _dropAction: "alias", _removeDropProperties: new List<string>(["_dropAction", "stayInCollection"]), _nativeWidth: 40, _nativeHeight: 40, _width: 40, _height: 40, system: true })) as any as Doc /// sets up the default list of buttons to be shown in the expanding button menu at the bottom of the Dash window static setupDockedButtons(doc: Doc) { if (doc["dockedBtn-undo"] === undefined) { - doc["dockedBtn-undo"] = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("undo()"), dontUndo: true, _stayInCollection: true, _dropAction: "alias", _hideContextMenu: true, _removeDropProperties: new List<string>(["dropAction", "_hideContextMenu", "stayInCollection"]), toolTip: "click to undo", title: "undo", icon: "undo-alt", system: true }); + doc["dockedBtn-undo"] = CurrentUserUtils.createToolButton({ onClick: ScriptField.MakeScript("undo()"), _width: 30, _height: 30, dontUndo: true, _stayInCollection: true, btnType: ButtonType.ToolButton, _dropAction: "alias", _hideContextMenu: true, _removeDropProperties: new List<string>(["dropAction", "_hideContextMenu", "stayInCollection"]), toolTip: "Click to undo", title: "undo", icon: "undo-alt", system: true }); } if (doc["dockedBtn-redo"] === undefined) { - doc["dockedBtn-redo"] = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("redo()"), dontUndo: true, _stayInCollection: true, _dropAction: "alias", _hideContextMenu: true, _removeDropProperties: new List<string>(["dropAction", "_hideContextMenu", "stayInCollection"]), toolTip: "click to redo", title: "redo", icon: "redo-alt", system: true }); + doc["dockedBtn-redo"] = CurrentUserUtils.createToolButton({ onClick: ScriptField.MakeScript("redo()"), _width: 30, _height: 30, dontUndo: true, _stayInCollection: true, btnType: ButtonType.ToolButton, _dropAction: "alias", _hideContextMenu: true, _removeDropProperties: new List<string>(["dropAction", "_hideContextMenu", "stayInCollection"]), toolTip: "Click to redo", title: "redo", icon: "redo-alt", system: true }); } if (doc.dockedBtns === undefined) { - doc.dockedBtns = CurrentUserUtils.blist({ title: "docked buttons", ignoreClick: true }, [doc["dockedBtn-undo"] as Doc, doc["dockedBtn-redo"] as Doc]); + doc.dockedBtns = CurrentUserUtils.linearButtonList({ title: "docked buttons", _height: 40, flexGap: 0, linearViewFloating: true, linearViewIsExpanded: true, linearViewExpandable: true, ignoreClick: true }, [doc["dockedBtn-undo"] as Doc, doc["dockedBtn-redo"] as Doc]); } (doc["dockedBtn-undo"] as Doc).dontUndo = true; (doc["dockedBtn-redo"] as Doc).dontUndo = true; } + static textTools(doc: Doc) { + const tools: Button[] = + [ + { + title: "Font", toolTip: "Font", width: 100, btnType: ButtonType.DropdownList, ignoreClick: true, + list: ["Roboto", "Roboto Mono", "Nunito", "Times New Roman", "Arial", "Georgia", + "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"], + script: 'setFont' + }, + { title: "Font size", toolTip: "Font size", width: 75, btnType: ButtonType.NumberButton, numBtnMax: 200, numBtnMin: 0, numBtnType: NumButtonType.DropdownOptions, ignoreClick: true, script: 'setFontSize' }, + { title: "Font color", toolTip: "Font color", btnType: ButtonType.ColorButton, icon: "font", ignoreClick: true, script: 'setFontColor' }, + { title: "Bold", toolTip: "Bold (Ctrl+B)", btnType: ButtonType.ToggleButton, icon: "bold", click: 'toggleBold()', checkResult: 'toggleBold(true)' }, + { title: "Italic", toolTip: "Italic (Ctrl+I)", btnType: ButtonType.ToggleButton, icon: "italic", click: 'toggleItalic()', checkResult: 'toggleItalic(true)' }, + { title: "Underline", toolTip: "Underline (Ctrl+U)", btnType: ButtonType.ToggleButton, icon: "underline", click: 'toggleUnderline()', checkResult: 'toggleUnderline(true)' }, + { title: "Bullet List", toolTip: "Bullet", btnType: ButtonType.ToggleButton, icon: "list", click: 'setBulletList("bullet")', checkResult: 'setBulletList("bullet", true)' }, + { title: "Number List", toolTip: "Number", btnType: ButtonType.ToggleButton, icon: "list-ol", click: 'setBulletList("decimal")', checkResult: 'setBulletList("decimal", true)' }, + + // { title: "Strikethrough", tooltip: "Strikethrough", btnType: ButtonType.ToggleButton, icon: "strikethrough", click: 'toggleStrikethrough()'}, + // { title: "Superscript", tooltip: "Superscript", btnType: ButtonType.ToggleButton, icon: "superscript", click: 'toggleSuperscript()'}, + // { title: "Subscript", tooltip: "Subscript", btnType: ButtonType.ToggleButton, icon: "subscript", click: 'toggleSubscript()'}, + { title: "Left align", toolTip: "Left align", btnType: ButtonType.ToggleButton, icon: "align-left", click: 'setAlignment("left")', checkResult: 'setAlignment("left", true)' }, + { title: "Center align", toolTip: "Center align", btnType: ButtonType.ToggleButton, icon: "align-center", click: 'setAlignment("center")', checkResult: 'setAlignment("center", true)' }, + { title: "Right align", toolTip: "Right align", btnType: ButtonType.ToggleButton, icon: "align-right", click: 'setAlignment("right")', checkResult: 'setAlignment("right", true)' }, + ]; + return tools; + } + + static inkTools(doc: Doc) { + const tools: Button[] = [ + { title: "Pen", toolTip: "Pen (Ctrl+P)", btnType: ButtonType.ToggleButton, icon: "pen", click: 'setActiveInkTool("pen")', checkResult: 'setActiveInkTool("pen" , true)' }, + // { title: "Highlighter", toolTip: "Highlighter (Ctrl+H)", btnType: ButtonType.ToggleButton, icon: "highlighter", click: 'setActiveInkTool("highlighter")', checkResult: 'setActiveInkTool("highlighter", true)' }, + { title: "Circle", toolTip: "Circle (Ctrl+Shift+C)", btnType: ButtonType.ToggleButton, icon: "circle", click: 'setActiveInkTool("circle")', checkResult: 'setActiveInkTool("circle" , true)' }, + // { title: "Square", toolTip: "Square (Ctrl+Shift+S)", btnType: ButtonType.ToggleButton, icon: "square", click: 'setActiveInkTool("square")', checkResult: 'setActiveInkTool("square" , true)' }, + { title: "Line", toolTip: "Line (Ctrl+Shift+L)", btnType: ButtonType.ToggleButton, icon: "minus", click: 'setActiveInkTool("line")', checkResult: 'setActiveInkTool("line" , true)' }, + { title: "Fill color", toolTip: "Fill color", btnType: ButtonType.ColorButton, ignoreClick: true, icon: "fill-drip", script: "setFillColor" }, + { title: "Stroke width", toolTip: "Stroke width", btnType: ButtonType.NumberButton, numBtnType: NumButtonType.Slider, numBtnMin: 1, ignoreClick: true, script: 'setStrokeWidth' }, + { title: "Stroke color", toolTip: "Stroke color", btnType: ButtonType.ColorButton, icon: "pen", ignoreClick: true, script: 'setStrokeColor' }, + ]; + return tools; + } + + static schemaTools(doc: Doc) { + const tools: Button[] = + [ + { + title: "Show preview", + toolTip: "Show preview of selected document", + btnType: ButtonType.ToggleButton, + buttonText: "Show Preview", + icon: "eye", + click: 'toggleSchemaPreview()', + checkResult: 'toggleSchemaPreview(true)' + }, + ]; + return tools; + } + + static webTools(doc: Doc) { + const tools: Button[] = + [ + { title: "Back", toolTip: "Go back", btnType: ButtonType.ClickButton, icon: "arrow-left", click: 'webBack()' }, + { title: "Forward", toolTip: "Go forward", btnType: ButtonType.ClickButton, icon: "arrow-right", click: 'webForward()' }, + //{ title: "Reload", toolTip: "Reload webpage", btnType: ButtonType.ClickButton, icon: "redo-alt", click: 'webReload()' }, + { title: "URL", toolTip: "URL", width: 250, btnType: ButtonType.EditableText, icon: "lock", ignoreClick: true, script: 'webSetURL' }, + ]; + + return tools; + } + + static async contextMenuTools(doc: Doc) { + return [ + { + title: "Perspective", toolTip: "View", width: 100, btnType: ButtonType.DropdownList, ignoreClick: true, + list: [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Tree, + CollectionViewType.Stacking, CollectionViewType.Masonry, CollectionViewType.Multicolumn, + CollectionViewType.Multirow, CollectionViewType.Time, CollectionViewType.Carousel, + CollectionViewType.Carousel3D, CollectionViewType.Linear, CollectionViewType.Map, + CollectionViewType.Grid], + script: 'setView', + }, // Always show + { + title: "Background Color", toolTip: "Background Color", btnType: ButtonType.ColorButton, ignoreClick: true, icon: "fill-drip", + script: "setBackgroundColor", hidden: 'selectedDocumentType()' + }, // Only when a document is selected + { + title: "Header Color", toolTip: "Header Color", btnType: ButtonType.ColorButton, ignoreClick: true, icon: "heading", + script: "setHeaderColor", hidden: 'selectedDocumentType()', + }, // Only when a document is selected + { title: "Overlay", toolTip: "Overlay", btnType: ButtonType.ToggleButton, icon: "layer-group", click: 'toggleOverlay()', checkResult: 'toggleOverlay(true)', hidden: 'selectedDocumentType(undefined, "freeform", true)' }, // Only when floating document is selected in freeform + // { title: "Alias", btnType: ButtonType.ClickButton, icon: "copy", hidden: 'selectedDocumentType()' }, // Only when a document is selected + { title: "Text", type: "textTools", subMenu: true, expanded: 'selectedDocumentType("rtf")' }, // Always available + { title: "Ink", type: "inkTools", subMenu: true, expanded: 'selectedDocumentType("ink")' }, // Always available + { title: "Web", type: "webTools", subMenu: true, hidden: 'selectedDocumentType("web")' }, // Only when Web is selected + { title: "Schema", type: "schemaTools", subMenu: true, hidden: 'selectedDocumentType(undefined, "schema")' } // Only when Schema is selected + ]; + } + + // Sets up the default context menu buttons + static async setupContextMenuButtons(doc: Doc) { + if (doc.contextMenuBtns === undefined) { + const docList: Doc[] = []; + + (await CurrentUserUtils.contextMenuTools(doc)).map(({ title, width, list, toolTip, ignoreClick, icon, type, btnType, click, script, subMenu, hidden, expanded, checkResult }) => { + const menuDocList: Doc[] = []; + if (subMenu) { + // default is textTools + let tools: Button[]; + switch (type) { + case "inkTools": + tools = CurrentUserUtils.inkTools(doc); + break; + case "schemaTools": + tools = CurrentUserUtils.schemaTools(doc); + break; + case "webTools": + tools = CurrentUserUtils.webTools(doc); + break; + case "textTools": + tools = CurrentUserUtils.textTools(doc); + break; + default: + tools = CurrentUserUtils.textTools(doc); + break; + } + tools.map(({ title, toolTip, icon, btnType, numBtnType, numBtnMax, numBtnMin, click, script, width, list, ignoreClick, switchToggle, checkResult }) => { + menuDocList.push(Docs.Create.FontIconDocument({ + _nativeWidth: width ? width : 25, + _nativeHeight: 25, + _width: width ? width : 25, + _height: 25, + icon, + toolTip, + numBtnType, + numBtnMin, + numBtnMax, + script, + btnType: btnType, + btnList: new List<string>(list), + ignoreClick: ignoreClick, + _stayInCollection: true, + _hideContextMenu: true, + _lockedPosition: true, + system: true, + dontUndo: true, + title, + switchToggle, + color: Colors.WHITE, + backgroundColor: checkResult ? ComputedField.MakeFunction(checkResult) as any : "transparent", + _dropAction: "alias", + _removeDropProperties: new List<string>(["dropAction", "_stayInCollection"]), + onClick: click ? ScriptField.MakeScript(click, { doc: Doc.name }) : undefined + })); + }); + docList.push(CurrentUserUtils.linearButtonList({ + linearViewSubMenu: true, + flexGap: 0, + ignoreClick: true, + linearViewExpandable: true, + icon: title, + _height: 30, + backgroundColor: checkResult ? ComputedField.MakeFunction(checkResult) as any : "transparent", + linearViewIsExpanded: expanded ? !(ComputedField.MakeFunction(expanded) as any) : undefined, + hidden: hidden ? ComputedField.MakeFunction(hidden) as any : undefined, + }, menuDocList)); + } else { + docList.push(Docs.Create.FontIconDocument({ + _nativeWidth: width ? width : 25, + _nativeHeight: 25, + _width: width ? width : 25, + _height: 25, + icon, + toolTip, + script, + btnType, + btnList: new List<string>(list), + ignoreClick, + _stayInCollection: true, + _hideContextMenu: true, + _lockedPosition: true, + system: true, + dontUndo: true, + title, + color: Colors.WHITE, + backgroundColor: "transparent", + _dropAction: "alias", + hidden: hidden ? ComputedField.MakeFunction(hidden) as any : undefined, + _removeDropProperties: new List<string>(["dropAction", "_stayInCollection"]), + onClick: click ? ScriptField.MakeScript(click, { scriptContext: "any" }) : undefined + })); + } + }); + + doc.contextMenuBtns = CurrentUserUtils.linearButtonList({ title: "menu buttons", flexGap: 0, linearViewIsExpanded: true, ignoreClick: true, linearViewExpandable: false, _height: 35 }, docList); + } + } + // sets up the default set of documents to be shown in the Overlay layer static setupOverlays(doc: Doc) { if (doc.myOverlayDocs === undefined) { @@ -935,13 +1196,15 @@ export class CurrentUserUtils { } doc.myLinkDatabase = new PrefetchProxy(linkDocs); } + // TODO:glr NOTE: treeViewHideTitle & _showTitle may be confusing, treeViewHideTitle is for the editable title (just for tree view), _showTitle is to show the Document title for any document if (doc.mySharedDocs === undefined) { let sharedDocs = Docs.newAccount ? undefined : await DocServer.GetRefField(sharingDocumentId + "outer"); if (!sharedDocs) { 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, + _showTitle: "title", treeViewHideTitle: true, ignoreClick: true, _lockedPosition: true, "acl-Public": SharingPermissions.Augment, "_acl-Public": SharingPermissions.Augment, _chromeHidden: true, boxShadow: "0 0", + explainer: "This is where documents or dashboards that other users have shared with you will appear. To share a document or dashboard right click and select 'Share'" }, sharingDocumentId + "outer", sharingDocumentId); (sharedDocs as Doc)["acl-Public"] = (sharedDocs as Doc)[DataSym]["acl-Public"] = SharingPermissions.Augment; } @@ -952,6 +1215,8 @@ export class CurrentUserUtils { sharedDocs.childContextMenuFilters = new List<ScriptField>([dashboardFilter!,]); sharedDocs.childContextMenuScripts = new List<ScriptField>([addToDashboards!,]); sharedDocs.childContextMenuLabels = new List<string>(["Add to Dashboards",]); + sharedDocs.childContextMenuIcons = new List<string>(["user-plus",]); + } doc.mySharedDocs = new PrefetchProxy(sharedDocs); } @@ -960,24 +1225,21 @@ export class CurrentUserUtils { // Import sidebar is where shared documents are contained static setupImportSidebar(doc: Doc) { if (doc.myImportDocs === undefined) { + const newImportButton: Doc = Docs.Create.FontIconDocument({ onClick: ScriptField.MakeScript("importDocument()"), _forceActive: true, toolTip: "Import from computer", _width: 30, _height: 30, _stayInCollection: true, _hideContextMenu: true, title: "Import", btnType: ButtonType.ClickButton, buttonText: "Import", icon: "upload", system: true }); doc.myImportDocs = new PrefetchProxy(Docs.Create.StackingDocument([], { - 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, + title: "My Imports", _forceActive: true, buttonMenu: true, buttonMenuDoc: newImportButton, ignoreClick: true, _showTitle: "title", _stayInCollection: true, _hideContextMenu: true, childLimitHeight: 0, + childDropAction: "copy", _autoHeight: true, _yMargin: 50, _gridGap: 15, boxShadow: "0 0", _lockedPosition: true, system: true, _chromeHidden: true, + explainer: "This is where documents that are Imported into Dash will go." })); } - 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, _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 + backgroundColor: "dimgray", ignoreClick: true, _searchDoc: true, + childDropAction: "alias", _lockedPosition: true, _viewType: CollectionViewType.Schema, title: "Search Panel", system: true })) as any as Doc; } } @@ -1041,14 +1303,14 @@ export class CurrentUserUtils { }, { 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.textAlign = StrCast(doc.textAlign, "left"); doc.activeInkColor = StrCast(doc.activeInkColor, "rgb(0, 0, 0)"); - doc.activeInkWidth = StrCast(doc.activeInkWidth, "1"); + doc.activeInkWidth = Number(StrCast(doc.activeInkWidth, "1")); doc.activeInkBezier = StrCast(doc.activeInkBezier, "0"); doc.activeFillColor = StrCast(doc.activeFillColor, ""); doc.activeArrowStart = StrCast(doc.activeArrowStart, ""); @@ -1059,7 +1321,6 @@ export class CurrentUserUtils { doc.fontColor = StrCast(doc.fontColor, "black"); doc.fontHighlight = StrCast(doc.fontHighlight, ""); 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); doc["constants-snapThreshold"] = NumCast(doc["constants-snapThreshold"], 10); // @@ -1069,10 +1330,11 @@ export class CurrentUserUtils { doc.filterDocCount = 0; this.setupDefaultIconTemplates(doc); // creates a set of icon templates triggered by the document deoration icon this.setupDocTemplates(doc); // sets up the template menu of templates - this.setupImportSidebar(doc); + this.setupImportSidebar(doc); // sets up the import sidebar this.setupSearchSidebar(doc); // sets up the search sidebar this.setupActiveMobileMenu(doc); // sets up the current mobile menu for Dash Mobile this.setupOverlays(doc); // documents in overlay layer + this.setupContextMenuButtons(doc); // set up context menu buttons this.setupDockedButtons(doc); // the bottom bar of font icons await this.setupSidebarButtons(doc); // the pop-out left sidebar of tools/panels await this.setupMenuPanel(doc, sharingDocumentId, linkDatabaseId); @@ -1095,6 +1357,13 @@ export class CurrentUserUtils { // }); setTimeout(() => DocServer.UPDATE_SERVER_CACHE(), 2500); doc.fieldInfos = await Docs.setupFieldInfos(); + if (doc.activeDashboard instanceof Doc) { + // undefined means ColorScheme.Light until all CSS is updated with values for each color scheme (e.g., see MainView.scss, DocumentDecorations.scss) + doc.activeDashboard.colorScheme = doc.activeDashboard.colorScheme === ColorScheme.Light ? undefined : doc.activeDashboard.colorScheme; + } + if (doc.activeCollectionBackground === "white") { // temporary to avoid having to rebuild the databse for old accounts that have this set by default. + doc.activeCollectionBackground = undefined; + } return doc; } @@ -1219,7 +1488,6 @@ export class CurrentUserUtils { } public static createNewDashboard = async (userDoc: Doc, id?: string) => { - const myPresentations = await userDoc.myPresentations as Doc; const presentation = Doc.MakeCopy(userDoc.emptyPresentation as Doc, true); const dashboards = await Cast(userDoc.myDashboards, Doc) as Doc; const dashboardCount = DocListCast(dashboards.data).length + 1; @@ -1231,6 +1499,7 @@ export class CurrentUserUtils { _width: 1500, _height: 1000, _fitWidth: true, + _backgroundGridShow: true, title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}`, }; const freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions); @@ -1255,18 +1524,16 @@ export class CurrentUserUtils { 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; Doc.AddDocToList(dashboards, "data", dashboardDoc); CurrentUserUtils.openDashboard(userDoc, dashboardDoc); } - public static GetNewTextDoc(title: string, x: number, y: number, width?: number, height?: number, noMargins?: boolean, annotationOn?: Doc, maxHeight?: number) { + public static GetNewTextDoc(title: string, x: number, y: number, width?: number, height?: number, noMargins?: boolean, annotationOn?: Doc, maxHeight?: number, backgroundColor?: string) { const tbox = Docs.Create.TextDocument("", { - _xMargin: noMargins ? 0 : undefined, _yMargin: noMargins ? 0 : undefined, annotationOn, docMaxAutoHeight: maxHeight, - _width: width || 200, _height: height || 100, x: x, y: y, _fitWidth: true, _autoHeight: true, _fontSize: StrCast(Doc.UserDoc().fontSize), - _fontFamily: StrCast(Doc.UserDoc().fontFamily), title + _xMargin: noMargins ? 0 : undefined, _yMargin: noMargins ? 0 : undefined, annotationOn, docMaxAutoHeight: maxHeight, backgroundColor: backgroundColor, + _width: width || 200, _height: height || 100, x: x, y: y, _fitWidth: true, _autoHeight: true, title }); const template = Doc.UserDoc().defaultTextLayout; if (template instanceof Doc) { @@ -1296,6 +1563,8 @@ Scripting.addGlobal(function openDragFactory(dragFactory: Doc) { view && SelectionManager.SelectView(view, false); } }); +Scripting.addGlobal(function MySharedDocs() { return Doc.SharingDoc(); }, + "document containing all shared Docs"); Scripting.addGlobal(function IsNoviceMode() { return Doc.UserDoc().noviceMode; }, "is Dash in novice mode"); Scripting.addGlobal(function snapshotDashboard() { CurrentUserUtils.snapshotDashboard(Doc.UserDoc()); }, @@ -1304,6 +1573,8 @@ Scripting.addGlobal(function createNewDashboard() { return CurrentUserUtils.crea "creates a new dashboard when called"); Scripting.addGlobal(function createNewPresentation() { return MainView.Instance.createNewPresentation(); }, "creates a new presentation when called"); +Scripting.addGlobal(function createNewFolder() { return MainView.Instance.createNewFolder(); }, + "creates a new folder in myFiles when called"); Scripting.addGlobal(function links(doc: any) { return new List(LinkManager.Instance.getAllRelatedLinks(doc)); }, "returns all the links to the document or its annotations", "(doc: any)"); Scripting.addGlobal(function importDocument() { return CurrentUserUtils.importDocument(); }, @@ -1356,3 +1627,22 @@ Scripting.addGlobal(function dynamicOffScreenDocs(dashboard: Doc) { } return []; }); +Scripting.addGlobal(function selectedDocumentType(docType?: DocumentType, colType?: CollectionViewType, checkParent?: boolean) { + let selected = SelectionManager.Docs().length ? SelectionManager.Docs()[0] : undefined; + if (selected && checkParent) { + const parentDoc: Doc = Cast(selected.context, Doc, null); + selected = parentDoc; + } + if (selected && docType && selected.type === docType) return false; + else if (selected && colType && selected.viewType === colType) return false; + else if (selected && !colType && !docType) return false; + else return true; +}); +Scripting.addGlobal(function makeTopLevelFolder() { + const folder = Docs.Create.TreeDocument([], { title: "Untitled folder", _stayInCollection: true, isFolder: true }); + TreeView._editTitleOnLoad = { id: folder[Id], parent: undefined }; + return Doc.AddDocToList(Doc.UserDoc().myFilesystem as Doc, "data", folder); +}); +Scripting.addGlobal(function toggleComicMode() { + Doc.UserDoc().renderStyle = Doc.UserDoc().renderStyle === "comic" ? undefined : "comic"; +});
\ No newline at end of file diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index cb0ee411c..66b6a1e44 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -1,19 +1,22 @@ import { action, observable, runInAction } from 'mobx'; import { Doc, DocListCast, DocListCastAsync, Opt } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; -import { Cast, NumCast, StrCast } from '../../fields/Types'; +import { Cast } from '../../fields/Types'; import { returnFalse } from '../../Utils'; import { DocumentType } from '../documents/DocumentTypes'; import { CollectionDockingView } from '../views/collections/CollectionDockingView'; import { CollectionView } from '../views/collections/CollectionView'; import { LightboxView } from '../views/LightboxView'; import { DocumentView, ViewAdjustment } from '../views/nodes/DocumentView'; +import { LinkAnchorBox } from '../views/nodes/LinkAnchorBox'; import { Scripting } from './Scripting'; +import { SelectionManager } from './SelectionManager'; export class DocumentManager { //global holds all of the nodes (regardless of which collection they're in) @observable public DocumentViews: DocumentView[] = []; + @observable public LinkAnchorBoxViews: DocumentView[] = []; @observable public RecordingEvent = 0; @observable public LinkedDocumentViews: { a: DocumentView, b: DocumentView, l: Doc }[] = []; @@ -25,24 +28,42 @@ export class DocumentManager { @action public AddView = (view: DocumentView) => { - DocListCast(view.rootDoc.links).forEach(link => { - const whichOtherAnchor = view.props.LayoutTemplateString?.includes("anchor2") ? "anchor1" : "anchor2"; - const otherDoc = link && (link[whichOtherAnchor] as Doc); - 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) { - this.LinkedDocumentViews.push({ a: whichOtherAnchor === "anchor1" ? otherView : view, b: whichOtherAnchor === "anchor1" ? view : otherView, l: link }); - } - }); - }); - this.DocumentViews.push(view); + //console.log("MOUNT " + view.props.Document.title + "/" + view.props.LayoutTemplateString); + if (view.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { + const viewAnchorIndex = view.props.LayoutTemplateString.includes("anchor2") ? "anchor2" : "anchor1"; + DocListCast(view.rootDoc.links).forEach(link => { + this.LinkAnchorBoxViews?.filter(dv => Doc.AreProtosEqual(dv.rootDoc, link) && !dv.props.LayoutTemplateString?.includes(viewAnchorIndex)). + forEach(otherView => this.LinkedDocumentViews.push( + { + a: viewAnchorIndex === "anchor2" ? otherView : view, + b: viewAnchorIndex === "anchor2" ? view : otherView, + l: link + }) + ); + }); + this.LinkAnchorBoxViews.push(view); + // this.LinkedDocumentViews.forEach(view => console.log(" LV = " + view.a.props.Document.title + "/" + view.a.props.LayoutTemplateString + " --> " + + // view.b.props.Document.title + "/" + view.b.props.LayoutTemplateString)); + } else { + this.DocumentViews.push(view); + } } public RemoveView = action((view: DocumentView) => { - const index = this.DocumentViews.indexOf(view); - index !== -1 && this.DocumentViews.splice(index, 1); + this.LinkedDocumentViews.slice().forEach(action(pair => { + if (pair.a === view || pair.b === view) { + const li = this.LinkedDocumentViews.indexOf(pair); + li !== -1 && this.LinkedDocumentViews.splice(li, 1); + } + })); - this.LinkedDocumentViews.slice().forEach(action((pair, i) => pair.a === view || pair.b === view ? this.LinkedDocumentViews.splice(i, 1) : null)); + if (view.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { + const index = this.LinkAnchorBoxViews.indexOf(view); + this.LinkAnchorBoxViews.splice(index, 1); + } else { + const index = this.DocumentViews.indexOf(view); + index !== -1 && this.DocumentViews.splice(index, 1); + } + SelectionManager.DeselectView(view); }); //gets all views @@ -139,7 +160,9 @@ export class DocumentManager { closeContextIfNotFound: boolean = false, // after opening a context where the document should be, this determines whether the context should be closed if the Doc isn't actually there originatingDoc: Opt<Doc> = undefined, // doc that initiated the display of the target odoc finished?: () => void, - originalTarget?: Doc + originalTarget?: Doc, + noSelect?: boolean, + presZoom?: number ): Promise<void> => { originalTarget = originalTarget ?? targetDoc; const getFirstDocView = LightboxView.LightboxDoc ? DocumentManager.Instance.getLightboxDocumentView : DocumentManager.Instance.getFirstDocumentView; @@ -153,7 +176,7 @@ export class DocumentManager { } } else { targetDoc.hidden && (targetDoc.hidden = undefined); - docView?.select(false); + !noSelect && docView?.select(false); } finished?.(); return false; @@ -170,9 +193,9 @@ export class DocumentManager { 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) { - Doc.linkFollowHighlight(focusView.rootDoc); + !noSelect && Doc.linkFollowHighlight(focusView.rootDoc); //TODO:glr make this a setting in PresBox focusView.focus(targetDoc, { - originalTarget, willZoom, afterFocus: (didFocus: boolean) => + originalTarget, willZoom, scale: presZoom, afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => { focusAndFinish(didFocus); res(); @@ -203,7 +226,7 @@ export class DocumentManager { retryDocView.props.focus(targetDoc, { willZoom, afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => { - focusAndFinish(didFocus); + !noSelect && focusAndFinish(didFocus); res(); }) }); // focus on the target in the context diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index f7ef9ae6f..f5704d2bf 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -145,7 +145,7 @@ export namespace DragManager { removeDropProperties?: string[]; moveDocument?: MoveFunction; removeDocument?: RemoveFunction; - isSelectionMove?: boolean; // indicates that an explicitly selected Document is being dragged. this will suppress onDragStart scripts + isDocDecorationMove?: boolean; // Flags that Document decorations are used to drag document which allows suppression of onDragStart scripts } export class LinkDragData { constructor(dragView: DocumentView, linkSourceGetAnchor: () => Doc,) { @@ -225,7 +225,7 @@ export namespace DragManager { if (docDragData && !docDragData.droppedDocuments.length) { docDragData.dropAction = dragData.userDropAction || dragData.dropAction; docDragData.droppedDocuments = - await Promise.all(dragData.draggedDocuments.map(async 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.isDocDecorationMove && !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" ? (await Doc.MakeClone(d)).clone : d)); @@ -365,6 +365,21 @@ export namespace DragManager { const dragElements = eles.map(ele => { if (!ele.parentNode) dragDiv.appendChild(ele); const dragElement = ele.parentNode === dragDiv ? ele : ele.cloneNode(true) as HTMLElement; + const children = Array.from(dragElement.children); + while (children.length) { // need to replace all the maker node reference ids with new unique ids. otherwise, the clone nodes will reference the original nodes which are all hidden during the drag + const next = children.pop(); + next && children.push(...Array.from(next.children)); + if (next) { + ["marker-start", "marker-mid", "marker-end"].forEach(field => { + if (next.localName.startsWith("path") && (next.attributes as any)[field]) { + next.setAttribute(field, (next.attributes as any)[field].value.replace("#", "#X")); + } + }); + if (next.localName.startsWith("marker")) { + next.id = "X" + next.id; + } + } + } const rect = ele.getBoundingClientRect(); const scaleX = rect.width / ele.offsetWidth; const scaleY = ele.offsetHeight ? rect.height / ele.offsetHeight : scaleX; diff --git a/src/client/util/HypothesisUtils.ts b/src/client/util/HypothesisUtils.ts index 635673025..e910a9118 100644 --- a/src/client/util/HypothesisUtils.ts +++ b/src/client/util/HypothesisUtils.ts @@ -29,7 +29,7 @@ export namespace Hypothesis { * Search for a WebDocument whose url field matches the given uri, return undefined if not found */ export const findWebDoc = async (uri: string) => { - const currentDoc = SelectionManager.Views().length && SelectionManager.Views()[0].props.Document; + const currentDoc = SelectionManager.Docs().lastElement(); if (currentDoc && Cast(currentDoc.data, WebField)?.url.href === uri) return currentDoc; // always check first whether the currently selected doc is the annotation's source, only use Search otherwise const results: Doc[] = []; diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx index e009fb3a9..a32a8eecc 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -1,8 +1,6 @@ import React = require("react"); -import * as beziercurve from 'bezier-curve'; -import * as fitCurve from 'fit-curve'; -import "./InteractionUtils.scss"; import { Utils } from "../../Utils"; +import "./InteractionUtils.scss"; export namespace InteractionUtils { export const MOUSETYPE = "mouse"; @@ -91,138 +89,57 @@ export namespace InteractionUtils { return myTouches; } - export function CreatePoints(points: { X: number, Y: number }[], left: number, top: number, - color: string, width: number, strokeWidth: number, bezier: string, fill: string, arrowStart: string, arrowEnd: string, - dash: string, scalex: number, scaley: number, shape: string, pevents: string, drawHalo: boolean, nodefs: boolean) { - let pts: { X: number; Y: number; }[] = []; - if (shape) { //if any of the shape are true - pts = makePolygon(shape, points); - } - else if ((points.length >= 5 && points[3].X === points[4].X) || (points.length === 4)) { - for (var i = 0; i < points.length - 3; i += 4) { - const array = [[points[i].X, points[i].Y], [points[i + 1].X, points[i + 1].Y], [points[i + 2].X, points[i + 2].Y], [points[i + 3].X, points[i + 3].Y]]; - for (var t = 0; t < 1; t += 0.01) { - const point = beziercurve(t, array); - pts.push({ X: point[0], Y: point[1] }); - } - } - } - else if (points.length > 1 && points[points.length - 1].X === points[0].X && points[points.length - 1].Y === points[0].Y) { - //pointer is up (first and last points are the same) - const newPoints = points.reduce((p, pts) => { p.push([pts.X, pts.Y]); return p; }, [] as number[][]); - newPoints.pop(); - - const bezierCurves = fitCurve(newPoints, parseInt(bezier)); - for (const curve of bezierCurves) { - for (var t = 0; t < 1; t += 0.01) { - const point = beziercurve(t, curve); - pts.push({ X: point[0], Y: point[1] }); - } - } - } else { - pts = points.slice(); - // bcz: Ugh... this is ugly, but shapes apprently have an extra point added that is = (p[0].x,p[0].y+1) as some sort of flag. need to remove it here. - if (pts.length > 2 && pts[pts.length - 2].X === pts[0].X && pts[pts.length - 2].Y === pts[0].Y) { - pts.pop(); - } - } - if (isNaN(scalex)) { - scalex = 1; - } - if (isNaN(scaley)) { - scaley = 1; - } - return pts; - } + export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, + color: string, width: number, strokeWidth: number, lineJoin: string, lineCap: string, bezier: string, fill: string, arrowStart: string, arrowEnd: string, + dash: string | undefined, scalex: number, scaley: number, shape: string, pevents: string, opacity: number, nodefs: boolean) { + const pts = shape ? makePolygon(shape, points) : points; + if (isNaN(scalex)) scalex = 1; + if (isNaN(scaley)) scaley = 1; + const toScr = (p: { X: number, Y: number }) => ` ${!p ? 0 : (p.X - left - width / 2) * scalex + width / 2}, ${!p ? 0 : (p.Y - top - width / 2) * scaley + width / 2} `; + const strpts = bezier ? + pts.reduce((acc: string, pt, i) => acc + (i % 4 !== 0 ? "" : (i === 0 ? "M" + toScr(pt) : "") + "C" + toScr(pts[i + 1]) + toScr(pts[i + 2]) + toScr(pts[i + 3])), "") : + pts.reduce((acc: string, pt) => acc + `${toScr(pt)} `, ""); - export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, - color: string, width: number, strokeWidth: number, bezier: string, fill: string, arrowStart: string, arrowEnd: string, - dash: string | undefined, scalex: number, scaley: number, shape: string, pevents: string, drawHalo: boolean, nodefs: boolean) { - let pts: { X: number; Y: number; }[] = []; - if (shape) { //if any of the shape are true - pts = makePolygon(shape, points); - } - else if ((points.length >= 5 && points[3].X === points[4].X) || (points.length === 4)) { - for (var i = 0; i < points.length - 3; i += 4) { - const array = [[points[i].X, points[i].Y], [points[i + 1].X, points[i + 1].Y], [points[i + 2].X, points[i + 2].Y], [points[i + 3].X, points[i + 3].Y]]; - for (var t = 0; t < 1; t += 0.01) { - const point = beziercurve(t, array); - pts.push({ X: point[0], Y: point[1] }); - } - } - } - else if (points.length > 1 && points[points.length - 1].X === points[0].X && points[points.length - 1].Y === points[0].Y) { - //pointer is up (first and last points are the same) - const newPoints = points.reduce((p, pts) => { p.push([pts.X, pts.Y]); return p; }, [] as number[][]); - newPoints.pop(); - - const bezierCurves = fitCurve(newPoints, parseInt(bezier)); - for (const curve of bezierCurves) { - for (var t = 0; t < 1; t += 0.01) { - const point = beziercurve(t, curve); - pts.push({ X: point[0], Y: point[1] }); - } - } - } else { - pts = points.slice(); - // bcz: Ugh... this is ugly, but shapes apprently have an extra point added that is = (p[0].x,p[0].y+1) as some sort of flag. need to remove it here. - if (pts.length > 2 && pts[pts.length - 2].X === pts[0].X && pts[pts.length - 2].Y === pts[0].Y) { - pts.pop(); - } - } - if (isNaN(scalex)) { - scalex = 1; - } - if (isNaN(scaley)) { - scaley = 1; - } - const strpts = pts.reduce((acc: string, pt: { X: number, Y: number }) => acc + - `${(pt.X - left - width / 2) * scalex + width / 2}, - ${(pt.Y - top - width / 2) * scaley + width / 2} `, ""); const dashArray = dash && Number(dash) ? String(Number(width) * Number(dash)) : undefined; const defGuid = Utils.GenerateGuid(); - const arrowDim = Math.max(0.5, 8 / Math.log(Math.max(2, strokeWidth))); - - const addables = pts.map((pts, i) => - <svg height="10" width="10"> - <circle cx={(pts.X - left - width / 2) * scalex + width / 2} cy={(pts.Y - top - width / 2) * scaley + width / 2} r={strokeWidth / 2} stroke="black" strokeWidth={1} fill="blue" - onDoubleClick={(e) => { console.log(i); }} pointerEvents="all" cursor="all-scroll" - /> - </svg>); - + const Tag = (bezier ? "path" : "polyline") as keyof JSX.IntrinsicElements; + const makerStrokeWidth = strokeWidth / 2; return (<svg fill={color}> {/* setting the svg fill sets the arrowStart fill */} {nodefs ? (null) : <defs> - {arrowStart !== "dot" && arrowEnd !== "dot" ? (null) : <marker id={`dot${defGuid}`} orient="auto" overflow="visible"> - <circle r={1} fill="context-stroke" /> - </marker>} - {arrowStart !== "arrow" && arrowEnd !== "arrow" ? (null) : <marker id={`arrowStart${defGuid}`} orient="auto" overflow="visible" refX="1.6" refY="0" markerWidth="10" markerHeight="7"> - <polygon points={`${arrowDim} ${-Math.max(1, arrowDim / 2)}, ${arrowDim} ${Math.max(1, arrowDim / 2)}, -1 0`} /> - </marker>} - {arrowStart !== "arrow" && arrowEnd !== "arrow" ? (null) : <marker id={`arrowEnd${defGuid}`} orient="auto" overflow="visible" refX="1.6" refY="0" markerWidth="10" markerHeight="7"> - <polygon points={`${2 - arrowDim} ${-Math.max(1, arrowDim / 2)}, ${2 - arrowDim} ${Math.max(1, arrowDim / 2)}, 3 0`} /> - </marker>} + {arrowStart !== "dot" && arrowEnd !== "dot" ? (null) : + <marker id={`dot${defGuid}`} orient="auto" markerUnits="userSpaceOnUse" refX={0} refY="0" overflow="visible"> + <circle r={strokeWidth * 1.5} fill="context-stroke" /> + </marker>} + {arrowStart !== "arrow" ? (null) : + <marker id={`arrowStart${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={makerStrokeWidth * 1.5} refY={0} markerWidth="10" markerHeight="7"> + <polygon style={{ stroke: color }} strokeLinejoin={lineJoin as any} strokeWidth={makerStrokeWidth * 2 / 3} points={`${3 * makerStrokeWidth} ${-makerStrokeWidth * 1.5}, ${makerStrokeWidth * 2} 0, ${3 * makerStrokeWidth} ${makerStrokeWidth * 1.5}, 0 0`} /> + </marker>} + {arrowEnd !== "arrow" ? (null) : + <marker id={`arrowEnd${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={makerStrokeWidth * 1.5} refY={0} markerWidth="10" markerHeight="7"> + <polygon style={{ stroke: color }} strokeLinejoin={lineJoin as any} strokeWidth={makerStrokeWidth * 2 / 3} points={`0 ${-makerStrokeWidth * 1.5}, ${makerStrokeWidth} 0, 0 ${makerStrokeWidth * 1.5}, ${3 * makerStrokeWidth} 0`} /> + </marker>} </defs>} - <polyline - points={strpts} + + <Tag + d={bezier ? strpts : undefined} + points={bezier ? undefined : strpts} style={{ // filter: drawHalo ? "url(#inkSelectionHalo)" : undefined, - fill: fill ? fill : "none", + fill: fill && fill !== "transparent" ? fill : "none", opacity: 1.0, // opacity: strokeWidth !== width ? 0.5 : undefined, pointerEvents: pevents as any, stroke: color ?? "rgb(0, 0, 0)", strokeWidth: strokeWidth, - strokeLinejoin: "round", - strokeLinecap: "round", + strokeLinecap: lineCap as any, strokeDasharray: dashArray }} - markerStart={`url(#${arrowStart + "Start" + defGuid})`} - markerEnd={`url(#${arrowEnd + "End" + defGuid})`} + markerStart={`url(#${arrowStart === "dot" ? arrowStart + defGuid : arrowStart + "Start" + defGuid})`} + markerEnd={`url(#${arrowEnd === "dot" ? arrowEnd + defGuid : arrowEnd + "End" + defGuid})`} /> - {/* {addables} */} </svg>); } diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 84ff3a3ff..90a8f2737 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -1,14 +1,12 @@ -import { observable, observe, action, reaction, computed } from "mobx"; +import { action, observable, observe } from "mobx"; import { computedFn } from "mobx-utils"; import { DirectLinksSym, Doc, DocListCast, Field, Opt } from "../../fields/Doc"; import { List } from "../../fields/List"; import { ProxyField } from "../../fields/Proxy"; -import { listSpec } from "../../fields/Schema"; -import { BoolCast, Cast, PromiseValue, StrCast } from "../../fields/Types"; +import { BoolCast, Cast, 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"; type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => void) => void; @@ -40,15 +38,19 @@ export class LinkManager { const addLinkToDoc = (link: Doc) => { const a1Prom = link?.anchor1; const a2Prom = link?.anchor2; - Promise.all([a1Prom, a2Prom]).then(action((all) => { + Promise.all([a1Prom, a2Prom]).then((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 a1ProtoProm = (link?.anchor1 as Doc)?.proto; + const a2ProtoProm = (link?.anchor2 as Doc)?.proto; + Promise.all([a1ProtoProm, a2ProtoProm]).then(action((all) => { + if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) { + Doc.GetProto(a1)[DirectLinksSym].add(link); + Doc.GetProto(a2)[DirectLinksSym].add(link); + Doc.GetProto(link)[DirectLinksSym].add(link); + } + })); + }); }; const remLinkFromDoc = (link: Doc) => { const a1 = link?.anchor1; @@ -102,12 +104,9 @@ export class LinkManager { 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; - } + !Doc.UserDoc().linkRelationshipList && (Doc.UserDoc().linkRelationshipList = new List<string>()); + !Doc.UserDoc().linkColorList && (Doc.UserDoc().linkColorList = new List<string>()); + !Doc.UserDoc().linkRelationshipSizes && (Doc.UserDoc().linkRelationshipSizes = new List<number>()); } public addLink(linkDoc: Doc, checkExists = false) { @@ -120,14 +119,29 @@ export class LinkManager { public getAllRelatedLinks(anchor: Doc) { return this.relatedLinker(anchor); } // finds all links that contain the given anchor public getAllDirectLinks(anchor: Doc): Doc[] { - return Array.from(Doc.GetProto(anchor)[DirectLinksSym]); + // FIXME:glr Why is Doc undefined? + if (Doc.GetProto(anchor)[DirectLinksSym]) { + return Array.from(Doc.GetProto(anchor)[DirectLinksSym]); + } else { + return []; + } } // finds all links that contain the given anchor relatedLinker = computedFn(function relatedLinker(this: any, anchor: Doc): Doc[] { const lfield = Doc.LayoutFieldKey(anchor); - const related = DocListCast(anchor[lfield + "-annotations"]).concat(DocListCast(anchor[lfield + "-annotations-timeline"])).reduce((list, anno) => + if (!anchor || anchor instanceof Promise || Doc.GetProto(anchor) instanceof Promise) { + console.log("WAITING FOR DOC/PROTO IN LINKMANAGER"); + return []; + } + const dirLinks = Doc.GetProto(anchor)[DirectLinksSym]; + const annos = DocListCast(anchor[lfield + "-annotations"]); + const timelineAnnos = DocListCast(anchor[lfield + "-annotations-timeline"]); + if (!annos || !timelineAnnos) { + debugger; + } + const related = [...annos, ...timelineAnnos].reduce((list, anno) => [...list, ...LinkManager.Instance.relatedLinker(anno)], - Array.from(Doc.GetProto(anchor)[DirectLinksSym]).slice()); + Array.from(dirLinks).slice()); return related; }, true); diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index f981f84cd..40b94024e 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -12,7 +12,7 @@ export { ts }; import * as typescriptlib from '!!raw-loader!./type_decls.d'; import { Doc, Field } from '../../fields/Doc'; -export interface ScriptSucccess { +export interface ScriptSuccess { success: true; result: any; } @@ -23,7 +23,7 @@ export interface ScriptError { result: any; } -export type ScriptResult = ScriptSucccess | ScriptError; +export type ScriptResult = ScriptSuccess | ScriptError; export type ScriptParam = { [name: string]: string }; @@ -171,10 +171,12 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an if (!options.editable) { batch = Doc.MakeReadOnly(); } + const result = compiledFunction.apply(thisParam, params).apply(thisParam, argsArray); if (batch) { batch.end(); } + return { success: true, result }; } catch (error) { @@ -314,9 +316,9 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp paramList.push(`${key}: ${typeof val === "object" ? Object.getPrototypeOf(val).constructor.name : typeof val}`); } const paramString = paramList.join(", "); - const funcScript = `(function(${paramString})${requiredType ? `: ${requiredType}` : ''} { - ${addReturn ? `return ${script};` : script} - })`; + const body = addReturn ? `return ${script};` : `return ${script};`; + const reqTypes = requiredType ? `: ${requiredType}` : ''; + const funcScript = `(function(${paramString})${reqTypes} { ${body} })`; host.writeFile("file.ts", funcScript); if (typecheck) host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib); diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 0cfaebbf2..e507ec3bf 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -2,7 +2,6 @@ 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"; @@ -11,14 +10,12 @@ export namespace SelectionManager { class Manager { @observable IsDragging: boolean = false; - SelectedViews: ObservableMap<DocumentView, boolean> = new ObservableMap(); + SelectedViews: ObservableMap<DocumentView, Doc> = new ObservableMap(); @observable SelectedSchemaDocument: Doc | undefined; - @observable SelectedSchemaCollection: CollectionSchemaView | undefined; @action - SelectSchemaView(collectionView: Opt<CollectionSchemaView>, doc: Opt<Doc>) { + SelectSchemaViewDoc(doc: Opt<Doc>) { manager.SelectedSchemaDocument = doc; - manager.SelectedSchemaCollection = collectionView; } @action SelectView(docView: DocumentView, ctrlPressed: boolean): void { @@ -28,19 +25,17 @@ export namespace SelectionManager { this.DeselectAll(); } - manager.SelectedViews.set(docView, true); + manager.SelectedViews.set(docView, docView.rootDoc); docView.props.whenChildContentsActiveChanged(true); } else if (!ctrlPressed && Array.from(manager.SelectedViews.entries()).length > 1) { Array.from(manager.SelectedViews.keys()).map(dv => dv !== docView && dv.props.whenChildContentsActiveChanged(false)); manager.SelectedSchemaDocument = undefined; - manager.SelectedSchemaCollection = undefined; manager.SelectedViews.clear(); - manager.SelectedViews.set(docView, true); + manager.SelectedViews.set(docView, docView.rootDoc); } } @action DeselectView(docView: DocumentView): void { - if (manager.SelectedViews.get(docView)) { manager.SelectedViews.delete(docView); docView.props.whenChildContentsActiveChanged(false); @@ -48,7 +43,6 @@ export namespace SelectionManager { } @action DeselectAll(): void { - manager.SelectedSchemaCollection = undefined; manager.SelectedSchemaDocument = undefined; Array.from(manager.SelectedViews.keys()).map(dv => dv.props.whenChildContentsActiveChanged(false)); manager.SelectedViews.clear(); @@ -63,8 +57,8 @@ export namespace SelectionManager { export function SelectView(docView: DocumentView, ctrlPressed: boolean): void { manager.SelectView(docView, ctrlPressed); } - export function SelectSchemaView(colSchema: Opt<CollectionSchemaView>, document: Opt<Doc>): void { - manager.SelectSchemaView(colSchema, document); + export function SelectSchemaViewDoc(document: Opt<Doc>): void { + manager.SelectSchemaViewDoc(document); } const IsSelectedCache = computedFn(function isSelected(doc: DocumentView) { // wrapping get() in a computedFn only generates mobx() invalidations when the return value of the function for the specific get parameters has changed @@ -92,12 +86,12 @@ export namespace SelectionManager { } export function Views(): Array<DocumentView> { - return Array.from(manager.SelectedViews.keys()).filter(dv => dv.props.Document._viewType !== CollectionViewType.Docking); + return Array.from(manager.SelectedViews.keys()).filter(dv => manager.SelectedViews.get(dv)?._viewType !== CollectionViewType.Docking); } export function SelectedSchemaDoc(): Doc | undefined { return manager.SelectedSchemaDocument; } - export function SelectedSchemaCollection(): CollectionSchemaView | undefined { - return manager.SelectedSchemaCollection; + export function Docs(): Doc[] { + return Array.from(manager.SelectedViews.values()).filter(doc => doc?._viewType !== CollectionViewType.Docking); } }
\ No newline at end of file diff --git a/src/client/util/SettingsManager.scss b/src/client/util/SettingsManager.scss index c9db94419..b7199f433 100644 --- a/src/client/util/SettingsManager.scss +++ b/src/client/util/SettingsManager.scss @@ -360,17 +360,18 @@ flex-direction: row; position: relative; min-height: 250px; + height: 100%; width: 100%; .settings-content { - background-color: #fdfdfd; + background-color: $off-white; } } .settings-panel { position: relative; min-width: 150px; - background-color: #e4e4e4; + background-color: $light-blue; .settings-user { position: absolute; diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index bd91db779..6a26dfdc7 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -19,9 +19,9 @@ export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; export enum ColorScheme { - Dark = "Dark", - Light = "Light", - System = "Match System" + Dark = "-Dark", + Light = "-Light", + System = "-MatchSystem" } @observer @@ -38,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; } + @computed get colorScheme() { return CurrentUserUtils.ActiveDashboard.colorScheme; } constructor(props: {}) { super(props); @@ -81,16 +81,16 @@ export class SettingsManager extends React.Component<{}> { const scheme: ColorScheme = (e.currentTarget as any).value; switch (scheme) { case ColorScheme.Light: - Doc.UserDoc().colorScheme = ColorScheme.Light; + CurrentUserUtils.ActiveDashboard.colorScheme = undefined; // undefined means ColorScheme.Light until all CSS is updated with values for each color scheme (e.g., see MainView.scss, DocumentDecorations.scss) addStyleSheetRule(SettingsManager._settingsStyle, "lm_header", { background: "#d3d3d3 !important" }); break; case ColorScheme.Dark: - Doc.UserDoc().colorScheme = ColorScheme.Dark; + CurrentUserUtils.ActiveDashboard.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; + CurrentUserUtils.ActiveDashboard.colorScheme = e.matches ? ColorScheme.Dark : undefined; // undefined means ColorScheme.Light until all CSS is updated with values for each color scheme (e.g., see MainView.scss, DocumentDecorations.scss) }); break; } @@ -119,6 +119,7 @@ export class SettingsManager extends React.Component<{}> { </div>; const colorSchemes = [ColorScheme.Light, ColorScheme.Dark, ColorScheme.System]; + const schemeMap = ["Light", "Dark", "Match system"]; return <div className="colors-content"> <div className="preferences-color"> @@ -132,8 +133,8 @@ export class SettingsManager extends React.Component<{}> { <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 className="scheme-select" onChange={this.changeColorScheme} defaultValue={StrCast(CurrentUserUtils.ActiveDashboard?.colorScheme)}> + {colorSchemes.map((scheme, i) => <option key={scheme} value={scheme}> {schemeMap[i]} </option>)} </select> </div> </div> diff --git a/src/client/util/SharingManager.scss b/src/client/util/SharingManager.scss index 9dc57dd1e..2de636f21 100644 --- a/src/client/util/SharingManager.scss +++ b/src/client/util/SharingManager.scss @@ -40,7 +40,6 @@ .permissions-select { z-index: 1; - margin-left: -115; border: none; outline: none; text-align: justify; // for Edge diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts index 05fb9f378..44e44d335 100644 --- a/src/client/util/UndoManager.ts +++ b/src/client/util/UndoManager.ts @@ -70,6 +70,7 @@ export namespace UndoManager { export interface UndoEvent { undo: () => void; redo: () => void; + prop: string; } type UndoBatch = UndoEvent[]; @@ -104,6 +105,23 @@ export namespace UndoManager { export function GetOpenBatches(): Without<Batch, 'end'>[] { return openBatches; } + export function FilterBatches(fieldTypes: string[]) { + const fieldCounts: { [key: string]: number } = {}; + const lastStack = UndoManager.undoStack.lastElement(); + if (lastStack) { + lastStack.forEach(ev => fieldTypes.includes(ev.prop) && (fieldCounts[ev.prop] = (fieldCounts[ev.prop] || 0) + 1)); + const fieldCount2: { [key: string]: number } = {}; + runInAction(() => + UndoManager.undoStack[UndoManager.undoStack.length - 1] = lastStack.filter(ev => { + if (fieldTypes.includes(ev.prop)) { + fieldCount2[ev.prop] = (fieldCount2[ev.prop] || 0) + 1; + if (fieldCount2[ev.prop] === 1 || fieldCount2[ev.prop] === fieldCounts[ev.prop]) return true; + return false; + } + return true; + })); + } + } export function TraceOpenBatches() { console.log(`Open batches:\n\t${openBatches.map(batch => batch.batchName).join("\n\t")}\n`); } @@ -140,6 +158,7 @@ export namespace UndoManager { batchCounter--; // console.log("End " + batchCounter); if (batchCounter === 0 && currentBatch?.length) { + // console.log("------ended----") if (!cancel) { undoStack.push(currentBatch); } diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx index fe6d39ca4..0f1fc6b69 100644 --- a/src/client/views/AntimodeMenu.tsx +++ b/src/client/views/AntimodeMenu.tsx @@ -16,7 +16,7 @@ export abstract class AntimodeMenu<T extends AntimodeMenuProps> extends React.Co @observable protected _top: number = -300; @observable protected _left: number = -300; - @observable protected _opacity: number = 1; + @observable protected _opacity: number = 0; @observable protected _transitionProperty: string = "opacity"; @observable protected _transitionDuration: string = "0.5s"; @observable protected _transitionDelay: string = ""; diff --git a/src/client/views/AudioWaveform.tsx b/src/client/views/AudioWaveform.tsx index 7ff9c1071..8f3b7c2cd 100644 --- a/src/client/views/AudioWaveform.tsx +++ b/src/client/views/AudioWaveform.tsx @@ -6,56 +6,121 @@ import Waveform from "react-audio-waveform"; import { Doc } from "../../fields/Doc"; import { List } from "../../fields/List"; import { listSpec } from "../../fields/Schema"; -import { Cast } from "../../fields/Types"; +import { Cast, NumCast } from "../../fields/Types"; import { numberRange } from "../../Utils"; import "./AudioWaveform.scss"; +import { Colors } from "./global/globalEnums"; export interface AudioWaveformProps { duration: number; mediaPath: string; - dataDoc: Doc; + layoutDoc: Doc; + trimming: boolean; PanelHeight: () => number; } @observer export class AudioWaveform extends React.Component<AudioWaveformProps> { public static NUMBER_OF_BUCKETS = 100; - @computed get _waveHeight() { return Math.max(50, this.props.PanelHeight()); } + @computed get _waveHeight() { + return Math.max(50, this.props.PanelHeight()); + } componentDidMount() { - const audioBuckets = Cast(this.props.dataDoc.audioBuckets, listSpec("number"), []); + const audioBuckets = Cast( + this.props.layoutDoc.audioBuckets, + listSpec("number"), + [] + ); if (!audioBuckets.length) { - this.props.dataDoc.audioBuckets = new List<number>([0, 0]); /// "lock" to prevent other views from computing the same data + this.props.layoutDoc.audioBuckets = new List<number>([0, 0]); /// "lock" to prevent other views from computing the same data setTimeout(this.createWaveformBuckets); } } + // decodes the audio file into peaks for generating the waveform createWaveformBuckets = async () => { - axios({ url: this.props.mediaPath, responseType: "arraybuffer" }) - .then(response => { + axios({ url: this.props.mediaPath, responseType: "arraybuffer" }).then( + (response) => { const context = new window.AudioContext(); - context.decodeAudioData(response.data, - action(buffer => { + context.decodeAudioData( + response.data, + action((buffer) => { const decodedAudioData = buffer.getChannelData(0); - const bucketDataSize = Math.floor(decodedAudioData.length / AudioWaveform.NUMBER_OF_BUCKETS); + + const bucketDataSize = Math.floor( + decodedAudioData.length / AudioWaveform.NUMBER_OF_BUCKETS + ); const brange = Array.from(Array(bucketDataSize)); - this.props.dataDoc.audioBuckets = new List<number>( - numberRange(AudioWaveform.NUMBER_OF_BUCKETS).map((i: number) => - brange.reduce((p, x, j) => Math.abs(Math.max(p, decodedAudioData[i * bucketDataSize + j])), 0) / 2)); - })); - }); + this.props.layoutDoc.audioBuckets = new List<number>( + numberRange(AudioWaveform.NUMBER_OF_BUCKETS).map( + (i: number) => + brange.reduce( + (p, x, j) => + Math.abs( + Math.max(p, decodedAudioData[i * bucketDataSize + j]) + ), + 0 + ) / 2 + ) + ); + }) + ); + } + ); + } + + @action + createTrimBuckets = () => { + const audioBuckets = Cast( + this.props.layoutDoc.audioBuckets, + listSpec("number"), + [] + ); + + const start = Math.floor( + (NumCast(this.props.layoutDoc.clipStart) / this.props.duration) * 100 + ); + const end = Math.floor( + (NumCast(this.props.layoutDoc.clipEnd) / this.props.duration) * 100 + ); + return audioBuckets.slice(start, end); } render() { - const audioBuckets = Cast(this.props.dataDoc.audioBuckets, listSpec("number"), []); - return <div className="audioWaveform"> - <Waveform - color={"darkblue"} - height={this._waveHeight} - barWidth={0.1} - pos={this.props.duration} - duration={this.props.duration} - peaks={audioBuckets.length === AudioWaveform.NUMBER_OF_BUCKETS ? audioBuckets : undefined} - progressColor={"blue"} /> - </div>; + const audioBuckets = Cast( + this.props.layoutDoc.audioBuckets, + listSpec("number"), + [] + ); + + return ( + <div className="audioWaveform"> + {this.props.trimming || !this.props.layoutDoc.clipEnd ? ( + <Waveform + color={Colors.MEDIUM_BLUE} + height={this._waveHeight} + barWidth={0.1} + pos={this.props.duration} + duration={this.props.duration} + peaks={ + audioBuckets.length === AudioWaveform.NUMBER_OF_BUCKETS + ? audioBuckets + : undefined + } + progressColor={Colors.MEDIUM_BLUE} + /> + ) : ( + <Waveform + color={Colors.MEDIUM_BLUE} + height={this._waveHeight} + barWidth={0.1} + pos={this.props.duration} + duration={this.props.duration} + peaks={this.createTrimBuckets()} + progressColor={Colors.MEDIUM_BLUE} + /> + )} + </div> + ); } -}
\ No newline at end of file +} diff --git a/src/client/views/ComponentDecorations.scss b/src/client/views/ComponentDecorations.scss new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/client/views/ComponentDecorations.scss diff --git a/src/client/views/ComponentDecorations.tsx b/src/client/views/ComponentDecorations.tsx new file mode 100644 index 000000000..66d1bd63d --- /dev/null +++ b/src/client/views/ComponentDecorations.tsx @@ -0,0 +1,14 @@ +import { observer } from "mobx-react"; +import { SelectionManager } from "../util/SelectionManager"; +import './ComponentDecorations.scss'; +import React = require("react"); + +@observer +export class ComponentDecorations extends React.Component<{ boundsTop: number, boundsLeft: number }, { value: string }> { + static Instance: ComponentDecorations; + + render() { + const seldoc = SelectionManager.Views().lastElement(); + return seldoc?.ComponentView?.componentUI?.(this.props.boundsLeft, this.props.boundsTop) ?? (null); + } +}
\ No newline at end of file diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss index 795529780..ea24dbf6d 100644 --- a/src/client/views/ContextMenu.scss +++ b/src/client/views/ContextMenu.scss @@ -3,14 +3,11 @@ .contextMenu-cont { position: absolute; display: flex; - z-index: $contextMenu-zindex; - box-shadow: $medium-gray 0.2vw 0.2vw 0.4vw; + z-index: 100000; + box-shadow: 0px 3px 4px rgba(0,0,0,30%); flex-direction: column; background: whitesmoke; - padding-top: 10px; - padding-bottom: 10px; - border-radius: 15px; - border: solid #BBBBBBBB 1px; + border-radius: 3px; } // .contextMenu-item:first-child { @@ -132,7 +129,7 @@ } .contextMenu-inlineMenu { - border-top: solid 1px; + // border-top: solid 1px; //TODO:glr clean } .contextMenu-item:hover { diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx index c4fabbf99..80ff16cf9 100644 --- a/src/client/views/ContextMenu.tsx +++ b/src/client/views/ContextMenu.tsx @@ -26,8 +26,7 @@ export class ContextMenu extends React.Component { @observable private _mouseX: number = -1; @observable private _mouseY: number = -1; @observable private _shouldDisplay: boolean = false; - @observable private _mouseDown: boolean = false; - + private _ignoreUp = false; private _reactionDisposer?: IReactionDisposer; constructor(props: Readonly<{}>) { @@ -36,27 +35,39 @@ export class ContextMenu extends React.Component { ContextMenu.Instance = this; } + public setIgnoreEvents(ignore: boolean) { + this._ignoreUp = ignore; + } + @action onPointerDown = (e: PointerEvent) => { - this._mouseDown = true; this._mouseX = e.clientX; this._mouseY = e.clientY; } @action onPointerUp = (e: PointerEvent) => { - this._mouseDown = false; const curX = e.clientX; const curY = e.clientY; - if (this._mouseX !== curX || this._mouseY !== curY) { + if (this._ignoreUp) { + this._ignoreUp = false; + return; + } + if (Math.abs(this._mouseX - curX) > 1 || Math.abs(this._mouseY - curY) > 1) { this._shouldDisplay = false; } - this._shouldDisplay && (this._display = true); + if (this._shouldDisplay) { + if (this._onDisplay) { + this._onDisplay(); + } else { + this._display = true; + } + } } componentWillUnmount() { document.removeEventListener("pointerdown", this.onPointerDown); document.removeEventListener("pointerup", this.onPointerUp); - this._reactionDisposer && this._reactionDisposer(); + this._reactionDisposer?.(); } @action @@ -64,10 +75,6 @@ export class ContextMenu extends React.Component { document.addEventListener("pointerdown", this.onPointerDown); document.addEventListener("pointerup", this.onPointerUp); - this._reactionDisposer = reaction( - () => this._shouldDisplay, - () => this._shouldDisplay && !this._mouseDown && runInAction(() => this._display = true) - ); } @action @@ -138,9 +145,9 @@ export class ContextMenu extends React.Component { return y; } - + _onDisplay?: () => void = undefined; @action - displayMenu = (x: number, y: number, initSearch = "", showSearch = false) => { + displayMenu = (x: number, y: number, initSearch = "", showSearch = false, onDisplay?: () => void) => { //maxX and maxY will change if the UI/font size changes, but will work for any amount //of items added to the menu @@ -149,6 +156,8 @@ export class ContextMenu extends React.Component { this._pageY = y; this._searchString = initSearch; this._shouldDisplay = true; + this._onDisplay = onDisplay; + this._display = !onDisplay; } @action diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx index 6fe2abd21..c3921d846 100644 --- a/src/client/views/ContextMenuItem.tsx +++ b/src/client/views/ContextMenuItem.tsx @@ -90,7 +90,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select </span> ) : null} <div className="contextMenu-description"> - {this.props.description} + {this.props.description.replace(":","")} </div> </div> ); @@ -116,12 +116,12 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select style={{ alignItems: where, borderTop: this.props.addDivider ? "solid 1px" : undefined }} onMouseLeave={this.onPointerLeave} onMouseEnter={this.onPointerEnter}> {this.props.icon ? ( - <span className="icon-background" onMouseEnter={this.onPointerLeave} style={{ alignItems: "center" }}> + <span className="icon-background" onMouseEnter={this.onPointerLeave} style={{ alignItems: "center", alignSelf: "center" }}> <FontAwesomeIcon icon={this.props.icon} size="sm" /> </span> ) : null} <div className="contextMenu-description" onMouseEnter={this.onPointerEnter} - style={{ alignItems: "center" }} > + style={{ alignItems: "center", alignSelf: "center" }} > {this.props.description} <FontAwesomeIcon icon={"angle-right"} size="lg" style={{ position: "absolute", right: "10px" }} /> </div> diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 33dff9da5..32c351bf5 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -115,7 +115,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T const style: { [key: string]: any } = {}; 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 || ""; + return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: "number" })?.script.run({ self: this.rootDoc, this: this.layoutDoc, scale }).result?.toString() ?? ""; }; divKeys.map((prop: string) => { const p = (this.props as any)[prop]; @@ -176,7 +176,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T return false; } @action.bound - addDocument(doc: Doc | Doc[], annotationKey?: string): boolean { + addDocument = (doc: Doc | Doc[], annotationKey?: string): boolean => { const docs = doc instanceof Doc ? [doc] : doc; if (this.props.filterAddDocument?.(docs) === false || docs.find(doc => Doc.AreProtosEqual(doc, this.props.Document) && Doc.LayoutField(doc) === Doc.LayoutField(this.props.Document))) { diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 5f09a322c..aa9318310 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -27,6 +27,8 @@ import React = require("react"); import { PresBox } from './nodes/trails/PresBox'; import { undoBatch } from '../util/UndoManager'; import { CollectionViewType } from './collections/CollectionView'; +import { Colors } from './global/globalEnums'; +import { DashFieldView } from './nodes/formattedText/DashFieldView'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -187,9 +189,9 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV get followLinkButton() { const targetDoc = this.view0?.props.Document; return !targetDoc ? (null) : <Tooltip title={ - <div className="dash-tooltip">{"follow primary link on click"}</div>}> + <div className="dash-tooltip">{"Set onClick to follow primary link"}</div>}> <div className="documentButtonBar-icon" - style={{ color: targetDoc.isLinkButton ? "black" : "white" }} + style={{ backgroundColor: targetDoc.isLinkButton ? Colors.LIGHT_BLUE : Colors.DARK_GRAY, color: targetDoc.isLinkButton ? Colors.BLACK : Colors.WHITE }} onClick={undoBatch(e => this.props.views().map(view => view?.docView?.toggleFollowLink(undefined, false, false)))}> <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="hand-point-right" /> </div> @@ -202,7 +204,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV <div className="dash-tooltip">{SelectionManager.Views().length > 1 ? "Pin multiple documents to presentation" : "Pin to presentation"}</div>}> <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 })))}> + onClick={undoBatch(e => this.props.views().map(view => view && this.pinWithView(view.props.Document)))}> <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" /> </div> </Tooltip>; @@ -335,9 +337,9 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV openContextMenu = (e: React.MouseEvent) => { let child = SelectionManager.Views()[0].ContentDiv!.children[0]; while (child.children.length) { - const next = Array.from(child.children).find(c => typeof (c.className) === "string"); - if (next?.className.includes(DocumentView.ROOT_DIV)) break; - if (next?.className.includes("dashFieldView")) break; + const next = Array.from(child.children).find(c => c.className?.toString().includes("SVGAnimatedString") || typeof (c.className) === "string"); + if (next?.className?.toString().includes(DocumentView.ROOT_DIV)) break; + if (next?.className?.toString().includes(DashFieldView.name)) break; if (next) child = next; else break; } diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index 316f63240..d8ad47ecb 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -1,12 +1,15 @@ @import "global/globalCssVariables"; -$linkGap : 3px; +$linkGap: 3px; +.documentDecorations-Dark, .documentDecorations { - position: absolute; - z-index: 2000; + position: absolute; + z-index: 2000; +} +.documentDecorations-Dark { + background: dimgray; } - .documentDecorations-container { z-index: $docDecorations-zindex; position: absolute; @@ -56,6 +59,12 @@ $linkGap : 3px; } } + .documentDecorations-resizer-Dark + { + background: $light-gray; + opacity: 0.2; + } + .documentDecorations-topLeftResizer, .documentDecorations-leftResizer, .documentDecorations-bottomLeftResizer { @@ -94,6 +103,7 @@ $linkGap : 3px; position: absolute; } } + .documentDecorations-rotation { background: transparent; right: -15; @@ -122,62 +132,105 @@ $linkGap : 3px; opacity: 1; } - .documentDecorations-bottomRightResizer { - grid-row: 4; - } + .documentDecorations-topLeftResizer, + .documentDecorations-leftResizer, + .documentDecorations-bottomLeftResizer { + grid-column: 1; + } - .documentDecorations-topRightResizer, - .documentDecorations-bottomLeftResizer { - cursor: nesw-resize; - background: unset; - opacity: 1; - } + .documentDecorations-topResizer, + .documentDecorations-bottomResizer { + grid-column-start: 2; + grid-column-end: 5; + } - .documentDecorations-topRightResizer { - border-right: 2px solid; - border-top: 2px solid; - } + .documentDecorations-bottomRightResizer, + .documentDecorations-topRightResizer, + .documentDecorations-rightResizer { + grid-column-start: 5; + grid-column-end: 7; + } + + .documentDecorations-rotation, + .documentDecorations-borderRadius { + grid-column: 5; + grid-row: 4; + border-radius: 100%; + background: black; + height: 8; + right: -12; + top: 12; + position: relative; + pointer-events: all; + cursor: nwse-resize; + + .borderRadiusTooltip { + width: 10px; + height: 10px; + position: absolute; + } + } + .documentDecorations-rotation { + background: transparent; + right: -15; + } + + .documentDecorations-topLeftResizer, + .documentDecorations-bottomRightResizer { + cursor: nwse-resize; + background: unset; + opacity: 1; + } - .documentDecorations-bottomLeftResizer { - border-left: 2px solid; - border-bottom: 2px solid; - } + .documentDecorations-topLeftResizer { + border-left: 2px solid; + border-top: solid 2px; + } - .documentDecorations-topRightResizer:hover, - .documentDecorations-bottomLeftResizer:hover { - cursor: nesw-resize; - background: black; - opacity: 1; - } + .documentDecorations-bottomRightResizer { + border-right: 2px solid; + border-bottom: solid 2px; + } - .documentDecorations-topResizer, - .documentDecorations-bottomResizer { - cursor: ns-resize; - } + .documentDecorations-topLeftResizer:hover, + .documentDecorations-bottomRightResizer:hover { + opacity: 1; + } - .documentDecorations-leftResizer, - .documentDecorations-rightResizer { - cursor: ew-resize; - } + .documentDecorations-bottomRightResizer { + grid-row: 4; + } - .documentDecorations-contextMenu { - width: 25px; - height: calc(100% + 8px); // 8px for the height of the top resizer bar - grid-column-start: 2; - grid-column-end: 2; - pointer-events: all; - padding-left: 5px; - cursor: pointer; - } + .documentDecorations-topRightResizer, + .documentDecorations-bottomLeftResizer { + cursor: nesw-resize; + background: unset; + opacity: 1; + } + + .documentDecorations-topRightResizer { + border-right: 2px solid; + border-top: 2px solid; + } + + .documentDecorations-bottomLeftResizer { + border-left: 2px solid; + border-bottom: 2px solid; + } + + .documentDecorations-topRightResizer:hover, + .documentDecorations-bottomLeftResizer:hover { + cursor: nesw-resize; + background: black; + opacity: 1; + } - .documentDecorations-titleBackground { - background: #ffffffcf; - border-radius: 8px; - width: 100%; - height: 100%; - position: absolute; - } + .documentDecorations-topResizer, + .documentDecorations-bottomResizer { + cursor: ns-resize; + } + .documentDecorations-title-Dark, .documentDecorations-title { opacity: 1; grid-column-start: 2; @@ -189,81 +242,128 @@ $linkGap : 3px; margin-left: 5px; height: 22px; position: absolute; - .documentDecorations-titleSpan { + + .documentDecorations-titleSpan, + .documentDecorations-titleSpan-Dark { width: 100%; border-radius: 8px; - background: #ffffffcf; + background: #ffffffa0; position: absolute; display: inline-block; cursor: move; } + .documentDecorations-titleSpan-Dark { + background: hsla(0, 0%, 0%, 0.412); + } } - - .focus-visible { - margin-left: 0px; + .documentDecorations-title-Dark { + color: white; + background: black; } -} + .documentDecorations-contextMenu { + width: 25px; + height: calc(100% + 8px); // 8px for the height of the top resizer bar + grid-column-start: 2; + grid-column-end: 2; + pointer-events: all; + padding-left: 5px; + cursor: pointer; + } -.documentDecorations-iconifyButton { + .documentDecorations-titleBackground { + background: #ffffffcf; + border-radius: 8px; + width: 100%; + height: 100%; + position: absolute; + } + + .documentDecorations-title { opacity: 1; - grid-column-start: 4; + grid-column-start: 2; grid-column-end: 4; - pointer-events: all; - right: 0; - cursor: pointer; + pointer-events: auto; + overflow: hidden; + text-align: center; + display: flex; + margin-left: 5px; + height: 22px; position: absolute; - width: 20px; + .documentDecorations-titleSpan { + width: 100%; + border-radius: 8px; + background: #ffffffcf; + position: absolute; + display: inline-block; + cursor: move; + } + } + + .focus-visible { + margin-left: 0px; + } +} + +.documentDecorations-iconifyButton { + opacity: 1; + grid-column-start: 4; + grid-column-end: 4; + pointer-events: all; + right: 0; + cursor: pointer; + position: absolute; + width: 20px; } .documentDecorations-openButton { - display: flex; - align-items: center; - opacity: 1; - grid-column-start: 5; - grid-column-end: 5; - pointer-events: all; - cursor: pointer; + display: flex; + align-items: center; + opacity: 1; + grid-column-start: 5; + grid-column-end: 5; + pointer-events: all; + cursor: pointer; } .documentDecorations-closeButton { - display: flex; - align-items: center; - opacity: 1; - grid-column-start: 1; - grid-column-end: 3; - pointer-events: all; - cursor: pointer; - - >svg { - margin: 0; - } + display: flex; + align-items: center; + opacity: 1; + grid-column-start: 1; + grid-column-end: 3; + pointer-events: all; + cursor: pointer; + + > svg { + margin: 0; + } } .documentDecorations-background { - background: lightblue; - position: absolute; - opacity: 0.1; + background: lightblue; + position: absolute; + opacity: 0.1; } .linkFlyout { - grid-column: 2/4; + grid-column: 2/4; } .linkButton-empty:hover { - background: $medium-gray; - transform: scale(1.05); - cursor: pointer; + background: $medium-gray; + transform: scale(1.05); + cursor: pointer; } .linkButton-nonempty:hover { - background: $medium-gray; - transform: scale(1.05); - cursor: pointer; + background: $medium-gray; + transform: scale(1.05); + cursor: pointer; } .link-button-container { - border-radius: 10px; + border-radius: 13px; width: max-content; height: auto; display: flex; @@ -277,135 +377,132 @@ $linkGap : 3px; } .linkButtonWrapper { - pointer-events: auto; - padding-right: 5px; - width: 25px; + pointer-events: auto; + padding-right: 5px; + width: 25px; } .linkButton-linker { - height: 20px; - width: 20px; - text-align: center; - border-radius: 50%; - pointer-events: auto; - color: $dark-gray; - border: $dark-gray 1px solid; + height: 20px; + width: 20px; + text-align: center; + border-radius: 50%; + pointer-events: auto; + color: $dark-gray; + border: $dark-gray 1px solid; } .linkButton-linker:hover { - cursor: pointer; - transform: scale(1.05); + cursor: pointer; + transform: scale(1.05); } .linkButton-empty, .linkButton-nonempty { - height: 20px; - width: 20px; - border-radius: 50%; - opacity: 0.9; - pointer-events: auto; - background-color: $dark-gray; - color: $white; - text-transform: uppercase; - letter-spacing: 2px; - font-size: 75%; - transition: transform 0.2s; - text-align: center; - display: flex; - justify-content: center; - align-items: center; - - &:hover { - background: $medium-gray; - transform: scale(1.05); - cursor: pointer; - } + height: 20px; + width: 20px; + border-radius: 50%; + opacity: 0.9; + pointer-events: auto; + background-color: $dark-gray; + color: $white; + text-transform: uppercase; + letter-spacing: 2px; + font-size: 75%; + transition: transform 0.2s; + text-align: center; + display: flex; + justify-content: center; + align-items: center; + + &:hover { + background: $medium-gray; + transform: scale(1.05); + cursor: pointer; + } } .templating-menu { - position: absolute; - pointer-events: auto; - text-transform: uppercase; - letter-spacing: 2px; - font-size: 75%; - transition: transform 0.2s; - text-align: center; - display: flex; - justify-content: center; - align-items: center; + position: absolute; + pointer-events: auto; + text-transform: uppercase; + letter-spacing: 2px; + font-size: 75%; + transition: transform 0.2s; + text-align: center; + display: flex; + justify-content: center; + align-items: center; } .documentdecorations-icon { - margin: 0px; + margin: 0px; } + .templating-button, .docDecs-tagButton { - width: 20px; - height: 20px; - border-radius: 50%; - opacity: 0.9; - font-size: 14; - background-color: $dark-gray; - color: $white; - text-align: center; - cursor: pointer; - - &:hover { - background: $medium-gray; - transform: scale(1.05); - } -} - -.documentDecorations-darkScheme { - background: dimgray; + width: 20px; + height: 20px; + border-radius: 50%; + opacity: 0.9; + font-size: 14; + background-color: $dark-gray; + color: $white; + text-align: center; + cursor: pointer; + + &:hover { + background: $medium-gray; + transform: scale(1.05); + } } #template-list { - position: absolute; - top: 25px; - left: 0px; - width: max-content; - font-family: $sans-serif; - font-size: 12px; - background-color: $light-gray; - padding: 2px 12px; - list-style: none; - - .templateToggle, - .chromeToggle { - text-align: left; - } - - input { - margin-right: 10px; - } + position: absolute; + top: 25px; + left: 0px; + width: max-content; + font-family: $sans-serif; + font-size: 12px; + background-color: $light-gray; + padding: 2px 12px; + list-style: none; + + .templateToggle, + .chromeToggle { + text-align: left; + } + + input { + margin-right: 10px; + } } @-moz-keyframes spin { - 100% { - -moz-transform: rotate(360deg); - } + 100% { + -moz-transform: rotate(360deg); + } } @-webkit-keyframes spin { - 100% { - -webkit-transform: rotate(360deg); - } + 100% { + -webkit-transform: rotate(360deg); + } } @keyframes spin { - 100% { - -webkit-transform: rotate(360deg); - transform: rotate(360deg); - } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } } @keyframes shadow-pulse { - 0% { - box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.8); - } + 0% { + box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.8); + } - 100% { - box-shadow: 0 0 0 10px rgba(0, 255, 0, 0); - } -}
\ No newline at end of file + 100% { + box-shadow: 0 0 0 10px rgba(0, 255, 0, 0); + } +} diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 118d2e7c7..bd9c3509b 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -3,15 +3,15 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Tooltip } from '@material-ui/core'; import { action, computed, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { AclAdmin, AclEdit, DataSym, Doc, Field, HeightSym, WidthSym } from "../../fields/Doc"; +import { DateField } from '../../fields/DateField'; +import { AclAdmin, AclEdit, DataSym, Doc, DocListCast, Field, HeightSym, WidthSym } from "../../fields/Doc"; import { Document } from '../../fields/documentSchemas'; -import { HtmlField } from '../../fields/HtmlField'; import { InkField } from "../../fields/InkField"; import { ScriptField } from '../../fields/ScriptField'; import { Cast, NumCast, StrCast } from "../../fields/Types"; import { GetEffectiveAcl } from '../../fields/util'; -import { setupMoveUpEvents, emptyFunction, returnFalse } from "../../Utils"; -import { Docs, DocUtils } from "../documents/Documents"; +import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../Utils"; +import { Docs } from "../documents/Documents"; import { DocumentType } from '../documents/DocumentTypes'; import { CurrentUserUtils } from '../util/CurrentUserUtils'; import { DragManager } from "../util/DragManager"; @@ -25,12 +25,13 @@ import { KeyManager } from './GlobalKeyHandler'; import { InkStrokeProperties } from './InkStrokeProperties'; import { LightboxView } from './LightboxView'; import { DocumentView } from "./nodes/DocumentView"; -import React = require("react"); import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; -import { DateField } from '../../fields/DateField'; +import React = require("react"); +import { dark } from '@material-ui/core/styles/createPalette'; +import { color } from 'd3-color'; @observer -export class DocumentDecorations extends React.Component<{ boundsLeft: number, boundsTop: number }, { value: string }> { +export class DocumentDecorations extends React.Component<{ PanelWidth: number, PanelHeight: number, boundsLeft: number, boundsTop: number }, { value: string }> { static Instance: DocumentDecorations; private _resizeHdlId = ""; private _keyinput = React.createRef<HTMLInputElement>(); @@ -123,7 +124,7 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b const dragData = new DragManager.DocumentDragData(SelectionManager.Views().map(dv => dv.props.Document), dragDocView.props.dropAction); dragData.offset = dragDocView.props.ScreenToLocalTransform().transformDirection(e.x - left, e.y - top); dragData.moveDocument = dragDocView.props.moveDocument; - dragData.isSelectionMove = true; + dragData.isDocDecorationMove = true; dragData.canEmbed = dragTitle; this._hidden = this.Interacting = true; DragManager.StartDocumentDrag(SelectionManager.Views().map(dv => dv.ContentDiv!), dragData, e.x, e.y, { @@ -136,7 +137,6 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b return true; } - @undoBatch onCloseClick = () => { const selected = SelectionManager.Views().slice(); SelectionManager.DeselectAll(); @@ -153,15 +153,13 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b return true; }, emptyFunction, this.onMaximizeClick, false, false); } - @undoBatch - @action + onMaximizeClick = (e: any): void => { const selectedDocs = SelectionManager.Views(); if (selectedDocs.length) { if (e.ctrlKey) { // open an alias in a new tab with Ctrl Key - selectedDocs[0].props.Document._fullScreenView = Doc.MakeAlias(selectedDocs[0].props.Document); - (selectedDocs[0].props.Document._fullScreenView as Doc).context = undefined; - CollectionDockingView.AddSplit(selectedDocs[0].props.Document._fullScreenView as Doc, "right"); + const bestAlias = DocListCast(selectedDocs[0].props.Document.aliases).find(doc => !doc.context && doc.author === Doc.CurrentUserEmail); + CollectionDockingView.AddSplit(bestAlias ?? Doc.MakeAlias(selectedDocs[0].props.Document), "right"); } else if (e.shiftKey) { // open centered in a new workspace with Shift Key const alias = Doc.MakeAlias(selectedDocs[0].props.Document); alias.context = undefined; @@ -169,7 +167,7 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b alias.y = -alias[HeightSym]() / 2; CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([alias], { title: "Tab for " + alias.title }), "right"); } else if (e.altKey) { // open same document in new tab - CollectionDockingView.ToggleSplit(Cast(selectedDocs[0].props.Document._fullScreenView, Doc, null) || selectedDocs[0].props.Document, "right"); + CollectionDockingView.ToggleSplit(selectedDocs[0].props.Document, "right"); } else { LightboxView.SetLightboxDoc(selectedDocs[0].props.Document, undefined, selectedDocs.slice(1).map(view => view.props.Document)); } @@ -177,7 +175,6 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b SelectionManager.DeselectAll(); } - @undoBatch onIconifyClick = (): void => { SelectionManager.Views().forEach(dv => dv?.iconify()); SelectionManager.DeselectAll(); @@ -205,7 +202,10 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b InkStrokeProperties.Instance?.rotateInk(2 * movement.X / angle * (Math.PI / 180)); return false; }, - () => this._rotateUndo?.end(), + () => { + this._rotateUndo?.end(); + UndoManager.FilterBatches(["data", "x", "y", "width", "height"]); + }, emptyFunction); this._prevY = e.clientY; this._inkCenterPts = SelectionManager.Views() @@ -246,7 +246,8 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b InkStrokeProperties.Instance?._lock && SelectionManager.Views().filter(dv => dv.rootDoc.type === DocumentType.INK) .forEach(dv => fixedAspect = Doc.NativeAspect(dv.rootDoc)); - if (fixedAspect && (this._resizeHdlId === "documentDecorations-bottomRightResizer" || this._resizeHdlId === "documentDecorations-topLeftResizer")) { // need to generalize for bl and tr drag handles + const resizeHdl = this._resizeHdlId.split(" ")[0]; + if (fixedAspect && (resizeHdl === "documentDecorations-bottomRightResizer" || resizeHdl === "documentDecorations-topLeftResizer")) { // need to generalize for bl and tr drag handles const project = (p: number[], a: number[], b: number[]) => { const atob = [b[0] - a[0], b[1] - a[1]]; const atop = [p[0] - a[0], p[1] - a[1]]; @@ -269,7 +270,7 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b this._snapY = thisPt.y; let dragBottom = false, dragRight = false, dragBotRight = false; let dX = 0, dY = 0, dW = 0, dH = 0; - switch (this._resizeHdlId) { + switch (this._resizeHdlId.split(" ")[0]) { case "": break; case "documentDecorations-topLeftResizer": dX = -1; @@ -285,6 +286,7 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b case "documentDecorations-topResizer": dY = -1; dH = -move[1]; + dragBottom = true; break; case "documentDecorations-bottomLeftResizer": dX = -1; @@ -320,11 +322,12 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b let height = (doc._height || (nheight / nwidth * width)); height = !height || isNaN(height) ? 20 : height; const scale = docView.props.ScreenToLocalTransform().Scale; + const modifyNativeDim = (e.ctrlKey || doc.forceReflow) && doc.nativeDimModifiable; if (nwidth && nheight) { if (nwidth / nheight !== width / height && !dragBottom) { height = nheight / nwidth * width; } - if (e.ctrlKey && !dragBottom) { // ctrl key enables modification of the nativeWidth or nativeHeight durin the interaction + if (modifyNativeDim && !dragBottom) { // ctrl key enables modification of the nativeWidth or nativeHeight durin the interaction if (Math.abs(dW) > Math.abs(dH)) dH = dW * nheight / nwidth; else dW = dH * nwidth / nheight; } @@ -334,34 +337,27 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b doc.x = (doc.x || 0) + dX * (actualdW - width); doc.y = (doc.y || 0) + dY * (actualdH - height); const fixedAspect = (nwidth && nheight); - if (e.ctrlKey && [DocumentType.IMG, DocumentType.SCREENSHOT, DocumentType.VID].includes(doc.type as DocumentType)) { - dW !== 0 && runInAction(() => { - const dataDoc = doc[DataSym]; - const nw = Doc.NativeWidth(dataDoc); - const nh = Doc.NativeHeight(dataDoc); - Doc.SetNativeWidth(dataDoc, nw + (dW > 0 ? 10 : -10)); - Doc.SetNativeHeight(dataDoc, nh + (dW > 0 ? 10 : -10) * nh / nw); - }); - } - else if (fixedAspect) { - if ((Math.abs(dW) > Math.abs(dH) && (!dragBottom || !e.ctrlKey)) || dragRight) { - if (dragRight && e.ctrlKey) { + if (fixedAspect) { + if ((Math.abs(dW) > Math.abs(dH) && (!dragBottom || !modifyNativeDim)) || dragRight) { + if (dragRight && modifyNativeDim) { doc._nativeWidth = actualdW / (doc._width || 1) * Doc.NativeWidth(doc); } else { if (!doc._fitWidth) doc._height = nheight / nwidth * actualdW; - else if (!e.ctrlKey || dragBotRight) doc._height = actualdH; + else if (!modifyNativeDim || dragBotRight) doc._height = actualdH; } doc._width = actualdW; } else { - if (dragBottom && (e.ctrlKey || docView.layoutDoc._fitWidth)) { // frozen web pages and others that fitWidth can't grow horizontally to match a vertical resize so the only choice is to change the nativeheight even if the ctrl key isn't used + if (dragBottom && (modifyNativeDim || + (docView.layoutDoc.nativeHeightUnfrozen && docView.layoutDoc._fitWidth))) { // frozen web pages, PDFs, and some RTFS have frozen nativewidth/height. But they are marked to allow their nativeHeight to be explicitly modified with fitWidth and vertical resizing. (ie, with fitWidth they can't grow horizontally to match a vertical resize so it makes more sense to change their nativeheight even if the ctrl key isn't used) doc._nativeHeight = actualdH / (doc._height || 1) * Doc.NativeHeight(doc); doc._autoHeight = false; } else { if (!doc._fitWidth) doc._width = nwidth / nheight * actualdH; - else if (!e.ctrlKey || dragBotRight) doc._width = actualdW; + else if (!modifyNativeDim || dragBotRight) doc._width = actualdW; } - doc._height = actualdH; + if (!modifyNativeDim) doc._height = Math.min(nheight / nwidth * NumCast(doc._width), actualdH); + else doc._height = actualdH; } } else { dH && (doc._height = actualdH); @@ -404,6 +400,9 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b get selectionTitle(): string { if (SelectionManager.Views().length === 1) { const selected = SelectionManager.Views()[0]; + if (selected.ComponentView?.getTitle?.()) { + return selected.ComponentView.getTitle(); + } if (this._titleControlString.startsWith("=")) { return ScriptField.MakeFunction(this._titleControlString.substring(1), { doc: Doc.name })!.script.run({ self: selected.rootDoc, this: selected.layoutDoc }, console.log).result?.toString() || ""; } @@ -421,7 +420,9 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 1 || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) { return (null); } - const canOpen = SelectionManager.Views().some(docView => !docView.props.Document._stayInCollection); + const hideResizers = seldoc.props.hideResizeHandles || seldoc.rootDoc.hideResizeHandles; + const hideTitle = seldoc.props.hideDecorationTitle || seldoc.rootDoc.hideDecorationTitle; + const canOpen = SelectionManager.Views().some(docView => !docView.props.Document._stayInCollection && !docView.props.Document.isGroup && !docView.props.Document.hideOpenButton); const canDelete = SelectionManager.Views().some(docView => { const collectionAcl = docView.props.ContainingCollectionView ? GetEffectiveAcl(docView.props.ContainingCollectionDoc?.[DataSym]) : AclEdit; return (!docView.rootDoc._stayInCollection || docView.rootDoc.isInkMask) && @@ -430,16 +431,22 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b const topBtn = (key: string, icon: string, pointerDown: undefined | ((e: React.PointerEvent) => void), click: undefined | ((e: any) => void), title: string) => ( <Tooltip key={key} title={<div className="dash-tooltip">{title}</div>} placement="top"> <div className={`documentDecorations-${key}Button`} onContextMenu={e => e.preventDefault()} - onPointerDown={pointerDown ?? (e => setupMoveUpEvents(this, e, returnFalse, click!, emptyFunction))} > + onPointerDown={pointerDown ?? (e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(e => click!(e))))} > <FontAwesomeIcon icon={icon as any} /> </div> </Tooltip>); + const colorScheme = StrCast(CurrentUserUtils.ActiveDashboard?.colorScheme); const titleArea = this._edtingTitle ? - <input ref={this._keyinput} className="documentDecorations-title" style={{ width: `calc(100% - ${seldoc?.props.hideResizeHandles ? 0 : 20}px` }} type="text" name="dynbox" autoComplete="on" value={this._accumulatedTitle} - onBlur={e => this.titleBlur()} onChange={action(e => this._accumulatedTitle = e.target.value)} onKeyPress={this.titleEntered} /> : + <input ref={this._keyinput} className={`documentDecorations-title${colorScheme}`} + style={{ width: `calc(100% - ${seldoc?.props.hideResizeHandles ? 0 : 20}px` }} + type="text" name="dynbox" autoComplete="on" + value={this._accumulatedTitle} + onBlur={e => this.titleBlur()} + onChange={action(e => this._accumulatedTitle = e.target.value)} + onKeyPress={this.titleEntered} /> : <div className="documentDecorations-title" style={{ width: `calc(100% - ${seldoc?.props.hideResizeHandles ? 0 : 20}px` }} key="title" onPointerDown={this.onTitleDown} > - <span className="documentDecorations-titleSpan">{`${this.selectionTitle}`}</span> + <span className={`documentDecorations-titleSpan${colorScheme}`}>{`${this.selectionTitle}`}</span> </div>; let inMainMenuPanel = false; @@ -453,9 +460,11 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b const borderRadiusDraggerWidth = 15; bounds.r = Math.max(bounds.x, Math.max(leftBounds, Math.min(window.innerWidth, bounds.r + borderRadiusDraggerWidth + this._resizeBorderWidth / 2) - this._resizeBorderWidth / 2 - borderRadiusDraggerWidth)); bounds.b = Math.max(bounds.y, Math.max(topBounds, Math.min(window.innerHeight, bounds.b + this._resizeBorderWidth / 2 + this._linkBoxHeight) - this._resizeBorderWidth / 2 - this._linkBoxHeight)); + const useRotation = seldoc.rootDoc.type === DocumentType.INK; + const resizerScheme = colorScheme ? "documentDecorations-resizer" + colorScheme : ""; - return (<div className="documentDecorations" style={{ background: CurrentUserUtils.ActiveDashboard?.darkScheme ? "dimgray" : "" }} > + return (<div className={`documentDecorations${colorScheme}`}> <div className="documentDecorations-background" style={{ width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px", height: (bounds.b - bounds.y + this._resizeBorderWidth) + "px", @@ -472,21 +481,21 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b top: bounds.y - this._resizeBorderWidth / 2 - this._titleHeight, }}> {!canDelete ? <div /> : topBtn("close", "times", undefined, this.onCloseClick, "Close")} - {seldoc.props.hideDecorationTitle || seldoc.props.Document.type === DocumentType.EQUATION ? (null) : titleArea} - {seldoc.props.hideResizeHandles || seldoc.props.Document.type === DocumentType.EQUATION ? (null) : + {hideTitle ? (null) : titleArea}{!canOpen ? (null) : topBtn("open", "external-link-alt", this.onMaximizeDown, undefined, "Open in Tab (ctrl: as alias, shift: in new collection)")} + + {hideResizers ? (null) : <> - {SelectionManager.Views().length !== 1 || seldoc.Document.type === DocumentType.INK ? (null) : + {SelectionManager.Views().length !== 1 || hideTitle ? (null) : topBtn("iconify", `window-${seldoc.finalLayoutKey.includes("icon") ? "restore" : "minimize"}`, undefined, this.onIconifyClick, `${seldoc.finalLayoutKey.includes("icon") ? "De" : ""}Iconify Document`)} - {!canOpen ? (null) : topBtn("open", "external-link-alt", this.onMaximizeDown, undefined, "Open in Tab (ctrl: as alias, shift: in new collection)")} - <div key="tl" className="documentDecorations-topLeftResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> - <div key="t" className="documentDecorations-topResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> - <div key="tr" className="documentDecorations-topRightResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> - <div key="l" className="documentDecorations-leftResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> - <div key="c" className="documentDecorations-centerCont"></div> - <div key="r" className="documentDecorations-rightResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> - <div key="bl" className="documentDecorations-bottomLeftResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> - <div key="b" className="documentDecorations-bottomResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> - <div key="br" className="documentDecorations-bottomRightResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> + <div key="tl" className={`documentDecorations-topLeftResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> + <div key="t" className={`documentDecorations-topResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> + <div key="tr" className={`documentDecorations-topRightResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> + <div key="l" className={`documentDecorations-leftResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> + <div key="c" className={`documentDecorations-centerCont ${resizerScheme}`}></div> + <div key="r" className={`documentDecorations-rightResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> + <div key="bl" className={`documentDecorations-bottomLeftResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> + <div key="b" className={`documentDecorations-bottomResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> + <div key="br" className={`documentDecorations-bottomRightResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> {seldoc.props.renderDepth <= 1 || !seldoc.props.ContainingCollectionView ? (null) : topBtn("selector", "arrow-alt-circle-up", undefined, this.onSelectorClick, "tap to select containing document")} diff --git a/src/client/views/EditableView.scss b/src/client/views/EditableView.scss index 1aebedf2e..ed7ec9dc1 100644 --- a/src/client/views/EditableView.scss +++ b/src/client/views/EditableView.scss @@ -5,6 +5,7 @@ hyphens: auto; overflow: hidden; min-width: 20; + text-overflow: ellipsis; } .editableView-container-editing-oneLine { diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index 03d9efff3..d7707a6fe 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -155,6 +155,8 @@ export class EditableView extends React.Component<EditableProps> { return wasFocused !== this._editing; } + + renderEditor() { return this.props.autosuggestProps ? <Autosuggest @@ -197,7 +199,7 @@ export class EditableView extends React.Component<EditableProps> { setTimeout(() => this.props.autosuggestProps?.resetValue()); return this.props.contents instanceof ObjectField ? (null) : <div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`} ref={this._ref} - style={{ display: this.props.display, textOverflow: this.props.overflow, minHeight: "17px", whiteSpace: "nowrap", height: this.props.height || "auto", maxHeight: this.props.maxHeight }} + style={{ display: this.props.display, textOverflow: this.props.overflow, minHeight: "10px", whiteSpace: "nowrap", height: this.props.height || "auto", maxHeight: this.props.maxHeight }} onClick={this.onClick} placeholder={this.props.placeholder}> <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize }} > {this.props.contents ? this.props.contents?.valueOf() : this.props.placeholder?.valueOf()} diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index bbf21f22c..f28485e43 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -14,6 +14,7 @@ import { DocUtils } from "../documents/Documents"; import { CurrentUserUtils } from "../util/CurrentUserUtils"; import { InteractionUtils } from "../util/InteractionUtils"; import { Scripting } from "../util/Scripting"; +import { SelectionManager } from "../util/SelectionManager"; import { Transform } from "../util/Transform"; import { CollectionFreeFormViewChrome } from "./collections/CollectionMenu"; import "./GestureOverlay.scss"; @@ -23,7 +24,6 @@ import { RadialMenu } from "./nodes/RadialMenu"; import HorizontalPalette from "./Palette"; import { Touchable } from "./Touchable"; import TouchScrollableMenu, { TouchScrollableMenuItem } from "./TouchScrollableMenu"; -import { SelectionManager } from "../util/SelectionManager"; @observer export class GestureOverlay extends Touchable { @@ -31,7 +31,7 @@ export class GestureOverlay extends Touchable { @observable public InkShape: string = ""; @observable public SavedColor?: string; - @observable public SavedWidth?: string; + @observable public SavedWidth?: number; @observable public Tool: ToolglassTools = ToolglassTools.None; @observable private _thumbX?: number; @@ -494,6 +494,7 @@ export class GestureOverlay extends Touchable { if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || [InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool)) { this._points.push({ X: e.clientX, Y: e.clientY }); setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction); + // if (CurrentUserUtils.SelectedTool === InkTool.Highlighter) SetActiveInkColor("rgba(245, 230, 95, 0.75)"); } } @@ -588,8 +589,9 @@ export class GestureOverlay extends Touchable { this.makePolygon(this.InkShape, false); this.dispatchGesture(GestureUtils.Gestures.Stroke); this._points = []; - if (!CollectionFreeFormViewChrome.Instance._keepPrimitiveMode) { + if (!CollectionFreeFormViewChrome.Instance?._keepPrimitiveMode) { this.InkShape = ""; + Doc.UserDoc().activeInkTool = InkTool.None; } } // if we're not drawing in a toolglass try to recognize as gesture @@ -625,6 +627,9 @@ export class GestureOverlay extends Touchable { } + const dist = Math.sqrt((controlPoints[0].X - controlPoints.lastElement().X) * (controlPoints[0].X - controlPoints.lastElement().X) + + (controlPoints[0].Y - controlPoints.lastElement().Y) * (controlPoints[0].Y - controlPoints.lastElement().Y)); + if (controlPoints.length > 4 && dist < 10) controlPoints[controlPoints.length - 1] = controlPoints[0]; this._points = controlPoints; this.dispatchGesture(GestureUtils.Gestures.Stroke); @@ -687,25 +692,26 @@ export class GestureOverlay extends Touchable { case "rectangle": this._points.push({ X: left, Y: top }); this._points.push({ X: left, Y: top }); - this._points.push({ X: right, Y: top }); this._points.push({ X: right, Y: top }); + this._points.push({ X: right, Y: top }); this._points.push({ X: right, Y: top }); - this._points.push({ X: right, Y: bottom }); this._points.push({ X: right, Y: bottom }); + this._points.push({ X: right, Y: bottom }); this._points.push({ X: right, Y: bottom }); - this._points.push({ X: left, Y: bottom }); this._points.push({ X: left, Y: bottom }); + this._points.push({ X: left, Y: bottom }); this._points.push({ X: left, Y: bottom }); - this._points.push({ X: left, Y: top }); this._points.push({ X: left, Y: top }); + break; + case "triangle": this._points.push({ X: left, Y: bottom }); this._points.push({ X: left, Y: bottom }); @@ -733,12 +739,8 @@ export class GestureOverlay extends Touchable { 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)); - // 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 }); + // Dividing the circle into four equal sections, and fitting each section to a cubic Bézier curve. 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) }); @@ -754,13 +756,18 @@ export class GestureOverlay extends Touchable { 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 }); + break; case "line": - if (Math.abs(firstx - lastx) < 20) { + if (Math.abs(firstx - lastx) < 10 && Math.abs(firsty - lasty) > 10) { lastx = firstx; } - if (Math.abs(firsty - lasty) < 20) { + if (Math.abs(firsty - lasty) < 10 && Math.abs(firstx - lastx) > 10) { lasty = firsty; } this._points.push({ X: firstx, Y: firsty }); @@ -834,20 +841,23 @@ export class GestureOverlay extends Touchable { B.bottom = B.bottom + width / 2; B.width += width; B.height += width; + const fillColor = ActiveFillColor(); + const strokeColor = fillColor && fillColor !== "transparent" ? fillColor : ActiveInkColor(); return [ this.props.children, this._palette, [this._strokes.map((l, i) => { const b = { left: -20000, right: 20000, top: -20000, bottom: 20000, width: 40000, height: 40000 };//this.getBounds(l, true); return <svg key={i} width={b.width} height={b.height} style={{ transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}> - {InteractionUtils.CreatePolyline(l, b.left, b.top, ActiveInkColor(), width, width, - ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), - ActiveDash(), 1, 1, this.InkShape, "none", false, false)} + {InteractionUtils.CreatePolyline(l, b.left, b.top, strokeColor, width, width, "miter", "round", + ActiveInkBezierApprox(), "none" /*ActiveFillColor()*/, ActiveArrowStart(), ActiveArrowEnd(), + ActiveDash(), 1, 1, this.InkShape, "none", 1.0, false)} </svg>; }), this._points.length <= 1 ? (null) : <svg key="svg" width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}> - {InteractionUtils.CreatePolyline(this._points.map(p => ({ X: p.X, Y: p.Y - (rect?.y || 0) })), B.left, B.top, ActiveInkColor(), width, width, ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), 1, 1, this.InkShape, "none", false, false)} + {InteractionUtils.CreatePolyline(this._points.map(p => ({ X: p.X, Y: p.Y - (rect?.y || 0) })), B.left, B.top, ActiveInkColor(), width, width, "miter", "round", "", + "none" /*ActiveFillColor()*/, ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), 1, 1, this.InkShape, "none", 1.0, false)} </svg>] ]; } @@ -952,7 +962,7 @@ Scripting.addGlobal(function setPen(width: any, color: any, fill: any, arrowStar Scripting.addGlobal(function resetPen() { runInAction(() => { SetActiveInkColor(GestureOverlay.Instance.SavedColor ?? "rgb(0, 0, 0)"); - SetActiveInkWidth(GestureOverlay.Instance.SavedWidth ?? "2"); + SetActiveInkWidth(GestureOverlay.Instance.SavedWidth?.toString() ?? "2"); }); }, "resets the pen tool"); Scripting.addGlobal(function createText(text: any, x: any, y: any) { diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 0127d3080..364bf05e2 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -10,10 +10,11 @@ import { Cast, PromiseValue } from "../../fields/Types"; import { GoogleAuthenticationManager } from "../apis/GoogleAuthenticationManager"; import { DocServer } from "../DocServer"; import { DocumentType } from "../documents/DocumentTypes"; -import { DictationManager } from "../util/DictationManager"; +import { CurrentUserUtils } from "../util/CurrentUserUtils"; import { DragManager } from "../util/DragManager"; import { GroupManager } from "../util/GroupManager"; import { SelectionManager } from "../util/SelectionManager"; +import { SettingsManager } from "../util/SettingsManager"; import { SharingManager } from "../util/SharingManager"; import { SnappingManager } from "../util/SnappingManager"; import { undoBatch, UndoManager } from "../util/UndoManager"; @@ -27,8 +28,6 @@ import { LightboxView } from "./LightboxView"; import { MainView } from "./MainView"; import { DocumentLinksButton } from "./nodes/DocumentLinksButton"; import { AnchorMenu } from "./pdf/AnchorMenu"; -import { CurrentUserUtils } from "../util/CurrentUserUtils"; -import { SettingsManager } from "../util/SettingsManager"; const modifiers = ["control", "meta", "shift", "alt"]; type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise<KeyControlInfo>; @@ -142,7 +141,7 @@ export class KeyManager { case "delete": case "backspace": if (document.activeElement?.tagName !== "INPUT" && document.activeElement?.tagName !== "TEXTAREA") { - const selected = SelectionManager.Views().slice(); + const selected = SelectionManager.Views().filter(dv => !dv.topMost); UndoManager.RunInBatch(() => { SelectionManager.DeselectAll(); selected.map(dv => !dv.props.Document._stayInCollection && dv.props.removeDocument?.(dv.props.Document)); @@ -222,15 +221,18 @@ 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": - const searchBtn = Doc.UserDoc().searchBtn as Doc; - - if (searchBtn) { - MainView.Instance.selectMenu(searchBtn); + if (SelectionManager.Views().length === 1 && SelectionManager.Views()[0].ComponentView?.search) { + SelectionManager.Views()[0].ComponentView?.search?.("", false, false); + } else { + const searchBtn = Doc.UserDoc().searchBtn as Doc; + if (searchBtn) { + MainView.Instance.selectMenu(searchBtn); + } } break; case "o": - const target = SelectionManager.Views()[0]; - target && CollectionDockingView.OpenFullScreen(target.props.Document); + const target = SelectionManager.Docs().lastElement(); + target && CollectionDockingView.OpenFullScreen(target); break; case "r": preventDefault = false; diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx new file mode 100644 index 000000000..0644488b3 --- /dev/null +++ b/src/client/views/InkControlPtHandles.tsx @@ -0,0 +1,174 @@ +import React = require("react"); +import { action, observable } from "mobx"; +import { observer } from "mobx-react"; +import { Doc } from "../../fields/Doc"; +import { ControlPoint, InkData, PointData } from "../../fields/InkField"; +import { listSpec } from "../../fields/Schema"; +import { Cast } from "../../fields/Types"; +import { setupMoveUpEvents } from "../../Utils"; +import { Transform } from "../util/Transform"; +import { UndoManager } from "../util/UndoManager"; +import { Colors } from "./global/globalEnums"; +import { InkStrokeProperties } from "./InkStrokeProperties"; +import { List } from "../../fields/List"; +import { InkingStroke } from "./InkingStroke"; + +export interface InkControlProps { + inkDoc: Doc; + inkCtrlPoints: InkData; + screenCtrlPoints: InkData; + screenSpaceLineWidth: number; + ScreenToLocalTransform: () => Transform; + nearestScreenPt: () => PointData | undefined; +} + +@observer +export class InkControlPtHandles extends React.Component<InkControlProps> { + + @observable private _overControl = -1; + + @observable controlUndo: UndoManager.Batch | undefined; + + componentDidMount() { + document.addEventListener("keydown", this.onDelete, true); + } + componentWillUnmount() { + document.removeEventListener("keydown", this.onDelete, true); + } + /** + * 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) { + const screenScale = this.props.ScreenToLocalTransform().Scale; + const order = controlIndex % 4; + const handleIndexA = ((order === 3 ? controlIndex - 1 : controlIndex - 2) + this.props.inkCtrlPoints.length) % this.props.inkCtrlPoints.length; + const handleIndexB = (order === 3 ? controlIndex + 2 : controlIndex + 1) % this.props.inkCtrlPoints.length; + const brokenIndices = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number")); + const wasSelected = InkStrokeProperties.Instance?._currentPoint === controlIndex; + setupMoveUpEvents(this, e, + action((e: PointerEvent, down: number[], delta: number[]) => { + if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("drag ink ctrl pt"); + InkStrokeProperties.Instance?.moveControlPtHandle(delta[0] * screenScale, delta[1] * screenScale, controlIndex); + return false; + }), + action(() => { + if (this.controlUndo) { + InkStrokeProperties.Instance?.snapControl(this.props.inkDoc, controlIndex); + } + this.controlUndo?.end(); + this.controlUndo = undefined; + UndoManager.FilterBatches(["data", "x", "y", "width", "height"]); + }), + action((e: PointerEvent, doubleTap: boolean | undefined) => { + const equivIndex = controlIndex === 0 ? this.props.inkCtrlPoints.length - 1 : controlIndex === this.props.inkCtrlPoints.length - 1 ? 0 : controlIndex; + if (doubleTap || e.button === 2) { + if (!brokenIndices?.includes(equivIndex) && !brokenIndices?.includes(controlIndex)) { + if (brokenIndices) brokenIndices.push(controlIndex); + else this.props.inkDoc.brokenInkIndices = new List<number>([controlIndex]); + } else { + if (brokenIndices?.includes(equivIndex)) { + if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("make smooth"); + InkStrokeProperties.Instance?.snapHandleTangent(equivIndex, handleIndexA, handleIndexB); + } + if (equivIndex !== controlIndex && brokenIndices?.includes(controlIndex)) { + if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("make smooth"); + InkStrokeProperties.Instance?.snapHandleTangent(controlIndex, handleIndexA, handleIndexB); + } + } + this.controlUndo?.end(); + this.controlUndo = undefined; + } + this.changeCurrPoint(controlIndex); + }), undefined, undefined, () => wasSelected && this.changeCurrPoint(-1)); + } + } + /** + * 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; }; + + /** + * Deletes the currently selected point. + */ + @action + onDelete = (e: KeyboardEvent) => { + if (["-", "Backspace", "Delete"].includes(e.key)) { + InkStrokeProperties.Instance?.deletePoints(); + e.stopPropagation(); + } + } + + /** + * Changes the current selected control point. + */ + @action + changeCurrPoint = (i: number) => { + if (InkStrokeProperties.Instance) { + InkStrokeProperties.Instance._currentPoint = i; + } + } + + render() { + // Accessing the current ink's data and extracting all control points. + const scrData = this.props.screenCtrlPoints; + const sreenCtrlPoints: ControlPoint[] = []; + for (let i = 0; i <= scrData.length - 4; i += 4) { + sreenCtrlPoints.push({ ...scrData[i], I: i }); + sreenCtrlPoints.push({ ...scrData[i + 3], I: i + 3 }); + } + + const inkData = this.props.inkCtrlPoints; + const inkCtrlPts: ControlPoint[] = []; + for (let i = 0; i <= inkData.length - 4; i += 4) { + inkCtrlPts.push({ ...inkData[i], I: i }); + inkCtrlPts.push({ ...inkData[i + 3], I: i + 3 }); + } + + const screenSpaceLineWidth = this.props.screenSpaceLineWidth; + const closed = InkingStroke.IsClosed(inkData); + const nearestScreenPt = this.props.nearestScreenPt(); + const TagType = (broken?: boolean) => broken ? "rect" : "circle"; + const hdl = (control: { X: number, Y: number, I: number }, scale: number, color: string) => { + const broken = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number"))?.includes(control.I); + const Tag = TagType((control.I === 0 || control.I === inkData.length - 1) && !closed) as keyof JSX.IntrinsicElements; + return <Tag key={control.I.toString() + scale} + x={control.X - screenSpaceLineWidth * 2 * scale} + y={control.Y - screenSpaceLineWidth * 2 * scale} + cx={control.X} + cy={control.Y} + r={screenSpaceLineWidth * 2 * scale} + opacity={this.controlUndo ? 0.15 : 1} + height={screenSpaceLineWidth * 4 * scale} + width={screenSpaceLineWidth * 4 * scale} + strokeWidth={screenSpaceLineWidth / 2} + stroke={Colors.MEDIUM_BLUE} + fill={broken ? Colors.MEDIUM_BLUE : color} + onPointerDown={(e: any) => this.onControlDown(e, control.I)} + onMouseEnter={() => this.onEnterControl(control.I)} + onMouseLeave={this.onLeaveControl} + pointerEvents="all" + cursor="default" + />; + }; + return (<svg> + {!nearestScreenPt ? (null) : + <circle key={"npt"} + cx={nearestScreenPt.X} + cy={nearestScreenPt.Y} + r={screenSpaceLineWidth * 2} + fill={"#00007777"} + stroke={"#00007777"} + strokeWidth={0} + pointerEvents="none" + /> + } + {sreenCtrlPoints.map(control => hdl(control, this._overControl !== control.I ? 1 : 3 / 2, Colors.WHITE))} + </svg> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/InkControls.tsx b/src/client/views/InkControls.tsx deleted file mode 100644 index 6213a4075..000000000 --- a/src/client/views/InkControls.tsx +++ /dev/null @@ -1,142 +0,0 @@ -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 deleted file mode 100644 index 0b24c3c32..000000000 --- a/src/client/views/InkHandles.tsx +++ /dev/null @@ -1,127 +0,0 @@ -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 index 812a79bd5..55e06c6ca 100644 --- a/src/client/views/InkStroke.scss +++ b/src/client/views/InkStroke.scss @@ -1,9 +1,26 @@ +.inkstroke-UI { + // transform-origin: top left; + position: absolute; + overflow: visible; + pointer-events: none; + z-index: 2001; // 1 higher than documentdecorations + + svg:not(:root) { + overflow: visible !important; + position: absolute; + left:0; + top:0; + } +} + .inkStroke { mix-blend-mode: multiply; stroke-linejoin: round; stroke-linecap: round; overflow: visible !important; transform-origin: top left; + width: 100%; + height: 100%; svg:not(:root) { overflow: visible !important; diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index d527b2a05..ee30caa3d 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -1,13 +1,16 @@ -import { action, computed, observable } from "mobx"; -import { Doc, DocListCast, Field, Opt } from "../../fields/Doc"; +import { Bezier } from "bezier-js"; +import { action, computed, observable, reaction } from "mobx"; +import { Doc } from "../../fields/Doc"; import { Document } from "../../fields/documentSchemas"; -import { InkField, InkData, PointData, ControlPoint } from "../../fields/InkField"; +import { InkData, InkField, InkTool, PointData } 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 { CurrentUserUtils } from "../util/CurrentUserUtils"; import { SelectionManager } from "../util/SelectionManager"; import { undoBatch } from "../util/UndoManager"; +import { InkingStroke } from "./InkingStroke"; export class InkStrokeProperties { static Instance: InkStrokeProperties | undefined; @@ -18,6 +21,8 @@ export class InkStrokeProperties { constructor() { InkStrokeProperties.Instance = this; + reaction(() => this._controlButton, button => button && (CurrentUserUtils.SelectedTool = InkTool.None)); + reaction(() => CurrentUserUtils.SelectedTool, tool => (tool !== InkTool.None) && (this._controlButton = false)); } @computed get selectedInk() { @@ -25,11 +30,6 @@ export class InkStrokeProperties { return inks.length ? inks : undefined; } - 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. @@ -45,14 +45,14 @@ export class InkStrokeProperties { if (ink) { 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 ptsXscale = NumCast(doc._width) / (oldXrange.max - oldXrange.min); - const ptsYscale = NumCast(doc._height) / (oldYrange.max - oldYrange.min); + const ptsXscale = ((NumCast(doc._width) - NumCast(doc.strokeWidth)) / ((oldXrange.max - oldXrange.min) || 1)) || 1; + const ptsYscale = ((NumCast(doc._height) - NumCast(doc.strokeWidth)) / ((oldYrange.max - oldYrange.min) || 1)) || 1; const newPoints = func(doc, ink, ptsXscale, ptsYscale); if (newPoints) { const newXrange = (xs => ({ min: Math.min(...xs), max: Math.max(...xs) }))(newPoints.map(p => p.X)); const newYrange = (ys => ({ min: Math.min(...ys), max: Math.max(...ys) }))(newPoints.map(p => p.Y)); - doc._width = (newXrange.max - newXrange.min) * ptsXscale; - doc._height = (newYrange.max - newYrange.min) * ptsYscale; + doc._width = (newXrange.max - newXrange.min) * ptsXscale + NumCast(doc.strokeWidth); + doc._height = (newYrange.max - newYrange.min) * ptsYscale + NumCast(doc.strokeWidth); doc.x = (oldXrange.coord + (newXrange.min - oldXrange.min) * ptsXscale); doc.y = (oldYrange.coord + (newYrange.min - oldYrange.min) * ptsYscale); Doc.GetProto(doc).data = new InkField(newPoints); @@ -67,66 +67,24 @@ export class InkStrokeProperties { /** * Adds a new control point to the ink instance when editing its format. - * @param index The index of the new point. + * @param t T-Value of new control point + * @param i index of first control point of segment being split * @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 }[]) => { + addPoints = (t: number, i: 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 }); - } + const array = [controls[i], controls[i + 1], controls[i + 2], controls[i + 3]]; + const newsegs = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))).split(t); + const splicepts = [...newsegs.left.points, ...newsegs.right.points]; + controls.splice(i, 4, ...splicepts.map(p => ({ X: p.x, Y: p.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; + doc.brokenInkIndices = new List(Cast(doc.brokenInkIndices, listSpec("number"), []).map(control => control > i ? control + 4 : control)); this._currentPoint = -1; - return newPoints; + + return controls; }); } @@ -140,8 +98,7 @@ export class InkStrokeProperties { 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; + return { X: handleVector.X * (newSize / prevSize), Y: handleVector.Y * (newSize / prevSize) }; } /** @@ -189,22 +146,16 @@ export class InkStrokeProperties { deletePoints = () => this.applyFunction((doc: Doc, ink: InkData) => { const newPoints: { X: number, Y: number }[] = []; const toRemove = Math.floor(((this._currentPoint + 2) / 4)); + const last = this._currentPoint === ink.length - 1; 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 }); } } + doc.brokenInkIndices = new List(Cast(doc.brokenInkIndices, listSpec("number"), []).map(control => control >= toRemove * 4 ? control - 4 : control)); + if (last) newPoints.splice(newPoints.length - 3, 2); this._currentPoint = -1; - if (newPoints.length < 4) return undefined; - if (newPoints.length === 4) { - const newerPoints: { X: number, Y: number }[] = []; - newerPoints.push({ X: newPoints[0].X, Y: newPoints[0].Y }); - newerPoints.push({ X: newPoints[0].X, Y: newPoints[0].Y }); - newerPoints.push({ X: newPoints[3].X, Y: newPoints[3].Y }); - newerPoints.push({ X: newPoints[3].X, Y: newPoints[3].Y }); - return newerPoints; - } - return newPoints; + return newPoints.length < 4 ? undefined : newPoints; }, true) /** @@ -218,11 +169,11 @@ export class InkStrokeProperties { 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 }; - const newPoints: { X: number, Y: number }[] = []; - ink.map(i => ({ X: i.X - centerPoint.X, Y: i.Y - centerPoint.Y })).forEach(i => { - const newX = Math.cos(angle) * i.X - Math.sin(angle) * i.Y; - const newY = Math.sin(angle) * i.X + Math.cos(angle) * i.Y; - newPoints.push({ X: newX + centerPoint.X, Y: newY + centerPoint.Y }); + const newPoints = ink.map(i => { + const pt = { X: i.X - centerPoint.X, Y: i.Y - centerPoint.Y }; + const newX = Math.cos(angle) * pt.X - Math.sin(angle) * pt.Y * yScale / xScale; + const newY = Math.sin(angle) * pt.X * xScale / yScale + Math.cos(angle) * pt.Y; + return { X: newX + centerPoint.X, Y: newY + centerPoint.Y }; }); doc.rotation = NumCast(doc.rotation) + angle; return newPoints; @@ -234,29 +185,85 @@ export class InkStrokeProperties { */ @undoBatch @action - moveControl = (deltaX: number, deltaY: number, controlIndex: number) => + moveControlPtHandle = (deltaX: number, deltaY: number, controlIndex: number) => this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => { - const newPoints: { X: number, Y: number }[] = []; const order = controlIndex % 4; - for (var i = 0; i < ink.length; i++) { + const closed = InkingStroke.IsClosed(ink); + + const newpts = ink.map((pt, i) => { 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 === 0 || order === 3) && (controlIndex === 0 || controlIndex === ink.length - 1) && (i === 1 || i === ink.length - 2) && closed) || (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 ({ X: pt.X + deltaX / xScale, Y: pt.Y + deltaY / yScale }); } - } - return newPoints; + return pt; + }); + return newpts; }) + + public static nearestPtToStroke(ctrlPoints: { X: number, Y: number }[], refPt: { X: number, Y: number }, excludeSegs?: number[]) { + var distance = Number.MAX_SAFE_INTEGER; + var nearestT = -1; + var nearestSeg = -1; + var nearestPt = { X: 0, Y: 0 }; + for (var i = 0; i < ctrlPoints.length - 3; i += 4) { + if (excludeSegs?.includes(i)) continue; + const array = [ctrlPoints[i], ctrlPoints[i + 1], ctrlPoints[i + 2], ctrlPoints[i + 3]]; + const point = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))).project({ x: refPt.X, y: refPt.Y }); + if (point.t !== undefined) { + const dist = Math.sqrt((point.x - refPt.X) * (point.x - refPt.X) + (point.y - refPt.Y) * (point.y - refPt.Y)); + if (dist < distance) { + distance = dist; + nearestT = point.t; + nearestSeg = i; + nearestPt = { X: point.x, Y: point.y }; + } + } + } + return { distance, nearestT, nearestSeg, nearestPt }; + } + + /** + * Handles the movement/scaling of a control point. + */ + snapControl = (inkDoc: Doc, controlIndex: number) => { + const ink = Cast(inkDoc.data, InkField)?.inkData; + if (ink) { + const closed = InkingStroke.IsClosed(ink); + + // figure out which segments we don't want to snap to - avoid the dragged control point's segment and the next and prev segments (when they exist -- ie not for endpoints of unclosed curve) + const thisseg = Math.floor(controlIndex / 4) * 4; + const which = controlIndex % 4; + const nextseg = which > 1 && (closed || controlIndex < ink.length - 1) ? (thisseg + 4) % ink.length : -1; + const prevseg = which < 2 && (closed || controlIndex > 0) ? (thisseg - 4 + ink.length) % ink.length : -1; + const refPt = ink[controlIndex]; + const { nearestPt } = InkStrokeProperties.nearestPtToStroke(ink, refPt, [thisseg, prevseg, nextseg]); + + // nearestPt is in inkDoc coordinates -- we need to compute the distance in screen coordinates. + // so we scale the X & Y distances by the internal ink scale factor and then transform the final distance by the ScreenToLocal.Scale of the inkDoc itself. + const oldXrange = (xs => ({ coord: NumCast(inkDoc.x), min: Math.min(...xs), max: Math.max(...xs) }))(ink.map(p => p.X)); + const oldYrange = (ys => ({ coord: NumCast(inkDoc.y), min: Math.min(...ys), max: Math.max(...ys) }))(ink.map(p => p.Y)); + const ptsXscale = ((NumCast(inkDoc._width) - NumCast(inkDoc.strokeWidth)) / ((oldXrange.max - oldXrange.min) || 1)) || 1; + const ptsYscale = ((NumCast(inkDoc._height) - NumCast(inkDoc.strokeWidth)) / ((oldYrange.max - oldYrange.min) || 1)) || 1; + const near = Math.sqrt((nearestPt.X - refPt.X) * (nearestPt.X - refPt.X) * ptsXscale * ptsXscale + + (nearestPt.Y - refPt.Y) * (nearestPt.Y - refPt.Y) * ptsYscale * ptsYscale); + + if (near / (this.selectedInk?.lastElement().props.ScreenToLocalTransform().Scale || 1) < 10) { + return this.moveControlPtHandle((nearestPt.X - ink[controlIndex].X) * ptsXscale, (nearestPt.Y - ink[controlIndex].Y) * ptsYscale, controlIndex); + } + } + return false; + } + /** * Snaps a control point with broken tangency back to synced rotation. * @param handleIndexA The handle point that retains its current position. @@ -264,21 +271,16 @@ export class InkStrokeProperties { */ 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 brokenIndices = Cast(doc.brokenInkIndices, listSpec("number"), []); + const ind = brokenIndices.findIndex(value => value === controlIndex); + if (ind !== -1) { + brokenIndices.splice(ind, 1); const [controlPoint, handleA, handleB] = [ink[controlIndex], ink[handleIndexA], ink[handleIndexB]]; const oppositeHandleA = this.rotatePoint(handleA, controlPoint, Math.PI); const angleDifference = this.angleChange(handleB, oppositeHandleA, controlPoint); - const newHandleB = this.rotatePoint(handleB, controlPoint, angleDifference); - ink[handleIndexB] = newHandleB; - return ink; + const inkCopy = ink.slice(); // have to make a new copy of the array to keep from corrupting undo/redo. without slicing, the same array will be stored in each undo step meaning earlier undo steps will be inadvertently updated to store the latest value. + inkCopy[handleIndexB] = this.rotatePoint(handleB, controlPoint, angleDifference); + return inkCopy; } }); } @@ -291,9 +293,7 @@ export class InkStrokeProperties { 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; + return { X: newX + origin.X, Y: newY + origin.Y }; } /** @@ -304,11 +304,11 @@ export class InkStrokeProperties { 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); + if (magnitudeA === 0 || magnitudeB === 0) return 0; // 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); + return Math.acos(vectorB.X * vectorA.X + vectorB.Y * vectorA.Y); } /** @@ -330,20 +330,23 @@ export class InkStrokeProperties { */ @undoBatch @action - moveHandle = (deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) => + moveTangentHandle = (deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) => this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => { + const closed = InkingStroke.IsClosed(ink); const oldHandlePoint = ink[handleIndex]; - let oppositeHandlePoint = ink[oppositeHandleIndex]; + const 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 inkCopy = ink.slice(); + inkCopy[handleIndex] = newHandlePoint; const brokenIndices = Cast(doc.brokenInkIndices, listSpec("number")); + const equivIndex = closed ? (controlIndex === 0 ? ink.length - 1 : controlIndex === ink.length - 1 ? 0 : -1) : -1; // 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) { + if ((!brokenIndices || (!brokenIndices?.includes(controlIndex) && !brokenIndices?.includes(equivIndex))) && + (closed || (handleIndex !== 1 && handleIndex !== ink.length - 2))) { const angle = this.angleChange(oldHandlePoint, newHandlePoint, controlPoint); - oppositeHandlePoint = this.rotatePoint(oppositeHandlePoint, controlPoint, angle); - ink[oppositeHandleIndex] = oppositeHandlePoint; + inkCopy[oppositeHandleIndex] = this.rotatePoint(oppositeHandlePoint, controlPoint, angle); } - return ink; + return inkCopy; }) }
\ No newline at end of file diff --git a/src/client/views/InkTangentHandles.tsx b/src/client/views/InkTangentHandles.tsx new file mode 100644 index 000000000..df5bebf31 --- /dev/null +++ b/src/client/views/InkTangentHandles.tsx @@ -0,0 +1,129 @@ +import React = require("react"); +import { action } from "mobx"; +import { observer } from "mobx-react"; +import { Doc } from "../../fields/Doc"; +import { HandleLine, HandlePoint, InkData } from "../../fields/InkField"; +import { List } from "../../fields/List"; +import { listSpec } from "../../fields/Schema"; +import { Cast } from "../../fields/Types"; +import { emptyFunction, setupMoveUpEvents } from "../../Utils"; +import { Transform } from "../util/Transform"; +import { UndoManager } from "../util/UndoManager"; +import { Colors } from "./global/globalEnums"; +import { InkStrokeProperties } from "./InkStrokeProperties"; +import { InkingStroke } from "./InkingStroke"; + +export interface InkHandlesProps { + inkDoc: Doc; + screenCtrlPoints: InkData; + screenSpaceLineWidth: number; + ScreenToLocalTransform: () => Transform; +} + +@observer +export class InkTangentHandles 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) { + var controlUndo: UndoManager.Batch | undefined; + const screenScale = this.props.ScreenToLocalTransform().Scale; + const order = handleIndex % 4; + const oppositeHandleRawIndex = order === 1 ? handleIndex - 3 : handleIndex + 3; + const oppositeHandleIndex = (oppositeHandleRawIndex < 0 ? this.props.screenCtrlPoints.length + oppositeHandleRawIndex : oppositeHandleRawIndex) % this.props.screenCtrlPoints.length; + const controlIndex = (order === 1 ? handleIndex - 1 : handleIndex + 2) % this.props.screenCtrlPoints.length; + setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => { + if (!controlUndo) controlUndo = UndoManager.StartBatch("DocDecs move tangent"); + if (e.altKey) this.onBreakTangent(controlIndex); + InkStrokeProperties.Instance?.moveTangentHandle(-delta[0] * screenScale, -delta[1] * screenScale, handleIndex, oppositeHandleIndex, controlIndex); + return false; + }, () => { + controlUndo?.end(); + UndoManager.FilterBatches(["data", "x", "y", "width", "height"]); + }, 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 = (controlIndex: number) => { + const closed = InkingStroke.IsClosed(this.props.screenCtrlPoints); + const brokenIndices = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number")); + if (!brokenIndices?.includes(controlIndex) && + ((controlIndex > 0 && controlIndex < this.props.screenCtrlPoints.length - 1) || closed)) { + if (brokenIndices) brokenIndices.push(controlIndex); + else this.props.inkDoc.brokenInkIndices = new List<number>([controlIndex]); + } + } + + 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.screenCtrlPoints; + const tangentHandles: HandlePoint[] = []; + const tangentLines: HandleLine[] = []; + const closed = InkingStroke.IsClosed(data); + if (data.length >= 4) { + for (let i = 0; i <= data.length - 4; i += 4) { + tangentHandles.push({ ...data[i + 1], I: i + 1, dot1: i, dot2: i === 0 ? (closed ? data.length - 1 : i) : i - 1 }); + tangentHandles.push({ ...data[i + 2], I: i + 2, dot1: i + 3, dot2: i === data.length ? (closed ? (i + 4) % data.length : i + 3) : i + 4 }); + } + // Adding first and last (single) handle lines. + if (closed) { + tangentLines.push({ X1: data[data.length - 2].X, Y1: data[data.length - 2].Y, X2: data[0].X, Y2: data[0].Y, X3: data[1].X, Y3: data[1].Y, dot1: 0, dot2: data.length - 1 }); + } + else { + tangentLines.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 }); + tangentLines.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) { + tangentLines.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 screenSpaceLineWidth = this.props.screenSpaceLineWidth; + + return ( + <> + {tangentHandles.map((pts, i) => + <svg height="10" width="10" key={`hdl${i}`}> + <circle + cx={pts.X} + cy={pts.Y} + r={screenSpaceLineWidth * 2} + fill={Colors.MEDIUM_BLUE} + strokeWidth={1} + stroke={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>)} + {tangentLines.map((pts, i) => { + const tangentLine = (x1: number, y1: number, x2: number, y2: number) => + <line + x1={x1} + y1={y1} + x2={x2} + y2={y2} + stroke={Colors.MEDIUM_BLUE} + strokeDasharray={"1 1"} + strokeWidth={1} + display={(pts.dot1 === formatInstance._currentPoint || pts.dot2 === formatInstance._currentPoint) ? "inherit" : "none"} />; + return <svg height="100" width="100" key={`line${i}`}> + {tangentLine(pts.X1, pts.Y1, pts.X2, pts.Y2)} + {tangentLine(pts.X2, pts.Y2, pts.X3, pts.Y3)} + </svg>; + })} + </> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 5fc159f14..752db1413 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -1,34 +1,46 @@ import React = require("react"); -import { action, observable } from "mobx"; +import { Bezier } from "bezier-js"; +import { action, IReactionDisposer, observable, reaction } from "mobx"; import { observer } from "mobx-react"; import { Doc } from "../../fields/Doc"; import { documentSchema } from "../../fields/documentSchemas"; import { InkData, InkField, InkTool } from "../../fields/InkField"; import { makeInterface } from "../../fields/Schema"; -import { Cast, StrCast } from "../../fields/Types"; +import { Cast, NumCast, StrCast, BoolCast } from "../../fields/Types"; import { TraceMobx } from "../../fields/util"; -import { setupMoveUpEvents, emptyFunction, returnFalse } from "../../Utils"; +import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../Utils"; import { CognitiveServices } from "../cognitive_services/CognitiveServices"; +import { CurrentUserUtils } from "../util/CurrentUserUtils"; import { InteractionUtils } from "../util/InteractionUtils"; -import { Scripting } from "../util/Scripting"; import { ContextMenu } from "./ContextMenu"; import { ViewBoxBaseComponent } from "./DocComponent"; +import { Colors } from "./global/globalEnums"; +import { InkControlPtHandles } from "./InkControlPtHandles"; import "./InkStroke.scss"; -import { FieldView, FieldViewProps } from "./nodes/FieldView"; 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"; +import { InkTangentHandles } from "./InkTangentHandles"; +import { FieldView, FieldViewProps } from "./nodes/FieldView"; +import { SnappingManager } from "../util/SnappingManager"; +import Color = require("color"); type InkDocument = makeInterface<[typeof documentSchema]>; const InkDocument = makeInterface(documentSchema); @observer export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocument>(InkDocument) { + public static LayoutString(fieldStr: string) { return FieldView.LayoutString(InkingStroke, fieldStr); } static readonly MaskDim = 50000; + public static IsClosed(inkData: InkData) { + return inkData && inkData.lastElement().X === inkData[0].X && inkData.lastElement().Y === inkData[0].Y; + } @observable private _properties?: InkStrokeProperties; + _handledClick = false; // flag denoting whether ink stroke has handled a psuedo-click onPointerUp so that the real onClick event can be stopPropagated + _selDisposer: IReactionDisposer | undefined; + + @observable _nearestT: number | undefined; + @observable _nearestSeg: number | undefined; + @observable _nearestScrPt: { X: number, Y: number } | undefined; + @observable _inkSamplePts: { X: number, Y: number }[] | undefined; constructor(props: FieldViewProps & InkDocument) { super(props); @@ -36,8 +48,13 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume this._properties = InkStrokeProperties.Instance; } - public static LayoutString(fieldStr: string) { - return FieldView.LayoutString(InkingStroke, fieldStr); + componentDidMount() { + this.props.setContentView?.(this); + this._selDisposer = reaction(() => this.props.isSelected(), // react to stroke being deselected by turning off ink handles + selected => !selected && this.toggleControlButton()); + } + componentWillUnmount() { + this._selDisposer?.(); } analyzeStrokes() { @@ -52,15 +69,23 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume inkDoc.color = "#9b9b9bff"; inkDoc._stayInCollection = inkDoc.isInkMask ? true : undefined; }); - /** * Handles the movement of the entire ink object when the user clicks and drags. */ onPointerDown = (e: React.PointerEvent) => { + this._handledClick = false; if (this.props.isSelected(true)) { setupMoveUpEvents(this, e, returnFalse, emptyFunction, - action((e: PointerEvent, doubleTap: boolean | undefined) => - doubleTap && this._properties && (this._properties._controlButton = true)) + action((e: PointerEvent, doubleTap: boolean | undefined) => { + doubleTap = doubleTap || this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick; + if (doubleTap && this._properties) { + this._properties._controlButton = true; + InkStrokeProperties.Instance && (InkStrokeProperties.Instance._currentPoint = -1); + this._handledClick = true; // mark the double-click pseudo pointerevent so we can block the real mouse event from propagating to DocumentView + } else if (this._properties?._controlButton) { + this._nearestT && this._nearestSeg !== undefined && InkStrokeProperties.Instance?.addPoints(this._nearestT, this._nearestSeg, this.inkScaledData().inkData.slice()); + } + }), this._properties?._controlButton, this._properties?._controlButton ); } } @@ -75,80 +100,140 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume } } + @action + checkHighlighter = () => { + if (CurrentUserUtils.SelectedTool === InkTool.Highlighter) { + // this._previousColor = ActiveInkColor(); + SetActiveInkColor("rgba(245, 230, 95, 0.75)"); + } + } + + inkScaledData = () => { + const inkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? []; + const inkStrokeWidth = NumCast(this.rootDoc.strokeWidth, 1); + const inkTop = Math.min(...inkData.map(p => p.Y)) - inkStrokeWidth / 2; + const inkBottom = Math.max(...inkData.map(p => p.Y)) + inkStrokeWidth / 2; + const inkLeft = Math.min(...inkData.map(p => p.X)) - inkStrokeWidth / 2; + const inkRight = Math.max(...inkData.map(p => p.X)) + inkStrokeWidth / 2; + const inkWidth = Math.max(1, inkRight - inkLeft); + const inkHeight = Math.max(1, inkBottom - inkTop); + return { + inkData, + inkStrokeWidth, + inkTop, + inkLeft, + inkWidth, + inkHeight, + inkScaleX: ((this.props.PanelWidth() - inkStrokeWidth) / ((inkWidth - inkStrokeWidth) || 1) || 1), + inkScaleY: ((this.props.PanelHeight() - inkStrokeWidth) / ((inkHeight - inkStrokeWidth) || 1) || 1) + }; + } + + @action + onPointerMove = (e: React.PointerEvent) => { + const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData(); + const screenPts = inkData.map(point => this.props.ScreenToLocalTransform().inverse().transformPoint( + (point.X - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2, + (point.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2)).map(p => ({ X: p[0], Y: p[1] })); + const { distance, nearestT, nearestSeg, nearestPt } = InkStrokeProperties.nearestPtToStroke(screenPts, { X: e.clientX, Y: e.clientY }); + + this._nearestT = nearestT; + this._nearestSeg = nearestSeg; + this._nearestScrPt = nearestPt; + } + + + nearestScreenPt = () => this._nearestScrPt; + componentUI = (boundsLeft: number, boundsTop: number) => { + const inkDoc = this.props.Document; + const screenSpaceCenterlineStrokeWidth = 3; // the width of the blue line widget that shows the centerline of the ink stroke + const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData(); + + const screenInkWidth = this.props.ScreenToLocalTransform().inverse().transformDirection(inkStrokeWidth, inkStrokeWidth); + const screenPts = inkData.map(point => this.props.ScreenToLocalTransform().inverse().transformPoint( + (point.X - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2, + (point.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2)).map(p => ({ X: p[0], Y: p[1] })); + const screenHdlPts = screenPts; + + const startMarker = StrCast(this.layoutDoc.strokeStartMarker); + const endMarker = StrCast(this.layoutDoc.strokeEndMarker); + return SnappingManager.GetIsDragging() ? (null) : <div className="inkstroke-UI" style={{ + clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` + }} > + {!this._properties?._controlButton ? (null) : + <> + {InteractionUtils.CreatePolyline(screenPts, 0, 0, Colors.MEDIUM_BLUE, screenInkWidth[0], screenSpaceCenterlineStrokeWidth, + StrCast(inkDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap), StrCast(inkDoc.strokeBezier), + "none", startMarker, endMarker, StrCast(inkDoc.strokeDash), 1, 1, "", "none", 1.0, false)} + <InkControlPtHandles + inkDoc={inkDoc} + inkCtrlPoints={inkData} + screenCtrlPoints={screenHdlPts} + nearestScreenPt={this.nearestScreenPt} + screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} + ScreenToLocalTransform={this.props.ScreenToLocalTransform} /> + <InkTangentHandles + inkDoc={inkDoc} + screenCtrlPoints={screenHdlPts} + screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} + ScreenToLocalTransform={this.props.ScreenToLocalTransform} /> + </>} + </div>; + } + render() { TraceMobx(); - this.toggleControlButton(); - // Extracting the ink data and formatting information of the current ink stroke. - // console.log(InkingStroke.InkShape); - const InkShape = GestureOverlay.Instance.InkShape; - const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? []; - const inkDoc: Doc = this.layoutDoc; - const strokeWidth = Number(this.layoutDoc.strokeWidth); - 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 = 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 dotsize = Math.max(width * scaleX, height * scaleY) / 40; + const { inkData, inkStrokeWidth, inkLeft, inkTop, inkScaleX, inkScaleY, inkWidth, inkHeight } = this.inkScaledData(); + + const startMarker = StrCast(this.layoutDoc.strokeStartMarker); + const endMarker = StrCast(this.layoutDoc.strokeEndMarker); + const closed = InkingStroke.IsClosed(inkData); + const fillColor = StrCast(this.layoutDoc.fillColor, "transparent"); + const strokeColor = !closed && fillColor && fillColor !== "transparent" ? fillColor : StrCast(this.layoutDoc.color); // Visually renders the polygonal line made by the user. - const inkLine = InteractionUtils.CreatePolyline(data, left, top, strokeColor, strokeWidth, strokeWidth, StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"), StrCast(this.layoutDoc.strokeStartMarker), - StrCast(this.layoutDoc.strokeEndMarker), StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", this.props.isSelected() && strokeWidth <= 5 && lineBottom - lineTop > 1 && lineRight - lineLeft > 1, false); + const inkLine = InteractionUtils.CreatePolyline(inkData, inkLeft, inkTop, strokeColor, inkStrokeWidth, inkStrokeWidth, + StrCast(this.layoutDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap), + StrCast(this.layoutDoc.strokeBezier), !closed ? "none" : fillColor === "transparent" ? "none" : fillColor, startMarker, endMarker, + StrCast(this.layoutDoc.strokeDash), inkScaleX, inkScaleY, "", "none", 1.0, 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); + // 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", 1.0, 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); + const highlightIndex = BoolCast(this.props.Document.isLinkButton) && Doc.isBrushedHighlightedDegree(this.props.Document); // bcz: Argh!! need to identify a tree view doc better than a LayoutTemlatString + const highlightColor = !highlightIndex ? + StrCast(this.layoutDoc.strokeOutlineColor, !closed && fillColor && fillColor !== "transparent" ? StrCast(this.layoutDoc.color, "transparent") : "transparent") : + ["transparent", "rgb(68, 118, 247)", "rgb(68, 118, 247)", "yellow", "magenta", "cyan", "orange"][highlightIndex]; + + const clickableLine = InteractionUtils.CreatePolyline(inkData, inkLeft, inkTop, highlightColor, + inkStrokeWidth, inkStrokeWidth + (highlightIndex && closed && (new Color(fillColor)).alpha() < 1 ? 6 : 15), + StrCast(this.layoutDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap), + StrCast(this.layoutDoc.strokeBezier), !closed ? "none" : fillColor === "transparent" ? "none" : fillColor, startMarker, endMarker, + undefined, inkScaleX, inkScaleY, "", this.props.pointerEvents ?? (this.props.layerProvider?.(this.props.Document) === false ? "none" : "visiblepainted"), 0.0, false); // 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="inkStroke" style={{ - pointerEvents: this.props.Document.isInkMask && this.props.layerProvider?.(this.props.Document) !== false ? "all" : "none", + pointerEvents: "none", transform: this.props.Document.isInkMask ? `translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined, mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? "multiply" : "unset", overflow: "visible", + cursor: this.props.isSelected() ? "default" : undefined }} + onPointerLeave={action(e => this._nearestScrPt = undefined)} + onPointerMove={this.props.isSelected() ? this.onPointerMove : undefined} onPointerDown={this.onPointerDown} + onClick={e => this._handledClick && e.stopPropagation()} onContextMenu={() => { const cm = ContextMenu.Instance; - 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(() => { if (this._properties) { this._properties._controlButton = !this._properties._controlButton; } }), icon: "paint-brush" }); - } + !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(() => { if (this._properties) { this._properties._controlButton = !this._properties._controlButton; } }), icon: "paint-brush" }); }} > - {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> ); } @@ -168,23 +253,5 @@ export function ActiveFillColor(): string { return StrCast(ActiveInkPen()?.activ export function ActiveArrowStart(): string { return StrCast(ActiveInkPen()?.activeArrowStart, ""); } export function ActiveArrowEnd(): string { return StrCast(ActiveInkPen()?.activeArrowEnd, ""); } export function ActiveDash(): string { return StrCast(ActiveInkPen()?.activeDash, "0"); } -export function ActiveInkWidth(): string { return StrCast(ActiveInkPen()?.activeInkWidth, "1"); } +export function ActiveInkWidth(): number { return Number(ActiveInkPen()?.activeInkWidth); } export function ActiveInkBezierApprox(): string { return StrCast(ActiveInkPen()?.activeInkBezier); } -Scripting.addGlobal(function activateBrush(pen: any, width: any, color: any, fill: any, arrowStart: any, arrowEnd: any, dash: any) { - CurrentUserUtils.SelectedTool = pen ? InkTool.Highlighter : InkTool.None; - SetActiveInkWidth(width); - SetActiveInkColor(color); - SetActiveFillColor(fill); - SetActiveArrowStart(arrowStart); - SetActiveArrowEnd(arrowEnd); - SetActiveDash(dash); -}); -Scripting.addGlobal(function activateEraser(pen: any) { return CurrentUserUtils.SelectedTool = pen ? InkTool.Eraser : InkTool.None; }); -Scripting.addGlobal(function activateStamp(pen: any) { return CurrentUserUtils.SelectedTool = pen ? InkTool.Stamp : InkTool.None; }); -Scripting.addGlobal(function deactivateInk() { return CurrentUserUtils.SelectedTool = InkTool.None; }); -Scripting.addGlobal(function setInkWidth(width: any) { return SetActiveInkWidth(width); }); -Scripting.addGlobal(function setInkColor(color: any) { return SetActiveInkColor(color); }); -Scripting.addGlobal(function setFillColor(fill: any) { return SetActiveFillColor(fill); }); -Scripting.addGlobal(function setActiveArrowStart(arrowStart: any) { return SetActiveArrowStart(arrowStart); }); -Scripting.addGlobal(function setActiveArrowEnd(arrowEnd: any) { return SetActiveArrowStart(arrowEnd); }); -Scripting.addGlobal(function setActiveDash(dash: any) { return SetActiveDash(dash); }); diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx index 88739fe91..ec3bf6c18 100644 --- a/src/client/views/LightboxView.tsx +++ b/src/client/views/LightboxView.tsx @@ -16,6 +16,7 @@ import { TabDocView } from './collections/TabDocView'; import "./LightboxView.scss"; import { DocumentView } from './nodes/DocumentView'; import { DefaultStyleProvider, wavyBorderPath } from './StyleProvider'; +import { CollectionMenu } from './collections/CollectionMenu'; interface LightboxViewProps { PanelWidth: number; @@ -213,6 +214,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { LightboxView.SetLightboxDoc(undefined); } }} > + <div className="lightboxView-contents" style={{ left: this.leftBorder, top: this.topBorder, @@ -220,6 +222,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { height: this.lightboxHeight(), clipPath: `path('${Doc.UserDoc().renderStyle === "comic" ? wavyBorderPath(this.lightboxWidth(), this.lightboxHeight()) : undefined}')` }}> + {/* <CollectionMenu /> TODO:glr This is where it would go*/} <DocumentView ref={action((r: DocumentView | null) => { LightboxView._docView = r !== null ? r : undefined; r && setTimeout(action(() => { @@ -234,7 +237,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { DataDoc={undefined} LayoutTemplate={LightboxView.LightboxDocTemplate} addDocument={undefined} - fitContentsToDoc={this.fitToBox} + // fitContentsToDoc={this.fitToBox} // bcz: why do we want this? when we initially open a colletion, we shrinkwrap it which allows for user navigation. if we later encounter a collection, it's not clear to me that we want to make it either shrinkwrap or fitContents... isDocumentActive={returnFalse} isContentActive={returnTrue} addDocTab={this.addDocTab} diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss index d913f2069..c3cdb7dde 100644 --- a/src/client/views/MainView.scss +++ b/src/client/views/MainView.scss @@ -15,8 +15,8 @@ margin-top: 10px; } -.mainContent-div-flyout, -.mainContent-div { +.mainView-dockingContent-flyout, +.mainView-dockingContent { position: relative; width: 100%; height: 100%; @@ -41,7 +41,7 @@ } .mainView-container, -.mainView-container-dark { +.mainView-container-Dark { width: 100%; height: 100%; position: absolute; @@ -65,7 +65,7 @@ } } -.mainView-container-dark { +.mainView-container-Dark { color: $light-gray; .lm_goldenlayout { @@ -91,7 +91,7 @@ .contextMenu-cont, .contextMenu-item { - background: $medium-gray; + background: $dark-gray; } .contextMenu-item:hover { @@ -99,7 +99,7 @@ } } -.mainView-mainContent { +.mainView-dashboardArea { width: 100%; height: 100%; position: absolute; @@ -109,9 +109,9 @@ .properties-container { height: 100%; - position: relative; - left: 100%; - top: calc(-100% - 36px); + position: absolute; + right: 0; + top: 0; z-index: 3000; } @@ -144,7 +144,7 @@ } } -.mainView-innerContent, .mainView-innerContent-dark { +.mainView-innerContent, .mainView-innerContent-Dark { display: contents; flex-direction: row; position: relative; @@ -175,7 +175,7 @@ .mainView-libraryHandle { background-color: $light-gray; } -.mainView-innerContent-dark +.mainView-innerContent-Dark { .propertiesView { background-color: #252525; @@ -198,7 +198,7 @@ background: #353535; } } -.mainView-container-dark { +.mainView-container-Dark { .contextMenu-cont { background: $medium-gray; color: $white; @@ -208,7 +208,7 @@ } } -.mainView-menuPanel { +.mainView-leftMenuPanel { min-width: var(--menuPanelWidth); background-color: $dark-gray; border-right: $standard-border; @@ -220,65 +220,6 @@ ::-webkit-scrollbar { width: 0; } - - .mainView-menuPanel-button { - padding: 7px; - padding-left: 7px; - width: 100%; - background: $dark-gray; - - .mainView-menuPanel-button-wrap { - width: 45px; - /* padding: 5px; */ - touch-action: none; - background: $dark-gray; - transform-origin: top left; - /* margin-bottom: 5px; */ - margin-top: 5px; - margin-right: 25px; - border-radius: 8px; - - &:hover { - background: $black; - cursor: pointer; - } - } - } - - .mainView-menuPanel-button-label { - color: white; - margin-left: px; - margin-right: 4px; - border-radius: 8px; - width: 42px; - position: relative; - text-align: center; - font-size: 8px; - margin-top: 1px; - letter-spacing: normal; - padding: 3px; - background-color: inherit; - } - - .mainView-menuPanel-button-icon { - width: auto; - height: 35px; - padding: 5px; - } - - svg { - width: 95% !important; - height: 95%; - } -} - -.mainView-searchPanel { - width: 100%; - height: $searchpanel-height; - background-color: black; - color: white; - text-align: center; - vertical-align: middle; } .mainView-mainDiv { @@ -357,9 +298,8 @@ width: var(--flyoutHandleWidth); height: 55px; top: 50%; - left: -10px; border-radius: 8px; - position: relative; + position: absolute; z-index: 41; // lm_maximised has a z-index of 40 and this needs to be above that touch-action: none; cursor: grab; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 8b5e18fb2..6d0d5eb39 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -11,9 +11,10 @@ import * as ReactDOM from 'react-dom'; import { Doc, DocListCast, Opt } from '../../fields/Doc'; import { List } from '../../fields/List'; import { PrefetchProxy } from '../../fields/Proxy'; -import { BoolCast, PromiseValue, StrCast } from '../../fields/Types'; +import { ScriptField } from '../../fields/ScriptField'; +import { PromiseValue, StrCast } from '../../fields/Types'; import { TraceMobx } from '../../fields/util'; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick, Utils } from '../../Utils'; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents, simulateMouseClick, Utils } from '../../Utils'; import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; import { DocServer } from '../DocServer'; import { Docs, DocUtils } from '../documents/Documents'; @@ -25,22 +26,23 @@ import { HistoryUtil } from '../util/History'; import { Hypothesis } from '../util/HypothesisUtils'; import { Scripting } from '../util/Scripting'; import { SelectionManager } from '../util/SelectionManager'; -import { SettingsManager } from '../util/SettingsManager'; +import { ColorScheme, SettingsManager } from '../util/SettingsManager'; import { SharingManager } from '../util/SharingManager'; import { SnappingManager } from '../util/SnappingManager'; import { Transform } from '../util/Transform'; import { TimelineMenu } from './animationtimeline/TimelineMenu'; import { CollectionDockingView } from './collections/CollectionDockingView'; import { MarqueeOptionsMenu } from './collections/collectionFreeForm/MarqueeOptionsMenu'; -import { CollectionLinearView } from './collections/CollectionLinearView'; +import { CollectionLinearView } from './collections/collectionLinear'; import { CollectionMenu } from './collections/CollectionMenu'; import { CollectionViewType } from './collections/CollectionView'; import "./collections/TreeView.scss"; +import { ComponentDecorations } from './ComponentDecorations'; import { ContextMenu } from './ContextMenu'; import { DictationOverlay } from './DictationOverlay'; import { DocumentDecorations } from './DocumentDecorations'; import { GestureOverlay } from './GestureOverlay'; -import { MENU_PANEL_WIDTH, SEARCH_PANEL_HEIGHT } from './global/globalCssVariables.scss'; +import { DASHBOARD_SELECTOR_HEIGHT, LEFT_MENU_WIDTH } from './global/globalCssVariables.scss'; import { Colors } from './global/globalEnums'; import { KeyManager } from './GlobalKeyHandler'; import { InkStrokeProperties } from './InkStrokeProperties'; @@ -48,9 +50,11 @@ import { LightboxView } from './LightboxView'; import { LinkMenu } from './linking/LinkMenu'; import "./MainView.scss"; import { AudioBox } from './nodes/AudioBox'; +import { ButtonType } from './nodes/button/FontIconBox'; import { DocumentLinksButton } from './nodes/DocumentLinksButton'; import { DocumentView } from './nodes/DocumentView'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; +import { RichTextMenu } from './nodes/formattedText/RichTextMenu'; import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup'; import { LinkDocPreview } from './nodes/LinkDocPreview'; import { RadialMenu } from './nodes/RadialMenu'; @@ -72,21 +76,32 @@ export class MainView extends React.Component { @observable public LastButton: Opt<Doc>; @observable private _windowWidth: number = 0; @observable private _windowHeight: number = 0; - @observable private _panelWidth: number = 0; - @observable private _panelHeight: number = 0; + @observable private _dashUIWidth: number = 0; // width of entire main dashboard region including left menu buttons and properties panel (but not including the dashboard selector button row) + @observable private _dashUIHeight: number = 0; // height of entire main dashboard region including top menu buttons @observable private _panelContent: string = "none"; @observable private _sidebarContent: any = this.userDoc?.sidebar; - @observable private _flyoutWidth: number = 0; + @observable private _leftMenuFlyoutWidth: number = 0; - @computed private get topOffset() { return Number(SEARCH_PANEL_HEIGHT.replace("px", "")); } //TODO remove - @computed private get leftOffset() { return this.menuPanelWidth() - 2; } + @computed private get dashboardTabHeight() { return 27; } // 27 comes form lm.config.defaultConfig.dimensions.headerHeight in goldenlayout.js + @computed private get topOfDashUI() { return Number(DASHBOARD_SELECTOR_HEIGHT.replace("px", "")); } + @computed private get topOfMainDoc() { return this.topOfDashUI + this.topMenuHeight(); } + @computed private get topOfMainDocContent() { return this.topOfMainDoc + this.dashboardTabHeight; } + @computed private get leftScreenOffsetOfMainDocView() { return this.leftMenuWidth() - 2; } @computed private get userDoc() { return Doc.UserDoc(); } - @computed private get darkScheme() { return BoolCast(CurrentUserUtils.ActiveDashboard?.darkScheme); } + @computed private get colorScheme() { return StrCast(CurrentUserUtils.ActiveDashboard?.colorScheme); } @computed private get mainContainer() { return this.userDoc ? CurrentUserUtils.ActiveDashboard : CurrentUserUtils.GuestDashboard; } - @computed public get mainFreeform(): Opt<Doc> { return (docs => (docs && docs.length > 1) ? docs[1] : undefined)(DocListCast(this.mainContainer!.data)); } - - menuPanelWidth = () => Number(MENU_PANEL_WIDTH.replace("px", "")); - propertiesWidth = () => Math.max(0, Math.min(this._panelWidth - 50, CurrentUserUtils.propertiesWidth || 0)); + @computed public get mainFreeform(): Opt<Doc> { return (docs => (docs?.length > 1) ? docs[1] : undefined)(DocListCast(this.mainContainer!.data)); } + + topMenuHeight = () => 35; + topMenuWidth = returnZero; // value is ignored ... + leftMenuWidth = () => Number(LEFT_MENU_WIDTH.replace("px", "")); + leftMenuHeight = () => this._dashUIHeight; + leftMenuFlyoutWidth = () => this._leftMenuFlyoutWidth; + leftMenuFlyoutHeight = () => this._dashUIHeight; + propertiesWidth = () => Math.max(0, Math.min(this._dashUIWidth - 50, CurrentUserUtils.propertiesWidth || 0)); + propertiesHeight = () => this._dashUIHeight; + mainDocViewWidth = () => this._dashUIWidth - this.propertiesWidth() - this.leftMenuWidth(); + mainDocViewHeight = () => this._dashUIHeight; componentDidMount() { document.getElementById("root")?.addEventListener("scroll", e => ((ele) => ele.scrollLeft = ele.scrollTop = 0)(document.getElementById("root")!)); @@ -172,7 +187,7 @@ export class MainView extends React.Component { fa.faArrowAltCircleDown, fa.faArrowAltCircleUp, fa.faArrowAltCircleLeft, fa.faArrowAltCircleRight, fa.faStopCircle, fa.faCheckCircle, fa.faGripVertical, fa.faSortUp, fa.faSortDown, fa.faTable, fa.faTh, fa.faThList, fa.faProjectDiagram, fa.faSignature, fa.faColumns, fa.faChevronCircleUp, fa.faUpload, fa.faBorderAll, fa.faBraille, fa.faChalkboard, fa.faPencilAlt, fa.faEyeSlash, fa.faSmile, fa.faIndent, fa.faOutdent, fa.faChartBar, fa.faBan, fa.faPhoneSlash, fa.faGripLines, - fa.faSave, fa.faBookmark); + fa.faSave, fa.faBookmark, fa.faList, fa.faListOl, fa.faFolderPlus, fa.faLightbulb, fa.faBookOpen); this.initAuthenticationRouters(); } @@ -199,6 +214,7 @@ export class MainView extends React.Component { } } }, false); + document.oncontextmenu = () => false; } initAuthenticationRouters = async () => { @@ -226,22 +242,39 @@ export class MainView extends React.Component { @action createNewPresentation = async () => { - if (!await this.userDoc.myPresentations) { - this.userDoc.myPresentations = new PrefetchProxy(Docs.Create.TreeDocument([], { - title: "PRESENTATION TRAILS", childDontRegisterViews: true, _height: 100, _forceActive: true, boxShadow: "0 0", _lockedPosition: true, treeViewOpen: true, system: true + if (!await this.userDoc.myTrails) { + this.userDoc.myTrails = new PrefetchProxy(Docs.Create.TreeDocument([], { + title: "TRAILS", childDontRegisterViews: true, _height: 100, _forceActive: true, boxShadow: "0 0", _lockedPosition: true, treeViewOpen: true, system: true })); } const pres = Docs.Create.PresDocument(new List<Doc>(), - { title: "Untitled Presentation", _viewType: CollectionViewType.Stacking, _width: 400, _height: 500, targetDropAction: "alias", _chromeHidden: true, boxShadow: "0 0" }); + { title: "Untitled Trail", _viewType: CollectionViewType.Stacking, _fitWidth: true, _width: 400, _height: 500, targetDropAction: "alias", _chromeHidden: true, boxShadow: "0 0" }); CollectionDockingView.AddSplit(pres, "right"); this.userDoc.activePresentation = pres; - Doc.AddDocToList(this.userDoc.myPresentations as Doc, "data", pres); + Doc.AddDocToList(this.userDoc.myTrails as Doc, "data", pres); } - getPWidth = () => this._panelWidth - this.propertiesWidth(); - getPHeight = () => this._panelHeight - (CollectionMenu.Instance?.Pinned ? 35 : 0); - getContentsHeight = () => this._panelHeight; - getMenuPanelHeight = () => this._panelHeight + (CollectionMenu.Instance?.Pinned ? 35 : 0); + @action + createNewFolder = async () => { + if (!await this.userDoc.myFilesystem) { + this.userDoc.myFileOrphans = Docs.Create.TreeDocument([], { title: "Unfiled", _stayInCollection: true, system: true, isFolder: true }); + const newFolder = ScriptField.MakeFunction(`createNewFolder()`, { scriptContext: "any" })!; + const newFolderButton: Doc = Docs.Create.FontIconDocument({ + onClick: newFolder, _forceActive: true, toolTip: "New folder", _stayInCollection: true, _hideContextMenu: true, title: "New folder", + btnType: ButtonType.ClickButton, _width: 30, _height: 30, buttonText: "New folder", icon: "folder-plus", system: true + }); + this.userDoc.myFilesystem = new PrefetchProxy(Docs.Create.TreeDocument([this.userDoc.myFileOrphans as Doc], { + title: "My Documents", _showTitle: "title", buttonMenu: true, buttonMenuDoc: newFolderButton, _height: 100, + treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, childDropAction: "alias", + treeViewTruncateTitleWidth: 150, ignoreClick: true, + isFolder: true, treeViewType: "fileSystem", childHideLinkButton: true, + _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "proto", system: true, + explainer: "This is your file manager where you can create folders to keep track of documents independently of your dashboard." + })); + } + const folder = Docs.Create.TreeDocument([], { title: "Untitled folder", _stayInCollection: true, isFolder: true }); + Doc.AddDocToList(this.userDoc.myFilesystem as Doc, "data", folder); + } @computed get mainDocView() { return <DocumentView key="main" @@ -257,8 +290,8 @@ export class MainView extends React.Component { isContentActive={returnTrue} removeDocument={undefined} ScreenToLocalTransform={Transform.Identity} - PanelWidth={this.getPWidth} - PanelHeight={this.getPHeight} + PanelWidth={this.mainDocViewWidth} + PanelHeight={this.mainDocViewHeight} focus={DocUtils.DefaultFocus} whenChildContentsActiveChanged={emptyFunction} bringToFront={emptyFunction} @@ -272,13 +305,10 @@ 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 + return <div key="docking" className={`mainView-dockingContent${this._leftMenuFlyoutWidth ? "-flyout" : ""}`} onDrop={e => { e.stopPropagation(); e.preventDefault(); }} style={{ - minWidth: `calc(100% - ${this._flyoutWidth + this.menuPanelWidth() + this.propertiesWidth()}px)`, + minWidth: `calc(100% - ${this._leftMenuFlyoutWidth + this.leftMenuWidth() + this.propertiesWidth()}px)`, transform: LightboxView.LightboxDoc ? "scale(0.0001)" : undefined, - //TODO:glr width: `calc(100% - ${this._flyoutWidth + this.menuPanelWidth() + this.propertiesWidth()}px)` }}> {!this.mainContainer ? (null) : this.mainDocView} </div>; @@ -287,22 +317,21 @@ export class MainView extends React.Component { @action onPropertiesPointerDown = (e: React.PointerEvent) => { setupMoveUpEvents(this, e, - action(e => (CurrentUserUtils.propertiesWidth = Math.max(0, this._panelWidth - e.clientX)) ? false : false), + action(e => (CurrentUserUtils.propertiesWidth = Math.max(0, this._dashUIWidth - e.clientX)) ? false : false), action(() => CurrentUserUtils.propertiesWidth < 5 && (CurrentUserUtils.propertiesWidth = 0)), - action(() => CurrentUserUtils.propertiesWidth = this.propertiesWidth() < 15 ? Math.min(this._panelWidth - 50, 250) : 0), false); + action(() => CurrentUserUtils.propertiesWidth = this.propertiesWidth() < 15 ? Math.min(this._dashUIWidth - 50, 250) : 0), false); } @action onFlyoutPointerDown = (e: React.PointerEvent) => { setupMoveUpEvents(this, e, - action(e => (this._flyoutWidth = Math.max(e.clientX - 58, 0)) ? false : false), - () => this._flyoutWidth < 5 && this.closeFlyout(), + action(e => (this._leftMenuFlyoutWidth = Math.max(e.clientX - 58, 0)) ? false : false), + () => this._leftMenuFlyoutWidth < 5 && this.closeFlyout(), this.closeFlyout); } - flyoutWidthFunc = () => this._flyoutWidth; - sidebarScreenToLocal = () => new Transform(0, -this.topOffset, 1); - mainContainerXf = () => this.sidebarScreenToLocal().translate(-this.leftOffset, 0); + sidebarScreenToLocal = () => new Transform(0, -this.topOfMainDoc, 1); + mainContainerXf = () => this.sidebarScreenToLocal().translate(-this.leftScreenOffsetOfMainDocView, 0); addDocTabFunc = (doc: Doc, where: string): boolean => { return where === "close" ? CollectionDockingView.CloseSplit(doc) : doc.dockingConfig ? CurrentUserUtils.openDashboard(Doc.UserDoc(), doc) : CollectionDockingView.AddSplit(doc, "right"); @@ -310,10 +339,10 @@ export class MainView extends React.Component { @computed get flyout() { - return !this._flyoutWidth ? <div key="flyout" className={`mainView-libraryFlyout-out`}> + return !this._leftMenuFlyoutWidth ? <div key="flyout" className={`mainView-libraryFlyout-out`}> {this.docButtons} </div> : - <div key="libFlyout" className="mainView-libraryFlyout" style={{ minWidth: this._flyoutWidth, width: this._flyoutWidth }} > + <div key="libFlyout" className="mainView-libraryFlyout" style={{ minWidth: this._leftMenuFlyoutWidth, width: this._leftMenuFlyoutWidth }} > <div className="mainView-contentArea" > <DocumentView Document={this._sidebarContent.proto || this._sidebarContent} @@ -327,11 +356,11 @@ export class MainView extends React.Component { rootSelected={returnTrue} removeDocument={returnFalse} ScreenToLocalTransform={this.mainContainerXf} - PanelWidth={this.flyoutWidthFunc} - PanelHeight={this.getContentsHeight} + PanelWidth={this.leftMenuFlyoutWidth} + PanelHeight={this.leftMenuFlyoutHeight} renderDepth={0} isContentActive={returnTrue} - scriptContext={CollectionDockingView.Instance.props.Document} + scriptContext={CollectionDockingView.Instance?.props.Document} focus={DocUtils.DefaultFocus} whenChildContentsActiveChanged={emptyFunction} bringToFront={emptyFunction} @@ -346,8 +375,8 @@ export class MainView extends React.Component { </div>; } - @computed get menuPanel() { - return <div key="menu" className="mainView-menuPanel"> + @computed get leftMenuPanel() { + return <div key="menu" className="mainView-leftMenuPanel"> <DocumentView Document={Doc.UserDoc().menuStack as Doc} DataDoc={undefined} @@ -357,8 +386,8 @@ export class MainView extends React.Component { rootSelected={returnTrue} removeDocument={returnFalse} ScreenToLocalTransform={this.sidebarScreenToLocal} - PanelWidth={this.menuPanelWidth} - PanelHeight={this.getMenuPanelHeight} + PanelWidth={this.leftMenuWidth} + PanelHeight={this.leftMenuHeight} renderDepth={0} docViewPath={returnEmptyDoclist} focus={DocUtils.DefaultFocus} @@ -380,7 +409,7 @@ export class MainView extends React.Component { @action selectMenu = (button: Doc) => { const title = StrCast(Doc.GetProto(button).title); - const willOpen = !this._flyoutWidth || this._panelContent !== title; + const willOpen = !this._leftMenuFlyoutWidth || this._panelContent !== title; this.closeFlyout(); if (willOpen) { switch (this._panelContent = title) { @@ -397,38 +426,40 @@ 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; + const leftMenuFlyoutWidth = this._leftMenuFlyoutWidth + this.leftMenuWidth(); + const width = this.propertiesWidth() + leftMenuFlyoutWidth; return <> - {this.menuPanel} - <div key="inner" className={`mainView-innerContent${this.darkScheme ? "-dark" : ""}`}> + {this.leftMenuPanel} + <div key="inner" className={`mainView-innerContent${this.colorScheme}`}> {this.flyout} - <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 className="mainView-libraryHandle" style={{ left: leftMenuFlyoutWidth - 10 /* ~half width of handle */, display: !this._leftMenuFlyoutWidth ? "none" : undefined }} onPointerDown={this.onFlyoutPointerDown} > + <FontAwesomeIcon icon="chevron-left" color={this.colorScheme === ColorScheme.Dark ? "white" : "black"} style={{ opacity: "50%" }} size="sm" /> </div> - <div className="mainView-innerContainer" style={{ width: `calc(100% - ${width}px)`, transform: transform }}> - <CollectionMenu /> + <div className="mainView-innerContainer" style={{ width: `calc(100% - ${width}px)` }}> {this.dockingContent} - <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 className="mainView-propertiesDragger" key="props" onPointerDown={this.onPropertiesPointerDown} style={{ right: this.propertiesWidth() - 1 }}> + <FontAwesomeIcon icon={this.propertiesWidth() < 10 ? "chevron-left" : "chevron-right"} color={this.colorScheme === ColorScheme.Dark ? 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 className="properties-container" style={{ width: this.propertiesWidth() }}> + {this.propertiesWidth() < 10 ? (null) : <PropertiesView styleProvider={DefaultStyleProvider} width={this.propertiesWidth()} height={this.propertiesHeight()} />} </div> </div> </div> </>; } - @computed get mainContent() { + @computed get mainDashboardArea() { return !this.userDoc ? (null) : - <div className="mainView-mainContent" ref={r => { - r && new _global.ResizeObserver(action(() => { this._panelWidth = r.getBoundingClientRect().width; this._panelHeight = r.getBoundingClientRect().height; })).observe(r); + <div className="mainView-dashboardArea" ref={r => { + r && new _global.ResizeObserver(action(() => { + this._dashUIWidth = r.getBoundingClientRect().width; + this._dashUIHeight = r.getBoundingClientRect().height; + })).observe(r); }} style={{ - color: this.darkScheme ? "rgb(205,205,205)" : "black", - height: `calc(100% - ${this.topOffset}px)`, + color: this.colorScheme === ColorScheme.Dark ? "rgb(205,205,205)" : "black", + height: `calc(100% - ${this.topOfDashUI + this.topMenuHeight()}px)`, width: "100%", }} > {this.mainInnerContent} @@ -436,7 +467,13 @@ export class MainView extends React.Component { } expandFlyout = action((button: Doc) => { - this._flyoutWidth = (this._flyoutWidth || 250); + // bcz: What's going on here!? + // Chrome(not firefox) seems to have a bug when the flyout expands and there's a zoomed freeform tab. All of the div below the CollectionFreeFormView's main div + // generate the wrong value from getClientRectangle() -- specifically they return an 'x' that is the flyout's width greater than it should be. + // interactively adjusting the flyout fixes the problem. So does programmatically changing the value after a timeout to something *fractionally* different (ie, 1.5, not 1);) + this._leftMenuFlyoutWidth = (this._leftMenuFlyoutWidth || 250); + setTimeout(action(() => this._leftMenuFlyoutWidth += 0.5), 0); + this._sidebarContent.proto = button.target as any; this.LastButton = button; }); @@ -445,7 +482,7 @@ export class MainView extends React.Component { this.LastButton = undefined; this._panelContent = "none"; this._sidebarContent.proto = undefined; - this._flyoutWidth = 0; + this._leftMenuFlyoutWidth = 0; }); remButtonDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg: boolean, doc) => flg && Doc.RemoveDocFromList(Doc.UserDoc().dockedBtns as Doc, "data", doc), true); @@ -472,8 +509,8 @@ export class MainView extends React.Component { rootSelected={returnTrue} bringToFront={emptyFunction} select={emptyFunction} - isContentActive={returnFalse} isAnyChildContentActive={returnFalse} + isContentActive={returnFalse} isSelected={returnFalse} docViewPath={returnEmptyDoclist} moveDocument={this.moveButtonDoc} @@ -483,8 +520,8 @@ export class MainView extends React.Component { pinToPres={emptyFunction} removeDocument={this.remButtonDoc} ScreenToLocalTransform={this.buttonBarXf} - PanelWidth={this.flyoutWidthFunc} - PanelHeight={this.getContentsHeight} + PanelWidth={this.leftMenuFlyoutWidth} + PanelHeight={this.leftMenuFlyoutHeight} renderDepth={0} focus={DocUtils.DefaultFocus} whenChildContentsActiveChanged={emptyFunction} @@ -568,7 +605,7 @@ export class MainView extends React.Component { } render() { - return (<div className={"mainView-container" + (this.darkScheme ? "-dark" : "")} + return (<div className={`mainView-container${this.colorScheme}`} onScroll={() => ((ele) => ele.scrollTop = ele.scrollLeft = 0)(document.getElementById("root")!)} ref={r => { r && new _global.ResizeObserver(action(() => { this._windowWidth = r.getBoundingClientRect().width; this._windowHeight = r.getBoundingClientRect().height; })).observe(r); @@ -580,13 +617,17 @@ export class MainView extends React.Component { <CaptureManager /> <GroupManager /> <GoogleAuthenticationManager /> - <DocumentDecorations boundsLeft={this.leftOffset} boundsTop={this.topOffset} /> + <DocumentDecorations boundsLeft={this.leftScreenOffsetOfMainDocView} boundsTop={this.topOfMainDoc} PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} /> + <ComponentDecorations boundsLeft={this.leftScreenOffsetOfMainDocView} boundsTop={this.topOfMainDocContent} /> {this.topbar} {LinkDescriptionPopup.descriptionPopup ? <LinkDescriptionPopup /> : null} {DocumentLinksButton.LinkEditorDocView ? <LinkMenu docView={DocumentLinksButton.LinkEditorDocView} changeFlyout={emptyFunction} /> : (null)} {LinkDocPreview.LinkInfo ? <LinkDocPreview {...LinkDocPreview.LinkInfo} /> : (null)} + <div style={{ position: "relative", display: LightboxView.LightboxDoc ? "none" : undefined, zIndex: 2001 }} > + <CollectionMenu panelWidth={this.topMenuWidth} panelHeight={this.topMenuHeight} /> + </div> <GestureOverlay > - {this.mainContent} + {this.mainDashboardArea} </GestureOverlay> <PreviewCursor /> <TaskCompletionBox /> @@ -596,6 +637,7 @@ export class MainView extends React.Component { <MarqueeOptionsMenu /> <OverlayView /> <TimelineMenu /> + <RichTextMenu /> {this.snapLines} <div className="mainView-webRef" ref={this.makeWebRef} /> <LightboxView PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} /> diff --git a/src/client/views/MainViewModal.scss b/src/client/views/MainViewModal.scss index 5f19590b4..0648e31c5 100644 --- a/src/client/views/MainViewModal.scss +++ b/src/client/views/MainViewModal.scss @@ -4,18 +4,17 @@ z-index: 10000; width: 100%; height: 100%; + .dialogue-box { + padding: 10px; position: absolute; z-index: 1000; text-align: center; justify-content: center; align-self: center; align-content: center; - padding: 20px; - // background: gainsboro; background: white; - border-radius: 10px; - border: 0.5px solid black; + // border-radius: 10px; box-shadow: #00000044 5px 5px 10px; transform: translate(-50%, -50%); top: 50%; diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx index 1897572f8..563261dec 100644 --- a/src/client/views/MarqueeAnnotator.tsx +++ b/src/client/views/MarqueeAnnotator.tsx @@ -31,7 +31,7 @@ export interface MarqueeAnnotatorProps { annotationLayer: HTMLDivElement; addDocument: (doc: Doc) => boolean; getPageFromScroll?: (top: number) => number; - finishMarquee: (x?: number, y?: number) => void; + finishMarquee: (x?: number, y?: number, PointerEvent?: PointerEvent) => void; anchorMenuClick?: () => undefined | ((anchor: Doc) => void); } @observer @@ -77,11 +77,11 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { e.stopPropagation(); const sourceAnchorCreator = () => { const annoDoc = this.highlight("rgba(173, 216, 230, 0.75)", true); // hyperlink color - this.props.addDocument(annoDoc); + annoDoc && this.props.addDocument(annoDoc); return annoDoc; }; const targetCreator = (annotationOn: Doc | undefined) => { - const target = CurrentUserUtils.GetNewTextDoc("Note linked to " + this.props.rootDoc.title, 0, 0, 100, 100, undefined, annotationOn); + const target = CurrentUserUtils.GetNewTextDoc("Note linked to " + this.props.rootDoc.title, 0, 0, 100, 100, undefined, annotationOn, undefined, "yellow"); FormattedTextBox.SelectOnLoad = target[Id]; return target; }; @@ -222,10 +222,10 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { if (AnchorMenu.Instance.Highlighting) {// when highlighter has been toggled when menu is pinned, we auto-highlight immediately on mouse up this.highlight("rgba(245, 230, 95, 0.75)", false); // yellowish highlight color for highlighted text (should match AnchorMenu's highlight color) } - this.props.finishMarquee(); + this.props.finishMarquee(undefined, undefined, e); } else { runInAction(() => this._width = this._height = 0); - this.props.finishMarquee(cliX, cliY); + this.props.finishMarquee(cliX, cliY, e); } } diff --git a/src/client/views/PreviewCursor.scss b/src/client/views/PreviewCursor.scss index de9bd69c4..60b7d14a0 100644 --- a/src/client/views/PreviewCursor.scss +++ b/src/client/views/PreviewCursor.scss @@ -1,4 +1,5 @@ +.previewCursor-Dark, .previewCursor { color: black; position: absolute; @@ -8,4 +9,8 @@ pointer-events: none; opacity: 1; z-index: 1001; +} + +.previewCursor-Dark { + color: white; }
\ No newline at end of file diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx index 2b82ef475..ef1360ef1 100644 --- a/src/client/views/PreviewCursor.tsx +++ b/src/client/views/PreviewCursor.tsx @@ -3,7 +3,7 @@ import { observer } from 'mobx-react'; import "normalize.css"; import * as React from 'react'; import { Doc } from '../../fields/Doc'; -import { Cast, NumCast } from '../../fields/Types'; +import { Cast, NumCast, StrCast } from '../../fields/Types'; import { DocServer } from '../DocServer'; import { Docs, DocUtils } from '../documents/Documents'; import { CurrentUserUtils } from '../util/CurrentUserUtils'; @@ -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?.focus()} + <div className={`previewCursor${StrCast(CurrentUserUtils.ActiveDashboard?.colorScheme)}`} 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 484522bc7..36b2df73e 100644 --- a/src/client/views/PropertiesButtons.scss +++ b/src/client/views/PropertiesButtons.scss @@ -44,14 +44,15 @@ $linkGap : 3px; } } .propertiesButtons-linkButton-empty.toggle-on { - background-color: white; - color: $dark-gray; + background-color: $medium-blue; + color: $white; } .propertiesButtons-linkButton-empty.toggle-hover { - background-color: gray; - color: $dark-gray; + background-color: $light-blue; + color: $black; } .propertiesButtons-linkButton-empty.toggle-off { + background-color: $dark-gray; color: white; } @@ -67,10 +68,12 @@ $linkGap : 3px; } .onClickFlyout-editScript { + cursor: pointer; text-align: center; - border: 0.5px solid grey; + margin-top: 5px; + border: 0.5px solid $medium-gray; background-color: rgb(230, 230, 230); - border-radius: 9px; + border-radius: 5px; padding: 4px; } @@ -84,9 +87,32 @@ $linkGap : 3px; margin-bottom: 8px; } +.propertiesButton-dropdownList { + width: 100%; + height: fit-content; + top: 100%; + z-index: 21; + + .list-item { + cursor: pointer; + color: $black; + width: 100%; + height: 25px; + font-weight: 400; + display: flex; + justify-content: left; + align-items: center; + padding-left: 5px; + } + + .list-item:hover { + background-color: lightgrey; + } +} + .propertiesButtons-title { - background: #121721; - color: white; + background: $dark-gray; + color: $white; font-size: 6px; width: 37px; padding: 3px; @@ -111,17 +137,11 @@ $linkGap : 3px; margin-left: 4px; &:hover { - background: $medium-gray; - transform: scale(1.05); + filter:brightness(0.85); cursor: pointer; } } -.propertiesButtons-linker:hover { - cursor: pointer; - transform: scale(1.05); -} - @-moz-keyframes spin { 100% { diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index 5c41a96d0..378c67253 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -15,6 +15,9 @@ import { InkingStroke } from './InkingStroke'; import { DocumentView } from './nodes/DocumentView'; import './PropertiesButtons.scss'; import React = require("react"); +import { Colors } from "./global/globalEnums"; +import { CollectionFreeFormView } from "./collections/collectionFreeForm"; +import { DocumentManager } from "../util/DocumentManager"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -29,6 +32,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { @observable public static Instance: PropertiesButtons; @computed get selectedDoc() { return SelectionManager.SelectedSchemaDoc() || SelectionManager.Views().lastElement()?.rootDoc; } + @computed get selectedTabView() { return !SelectionManager.SelectedSchemaDoc() && SelectionManager.Views().lastElement()?.topMost; } propertyToggleBtn = (label: string, property: string, tooltip: (on?: any) => string, icon: (on: boolean) => string, onClick?: (dv: Opt<DocumentView>, doc: Doc, property: string) => void, useUserDoc?: boolean) => { const targetDoc = useUserDoc ? Doc.UserDoc() : this.selectedDoc; @@ -65,10 +69,10 @@ export class PropertiesButtons extends React.Component<{}, {}> { return this.propertyToggleBtn("Lock\xA0View", "_lockedTransform", on => `${on ? "Unlock" : "Lock"} panning of view`, on => "lock"); } @computed get fitContentButton() { - return this.propertyToggleBtn("View All", "_fitToBox", on => `${on ? "Don't" : ""} fit content to container visible area`, on => "eye"); + return this.propertyToggleBtn("View All", "_fitToBox", on => `${on ? "Don't" : "Do"} fit content to container visible area`, on => "eye"); } @computed get fitWidthButton() { - return this.propertyToggleBtn("Fit\xA0Width", "_fitWidth", on => `${on ? "Don't" : ""} fit content to width of container`, on => "arrows-alt-h"); + return this.propertyToggleBtn("Fit\xA0Width", "_fitWidth", on => `${on ? "Don't" : "Do"} fit content to width of container`, on => "arrows-alt-h"); } @computed get captionButton() { return this.propertyToggleBtn("Caption", "_showCaption", on => `${on ? "Hide" : "Show"} caption footer`, on => "closed-captioning", (dv, doc) => (dv?.rootDoc || doc)._showCaption = (dv?.rootDoc || doc)._showCaption === undefined ? "caption" : undefined); @@ -83,7 +87,10 @@ export class PropertiesButtons extends React.Component<{}, {}> { return this.propertyToggleBtn("Auto\xA0Size", "_autoHeight", on => `Automatical vertical sizing to show all content`, on => "arrows-alt-v"); } @computed get gridButton() { - return this.propertyToggleBtn("Grid", "_backgroundGrid-show", on => `Display background grid in collection`, on => "border-all"); + return this.propertyToggleBtn("Grid", "_backgroundGridShow", on => `Display background grid in collection`, on => "border-all"); + } + @computed get groupButton() { + return this.propertyToggleBtn("Group", "isGroup", on => `Display collection as a Group`, on => "object-group", (dv, doc) => { doc.isGroup = !doc.isGroup; doc.forceActive = doc.isGroup; }); } @computed get snapButton() { return this.propertyToggleBtn("Snap\xA0Lines", "showSnapLines", on => `Display snapping lines when objects are dragged`, on => "border-all", undefined, true); @@ -128,13 +135,13 @@ export class PropertiesButtons extends React.Component<{}, {}> { @undoBatch @action - handleOptionChange = (e: any) => { - this.selectedDoc && (this.selectedDoc.onClickBehavior = e.target.value); + handleOptionChange = (onClick: string) => { + this.selectedDoc && (this.selectedDoc.onClickBehavior = onClick); SelectionManager.Views().filter(dv => dv.docView).map(dv => dv.docView!).forEach(docView => { docView.noOnClick(); - switch (e.target.value) { + switch (onClick) { case "enterPortal": docView.makeIntoPortal(); break; - case "toggleDetail": docView.toggleDetail(); break; + case "toggleDetail": docView.setToggleDetail(); break; case "linkInPlace": docView.toggleFollowLink("inPlace", true, false); break; case "linkOnRight": docView.toggleFollowLink("add:right", false, false); break; } @@ -149,20 +156,34 @@ export class PropertiesButtons extends React.Component<{}, {}> { @computed get onClickFlyout() { - const makeLabel = (value: string, label: string) => <div className="radio"> - <label> - <input type="radio" value={value} checked={(this.selectedDoc?.onClickBehavior ?? "nothing") === value} onChange={this.handleOptionChange} /> - {label} - </label> - </div>; + const buttonList = [ + ["nothing", "Select Document"], + ["enterPortal", "Enter Portal"], + ["toggleDetail", "Toggle Detail"], + ["linkInPlace", "Follow Link"], + ["linkOnRight", "Open Link on Right"] + ]; + const currentSelection = this.selectedDoc.onClickBehavior; + // Get items to place into the list + + const list = buttonList.map((value) => { + const click = () => { + this.handleOptionChange(value[0]); + }; + return <div className="list-item" key={`${value}`} + style={{ + backgroundColor: value[0] === currentSelection ? Colors.LIGHT_BLUE : undefined + }} + onClick={click}> + {value[1]} + </div>; + }); return <div> - <form> - {makeLabel("nothing", "Select Document")} - {makeLabel("enterPortal", "Enter Portal")} - {makeLabel("toggleDetail", "Toggle Detail")} - {makeLabel("linkInPlace", "Follow Link")} - {makeLabel("linkOnRight", "Open Link on Right")} - </form> + <div> + <div className="propertiesButton-dropdownList"> + {list} + </div> + </div> {Doc.UserDoc().noviceMode ? (null) : <div onPointerDown={this.editOnClickScript} className="onClickFlyout-editScript"> Edit onClick Script</div>} </div>; } @@ -189,25 +210,27 @@ export class PropertiesButtons extends React.Component<{}, {}> { const isStacking = this.selectedDoc?._viewType === CollectionViewType.Stacking; const isFreeForm = this.selectedDoc?._viewType === CollectionViewType.Freeform; const isTree = this.selectedDoc?._viewType === CollectionViewType.Tree; + const isTabView = this.selectedTabView; const toggle = (ele: JSX.Element | null, style?: React.CSSProperties) => <div className="propertiesButtons-button" style={style}> {ele} </div>; - + const isNovice = Doc.UserDoc().noviceMode; return !this.selectedDoc ? (null) : <div className="propertiesButtons"> {toggle(this.titleButton)} {toggle(this.captionButton)} {toggle(this.lockButton)} - {toggle(this.dictationButton)} + {toggle(this.dictationButton, { display: isNovice ? "none" : "" })} {toggle(this.onClickButton)} {toggle(this.fitWidthButton)} {toggle(this.fitContentButton, { display: !isFreeForm ? "none" : "" })} {toggle(this.autoHeightButton, { display: !isText && !isStacking && !isTree ? "none" : "" })} {toggle(this.maskButton, { display: !isInk ? "none" : "" })} - {toggle(this.chromeButton, { display: isCollection ? "" : "none" })} - {toggle(this.gridButton, { display: isCollection ? "" : "none" })} - {toggle(this.snapButton, { display: isCollection ? "" : "none" })} + {toggle(this.chromeButton, { display: !isCollection || isNovice ? "none" : "" })} + {toggle(this.gridButton, { display: !isCollection ? "none" : "" })} + {toggle(this.groupButton, { display: isTabView || !isCollection ? "none" : "" })} + {toggle(this.snapButton, { display: !isCollection ? "none" : "" })} {toggle(this.clustersButton, { display: !isFreeForm ? "none" : "" })} {toggle(this.panButton, { display: !isFreeForm ? "none" : "" })} - {toggle(this.perspectiveButton, { display: !isCollection ? "none" : "" })} + {toggle(this.perspectiveButton, { display: !isCollection || isNovice ? "none" : "" })} </div>; } }
\ No newline at end of file diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss index 321b83f52..554f137cb 100644 --- a/src/client/views/PropertiesView.scss +++ b/src/client/views/PropertiesView.scss @@ -825,9 +825,8 @@ } .editable-title { - padding: 6px; - padding-bottom: 2px; - border: solid 1px $dark-gray; + border: solid 1px #323232; + height: fit-content; &:hover { border: 0.75px solid $medium-blue; diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 17b137c31..ab9022a84 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -2,17 +2,19 @@ import React = require("react"); import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Checkbox, Tooltip } from "@material-ui/core"; import { intersection } from "lodash"; -import { action, autorun, computed, Lambda, observable, reaction, runInAction } from "mobx"; +import { action, autorun, computed, Lambda, observable } from "mobx"; import { observer } from "mobx-react"; import { ColorState, SketchPicker } from "react-color"; -import { AclAugment, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, AclUnset, DataSym, Doc, Field, HeightSym, Opt, WidthSym, AclSelfEdit } from "../../fields/Doc"; +import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, AclSelfEdit, AclSym, AclUnset, DataSym, Doc, Field, HeightSym, Opt, WidthSym } from "../../fields/Doc"; import { Id } from "../../fields/FieldSymbols"; import { InkField } from "../../fields/InkField"; +import { List } from "../../fields/List"; import { ComputedField } from "../../fields/ScriptField"; -import { Cast, NumCast, StrCast } from "../../fields/Types"; +import { BoolCast, Cast, NumCast, StrCast } from "../../fields/Types"; import { denormalizeEmail, GetEffectiveAcl, SharingPermissions } from "../../fields/util"; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from "../../Utils"; import { DocumentType } from "../documents/DocumentTypes"; +import { CurrentUserUtils } from "../util/CurrentUserUtils"; import { DocumentManager } from "../util/DocumentManager"; import { SelectionManager } from "../util/SelectionManager"; import { SharingManager } from "../util/SharingManager"; @@ -29,8 +31,6 @@ import { PropertiesButtons } from "./PropertiesButtons"; import { PropertiesDocContextSelector } from "./PropertiesDocContextSelector"; import "./PropertiesView.scss"; import { DefaultStyleProvider } from "./StyleProvider"; -import { CurrentUserUtils } from "../util/CurrentUserUtils"; -import { List } from "../../fields/List"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -366,7 +366,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { return <Tooltip title={<div className="dash-tooltip">{"Show more permissions"}</div>}> <div className="expansion-button" onPointerDown={() => { if (this.selectedDocumentView || this.selectedDoc) { - SharingManager.Instance.open(this.selectedDocumentView?.props.Document === this.selectedDocumentView ? this.selectedDocumentView : undefined, this.selectedDoc); + SharingManager.Instance.open(this.selectedDocumentView?.props.Document === this.selectedDoc ? this.selectedDocumentView : undefined, this.selectedDoc); } }}> <FontAwesomeIcon className="expansion-button-icon" icon="ellipsis-h" color="black" size="sm" /> @@ -1086,6 +1086,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { // } render() { + const isNovice = BoolCast(Doc.UserDoc().noviceMode); if (!this.selectedDoc && !this.isPres) { return <div className="propertiesView" style={{ width: this.props.width }}> <div className="propertiesView-title" style={{ width: this.props.width }}> @@ -1110,15 +1111,15 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { {this.sharingSubMenu} - {this.filtersSubMenu} + {isNovice ? null : this.filtersSubMenu} {this.inkSubMenu} - {this.fieldsSubMenu} + {isNovice ? null : this.fieldsSubMenu} - {this.contextsSubMenu} + {isNovice ? null : this.contextsSubMenu} - {this.layoutSubMenu} + {isNovice ? null : this.layoutSubMenu} </div>; } if (this.isPres) { diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx index 1f9763d18..6a5b0a3ae 100644 --- a/src/client/views/SidebarAnnos.tsx +++ b/src/client/views/SidebarAnnos.tsx @@ -38,10 +38,10 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { // 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() { + @computed get allMetadata() { const keys = new Set<string>(); 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(); + return Array.from(keys.keys()).filter(key => key[0]).filter(key => key[0] !== "_" && (key[0] === key[0].toUpperCase())).sort(); } @computed get allUsers() { const keys = new Set<string>(); @@ -59,7 +59,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { }); FormattedTextBox.SelectOnLoad = target[Id]; FormattedTextBox.DontSelectInitialText = true; - this.allHashtags.map(tag => target[tag] = tag); + this.allMetadata.map(tag => target[tag] = tag); DocUtils.MakeLink({ doc: anchor }, { doc: target }, "inline markup", "annotation"); this.addDocument(target); this._stackRef.current?.focusDocument(target); @@ -68,8 +68,8 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { 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; } + return true; } return false; } @@ -83,13 +83,8 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { 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 doc === this.props.rootDoc ? undefined : StrCast(this.props.layoutDoc["sidebar-childShowTitle"], "title"); - } - return this.props.styleProvider?.(doc, props, property); - } + showTitle = () => "title"; + setHeightCallback = (height: number) => this.props.setHeight(height + this.filtersHeight()); render() { const renderTag = (tag: string) => { const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`${tag}:${tag}:check`); @@ -98,6 +93,13 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { {tag} </div>; }; + const renderMeta = (tag: string) => { + const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`${tag}:${tag}:exists`); + return <div key={tag} className={`sidebarAnnos-filterTag${active ? "-active" : ""}`} + onClick={e => Doc.setDocFilter(this.props.rootDoc, tag, tag, "exists", 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" : ""}`} @@ -105,9 +107,10 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { {user} </div>; }; + // TODO: Calculation of the topbar is hardcoded and different for text nodes - it should all be the same and all be part of SidebarAnnos return !this.props.showSidebar ? (null) : <div style={{ - position: "absolute", pointerEvents: this.props.isContentActive() ? "all" : undefined, top: 0, right: 0, + position: "absolute", pointerEvents: this.props.isContentActive() ? "all" : undefined, top: this.props.rootDoc.type !== DocumentType.RTF && StrCast(this.props.rootDoc._showTitle) === "title" ? 15 : 0, right: 0, background: this.props.styleProvider?.(this.props.rootDoc, this.props, StyleProp.WidgetColor), width: `${this.panelWidth()}px`, height: "100%" @@ -115,7 +118,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { <div className="sidebarAnnos-tagList" style={{ height: this.filtersHeight(), width: this.panelWidth() }} onWheel={e => e.stopPropagation()}> {this.allUsers.map(renderUsers)} - {this.allHashtags.map(renderTag)} + {this.allMetadata.map(renderMeta)} </div> <div style={{ width: "100%", height: this.panelHeight(), position: "relative" }}> <CollectionStackingView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} ref={this._stackRef} @@ -123,13 +126,13 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { NativeHeight={returnZero} PanelHeight={this.panelHeight} PanelWidth={this.panelWidth} - styleProvider={this.sidebarStyleProvider} docFilters={this.docFilters} scaleField={this.sidebarKey + "-scale"} - setHeight={(height) => this.props.setHeight(height + this.filtersHeight())} + setHeight={this.setHeightCallback} isAnnotationOverlay={false} select={emptyFunction} scaling={returnOne} + childShowTitle={this.showTitle} whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} childHideDecorationTitle={returnTrue} removeDocument={this.removeDocument} diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index c9e532745..3f120d385 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -1,26 +1,26 @@ -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Colors } from './global/globalEnums'; import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import 'golden-layout/src/css/goldenlayout-base.css'; import 'golden-layout/src/css/goldenlayout-dark-theme.css'; -import { runInAction, action } from 'mobx'; +import { action, runInAction } from 'mobx'; import { Doc, Opt, StrListCast } from "../../fields/Doc"; import { List } from '../../fields/List'; import { listSpec } from '../../fields/Schema'; import { BoolCast, Cast, NumCast, StrCast } from "../../fields/Types"; +import { lightOrDark, DashColor } from '../../Utils'; import { DocumentType } from '../documents/DocumentTypes'; import { CurrentUserUtils } from '../util/CurrentUserUtils'; +import { ColorScheme } from '../util/SettingsManager'; import { SnappingManager } from '../util/SnappingManager'; -import { UndoManager, undoBatch } from '../util/UndoManager'; +import { undoBatch, UndoManager } from '../util/UndoManager'; import { CollectionViewType } from './collections/CollectionView'; -import "./collections/TreeView.scss"; +import { Colors } from './global/globalEnums'; import { MainView } from './MainView'; import { DocumentViewProps } from "./nodes/DocumentView"; import { FieldViewProps } from './nodes/FieldView'; -import "./nodes/FilterBox.scss"; +import { SliderBox } from './nodes/SliderBox'; import "./StyleProvider.scss"; import React = require("react"); -import Color = require('color'); export enum StyleLayers { Background = "background" @@ -48,7 +48,7 @@ export enum StyleProp { FontSize = "fontSize", // size of text font } -function darkScheme() { return BoolCast(CurrentUserUtils.ActiveDashboard?.darkScheme); } +function darkScheme() { return CurrentUserUtils.ActiveDashboard?.colorScheme === ColorScheme.Dark; } function toggleBackground(doc: Doc) { UndoManager.RunInBatch(() => runInAction(() => { @@ -88,34 +88,33 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps switch (property.split(":")[0]) { case StyleProp.TreeViewIcon: return Doc.toIcon(doc, isOpen); case StyleProp.DocContents: return undefined; - case StyleProp.WidgetColor: return isAnnotated ? "lightBlue" : darkScheme() ? "lightgrey" : "dimgrey"; + case StyleProp.WidgetColor: return isAnnotated ? Colors.LIGHT_BLUE : darkScheme() ? "lightgrey" : "dimgrey"; case StyleProp.Opacity: return Cast(doc?._opacity, "number", Cast(doc?.opacity, "number", null)); case StyleProp.HideLinkButton: return props?.hideLinkButton || (!selected && (doc?.isLinkButton || doc?.hideLinkButton)); case StyleProp.FontSize: return StrCast(doc?.[fieldKey + "fontSize"]); - case StyleProp.ShowTitle: return doc && !doc.presentationTargetDoc && StrCast(doc._showTitle, - !Doc.IsSystem(doc) && doc.type === DocumentType.RTF ? - (doc.author === Doc.CurrentUserEmail ? StrCast(Doc.UserDoc().showTitle) : "author;creationDate") : "") || ""; + case StyleProp.ShowTitle: return (doc && !doc.presentationTargetDoc && + StrCast(doc._showTitle, + props?.showTitle?.() || + (!Doc.IsSystem(doc) && [DocumentType.COL, DocumentType.RTF, DocumentType.IMG, DocumentType.VID].includes(doc.type as any) ? + (doc.author === Doc.CurrentUserEmail ? StrCast(Doc.UserDoc().showTitle) : + "author;creationDate") : "")) || ""); case StyleProp.Color: + if (MainView.Instance.LastButton === doc) return Colors.DARK_GRAY; const docColor: Opt<string> = StrCast(doc?.[fieldKey + "color"], StrCast(doc?._color)); if (docColor) return docColor; const backColor = backgroundCol(); if (!backColor) return undefined; - const nonAlphaColor = backColor.startsWith("#") ? (backColor as string).substring(0, 7) : - backColor.startsWith("rgba") ? backColor.replace(/,.[^,]*\)/, ")").replace("rgba", "rgb") : backColor; - const col = Color(nonAlphaColor).rgb(); - const colsum = (col.red() + col.green() + col.blue()); - if (colsum / col.alpha() > 400 || col.alpha() < 0.25) return Colors.DARK_GRAY; - return Colors.WHITE; + return lightOrDark(backColor); case StyleProp.Hidden: return BoolCast(doc?._hidden); case StyleProp.BorderRounding: return StrCast(doc?.[fieldKey + "borderRounding"], doc?._viewType === CollectionViewType.Pile ? "50%" : ""); case StyleProp.TitleHeight: return 15; - 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.BorderPath: return comicStyle() && props?.renderDepth && doc?.type !== DocumentType.INK ? { 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; - case StyleProp.HeaderMargin: return ([CollectionViewType.Stacking, CollectionViewType.Masonry].includes(doc?._viewType as any) || + case StyleProp.HeaderMargin: return ([CollectionViewType.Stacking, CollectionViewType.Masonry, CollectionViewType.Tree].includes(doc?._viewType as any) || doc?.type === DocumentType.RTF) && showTitle() && !StrCast(doc?.showTitle).includes(":hover") ? 15 : 0; case StyleProp.BackgroundColor: { + if (MainView.Instance.LastButton === doc) return Colors.LIGHT_GRAY; let docColor: Opt<string> = StrCast(doc?.[fieldKey + "backgroundColor"], StrCast(doc?._backgroundColor, isCaption ? "rgba(0,0,0,0.4)" : "")); - if (MainView.Instance.LastButton === doc) return darkScheme() ? Colors.MEDIUM_GRAY : Colors.LIGHT_GRAY; switch (doc?.type) { case DocumentType.PRESELEMENT: docColor = docColor || (darkScheme() ? "" : ""); break; case DocumentType.PRES: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.WHITE); break; @@ -135,7 +134,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps case DocumentType.SCREENSHOT: case DocumentType.VID: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY); break; case DocumentType.COL: - if (StrCast(Doc.LayoutField(doc)).includes("SliderBox")) break; + if (StrCast(Doc.LayoutField(doc)).includes(SliderBox.name)) break; 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 @@ -143,18 +142,18 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps 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 ?? (darkScheme() ? Colors.BLACK : Colors.WHITE)))); break; //if (doc._viewType !== CollectionViewType.Freeform && doc._viewType !== CollectionViewType.Time) return "rgb(62,62,62)"; 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(); + if (docColor && (!doc || props?.layerProvider?.(doc) === false)) docColor = DashColor(docColor).fade(0.5).toString(); return docColor; } case StyleProp.BoxShadow: { if (!doc || opacity() === 0) return undefined; // if it's not visible, then no shadow) - if (doc?.isLinkButton && doc.type !== DocumentType.LINK) return StrCast(doc?._linkButtonShadow, "lightblue 0em 0em 1em"); + if (doc?.isLinkButton && ![DocumentType.LINK, DocumentType.INK].includes(doc.type as any)) return StrCast(doc?._linkButtonShadow, "lightblue 0em 0em 1em"); switch (doc?.type) { case DocumentType.COL: @@ -182,13 +181,11 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps if (doc?.type !== DocumentType.INK && layer === true) return "all"; return undefined; case StyleProp.Decorations: - // if (isFooter) - if (props?.ContainingCollectionDoc?._viewType === CollectionViewType.Freeform) { return doc && (isBackground() || selected) && (props?.renderDepth || 0) > 0 && ((doc.type === DocumentType.COL && doc._viewType !== CollectionViewType.Pile) || [DocumentType.RTF, DocumentType.IMG, DocumentType.INK].includes(doc.type as DocumentType)) ? <div className="styleProvider-lock" onClick={() => toggleBackground(doc)}> - <FontAwesomeIcon icon={isBackground() ? "unlock" : "lock"} style={{ color: isBackground() ? "red" : undefined }} size="lg" /> + <FontAwesomeIcon icon={isBackground() ? "lock" : "unlock"} style={{ color: isBackground() ? "red" : undefined }} size="lg" /> </div> : (null); } diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index cae08e1f4..5325d5827 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -428,7 +428,7 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) { const emptyPane = CurrentUserUtils.EmptyPane; emptyPane["dragFactory-count"] = NumCast(emptyPane["dragFactory-count"]) + 1; const docToAdd = Docs.Create.FreeformDocument([], { - _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), _fitWidth: true, title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}`, + _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), _backgroundGridShow: true, _fitWidth: true, title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}`, }); this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd); CollectionDockingView.AddSplit(docToAdd, "", stack); @@ -453,7 +453,7 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) { const emptyPane = CurrentUserUtils.EmptyPane; emptyPane["dragFactory-count"] = NumCast(emptyPane["dragFactory-count"]) + 1; const docToAdd = Docs.Create.FreeformDocument([], { - _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), _fitWidth: true, title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}` + _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), _fitWidth: true, _backgroundGridShow: true, title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}` }); this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd); CollectionDockingView.AddSplit(docToAdd, "", stack); diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx deleted file mode 100644 index 52c836556..000000000 --- a/src/client/views/collections/CollectionLinearView.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import { Tooltip } from '@material-ui/core'; -import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; -import { observer } from 'mobx-react'; -import * as React from 'react'; -import { Doc, HeightSym, WidthSym } from '../../../fields/Doc'; -import { documentSchema } from '../../../fields/documentSchemas'; -import { Id } from '../../../fields/FieldSymbols'; -import { makeInterface } from '../../../fields/Schema'; -import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; -import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, Utils } from '../../../Utils'; -import { DragManager } from '../../util/DragManager'; -import { Transform } from '../../util/Transform'; -import { DocumentLinksButton } from '../nodes/DocumentLinksButton'; -import { DocumentView } from '../nodes/DocumentView'; -import { LinkDescriptionPopup } from '../nodes/LinkDescriptionPopup'; -import { StyleProp } from '../StyleProvider'; -import "./CollectionLinearView.scss"; -import { CollectionSubView } from './CollectionSubView'; -import { CollectionViewType } from './CollectionView'; - - -type LinearDocument = makeInterface<[typeof documentSchema,]>; -const LinearDocument = makeInterface(documentSchema); - -@observer -export class CollectionLinearView extends CollectionSubView(LinearDocument) { - @observable public addMenuToggle = React.createRef<HTMLInputElement>(); - @observable private _selectedIndex = -1; - private _dropDisposer?: DragManager.DragDropDisposer; - private _widthDisposer?: IReactionDisposer; - private _selectedDisposer?: IReactionDisposer; - - componentWillUnmount() { - this._dropDisposer?.(); - this._widthDisposer?.(); - this._selectedDisposer?.(); - this.childLayoutPairs.map((pair, ind) => ScriptCast(pair.layout.proto?.onPointerUp)?.script.run({ this: pair.layout.proto }, console.log)); - } - - componentDidMount() { - this._widthDisposer = reaction(() => 5 + (this.layoutDoc.linearViewIsExpanded ? this.childDocs.length * (this.rootDoc[HeightSym]()) : 10), - width => this.childDocs.length && (this.layoutDoc._width = width), - { fireImmediately: true } - ); - - this._selectedDisposer = reaction( - () => NumCast(this.layoutDoc.selectedIndex), - (i) => runInAction(() => { - this._selectedIndex = i; - let selected: any = undefined; - this.childLayoutPairs.map(async (pair, ind) => { - const isSelected = this._selectedIndex === ind; - if (isSelected) { - selected = pair; - } - else { - ScriptCast(pair.layout.proto?.onPointerUp)?.script.run({ this: pair.layout.proto }, console.log); - } - }); - if (selected && selected.layout) { - ScriptCast(selected.layout.proto?.onPointerDown)?.script.run({ this: selected.layout.proto }, console.log); - } - }), - { fireImmediately: true } - ); - } - protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view - this._dropDisposer && this._dropDisposer(); - if (ele) { - this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc); - } - } - - dimension = () => NumCast(this.rootDoc._height); // 2 * the padding - getTransform = (ele: React.RefObject<HTMLDivElement>) => () => { - if (!ele.current) return Transform.Identity(); - const { scale, translateX, translateY } = Utils.GetScreenTransform(ele.current); - return new Transform(-translateX, -translateY, 1); - } - - @action - exitLongLinks = () => { - if (DocumentLinksButton.StartLink) { - if (DocumentLinksButton.StartLink.Document) { - action((e: React.PointerEvent<HTMLDivElement>) => { - Doc.UnBrushDoc(DocumentLinksButton.StartLink?.Document as Doc); - }); - } - } - DocumentLinksButton.StartLink = undefined; - DocumentLinksButton.StartLinkView = undefined; - } - - @action - changeDescriptionSetting = () => { - if (LinkDescriptionPopup.showDescriptions) { - if (LinkDescriptionPopup.showDescriptions === "ON") { - LinkDescriptionPopup.showDescriptions = "OFF"; - LinkDescriptionPopup.descriptionPopup = false; - } else { - LinkDescriptionPopup.showDescriptions = "ON"; - } - } else { - LinkDescriptionPopup.showDescriptions = "OFF"; - LinkDescriptionPopup.descriptionPopup = false; - } - } - - render() { - const guid = Utils.GenerateGuid(); - const flexDir: any = StrCast(this.Document.flexDirection); - const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); - const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); - - const menuOpener = <label htmlFor={`${guid}`} style={{ pointerEvents: "all", cursor: "pointer", background: backgroundColor === color ? "black" : backgroundColor, }} - onPointerDown={e => e.stopPropagation()} > - <p>{BoolCast(this.layoutDoc.linearViewIsExpanded) ? "–" : "+"}</p> - </label>; - - return <div className="collectionLinearView-outer"> - <div className="collectionLinearView" ref={this.createDashEventsTarget} > - <Tooltip title={<><div className="dash-tooltip">{BoolCast(this.layoutDoc.linearViewIsExpanded) ? "Close menu" : "Open menu"}</div></>} placement="top"> - {menuOpener} - </Tooltip> - <input id={`${guid}`} type="checkbox" checked={BoolCast(this.layoutDoc.linearViewIsExpanded)} ref={this.addMenuToggle} - onChange={action(() => this.layoutDoc.linearViewIsExpanded = this.addMenuToggle.current!.checked)} /> - - <div className="collectionLinearView-content" style={{ height: this.dimension(), flexDirection: flexDir }}> - {this.childLayoutPairs.map((pair, ind) => { - const nested = pair.layout._viewType === CollectionViewType.Linear; - const dref = React.createRef<HTMLDivElement>(); - const scalable = pair.layout.onClick || pair.layout.onDragStart; - return <div className={`collectionLinearView-docBtn` + (scalable ? "-scalable" : "")} key={pair.layout[Id]} ref={dref} - style={{ - pointerEvents: "all", - minWidth: 30, - width: nested ? pair.layout[WidthSym]() : this.dimension(), - height: nested && pair.layout.linearViewIsExpanded ? pair.layout[HeightSym]() : this.dimension(), - }} > - <DocumentView - Document={pair.layout} - DataDoc={pair.data} - isContentActive={returnFalse} - isDocumentActive={returnTrue} - addDocument={this.props.addDocument} - moveDocument={this.props.moveDocument} - addDocTab={this.props.addDocTab} - pinToPres={emptyFunction} - rootSelected={this.props.isSelected} - removeDocument={this.props.removeDocument} - ScreenToLocalTransform={this.getTransform(dref)} - PanelWidth={nested ? pair.layout[WidthSym] : this.dimension} - PanelHeight={nested ? pair.layout[HeightSym] : this.dimension} - renderDepth={this.props.renderDepth + 1} - focus={emptyFunction} - styleProvider={this.props.styleProvider} - layerProvider={this.props.layerProvider} - docViewPath={returnEmptyDoclist} - whenChildContentsActiveChanged={emptyFunction} - bringToFront={emptyFunction} - docFilters={this.props.docFilters} - docRangeFilters={this.props.docRangeFilters} - searchFilterDocs={this.props.searchFilterDocs} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} /> - </div>; - })} - </div> - {DocumentLinksButton.StartLink ? <span className="bottomPopup-background" style={{ - pointerEvents: "all" - }} - onPointerDown={e => e.stopPropagation()} > - <span className="bottomPopup-text" > - 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">{"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 linking mode</div></>} placement="top"> - <span className="bottomPopup-exit" onClick={this.exitLongLinks}> - Stop - </span> - </Tooltip> - - </span> : null} - </div> - </div>; - } -}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionMenu.scss b/src/client/views/collections/CollectionMenu.scss index f04b19ef7..c35f088a6 100644 --- a/src/client/views/collections/CollectionMenu.scss +++ b/src/client/views/collections/CollectionMenu.scss @@ -1,628 +1,659 @@ @import "../global/globalCssVariables"; -.collectionMenu-cont { - position: relative; - display: inline-flex; - width: 100%; - opacity: 0.9; - z-index: 901; - transition: top .5s; - background: $dark-gray; - color: $white; - transform-origin: top left; - top: 0; - width: 100%; - - .recordButtonOutline { - border-radius: 100%; - width: 18px; - height: 18px; - border: solid 1px $white; - display: flex; - align-items: center; - justify-content: center; - } - - .recordButtonInner { - border-radius: 100%; - width: 70%; - height: 70%; - background: $white; - } - - .collectionMenu { - display: flex; - height: 100%; - overflow: visible; - z-index: 901; - border: unset; - - .collectionMenu-divider { - height: 100%; - margin-left: 3px; - margin-right: 3px; - width: 2px; - background-color: $medium-gray; - } - - .collectionViewBaseChrome { - display: flex; - align-items: center; - - .collectionViewBaseChrome-viewPicker { - font-size: $small-text; - outline-color: $black; - color: $white; - border: none; - background: $dark-gray; - } - - .collectionViewBaseChrome-viewPicker:focus { - outline: none; - border: none; - } - - .collectionViewBaseChrome-viewPicker:active { - outline-color: $black; - } - - .collectionViewBaseChrome-button { - font-size: $small-text; - text-transform: uppercase; - letter-spacing: 2px; - background: $white; - color: $pink; - outline-color: $black; - border: none; - padding: 12px 10px 11px 10px; - margin-left: 10px; - } - - .collectionViewBaseChrome-cmdPicker { - margin-left: 3px; - margin-right: 0px; - font-size: $small-text; - text-transform: capitalize; - color: $white; - border: none; - background: $dark-gray; - } - - .collectionViewBaseChrome-cmdPicker:focus { - border: none; - outline: none; - } - - .commandEntry-outerDiv { - pointer-events: all; - background-color: transparent; - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - height: 100%; - overflow: hidden; - - .commandEntry-drop { - color: $white; - width: 30px; - margin-top: auto; - margin-bottom: auto; - } - } - - .commandEntry-outerDiv:hover{ - background-color: $drop-shadow; - - .collectionViewBaseChrome-viewPicker, - .collectionViewBaseChrome-cmdPicker{ - background: $dark-gray; - } - } - - .collectionViewBaseChrome-collapse { - transition: all .5s, opacity 0.3s; - position: absolute; - width: 30px; - transform-origin: top left; - pointer-events: all; - // margin-top: 10px; - } - - @media only screen and (max-device-width: 480px) { - .collectionViewBaseChrome-collapse { - display: none; - } - } - - .collectionViewBaseChrome-template, - .collectionViewBaseChrome-viewModes { - align-items: center; - height: 100%; - display: flex; - background: transparent; - color: $medium-gray; - justify-content: center; - } - - .collectionViewBaseChrome-viewSpecs { - margin-left: 5px; - display: grid; - border: none; - border-right: solid $medium-gray 1px; - - .collectionViewBaseChrome-filterIcon { - position: relative; - display: flex; - margin: auto; - background: $dark-gray; - color: $white; - width: 30px; - height: 30px; - align-items: center; - justify-content: center; - border: none; - border-right: solid $medium-gray 1px; - } - - .collectionViewBaseChrome-viewSpecsInput { - padding: 12px 10px 11px 10px; - border: 0px; - color: $medium-gray; - text-align: center; - letter-spacing: 2px; - outline-color: $black; - font-size: $small-text; - background: $white; - height: 100%; - width: 75px; - } - - .collectionViewBaseChrome-viewSpecsMenu { - overflow: hidden; - transition: height .5s, display .5s; - position: absolute; - top: 60px; - z-index: 100; - display: flex; - flex-direction: column; - background: $white; - box-shadow: $medium-gray 2px 2px 4px; - - .qs-datepicker { - left: unset; - right: 0; - } - - .collectionViewBaseChrome-viewSpecsMenu-row { - display: grid; - grid-template-columns: 150px 200px 150px; - margin-top: 10px; - margin-right: 10px; - - .collectionViewBaseChrome-viewSpecsMenu-rowLeft, - .collectionViewBaseChrome-viewSpecsMenu-rowMiddle, - .collectionViewBaseChrome-viewSpecsMenu-rowRight { - font-size: $small-text; - letter-spacing: 2px; - color: $medium-gray; - margin-left: 10px; - padding: 5px; - border: none; - outline-color: $black; - } - } - - .collectionViewBaseChrome-viewSpecsMenu-lastRow { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - grid-gap: 10px; - margin: 10px; - } - } - } - } - - .collectionStackingViewChrome-cont, - .collectionTreeViewChrome-cont, - .collection3DCarouselViewChrome-cont { - display: flex; - justify-content: space-between; - } - - .collectionGridViewChrome-cont { - display: flex; - margin-left: 10; - - .collectionGridViewChrome-viewPicker { - font-size: $small-text; - //text-transform: uppercase; - //letter-spacing: 2px; - background: $dark-gray; - color: $white; - outline-color: $black; - color: $white; - border: none; - border-right: solid $medium-gray 1px; - } - - .collectionGridViewChrome-viewPicker:active { - outline-color: $black; - } - - .grid-control { - align-self: center; - display: flex; - flex-direction: row; - margin-right: 5px; - - .grid-icon { - margin-right: 5px; - align-self: center; - } - - .flexLabel { - margin-bottom: 0; - } - - .collectionGridViewChrome-entryBox { - width: 50%; - color: $black; - } - - .collectionGridViewChrome-columnButton { - color: $black; - } - } - } - - .collectionStackingViewChrome-sort, - .collectionTreeViewChrome-sort { - display: flex; - align-items: center; - justify-content: space-between; - - .collectionStackingViewChrome-sortIcon, - .collectionTreeViewChrome-sortIcon { - transition: transform .5s; - margin-left: 10px; - } - } - - button:hover { - transform: scale(1); - } - - - .collectionStackingViewChrome-pivotField-cont, - .collectionTreeViewChrome-pivotField-cont, - .collection3DCarouselViewChrome-scrollSpeed-cont { - justify-self: right; - align-items: center; - display: flex; - grid-auto-columns: auto; - font-size: $small-text; - letter-spacing: 2px; - - .collectionStackingViewChrome-pivotField-label, - .collectionTreeViewChrome-pivotField-label, - .collection3DCarouselViewChrome-scrollSpeed-label { - grid-column: 1; - margin-right: 7px; - user-select: none; - font-family: $sans-serif; - letter-spacing: normal; - } - - .collectionStackingViewChrome-sortIcon { - transition: transform .5s; - grid-column: 3; - text-align: center; - display: flex; - justify-content: center; - align-items: center; - cursor: pointer; - width: 25px; - height: 25px; - border-radius: 100%; - } - - .collectionStackingViewChrome-sortIcon:hover { - background-color: $drop-shadow; - } - - .collectionStackingViewChrome-pivotField, - .collectionTreeViewChrome-pivotField, - .collection3DCarouselViewChrome-scrollSpeed { - color: $white; - grid-column: 2; - grid-row: 1; - width: 90%; - min-width: 100px; - display: flex; - height: 80%; - border-radius: 7px; - align-items: center; - background: $white; - - .editable-view-input, - input, - .editableView-container-editing-oneLine, - .editableView-container-editing { - margin: auto; - border: 0px; - color: $light-gray !important; - text-align: center; - letter-spacing: 2px; - outline-color: $black; - height: 100%; - } - - .react-autosuggest__container { - margin: 0; - color: $medium-gray; - padding: 0px; - } - } - } - - .collectionStackingViewChrome-pivotField:hover, - .collectionTreeViewChrome-pivotField:hover, - .collection3DCarouselViewChrome-scrollSpeed:hover { - cursor: text; - } - - } -} - -.collectionMenu-webUrlButtons { - margin-left: 44; - background: lightGray; +.collectionMenu-container { display: flex; -} - -.webBox-urlEditor { - position: relative; - opacity: 0.9; - z-index: 901; - transition: top .5s; - - .urlEditor { - display: grid; - grid-template-columns: 1fr auto; - padding-bottom: 10px; - overflow: hidden; - margin-top: 5px; - height: 35px; - - .editorBase { - display: flex; - - .editor-collapse { - transition: all .5s, opacity 0.3s; - position: absolute; - width: 40px; - transform-origin: top left; - } - - .switchToText { - color: $medium-gray; - } - - .switchToText:hover { - color: $dark-gray; - } - } - - button:hover { - transform: scale(1); - } - } -} - -.collectionMenu-urlInput { - padding: 12px 10px 11px 10px; - border: 0px; - color: $black; - font-size: $small-text; - letter-spacing: 2px; - outline-color: $black; - background: $white; - width: 100%; - min-width: 350px; - margin-right: 10px; - height: 100%; -} - -.collectionFreeFormMenu-cont { - display: inline-flex; position: relative; + align-content: center; + justify-content: space-between; + background-color: $dark-gray; + height: 35px; + border-bottom: $standard-border; + padding-right: 5px; align-items: center; - height: 100%; - - .color-previewI { - width: 60%; - top: 80%; - position: absolute; - height: 4px; - } - - .color-previewII { - width: 80%; - height: 80%; - margin-left: 10%; - position: absolute; - bottom: 5; - } - - .btn-group { - display: grid; - grid-template-columns: auto auto auto auto; - margin: auto; - /* Make the buttons appear below each other */ - } - .btn-draw { - display: inline-flex; - margin: auto; - /* Make the buttons appear below each other */ - } - - .fwdKeyframe, - .numKeyframe, - .backKeyframe { + .collectionMenu-hardCodedButton { cursor: pointer; - position: relative; - width: 20; - height: 30; - bottom: 0; - background: $dark-gray; - display: inline-flex; - align-items: center; color: $white; - } - - .backKeyframe { - svg { - display: block; - margin: auto; - } - } - - - .numKeyframe { - flex-direction: column; - padding-top: 5px; - } - - .fwdKeyframe { - svg { - display: block; - margin: auto; - } - - border-right: solid $medium-gray 1px; - } -} - -.collectionSchemaViewChrome-cont { - display: flex; - font-size: $small-text; - - .collectionSchemaViewChrome-toggle { - display: flex; - margin-left: 10px; - } - - .collectionSchemaViewChrome-label { - text-transform: uppercase; - letter-spacing: 2px; - margin-right: 5px; + width: 25px; + height: 25px; + padding: 5; + text-align: center; display: flex; - flex-direction: column; justify-content: center; - } - - .collectionSchemaViewChrome-toggler { - width: 100px; - height: 35px; - background-color: $black; + align-items: center; position: relative; - } - - .collectionSchemaViewChrome-togglerButton { - width: 47px; - height: 30px; - background-color: $light-gray; - // position: absolute; - transition: all 0.5s ease; - // top: 3px; - margin-top: 3px; - color: $medium-gray; - letter-spacing: 2px; - text-transform: uppercase; - display: flex; - flex-direction: column; - justify-content: center; - text-align: center; + transition: 0.2s; + border-radius: 3px; - &.on { - margin-left: 3px; - } - - &.off { - margin-left: 50px; + &:hover { + background-color: rgba(0, 0, 0, 0.2); } } } - -.commandEntry-outerDiv { - display: flex; - flex-direction: column; - height: 40px; -} - -.commandEntry-inputArea { - display: flex; - flex-direction: row; - width: 150px; - margin: auto auto auto auto; -} - -.react-autosuggest__container { - position: relative; - width: 100%; - margin-left: 5px; - margin-right: 5px; -} - -.react-autosuggest__input { - border: 1px solid $light-gray; - border-radius: 4px; - width: 100%; -} - -.react-autosuggest__input--focused { - outline: none; -} - -.react-autosuggest__input--open { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; -} - -.react-autosuggest__suggestions-container { - display: none; -} - -.react-autosuggest__suggestions-container--open { - display: block; - position: fixed; - overflow-y: auto; - max-height: 400px; - width: 180px; - border: 1px solid $light-gray; - background-color: $white; - font-family: $sans-serif; - font-weight: 300; - font-size: $large-header; - border-bottom-left-radius: 4px; - border-bottom-right-radius: 4px; - z-index: 2; -} - -.react-autosuggest__suggestions-list { - margin: 0; - padding: 0; - list-style-type: none; -} - -.react-autosuggest__suggestion { - cursor: pointer; - padding: 10px 20px; -} - -.react-autosuggest__suggestion--highlighted { - background-color: $light-gray; -}
\ No newline at end of file +// .collectionMenu-cont { +// position: relative; +// display: inline-flex; +// width: 100%; +// opacity: 0.9; +// z-index: 901; +// transition: top .5s; +// background: $dark-gray; +// color: $white; +// transform-origin: top left; +// top: 0; +// width: 100%; + +// .recordButtonOutline { +// border-radius: 100%; +// width: 18px; +// height: 18px; +// border: solid 1px $white; +// display: flex; +// align-items: center; +// justify-content: center; +// } + +// .recordButtonInner { +// border-radius: 100%; +// width: 70%; +// height: 70%; +// background: $white; +// } + +// .collectionMenu { +// display: flex; +// height: 100%; +// overflow: visible; +// z-index: 901; +// border: unset; + +// .collectionMenu-divider { +// height: 100%; +// margin-left: 3px; +// margin-right: 3px; +// width: 2px; +// background-color: $medium-gray; +// } + +// .collectionViewBaseChrome { +// display: flex; +// align-items: center; + +// .collectionViewBaseChrome-viewPicker { +// font-size: $small-text; +// outline-color: $black; +// color: $white; +// border: none; +// background: $dark-gray; +// } + +// .collectionViewBaseChrome-viewPicker:focus { +// outline: none; +// border: none; +// } + +// .collectionViewBaseChrome-viewPicker:active { +// outline-color: $black; +// } + +// .collectionViewBaseChrome-button { +// font-size: $small-text; +// text-transform: uppercase; +// letter-spacing: 2px; +// background: $white; +// color: $pink; +// outline-color: $black; +// border: none; +// padding: 12px 10px 11px 10px; +// margin-left: 10px; +// } + +// .collectionViewBaseChrome-cmdPicker { +// margin-left: 3px; +// margin-right: 0px; +// font-size: $small-text; +// text-transform: capitalize; +// color: $white; +// border: none; +// background: $dark-gray; +// } + +// .collectionViewBaseChrome-cmdPicker:focus { +// border: none; +// outline: none; +// } + +// .commandEntry-outerDiv { +// pointer-events: all; +// background-color: transparent; +// display: flex; +// flex-direction: row; +// align-items: center; +// justify-content: center; +// height: 100%; +// overflow: hidden; + +// .commandEntry-drop { +// color: $white; +// width: 30px; +// margin-top: auto; +// margin-bottom: auto; +// } +// } + +// .commandEntry-outerDiv:hover{ +// background-color: $drop-shadow; + +// .collectionViewBaseChrome-viewPicker, +// .collectionViewBaseChrome-cmdPicker{ +// background: $dark-gray; +// } +// } + +// .collectionViewBaseChrome-collapse { +// transition: all .5s, opacity 0.3s; +// position: absolute; +// width: 30px; +// transform-origin: top left; +// pointer-events: all; +// // margin-top: 10px; +// } + +// @media only screen and (max-device-width: 480px) { +// .collectionViewBaseChrome-collapse { +// display: none; +// } +// } + +// .collectionViewBaseChrome-template, +// .collectionViewBaseChrome-viewModes { +// align-items: center; +// height: 100%; +// display: flex; +// background: transparent; +// color: $medium-gray; +// justify-content: center; +// } + +// .collectionViewBaseChrome-viewSpecs { +// margin-left: 5px; +// display: grid; +// border: none; +// border-right: solid $medium-gray 1px; + +// .collectionViewBaseChrome-filterIcon { +// position: relative; +// display: flex; +// margin: auto; +// background: $dark-gray; +// color: $white; +// width: 30px; +// height: 30px; +// align-items: center; +// justify-content: center; +// border: none; +// border-right: solid $medium-gray 1px; +// } + +// .collectionViewBaseChrome-viewSpecsInput { +// padding: 12px 10px 11px 10px; +// border: 0px; +// color: $medium-gray; +// text-align: center; +// letter-spacing: 2px; +// outline-color: $black; +// font-size: $small-text; +// background: $white; +// height: 100%; +// width: 75px; +// } + +// .collectionViewBaseChrome-viewSpecsMenu { +// overflow: hidden; +// transition: height .5s, display .5s; +// position: absolute; +// top: 60px; +// z-index: 100; +// display: flex; +// flex-direction: column; +// background: $white; +// box-shadow: $medium-gray 2px 2px 4px; + +// .qs-datepicker { +// left: unset; +// right: 0; +// } + +// .collectionViewBaseChrome-viewSpecsMenu-row { +// display: grid; +// grid-template-columns: 150px 200px 150px; +// margin-top: 10px; +// margin-right: 10px; + +// .collectionViewBaseChrome-viewSpecsMenu-rowLeft, +// .collectionViewBaseChrome-viewSpecsMenu-rowMiddle, +// .collectionViewBaseChrome-viewSpecsMenu-rowRight { +// font-size: $small-text; +// letter-spacing: 2px; +// color: $medium-gray; +// margin-left: 10px; +// padding: 5px; +// border: none; +// outline-color: $black; +// } +// } + +// .collectionViewBaseChrome-viewSpecsMenu-lastRow { +// display: grid; +// grid-template-columns: 1fr 1fr 1fr; +// grid-gap: 10px; +// margin: 10px; +// } +// } +// } +// } + +// .collectionStackingViewChrome-cont, +// .collectionTreeViewChrome-cont, +// .collection3DCarouselViewChrome-cont { +// display: flex; +// justify-content: space-between; +// } + +// .collectionGridViewChrome-cont { +// display: flex; +// margin-left: 10; + +// .collectionGridViewChrome-viewPicker { +// font-size: $small-text; +// //text-transform: uppercase; +// //letter-spacing: 2px; +// background: $dark-gray; +// color: $white; +// outline-color: $black; +// color: $white; +// border: none; +// border-right: solid $medium-gray 1px; +// } + +// .collectionGridViewChrome-viewPicker:active { +// outline-color: $black; +// } + +// .grid-control { +// align-self: center; +// display: flex; +// flex-direction: row; +// margin-right: 5px; + +// .grid-icon { +// margin-right: 5px; +// align-self: center; +// } + +// .flexLabel { +// margin-bottom: 0; +// } + +// .collectionGridViewChrome-entryBox { +// width: 50%; +// color: $black; +// } + +// .collectionGridViewChrome-columnButton { +// color: $black; +// } +// } +// } + +// .collectionStackingViewChrome-sort, +// .collectionTreeViewChrome-sort { +// display: flex; +// align-items: center; +// justify-content: space-between; + +// .collectionStackingViewChrome-sortIcon, +// .collectionTreeViewChrome-sortIcon { +// transition: transform .5s; +// margin-left: 10px; +// } +// } + +// button:hover { +// transform: scale(1); +// } + + +// .collectionStackingViewChrome-pivotField-cont, +// .collectionTreeViewChrome-pivotField-cont, +// .collection3DCarouselViewChrome-scrollSpeed-cont { +// justify-self: right; +// align-items: center; +// display: flex; +// grid-auto-columns: auto; +// font-size: $small-text; +// letter-spacing: 2px; + +// .collectionStackingViewChrome-pivotField-label, +// .collectionTreeViewChrome-pivotField-label, +// .collection3DCarouselViewChrome-scrollSpeed-label { +// grid-column: 1; +// margin-right: 7px; +// user-select: none; +// font-family: $sans-serif; +// letter-spacing: normal; +// } + +// .collectionStackingViewChrome-sortIcon { +// transition: transform .5s; +// grid-column: 3; +// text-align: center; +// display: flex; +// justify-content: center; +// align-items: center; +// cursor: pointer; +// width: 25px; +// height: 25px; +// border-radius: 100%; +// } + +// .collectionStackingViewChrome-sortIcon:hover { +// background-color: $drop-shadow; +// } + +// .collectionStackingViewChrome-pivotField, +// .collectionTreeViewChrome-pivotField, +// .collection3DCarouselViewChrome-scrollSpeed { +// color: $white; +// grid-column: 2; +// grid-row: 1; +// width: 90%; +// min-width: 100px; +// display: flex; +// height: 80%; +// border-radius: 7px; +// align-items: center; +// background: $white; + +// .editable-view-input, +// input, +// .editableView-container-editing-oneLine, +// .editableView-container-editing { +// margin: auto; +// border: 0px; +// color: $light-gray !important; +// text-align: center; +// letter-spacing: 2px; +// outline-color: $black; +// height: 100%; +// } + +// .react-autosuggest__container { +// margin: 0; +// color: $medium-gray; +// padding: 0px; +// } +// } +// } + +// .collectionStackingViewChrome-pivotField:hover, +// .collectionTreeViewChrome-pivotField:hover, +// .collection3DCarouselViewChrome-scrollSpeed:hover { +// cursor: text; +// } + +// } +// } + +// .collectionMenu-webUrlButtons { +// margin-left: 44; +// background: lightGray; +// display: flex; +// } + +// .webBox-urlEditor { +// position: relative; +// opacity: 0.9; +// z-index: 901; +// transition: top .5s; + +// .urlEditor { +// display: grid; +// grid-template-columns: 1fr auto; +// padding-bottom: 10px; +// overflow: hidden; +// margin-top: 5px; +// height: 35px; + +// .editorBase { +// display: flex; + +// .editor-collapse { +// transition: all .5s, opacity 0.3s; +// position: absolute; +// width: 40px; +// transform-origin: top left; +// } + +// .switchToText { +// color: $medium-gray; +// } + +// .switchToText:hover { +// color: $dark-gray; +// } +// } + +// button:hover { +// transform: scale(1); +// } +// } +// } + +// .collectionMenu-urlInput { +// padding: 12px 10px 11px 10px; +// border: 0px; +// color: $black; +// font-size: $small-text; +// letter-spacing: 2px; +// outline-color: $black; +// background: $white; +// width: 100%; +// min-width: 350px; +// margin-right: 10px; +// height: 100%; +// } + +// .collectionFreeFormMenu-cont { +// display: inline-flex; +// position: relative; +// align-items: center; +// height: 100%; + +// .color-previewI { +// width: 60%; +// top: 80%; +// position: absolute; +// height: 4px; +// } + +// .color-previewII { +// width: 80%; +// height: 80%; +// margin-left: 10%; +// position: absolute; +// bottom: 5; +// } + +// .btn-group { +// display: grid; +// grid-template-columns: auto auto auto auto; +// margin: auto; +// /* Make the buttons appear below each other */ +// } + +// .btn-draw { +// display: inline-flex; +// margin: auto; +// /* Make the buttons appear below each other */ +// } + +// .fwdKeyframe, +// .numKeyframe, +// .backKeyframe { +// cursor: pointer; +// position: relative; +// width: 20; +// height: 30; +// bottom: 0; +// background: $dark-gray; +// display: inline-flex; +// align-items: center; +// color: $white; +// } + +// .backKeyframe { +// svg { +// display: block; +// margin: auto; +// } +// } + + +// .numKeyframe { +// flex-direction: column; +// padding-top: 5px; +// } + +// .fwdKeyframe { +// svg { +// display: block; +// margin: auto; +// } + +// border-right: solid $medium-gray 1px; +// } +// } + +// .collectionSchemaViewChrome-cont { +// display: flex; +// font-size: $small-text; + +// .collectionSchemaViewChrome-toggle { +// display: flex; +// margin-left: 10px; +// } + +// .collectionSchemaViewChrome-label { +// text-transform: uppercase; +// letter-spacing: 2px; +// margin-right: 5px; +// display: flex; +// flex-direction: column; +// justify-content: center; +// } + +// .collectionSchemaViewChrome-toggler { +// width: 100px; +// height: 35px; +// background-color: $black; +// position: relative; +// } + +// .collectionSchemaViewChrome-togglerButton { +// width: 47px; +// height: 30px; +// background-color: $light-gray; +// // position: absolute; +// transition: all 0.5s ease; +// // top: 3px; +// margin-top: 3px; +// color: $medium-gray; +// letter-spacing: 2px; +// text-transform: uppercase; +// display: flex; +// flex-direction: column; +// justify-content: center; +// text-align: center; + +// &.on { +// margin-left: 3px; +// } + +// &.off { +// margin-left: 50px; +// } +// } +// } + + +// .commandEntry-outerDiv { +// display: flex; +// flex-direction: column; +// height: 40px; +// } + +// .commandEntry-inputArea { +// display: flex; +// flex-direction: row; +// width: 150px; +// margin: auto auto auto auto; +// } + +// .react-autosuggest__container { +// position: relative; +// width: 100%; +// margin-left: 5px; +// margin-right: 5px; +// } + +// .react-autosuggest__input { +// border: 1px solid $light-gray; +// border-radius: 4px; +// width: 100%; +// } + +// .react-autosuggest__input--focused { +// outline: none; +// } + +// .react-autosuggest__input--open { +// border-bottom-left-radius: 0; +// border-bottom-right-radius: 0; +// } + +// .react-autosuggest__suggestions-container { +// display: none; +// } + +// .react-autosuggest__suggestions-container--open { +// display: block; +// position: fixed; +// overflow-y: auto; +// max-height: 400px; +// width: 180px; +// border: 1px solid $light-gray; +// background-color: $white; +// font-family: $sans-serif; +// font-weight: 300; +// font-size: $large-header; +// border-bottom-left-radius: 4px; +// border-bottom-right-radius: 4px; +// z-index: 2; +// } + +// .react-autosuggest__suggestions-list { +// margin: 0; +// padding: 0; +// list-style-type: none; +// } + +// .react-autosuggest__suggestion { +// cursor: pointer; +// padding: 10px 20px; +// } + +// .react-autosuggest__suggestion--highlighted { +// background-color: $light-gray; +// }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index 8f4df4a92..2c2d5dc75 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -2,7 +2,7 @@ import React = require("react"); import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon, FontAwesomeIconProps } from "@fortawesome/react-fontawesome"; import { Tooltip } from "@material-ui/core"; -import { action, computed, Lambda, observable, reaction, runInAction } from "mobx"; +import { action, computed, Lambda, observable, reaction, runInAction, trace } from "mobx"; import { observer } from "mobx-react"; import { ColorState } from "react-color"; import { Doc, DocListCast, Opt } from "../../../fields/Doc"; @@ -15,37 +15,47 @@ import { RichTextField } from "../../../fields/RichTextField"; import { listSpec } from "../../../fields/Schema"; import { ScriptField } from "../../../fields/ScriptField"; import { BoolCast, Cast, NumCast, StrCast } from "../../../fields/Types"; -import { emptyFunction, setupMoveUpEvents, Utils } from "../../../Utils"; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents, Utils } from "../../../Utils"; +import { Docs } from "../../documents/Documents"; import { DocumentType } from "../../documents/DocumentTypes"; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; import { DragManager } from "../../util/DragManager"; import { Scripting } from "../../util/Scripting"; import { SelectionManager } from "../../util/SelectionManager"; +import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; import { AntimodeMenu, AntimodeMenuProps } from "../AntimodeMenu"; import { EditableView } from "../EditableView"; import { GestureOverlay } from "../GestureOverlay"; -import { ActiveFillColor, ActiveInkColor, SetActiveArrowEnd, SetActiveArrowStart, SetActiveBezierApprox, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth, ActiveArrowStart, ActiveArrowEnd } from "../InkingStroke"; +import { ActiveFillColor, ActiveInkColor, SetActiveArrowEnd, SetActiveArrowStart, SetActiveBezierApprox, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth } from "../InkingStroke"; +import { LightboxView } from "../LightboxView"; import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; import { DocumentView } from "../nodes/DocumentView"; +import { FormattedTextBox } from "../nodes/formattedText/FormattedTextBox"; import { RichTextMenu } from "../nodes/formattedText/RichTextMenu"; import { PresBox } from "../nodes/trails/PresBox"; +import { DefaultStyleProvider } from "../StyleProvider"; +import { CollectionDockingView } from "./CollectionDockingView"; +import { CollectionLinearView } from "./collectionLinear"; import "./CollectionMenu.scss"; import { CollectionViewType, COLLECTION_BORDER_WIDTH } from "./CollectionView"; import { TabDocView } from "./TabDocView"; -import { LightboxView } from "../LightboxView"; -import { Docs } from "../../documents/Documents"; -import { DocumentManager } from "../../util/DocumentManager"; -import { CollectionDockingView } from "./CollectionDockingView"; -import { FormattedTextBox } from "../nodes/formattedText/FormattedTextBox"; +import { Colors } from "../global/globalEnums"; + +interface CollectionMenuProps { + panelHeight: () => number; + panelWidth: () => number; +} @observer -export class CollectionMenu extends AntimodeMenu<AntimodeMenuProps> { +export class CollectionMenu extends AntimodeMenu<CollectionMenuProps>{ @observable static Instance: CollectionMenu; @observable SelectedCollection: DocumentView | undefined; @observable FieldKey: string; + private _docBtnRef = React.createRef<HTMLDivElement>(); + constructor(props: any) { super(props); this.FieldKey = ""; @@ -57,7 +67,7 @@ export class CollectionMenu extends AntimodeMenu<AntimodeMenuProps> { componentDidMount() { reaction(() => SelectionManager.Views().length && SelectionManager.Views()[0], - (doc) => doc && this.SetSelection(doc)); + view => view && this.SetSelection(view)); } @action @@ -82,34 +92,92 @@ export class CollectionMenu extends AntimodeMenu<AntimodeMenuProps> { } } + buttonBarXf = () => { + if (!this._docBtnRef.current) return Transform.Identity(); + const { scale, translateX, translateY } = Utils.GetScreenTransform(this._docBtnRef.current); + return new Transform(-translateX, -translateY, 1 / scale); + } + + @computed get contMenuButtons() { + const selDoc = Doc.UserDoc().contextMenuBtns; + return !(selDoc instanceof Doc) ? (null) : <div className="collectionMenu-contMenuButtons" ref={this._docBtnRef} style={{ height: this.props.panelHeight() }} > + <CollectionLinearView + Document={selDoc} + DataDoc={undefined} + fieldKey={"data"} + dropAction={"alias"} + setHeight={returnFalse} + styleProvider={DefaultStyleProvider} + layerProvider={undefined} + rootSelected={returnTrue} + bringToFront={emptyFunction} + select={emptyFunction} + isContentActive={returnTrue} + isAnyChildContentActive={returnFalse} + isSelected={returnFalse} + docViewPath={returnEmptyDoclist} + moveDocument={returnFalse} + CollectionView={undefined} + addDocument={returnFalse} + addDocTab={returnFalse} + pinToPres={emptyFunction} + removeDocument={returnFalse} + ScreenToLocalTransform={this.buttonBarXf} + PanelWidth={this.props.panelWidth} + PanelHeight={this.props.panelHeight} + renderDepth={0} + focus={emptyFunction} + whenChildContentsActiveChanged={emptyFunction} + docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} /> + </div>; + } + render() { - const button = <Tooltip title={<div className="dash-tooltip">Pin Menu</div>} key="pin menu" placement="bottom"> - <button className="antimodeMenu-button" onClick={this.toggleMenuPin} style={{ backgroundColor: "#121721" }}> - <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.Pinned ? 45 : 0}deg)` }} /> - </button> - </Tooltip>; const propIcon = CurrentUserUtils.propertiesWidth > 0 ? "angle-double-right" : "angle-double-left"; const propTitle = CurrentUserUtils.propertiesWidth > 0 ? "Close Properties Panel" : "Open Properties Panel"; const prop = <Tooltip title={<div className="dash-tooltip">{propTitle}</div>} key="properties" placement="bottom"> - <button className="antimodeMenu-button" key="properties" style={{ backgroundColor: "#424242" }} + <div className="collectionMenu-hardCodedButton" + style={{ backgroundColor: CurrentUserUtils.propertiesWidth > 0 ? Colors.MEDIUM_BLUE : undefined }} + key="properties" onPointerDown={this.toggleProperties}> <FontAwesomeIcon icon={propIcon} size="lg" /> - </button> + </div> </Tooltip>; - return this.getElement(!this.SelectedCollection ? [/*button*/] : - [<CollectionViewBaseChrome key="chrome" - docView={this.SelectedCollection} - fieldKey={this.SelectedCollection.LayoutFieldKey} - type={StrCast(this.SelectedCollection?.props.Document._viewType, CollectionViewType.Invalid) as CollectionViewType} />, - prop, - /*button*/]); + // NEW BUTTONS + //dash col linear view buttons + const contMenuButtons = + <div className="collectionMenu-container"> + {this.contMenuButtons} + {prop} + </div>; + + return contMenuButtons; + + // const button = <Tooltip title={<div className="dash-tooltip">Pin Menu</div>} key="pin menu" placement="bottom"> + // <button className="antimodeMenu-button" onClick={this.toggleMenuPin} style={{ backgroundColor: "#121721" }}> + // <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.Pinned ? 45 : 0}deg)` }} /> + // </button> + // </Tooltip>; + + // OLD BUTTONS + // return this.getElement(!this.SelectedCollection ? [/*button*/] : + // [<CollectionViewBaseChrome key="chrome" + // docView={this.SelectedCollection} + // fieldKey={this.SelectedCollection.LayoutFieldKey} + // type={StrCast(this.SelectedCollection?.props.Document._viewType, CollectionViewType.Invalid) as CollectionViewType} />, + // prop, + // /*button*/]); } } -interface CollectionMenuProps { +interface CollectionViewMenuProps { type: CollectionViewType; fieldKey: string; docView: DocumentView; @@ -118,7 +186,7 @@ interface CollectionMenuProps { const stopPropagation = (e: React.SyntheticEvent) => e.stopPropagation(); @observer -export class CollectionViewBaseChrome extends React.Component<CollectionMenuProps> { +export class CollectionViewBaseChrome extends React.Component<CollectionViewMenuProps> { //(!)?\(\(\(doc.(\w+) && \(doc.\w+ as \w+\).includes\(\"(\w+)\"\) get document() { return this.props.docView?.props.Document; } @@ -374,10 +442,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp </div>); } - @computed get selectedDocumentView() { - return SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined; - } - @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } + @computed get selectedDocumentView() { return SelectionManager.Views().lastElement(); } + @computed get selectedDoc() { return SelectionManager.Docs().lastElement(); } @computed get notACollection() { if (this.selectedDoc) { const layoutField = Doc.LayoutField(this.selectedDoc); @@ -485,8 +551,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp @undoBatch onAliasButtonMoved = (e: PointerEvent) => { const contentDiv = this.selectedDocumentView?.ContentDiv; - if (contentDiv) { - const dragData = new DragManager.DocumentDragData([this.selectedDocumentView!.props.Document]); + if (contentDiv && this.selectedDoc) { + const dragData = new DragManager.DocumentDragData([this.selectedDoc]); const offset = [e.clientX - contentDiv.getBoundingClientRect().x, e.clientY - contentDiv.getBoundingClientRect().y]; dragData.defaultDropAction = "alias"; dragData.canEmbed = true; @@ -563,14 +629,14 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp } @observer -export class CollectionDockingChrome extends React.Component<CollectionMenuProps> { +export class CollectionDockingChrome extends React.Component<CollectionViewMenuProps> { render() { return (null); } } @observer -export class CollectionFreeFormViewChrome extends React.Component<CollectionMenuProps & { isOverlay: boolean, isDoc?: boolean }> { +export class CollectionFreeFormViewChrome extends React.Component<CollectionViewMenuProps & { isOverlay: boolean, isDoc?: boolean }> { public static Instance: CollectionFreeFormViewChrome; constructor(props: any) { super(props); @@ -581,11 +647,9 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu return this.document[this.props.docView.LayoutFieldKey + (this.props.isOverlay ? "-annotations" : "")]; } @computed get childDocs() { return DocListCast(this.dataField); } - @computed get selectedDocumentView() { return SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined; } - @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } - @computed get isText() { - return this.selectedDoc?.type === DocumentType.RTF || (RichTextMenu.Instance?.view as any) ? true : false; - } + @computed get selectedDocumentView() { return SelectionManager.Views().lastElement(); } + @computed get selectedDoc() { return SelectionManager.Docs().lastElement(); } + @computed get isText() { return this.selectedDoc?.type === DocumentType.RTF || (RichTextMenu.Instance?.view as any) ? true : false; } @undoBatch @action @@ -614,12 +678,12 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu private _palette = ["#D0021B", "#F5A623", "#F8E71C", "#8B572A", "#7ED321", "#417505", "#9013FE", "#4A90E2", "#50E3C2", "#B8E986", "#000000", "#4A4A4A", "#9B9B9B", "#FFFFFF", ""]; private _width = ["1", "5", "10", "100"]; private _dotsize = [10, 20, 30, 40]; - private _draw = ["∿", "⎯", "→", "↔︎", "ロ", "O"]; - private _head = ["", "", "", "arrow", "", ""]; - private _end = ["", "", "arrow", "arrow", "", ""]; - private _shapePrims = ["", "line", "line", "line", "rectangle", "circle"]; - private _title = ["pen", "line", "line with arrow", "line with double arrows", "square", "circle",]; - private _faName = ["pen-fancy", "minus", "long-arrow-alt-right", "arrows-alt-h", "square", "circle"]; + private _draw = ["∿", "=", "⎯", "→", "↔︎", "ロ", "O"]; + private _head = ["", "", "", "", "arrow", "", ""]; + private _end = ["", "", "", "arrow", "arrow", "", ""]; + private _shapePrims = ["", "", "line", "line", "line", "rectangle", "circle"]; + private _title = ["pen", "highlighter", "line", "line with arrow", "line with double arrows", "square", "circle"]; + private _faName = ["pen-fancy", "highlighter", "minus", "long-arrow-alt-right", "arrows-alt-h", "square", "circle"]; @observable _selectedPrimitive = this._shapePrims.length; @observable _keepPrimitiveMode = false; // for whether primitive selection enters a one-shot or persistent mode @observable _colorBtn = false; @@ -629,6 +693,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu @action clearKeepPrimitiveMode() { this._selectedPrimitive = this._shapePrims.length; } @action primCreated() { if (!this._keepPrimitiveMode) { //get out of ink mode after each stroke= + if (CurrentUserUtils.SelectedTool === InkTool.Highlighter && GestureOverlay.Instance.SavedColor) SetActiveInkColor(GestureOverlay.Instance.SavedColor); CurrentUserUtils.SelectedTool = InkTool.None; this._selectedPrimitive = this._shapePrims.length; SetActiveArrowStart("none"); @@ -669,7 +734,13 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu this._keepPrimitiveMode = keep; if (this._selectedPrimitive !== i) { this._selectedPrimitive = i; - CurrentUserUtils.SelectedTool = InkTool.Pen; + if (this._title[i] === "highlighter") { + CurrentUserUtils.SelectedTool = InkTool.Highlighter; + GestureOverlay.Instance.SavedColor = ActiveInkColor(); + SetActiveInkColor("rgba(245, 230, 95, 0.75)"); + } else { + CurrentUserUtils.SelectedTool = InkTool.Pen; + } SetActiveArrowStart(this._head[i]); SetActiveArrowEnd(this._end[i]); SetActiveBezierApprox("300"); @@ -720,7 +791,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu onPointerDown={action(() => { SetActiveInkWidth(wid); this._widthBtn = false; this.editProperties(wid, "width"); })} style={{ backgroundColor: this._widthBtn ? "121212" : "", zIndex: 1001, fontSize: this._dotsize[i], padding: 0, textAlign: "center" }}> • - </button> + </button> </Tooltip>)} </div>; } @@ -759,7 +830,6 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu </div>; } - @observable viewType = this.selectedDoc?._viewType; render() { return !this.props.docView.layoutDoc ? (null) : @@ -797,7 +867,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu } } @observer -export class CollectionStackingViewChrome extends React.Component<CollectionMenuProps> { +export class CollectionStackingViewChrome extends React.Component<CollectionViewMenuProps> { @observable private _currentKey: string = ""; @observable private suggestions: string[] = []; @@ -918,7 +988,7 @@ export class CollectionStackingViewChrome extends React.Component<CollectionMenu @observer -export class CollectionSchemaViewChrome extends React.Component<CollectionMenuProps> { +export class CollectionSchemaViewChrome extends React.Component<CollectionViewMenuProps> { // private _textwrapAllRows: boolean = Cast(this.document.textwrappedSchemaRows, listSpec("string"), []).length > 0; get document() { return this.props.docView.props.Document; } @@ -966,7 +1036,7 @@ export class CollectionSchemaViewChrome extends React.Component<CollectionMenuPr } @observer -export class CollectionTreeViewChrome extends React.Component<CollectionMenuProps> { +export class CollectionTreeViewChrome extends React.Component<CollectionViewMenuProps> { get document() { return this.props.docView.props.Document; } get sortAscending() { @@ -991,7 +1061,7 @@ export class CollectionTreeViewChrome extends React.Component<CollectionMenuProp <button className="collectionTreeViewChrome-sort" onClick={this.toggleSort}> <div className="collectionTreeViewChrome-sortLabel"> Sort - </div> + </div> <div className="collectionTreeViewChrome-sortIcon" style={{ transform: `rotate(${this.ascending === undefined ? "90" : this.ascending ? "180" : "0"}deg)` }}> <FontAwesomeIcon icon="caret-up" size="2x" color="white" /> </div> @@ -1003,7 +1073,7 @@ export class CollectionTreeViewChrome extends React.Component<CollectionMenuProp // Enter scroll speed for 3D Carousel @observer -export class Collection3DCarouselViewChrome extends React.Component<CollectionMenuProps> { +export class Collection3DCarouselViewChrome extends React.Component<CollectionViewMenuProps> { get document() { return this.props.docView.props.Document; } @computed get scrollSpeed() { return this.document._autoScrollSpeed; @@ -1044,7 +1114,7 @@ export class Collection3DCarouselViewChrome extends React.Component<CollectionMe * Chrome for grid view. */ @observer -export class CollectionGridViewChrome extends React.Component<CollectionMenuProps> { +export class CollectionGridViewChrome extends React.Component<CollectionViewMenuProps> { private clicked: boolean = false; private entered: boolean = false; @@ -1225,3 +1295,4 @@ Scripting.addGlobal(function gotoFrame(doc: any, newFrame: any) { CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0); doc._currentFrame = newFrame === undefined ? 0 : Math.max(0, newFrame); }); + diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx deleted file mode 100644 index 6bdeaf722..000000000 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ /dev/null @@ -1,575 +0,0 @@ -import React = require("react"); -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable, untracked } from "mobx"; -import { observer } from "mobx-react"; -import Measure from "react-measure"; -import { Resize } from "react-table"; -import "react-table/react-table.css"; -import { Doc, Opt } from "../../../fields/Doc"; -import { List } from "../../../fields/List"; -import { listSpec } from "../../../fields/Schema"; -import { PastelSchemaPalette, SchemaHeaderField } from "../../../fields/SchemaHeaderField"; -import { Cast, NumCast } from "../../../fields/Types"; -import { TraceMobx } from "../../../fields/util"; -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/global/globalCssVariables.scss'; -import { SchemaTable } from "../collections/collectionSchema/SchemaTable"; -import { ContextMenu } from "../ContextMenu"; -import { ContextMenuProps } from "../ContextMenuItem"; -import '../DocumentDecorations.scss'; -import { DocumentView } from "../nodes/DocumentView"; -import { DefaultStyleProvider } from "../StyleProvider"; -import "./CollectionSchemaView.scss"; -import { CollectionSubView } from "./CollectionSubView"; -// 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 { - Any, - Number, - String, - Boolean, - Doc, - Image, - List, - Date -} -// this map should be used for keys that should have a const type of value -const columnTypes: Map<string, ColumnType> = new Map([ - ["title", ColumnType.String], - ["x", ColumnType.Number], ["y", ColumnType.Number], ["_width", ColumnType.Number], ["_height", ColumnType.Number], - ["_nativeWidth", ColumnType.Number], ["_nativeHeight", ColumnType.Number], ["isPrototype", ColumnType.Boolean], - ["_curPage", ColumnType.Number], ["_currentTimecode", ColumnType.Number], ["zIndex", ColumnType.Number] -]); - -@observer -export class CollectionSchemaView extends CollectionSubView(doc => doc) { - private _previewCont?: HTMLDivElement; - - @observable _previewDoc: Doc | undefined = undefined; - @observable _focusedTable: Doc = this.props.Document; - @observable _col: any = ""; - @observable _menuWidth = 0; - @observable _headerOpen = false; - @observable _headerIsEditing = false; - @observable _menuHeight = 0; - @observable _pointerX = 0; - @observable _pointerY = 0; - @observable _openTypes: boolean = false; - - @computed get previewWidth() { return () => NumCast(this.props.Document.schemaPreviewWidth); } - @computed get previewHeight() { return () => this.props.PanelHeight() - 2 * this.borderWidth; } - @computed get tableWidth() { return this.props.PanelWidth() - 2 * this.borderWidth - Number(SCHEMA_DIVIDER_WIDTH) - this.previewWidth(); } - @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } - @computed get scale() { return this.props.ScreenToLocalTransform().Scale; } - @computed get columns() { return Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []); } - set columns(columns: SchemaHeaderField[]) { this.props.Document._schemaHeaders = new List<SchemaHeaderField>(columns); } - - @computed get menuCoordinates() { - let searchx = 0; - let searchy = 0; - if (this.props.Document._searchDoc) { - const el = document.getElementsByClassName("collectionSchemaView-searchContainer")[0]; - if (el !== undefined) { - const rect = el.getBoundingClientRect(); - searchx = rect.x; - searchy = rect.y; - } - } - const x = Math.max(0, Math.min(document.body.clientWidth - this._menuWidth, this._pointerX)) - searchx; - const y = Math.max(0, Math.min(document.body.clientHeight - this._menuHeight, this._pointerY)) - searchy; - return this.props.ScreenToLocalTransform().transformPoint(x, y); - } - - get documentKeys() { - const docs = this.childDocs; - 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 - untracked(() => docs.map(doc => Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => keys[key] = false)))); - - this.columns.forEach(key => keys[key.heading] = true); - return Array.from(Object.keys(keys)); - } - - @action setHeaderIsEditing = (isEditing: boolean) => this._headerIsEditing = isEditing; - - @undoBatch - setColumnType = action((columnField: SchemaHeaderField, type: ColumnType): void => { - this._openTypes = false; - if (columnTypes.get(columnField.heading)) return; - - const columns = this.columns; - const index = columns.indexOf(columnField); - if (index > -1) { - columnField.setType(NumCast(type)); - columns[index] = columnField; - this.columns = columns; - } - }); - - @undoBatch - setColumnColor = (columnField: SchemaHeaderField, color: string): void => { - const columns = this.columns; - const index = columns.indexOf(columnField); - if (index > -1) { - columnField.setColor(color); - columns[index] = columnField; - this.columns = columns; // need to set the columns to trigger rerender - } - } - - @undoBatch - @action - setColumnSort = (columnField: SchemaHeaderField, descending: boolean | undefined) => { - const columns = this.columns; - columns.forEach(col => col.setDesc(undefined)); - - const index = columns.findIndex(c => c.heading === columnField.heading); - const column = columns[index]; - column.setDesc(descending); - columns[index] = column; - this.columns = columns; - } - - renderTypes = (col: any) => { - if (columnTypes.get(col.heading)) return (null); - - const type = col.type; - - const anyType = <div className={"columnMenu-option" + (type === ColumnType.Any ? " active" : "")} onClick={() => this.setColumnType(col, ColumnType.Any)}> - <FontAwesomeIcon icon={"align-justify"} size="sm" /> - Any - </div>; - - const numType = <div className={"columnMenu-option" + (type === ColumnType.Number ? " active" : "")} onClick={() => this.setColumnType(col, ColumnType.Number)}> - <FontAwesomeIcon icon={"hashtag"} size="sm" /> - Number - </div>; - - const textType = <div className={"columnMenu-option" + (type === ColumnType.String ? " active" : "")} onClick={() => this.setColumnType(col, ColumnType.String)}> - <FontAwesomeIcon icon={"font"} size="sm" /> - Text - </div>; - - const boolType = <div className={"columnMenu-option" + (type === ColumnType.Boolean ? " active" : "")} onClick={() => this.setColumnType(col, ColumnType.Boolean)}> - <FontAwesomeIcon icon={"check-square"} size="sm" /> - Checkbox - </div>; - - const listType = <div className={"columnMenu-option" + (type === ColumnType.List ? " active" : "")} onClick={() => this.setColumnType(col, ColumnType.List)}> - <FontAwesomeIcon icon={"list-ul"} size="sm" /> - List - </div>; - - const docType = <div className={"columnMenu-option" + (type === ColumnType.Doc ? " active" : "")} onClick={() => this.setColumnType(col, ColumnType.Doc)}> - <FontAwesomeIcon icon={"file"} size="sm" /> - Document - </div>; - - const imageType = <div className={"columnMenu-option" + (type === ColumnType.Image ? " active" : "")} onClick={() => this.setColumnType(col, ColumnType.Image)}> - <FontAwesomeIcon icon={"image"} size="sm" /> - Image - </div>; - - const dateType = <div className={"columnMenu-option" + (type === ColumnType.Date ? " active" : "")} onClick={() => this.setColumnType(col, ColumnType.Date)}> - <FontAwesomeIcon icon={"calendar"} size="sm" /> - Date - </div>; - - - const allColumnTypes = <div className="columnMenu-types"> - {anyType} - {numType} - {textType} - {boolType} - {listType} - {docType} - {imageType} - {dateType} - </div>; - - const justColType = type === ColumnType.Any ? anyType : type === ColumnType.Number ? numType : - type === ColumnType.String ? textType : type === ColumnType.Boolean ? boolType : - type === ColumnType.List ? listType : type === ColumnType.Doc ? docType : - type === ColumnType.Date ? dateType : imageType; - - return ( - <div className="collectionSchema-headerMenu-group" onClick={action(() => this._openTypes = !this._openTypes)}> - <div> - <label style={{ cursor: "pointer" }}>Column type:</label> - <FontAwesomeIcon icon={"caret-down"} size="lg" style={{ float: "right", transform: `rotate(${this._openTypes ? "180deg" : 0})`, transition: "0.2s all ease" }} /> - </div> - {this._openTypes ? allColumnTypes : justColType} - </div > - ); - } - - renderSorting = (col: any) => { - const sort = col.desc; - return ( - <div className="collectionSchema-headerMenu-group"> - <label>Sort by:</label> - <div className="columnMenu-sort"> - <div className={"columnMenu-option" + (sort === true ? " active" : "")} onClick={() => this.setColumnSort(col, true)}> - <FontAwesomeIcon icon="sort-amount-down" size="sm" /> - Sort descending - </div> - <div className={"columnMenu-option" + (sort === false ? " active" : "")} onClick={() => this.setColumnSort(col, false)}> - <FontAwesomeIcon icon="sort-amount-up" size="sm" /> - Sort ascending - </div> - <div className="columnMenu-option" onClick={() => this.setColumnSort(col, undefined)}> - <FontAwesomeIcon icon="times" size="sm" /> - Clear sorting - </div> - </div> - </div> - ); - } - - renderColors = (col: any) => { - const selected = col.color; - - const pink = PastelSchemaPalette.get("pink2"); - const purple = PastelSchemaPalette.get("purple2"); - const blue = PastelSchemaPalette.get("bluegreen1"); - const yellow = PastelSchemaPalette.get("yellow4"); - const red = PastelSchemaPalette.get("red2"); - const gray = "#f1efeb"; - - return ( - <div className="collectionSchema-headerMenu-group"> - <label>Color:</label> - <div className="columnMenu-colors"> - <div className={"columnMenu-colorPicker" + (selected === pink ? " active" : "")} style={{ backgroundColor: pink }} onClick={() => this.setColumnColor(col, pink!)}></div> - <div className={"columnMenu-colorPicker" + (selected === purple ? " active" : "")} style={{ backgroundColor: purple }} onClick={() => this.setColumnColor(col, purple!)}></div> - <div className={"columnMenu-colorPicker" + (selected === blue ? " active" : "")} style={{ backgroundColor: blue }} onClick={() => this.setColumnColor(col, blue!)}></div> - <div className={"columnMenu-colorPicker" + (selected === yellow ? " active" : "")} style={{ backgroundColor: yellow }} onClick={() => this.setColumnColor(col, yellow!)}></div> - <div className={"columnMenu-colorPicker" + (selected === red ? " active" : "")} style={{ backgroundColor: red }} onClick={() => this.setColumnColor(col, red!)}></div> - <div className={"columnMenu-colorPicker" + (selected === gray ? " active" : "")} style={{ backgroundColor: gray }} onClick={() => this.setColumnColor(col, gray)}></div> - </div> - </div> - ); - } - - @undoBatch - @action - changeColumns = (oldKey: string, newKey: string, addNew: boolean, filter?: string) => { - const columns = this.columns; - if (columns === undefined) { - this.columns = new List<SchemaHeaderField>([new SchemaHeaderField(newKey, "f1efeb")]); - } else { - if (addNew) { - columns.push(new SchemaHeaderField(newKey, "f1efeb")); - this.columns = columns; - } else { - const index = columns.map(c => c.heading).indexOf(oldKey); - if (index > -1) { - const column = columns[index]; - column.setHeading(newKey); - columns[index] = column; - this.columns = columns; - if (filter) { - Doc.setDocFilter(this.props.Document, newKey, filter, "match"); - } - else { - this.props.Document._docFilters = undefined; - } - } - } - } - } - - @action - openHeader = (col: any, screenx: number, screeny: number) => { - this._col = col; - this._headerOpen = true; - this._pointerX = screenx; - this._pointerY = screeny; - } - - @action - closeHeader = () => { this._headerOpen = false; } - - @undoBatch - @action - deleteColumn = (key: string) => { - const columns = this.columns; - if (columns === undefined) { - this.columns = new List<SchemaHeaderField>([]); - } else { - const index = columns.map(c => c.heading).indexOf(key); - if (index > -1) { - columns.splice(index, 1); - this.columns = columns; - } - } - this.closeHeader(); - } - - getPreviewTransform = (): Transform => { - return this.props.ScreenToLocalTransform().translate(- this.borderWidth - NumCast(COLLECTION_BORDER_WIDTH) - this.tableWidth, - this.borderWidth); - } - - @action - onHeaderClick = (e: React.PointerEvent) => { - e.stopPropagation(); - } - - @action - onWheel(e: React.WheelEvent) { - const scale = this.props.ScreenToLocalTransform().Scale; - this.props.isContentActive(true) && e.stopPropagation(); - } - - @computed get renderMenuContent() { - TraceMobx(); - return <div className="collectionSchema-header-menuOptions"> - {this.renderTypes(this._col)} - {this.renderColors(this._col)} - <div className="collectionSchema-headerMenu-group"> - <button onClick={() => { this.deleteColumn(this._col.heading); }} - >Delete Column</button> - </div> - </div>; - } - - private createTarget = (ele: HTMLDivElement) => { - this._previewCont = ele; - super.CreateDropTarget(ele); - } - - isFocused = (doc: Doc, outsideReaction: boolean): boolean => this.props.isSelected(outsideReaction) && doc === this._focusedTable; - - @action setFocused = (doc: Doc) => this._focusedTable = doc; - - @action setPreviewDoc = (doc: Opt<Doc>) => { - SelectionManager.SelectSchemaView(this, doc); - this._previewDoc = doc; - } - - //toggles preview side-panel of schema - @action - toggleExpander = () => { - this.props.Document.schemaPreviewWidth = this.previewWidth() === 0 ? Math.min(this.tableWidth / 3, 200) : 0; - } - - onDividerDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, this.onDividerMove, emptyFunction, this.toggleExpander); - } - @action - onDividerMove = (e: PointerEvent, down: number[], delta: number[]) => { - const nativeWidth = this._previewCont!.getBoundingClientRect(); - const minWidth = 40; - const maxWidth = 1000; - const movedWidth = this.props.ScreenToLocalTransform().transformDirection(nativeWidth.right - e.clientX, 0)[0]; - const width = movedWidth < minWidth ? minWidth : movedWidth > maxWidth ? maxWidth : movedWidth; - this.props.Document.schemaPreviewWidth = width; - return false; - } - - onPointerDown = (e: React.PointerEvent): void => { - if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) { - if (this.props.isSelected(true)) e.stopPropagation(); - else this.props.select(false); - } - } - - @computed - get previewDocument(): Doc | undefined { return this._previewDoc; } - - @computed - get dividerDragger() { - return this.previewWidth() === 0 ? (null) : - <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} > - <div className="collectionSchemaView-dividerDragger" /> - </div>; - } - - @computed - get previewPanel() { - return <div ref={this.createTarget} style={{ width: `${this.previewWidth()}px` }}> - {!this.previewDocument ? (null) : - <DocumentView - Document={this.previewDocument} - DataDoc={undefined} - fitContentsToDoc={returnTrue} - freezeDimensions={true} - dontCenter={"y"} - focus={DocUtils.DefaultFocus} - renderDepth={this.props.renderDepth} - rootSelected={this.rootSelected} - PanelWidth={this.previewWidth} - PanelHeight={this.previewHeight} - isContentActive={returnTrue} - isDocumentActive={returnFalse} - ScreenToLocalTransform={this.getPreviewTransform} - docFilters={this.childDocFilters} - docRangeFilters={this.childDocRangeFilters} - searchFilterDocs={this.searchFilterDocs} - styleProvider={DefaultStyleProvider} - layerProvider={undefined} - docViewPath={returnEmptyDoclist} - ContainingCollectionDoc={this.props.CollectionView?.props.Document} - ContainingCollectionView={this.props.CollectionView} - moveDocument={this.props.moveDocument} - addDocument={this.props.addDocument} - removeDocument={this.props.removeDocument} - whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} - addDocTab={this.props.addDocTab} - pinToPres={this.props.pinToPres} - bringToFront={returnFalse} - />} - </div>; - } - - @computed - get schemaTable() { - return <SchemaTable - Document={this.props.Document} - PanelHeight={this.props.PanelHeight} - PanelWidth={this.props.PanelWidth} - childDocs={this.childDocs} - CollectionView={this.props.CollectionView} - ContainingCollectionView={this.props.ContainingCollectionView} - ContainingCollectionDoc={this.props.ContainingCollectionDoc} - fieldKey={this.props.fieldKey} - renderDepth={this.props.renderDepth} - moveDocument={this.props.moveDocument} - ScreenToLocalTransform={this.props.ScreenToLocalTransform} - active={this.props.isContentActive} - onDrop={this.onExternalDrop} - addDocTab={this.props.addDocTab} - pinToPres={this.props.pinToPres} - isSelected={this.props.isSelected} - isFocused={this.isFocused} - setFocused={this.setFocused} - setPreviewDoc={this.setPreviewDoc} - deleteDocument={this.props.removeDocument} - addDocument={this.props.addDocument} - dataDoc={this.props.DataDoc} - columns={this.columns} - documentKeys={this.documentKeys} - headerIsEditing={this._headerIsEditing} - openHeader={this.openHeader} - onClick={this.onTableClick} - onPointerDown={emptyFunction} - onResizedChange={this.onResizedChange} - setColumns={this.setColumns} - reorderColumns={this.reorderColumns} - changeColumns={this.changeColumns} - setHeaderIsEditing={this.setHeaderIsEditing} - changeColumnSort={this.setColumnSort} - />; - } - - @computed - public get schemaToolbar() { - return <div className="collectionSchemaView-toolbar"> - <div className="collectionSchemaView-toolbar-item"> - <div id="preview-schema-checkbox-div"> - <input type="checkbox" key={"Show Preview"} checked={this.previewWidth() !== 0} onChange={this.toggleExpander} /> - Show Preview - </div> - </div> - </div>; - } - - onSpecificMenu = (e: React.MouseEvent) => { - if ((e.target as any)?.className?.includes?.("collectionSchemaView-cell") || (e.target instanceof HTMLSpanElement)) { - const cm = ContextMenu.Instance; - const options = cm.findByDescription("Options..."); - const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : []; - optionItems.push({ description: "remove", event: () => this._previewDoc && this.props.removeDocument?.(this._previewDoc), icon: "trash" }); - !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "compass" }); - cm.displayMenu(e.clientX, e.clientY); - (e.nativeEvent as any).SchemaHandled = true; // not sure why this is needed, but if you right-click quickly on a cell, the Document/Collection contextMenu handlers still fire without this. - e.stopPropagation(); - } - } - - @action - onTableClick = (e: React.MouseEvent): void => { - if (!(e.target as any)?.className?.includes?.("collectionSchemaView-cell") && !(e.target instanceof HTMLSpanElement)) { - this.setPreviewDoc(undefined); - } else { - e.stopPropagation(); - } - this.setFocused(this.props.Document); - this.closeHeader(); - } - - onResizedChange = (newResized: Resize[], event: any) => { - const columns = this.columns; - newResized.forEach(resized => { - const index = columns.findIndex(c => c.heading === resized.id); - const column = columns[index]; - column.setWidth(resized.value); - columns[index] = column; - }); - this.columns = columns; - } - - @action - setColumns = (columns: SchemaHeaderField[]) => this.columns = columns - - @undoBatch - reorderColumns = (toMove: SchemaHeaderField, relativeTo: SchemaHeaderField, before: boolean, columnsValues: SchemaHeaderField[]) => { - const columns = [...columnsValues]; - const oldIndex = columns.indexOf(toMove); - const relIndex = columns.indexOf(relativeTo); - const newIndex = (oldIndex > relIndex && !before) ? relIndex + 1 : (oldIndex < relIndex && before) ? relIndex - 1 : relIndex; - - if (oldIndex === newIndex) return; - - columns.splice(newIndex, 0, columns.splice(oldIndex, 1)[0]); - this.columns = columns; - } - - onZoomMenu = (e: React.WheelEvent) => this.props.isContentActive(true) && e.stopPropagation(); - - render() { - TraceMobx(); - if (!this.props.isContentActive()) setTimeout(() => this.closeHeader(), 0); - const menuContent = this.renderMenuContent; - const menu = <div className="collectionSchema-header-menu" - onWheel={e => this.onZoomMenu(e)} - onPointerDown={e => this.onHeaderClick(e)} - style={{ transform: `translate(${(this.menuCoordinates[0])}px, ${(this.menuCoordinates[1])}px)` }}> - <Measure offset onResize={action((r: any) => { - const dim = this.props.ScreenToLocalTransform().inverse().transformDirection(r.offset.width, r.offset.height); - this._menuWidth = dim[0]; this._menuHeight = dim[1]; - })}> - {({ measureRef }) => <div ref={measureRef}> {menuContent} </div>} - </Measure> - </div>; - return <div className={"collectionSchemaView" + (this.props.Document._searchDoc ? "-searchContainer" : "-container")} - style={{ - overflow: this.props.scrollOverflow === true ? "scroll" : undefined, backgroundColor: "white", - pointerEvents: this.props.Document._searchDoc !== undefined && !this.props.isContentActive() && !SnappingManager.GetIsDragging() ? "none" : undefined, - width: this.props.PanelWidth() || "100%", height: this.props.PanelHeight() || "100%", position: "relative", - }} > - <div className="collectionSchemaView-tableContainer" - style={{ width: `calc(100% - ${this.previewWidth()}px)` }} - onContextMenu={this.onSpecificMenu} - onPointerDown={this.onPointerDown} - onWheel={e => this.props.isContentActive(true) && e.stopPropagation()} - onDrop={e => this.onExternalDrop(e, {})} - ref={this.createTarget}> - {this.schemaTable} - </div> - {this.dividerDragger} - {!this.previewWidth() ? (null) : this.previewPanel} - {this._headerOpen && this.props.isContentActive() ? menu : null} - </div>; - } -}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss index e456c0664..59c21210a 100644 --- a/src/client/views/collections/CollectionStackedTimeline.scss +++ b/src/client/views/collections/CollectionStackedTimeline.scss @@ -1,70 +1,94 @@ +@import "../global/globalCssVariables.scss"; + .collectionStackedTimeline { + position: absolute; + width: 100%; + height: 100%; + z-index: 1000; + overflow: hidden; + top: 0px; + + .collectionStackedTimeline-trim-shade { position: absolute; - width: 100%; height: 100%; - border: gray solid 1px; - border-radius: 3px; - z-index: 1000; - overflow: hidden; - top: 0px; + background-color: $dark-gray; + opacity: 0.3; + } - .collectionStackedTimeline-selector { - position: absolute; - width: 10px; - top: 2.5%; - height: 95%; - background: lightblue; - border-radius: 5px; - opacity: 0.3; - z-index: 500; - border-style: solid; - border-color: darkblue; - border-width: 1px; - } + .collectionStackedTimeline-trim-controls { + height: 100%; + position: absolute; + box-sizing: border-box; + border: 2px solid $medium-blue; + display: flex; + justify-content: space-between; + max-width: 100%; - .collectionStackedTimeline-current { - width: 1px; - height: 100%; - background-color: red; - position: absolute; - top: 0px; - pointer-events: none; + .collectionStackedTimeline-trim-handle { + background-color: $medium-blue; + height: 100%; + width: 5px; + cursor: ew-resize; } + } - .collectionStackedTimeline-marker-timeline { - position: absolute; - top: 2.5%; - height: 95%; - border-radius: 4px; - &:hover { - opacity: 1; - } + .collectionStackedTimeline-selector { + position: absolute; + width: 10px; + top: 2.5%; + height: 95%; + background: $light-blue; + border-radius: 3px; + opacity: 0.3; + z-index: 500; + border-style: solid; + border-color: $medium-blue; + border-width: 1px; + } - .collectionStackedTimeline-left-resizer, - .collectionStackedTimeline-resizer { - background: dimgrey; - position: absolute; - top: 0; - height: 100%; - width: 10px; - pointer-events: all; - cursor: ew-resize; - z-index: 100; - } - .collectionStackedTimeline-resizer { - right: 0; - } - .collectionStackedTimeline-left-resizer { - left: 0; - } + .collectionStackedTimeline-current { + width: 1px; + height: 100%; + background-color: $pink; + position: absolute; + top: 0px; + pointer-events: none; + } + + .collectionStackedTimeline-marker-timeline { + position: absolute; + top: 2.5%; + height: 95%; + border-radius: 4px; + &:hover { + opacity: 1; } - .collectionStackedTimeline-waveform { - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - pointer-events: none; + .collectionStackedTimeline-left-resizer, + .collectionStackedTimeline-resizer { + background: $medium-gray; + position: absolute; + top: 0; + height: 100%; + width: 10px; + pointer-events: all; + cursor: ew-resize; + z-index: 100; + } + .collectionStackedTimeline-resizer { + right: 0; } -}
\ No newline at end of file + .collectionStackedTimeline-left-resizer { + left: 0; + } + } + + .collectionStackedTimeline-waveform { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + pointer-events: none; + } +} diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index a2c95df6e..89da6692a 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -1,5 +1,12 @@ import React = require("react"); -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; +import { + action, + computed, + IReactionDisposer, + observable, + reaction, + runInAction, +} from "mobx"; import { observer } from "mobx-react"; import { computedFn } from "mobx-utils"; import { Doc, DocListCast } from "../../../fields/Doc"; @@ -8,7 +15,16 @@ import { List } from "../../../fields/List"; import { listSpec, makeInterface } from "../../../fields/Schema"; import { ComputedField, ScriptField } from "../../../fields/ScriptField"; import { Cast, NumCast } from "../../../fields/Types"; -import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, StopEvent, returnTrue } from "../../../Utils"; +import { + emptyFunction, + formatTime, + OmitKeys, + returnFalse, + returnOne, + setupMoveUpEvents, + StopEvent, + returnTrue, +} from "../../../Utils"; import { Docs } from "../../documents/Documents"; import { LinkManager } from "../../util/LinkManager"; import { Scripting } from "../../util/Scripting"; @@ -18,9 +34,18 @@ import { undoBatch } from "../../util/UndoManager"; import { AudioWaveform } from "../AudioWaveform"; import { CollectionSubView } from "../collections/CollectionSubView"; import { LightboxView } from "../LightboxView"; -import { DocAfterFocusFunc, DocFocusFunc, DocumentView, DocumentViewProps } from "../nodes/DocumentView"; +import { + DocAfterFocusFunc, + DocFocusFunc, + DocumentView, + DocumentViewProps, +} from "../nodes/DocumentView"; import { LabelBox } from "../nodes/LabelBox"; import "./CollectionStackedTimeline.scss"; +import { Colors } from "../global/globalEnums"; +import { DocumentManager } from "../../util/DocumentManager"; +import { SnappingManager } from "../../util/SnappingManager"; +import { DragManager } from "../../util/DragManager"; type PanZoomDocument = makeInterface<[]>; const PanZoomDocument = makeInterface(); @@ -36,11 +61,21 @@ export type CollectionStackedTimelineProps = { endTag: string; mediaPath: string; dictationKey: string; + trimming: boolean; + trimStart: number; + trimEnd: number; + trimDuration: number; + setStartTrim: (newStart: number) => void; + setEndTrim: (newEnd: number) => void; }; @observer -export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument, CollectionStackedTimelineProps>(PanZoomDocument) { - @observable static SelectingRegion: CollectionStackedTimeline | undefined = undefined; +export class CollectionStackedTimeline extends CollectionSubView< + PanZoomDocument, + CollectionStackedTimelineProps +>(PanZoomDocument) { + @observable static SelectingRegion: CollectionStackedTimeline | undefined = + undefined; static RangeScript: ScriptField; static LabelScript: ScriptField; static RangePlayScript: ScriptField; @@ -50,48 +85,111 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument private _markerStart: number = 0; @observable _markerEnd: number = 0; - get duration() { return this.props.duration; } - @computed get currentTime() { return NumCast(this.layoutDoc._currentTimecode); } + get minLength() { + const rect = this._timeline?.getBoundingClientRect(); + if (rect) { + return 0.05 * this.duration; + } + return 0; + } + + get trimStart() { + return this.props.trimStart; + } + + get trimEnd() { + return this.props.trimEnd; + } + + get duration() { + return this.props.duration; + } + + @computed get currentTime() { + return NumCast(this.layoutDoc._currentTimecode); + } @computed get selectionContainer() { - return CollectionStackedTimeline.SelectingRegion !== this ? (null) : <div className="collectionStackedTimeline-selector" style={{ - left: `${Math.min(NumCast(this._markerStart), NumCast(this._markerEnd)) / this.duration * 100}%`, - width: `${Math.abs(this._markerStart - this._markerEnd) / this.duration * 100}%` - }} />; + return CollectionStackedTimeline.SelectingRegion !== this ? null : ( + <div + className="collectionStackedTimeline-selector" + style={{ + left: `${((Math.min(this._markerStart, this._markerEnd) - this.trimStart) / this.props.trimDuration) * 100}%`, + width: `${(Math.abs(this._markerStart - this._markerEnd) / this.props.trimDuration) * 100}%`, + }} + /> + ); } constructor(props: any) { super(props); // onClick play scripts - CollectionStackedTimeline.RangeScript = CollectionStackedTimeline.RangeScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this, clientX)`, { self: Doc.name, scriptContext: "any", clientX: "number" })!; - CollectionStackedTimeline.RangePlayScript = CollectionStackedTimeline.RangePlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this, clientX)`, { self: Doc.name, scriptContext: "any", clientX: "number" })!; + CollectionStackedTimeline.RangeScript = + CollectionStackedTimeline.RangeScript || + ScriptField.MakeFunction(`scriptContext.clickAnchor(this, clientX)`, { + self: Doc.name, + scriptContext: "any", + clientX: "number", + })!; + CollectionStackedTimeline.RangePlayScript = + CollectionStackedTimeline.RangePlayScript || + ScriptField.MakeFunction(`scriptContext.playOnClick(this, clientX)`, { + self: Doc.name, + scriptContext: "any", + clientX: "number", + })!; } - componentDidMount() { document.addEventListener("keydown", this.keyEvents, true); } + componentDidMount() { + document.addEventListener("keydown", this.keyEvents, true); + } componentWillUnmount() { document.removeEventListener("keydown", this.keyEvents, true); - if (CollectionStackedTimeline.SelectingRegion === this) runInAction(() => CollectionStackedTimeline.SelectingRegion = undefined); + if (CollectionStackedTimeline.SelectingRegion === this) { + runInAction( + () => (CollectionStackedTimeline.SelectingRegion = undefined) + ); + } } - anchorStart = (anchor: Doc) => NumCast(anchor._timecodeToShow, NumCast(anchor[this.props.startTag])); + anchorStart = (anchor: Doc) => + NumCast(anchor._timecodeToShow, NumCast(anchor[this.props.startTag])) anchorEnd = (anchor: Doc, val: any = null) => { const endVal = NumCast(anchor[this.props.endTag], val); - return NumCast(anchor._timecodeToHide, endVal === undefined ? null : endVal); + return NumCast( + anchor._timecodeToHide, + endVal === undefined ? null : endVal + ); } - toTimeline = (screen_delta: number, width: number) => Math.max(0, Math.min(this.duration, screen_delta / width * this.duration)); + toTimeline = (screen_delta: number, width: number) => { + return Math.max( + this.trimStart, + Math.min(this.trimEnd, (screen_delta / width) * this.props.trimDuration + this.trimStart)); + } + rangeClickScript = () => CollectionStackedTimeline.RangeScript; rangePlayScript = () => CollectionStackedTimeline.RangePlayScript; // for creating key anchors with key events @action keyEvents = (e: KeyboardEvent) => { - if (!(e.target instanceof HTMLInputElement) && this.props.isSelected(true)) { + if ( + !(e.target instanceof HTMLInputElement) && + this.props.isSelected(true) + ) { switch (e.key) { case " ": if (!CollectionStackedTimeline.SelectingRegion) { this._markerStart = this._markerEnd = this.currentTime; CollectionStackedTimeline.SelectingRegion = this; } else { - CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this.props.startTag, this.props.endTag, this.currentTime); + CollectionStackedTimeline.createAnchor( + this.rootDoc, + this.dataDoc, + this.props.fieldKey, + this.props.startTag, + this.props.endTag, + this.currentTime + ); CollectionStackedTimeline.SelectingRegion = undefined; } } @@ -101,7 +199,10 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument getLinkData(l: Doc) { let la1 = l.anchor1 as Doc; let la2 = l.anchor2 as Doc; - const linkTime = NumCast(la2[this.props.startTag], NumCast(la1[this.props.startTag])); + const linkTime = NumCast( + la2[this.props.startTag], + NumCast(la1[this.props.startTag]) + ); if (Doc.AreProtosEqual(la1, this.dataDoc)) { la1 = l.anchor2 as Doc; la2 = l.anchor1 as Doc; @@ -118,10 +219,18 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument const wasPlaying = this.props.playing(); if (wasPlaying) this.props.Pause(); const wasSelecting = CollectionStackedTimeline.SelectingRegion === this; - setupMoveUpEvents(this, e, - action(e => { - if (!wasSelecting && CollectionStackedTimeline.SelectingRegion !== this) { - this._markerStart = this._markerEnd = this.toTimeline(clientX - rect.x, rect.width); + setupMoveUpEvents( + this, + e, + action((e) => { + if ( + !wasSelecting && + CollectionStackedTimeline.SelectingRegion !== this + ) { + this._markerStart = this._markerEnd = this.toTimeline( + clientX - rect.x, + rect.width + ); CollectionStackedTimeline.SelectingRegion = this; } this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width); @@ -134,32 +243,148 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument this._markerStart = this._markerEnd; this._markerEnd = tmp; } - if (!isClick && CollectionStackedTimeline.SelectingRegion === this && (Math.abs(movement[0]) > 15)) { - CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this.props.startTag, this.props.endTag, - this._markerStart, this._markerEnd); + if ( + !isClick && + CollectionStackedTimeline.SelectingRegion === this && + Math.abs(movement[0]) > 15 && + !this.props.trimming + ) { + const anchor = CollectionStackedTimeline.createAnchor( + this.rootDoc, + this.dataDoc, + this.props.fieldKey, + this.props.startTag, + this.props.endTag, + this._markerStart, + this._markerEnd + ); + setTimeout(() => DocumentManager.Instance.getDocumentView(anchor)?.select(false)); } - (!isClick || !wasSelecting) && (CollectionStackedTimeline.SelectingRegion = undefined); + (!isClick || !wasSelecting) && + (CollectionStackedTimeline.SelectingRegion = undefined); }), (e, doubleTap) => { this.props.select(false); - e.shiftKey && CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this.props.startTag, this.props.endTag, this.currentTime); + e.shiftKey && + CollectionStackedTimeline.createAnchor( + this.rootDoc, + this.dataDoc, + this.props.fieldKey, + this.props.startTag, + this.props.endTag, + this.currentTime + ); !wasPlaying && doubleTap && this.props.Play(); }, - this.props.isSelected(true) || this.props.isContentActive(), undefined, - () => !wasPlaying && this.props.setTime((clientX - rect.x) / rect.width * this.duration)); + this.props.isSelected(true) || this.props.isContentActive(), + undefined, + () => { + !wasPlaying && + (this.props.trimming && this.duration ? + this.props.setTime(((clientX - rect.x) / rect.width) * this.duration) + : + this.props.setTime(((clientX - rect.x) / rect.width) * this.props.trimDuration + this.trimStart) + ); + } + ); } + + } + + @action + trimLeft = (e: React.PointerEvent): void => { + const rect = this._timeline?.getBoundingClientRect(); + const clientX = e.movementX; + setupMoveUpEvents( + this, + e, + action((e, [], []) => { + if (rect && this.props.isContentActive()) { + this.props.setStartTrim(Math.min( + Math.max( + this.trimStart + (e.movementX / rect.width) * this.duration, + 0 + ), + this.trimEnd - this.minLength + )); + } + return false; + }), + emptyFunction, + action((e, doubleTap) => { + if (doubleTap) { + this.props.setStartTrim(0); + } + }) + ); + } + + @action + trimRight = (e: React.PointerEvent): void => { + const rect = this._timeline?.getBoundingClientRect(); + const clientX = e.movementX; + setupMoveUpEvents( + this, + e, + action((e, [], []) => { + if (rect && this.props.isContentActive()) { + this.props.setEndTrim(Math.max( + Math.min( + this.trimEnd + (e.movementX / rect.width) * this.duration, + this.duration + ), + this.trimStart + this.minLength + )); + } + return false; + }), + emptyFunction, + action((e, doubleTap) => { + if (doubleTap) { + this.props.setEndTrim(this.duration); + } + }) + ); + } + + @action + internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData, xp: number) { + if (!de.embedKey && this.props.layerProvider?.(this.props.Document) !== false && this.props.Document._isGroup) return false; + if (!super.onInternalDrop(e, de)) return false; + + + // determine x coordinate of drop and assign it to the documents being dragged --- see internalDocDrop of collectionFreeFormView.tsx for how it's done when dropping onto a 2D freeform view + + return true; + } + + onInternalDrop = (e: Event, de: DragManager.DropEvent) => { + if (de.complete.docDragData?.droppedDocuments.length) return this.internalDocDrop(e, de, de.complete.docDragData, 0); + return false; } @undoBatch @action - static createAnchor(rootDoc: Doc, dataDoc: Doc, fieldKey: string, startTag: string, endTag: string, anchorStartTime?: number, anchorEndTime?: number) { + static createAnchor( + rootDoc: Doc, + dataDoc: Doc, + fieldKey: string, + startTag: string, + endTag: string, + anchorStartTime?: number, + anchorEndTime?: number, + docAnchor?: Doc + ) { if (anchorStartTime === undefined) return rootDoc; - const anchor = Docs.Create.LabelDocument({ - title: ComputedField.MakeFunction(`"#" + formatToTime(self["${startTag}"]) + "-" + formatToTime(self["${endTag}"])`) as any, + const anchor = docAnchor ?? Docs.Create.LabelDocument({ + title: ComputedField.MakeFunction( + `"#" + formatToTime(self["${startTag}"]) + "-" + formatToTime(self["${endTag}"])` + ) as any, + _stayInCollection: true, useLinkSmallAnchor: true, hideLinkButton: true, annotationOn: rootDoc, - _timelineLabel: true + _timelineLabel: true, }); Doc.GetProto(anchor)[startTag] = anchorStartTime; Doc.GetProto(anchor)[endTag] = anchorEndTime; @@ -179,7 +404,10 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument if (this.props.playing()) this.props.Pause(); else this.props.playFrom(seekTimeInSeconds, endTime); } else { - if (seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) && endTime > NumCast(this.layoutDoc._currentTimecode)) { + if ( + seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) && + endTime > NumCast(this.layoutDoc._currentTimecode) + ) { if (!this.layoutDoc.autoPlayAnchors && this.props.playing()) { this.props.Pause(); } else { @@ -194,39 +422,60 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument @action clickAnchor = (anchorDoc: Doc, clientX: number) => { - if (anchorDoc.isLinkButton) LinkManager.FollowLink(undefined, anchorDoc, this.props, false); + if (anchorDoc.isLinkButton) { + LinkManager.FollowLink(undefined, anchorDoc, this.props, false); + } const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.25; const endTime = this.anchorEnd(anchorDoc); - if (seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) + 1e-4 && endTime > NumCast(this.layoutDoc._currentTimecode) - 1e-4) { + if ( + seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) + 1e-4 && + endTime > NumCast(this.layoutDoc._currentTimecode) - 1e-4 + ) { if (this.props.playing()) this.props.Pause(); else if (this.layoutDoc.autoPlayAnchors) this.props.Play(); else if (!this.layoutDoc.autoPlayAnchors) { const rect = this._timeline?.getBoundingClientRect(); - rect && this.props.setTime(this.toTimeline(clientX - rect.x, rect.width)); + rect && + this.props.setTime(this.toTimeline(clientX - rect.x, rect.width)); } } else { - if (this.layoutDoc.autoPlayAnchors) this.props.playFrom(seekTimeInSeconds, endTime); - else this.props.setTime(seekTimeInSeconds); + if (this.layoutDoc.autoPlayAnchors) { + this.props.playFrom(seekTimeInSeconds, endTime); + } + else { + this.props.setTime(seekTimeInSeconds); + } } return { select: true }; } - // makes sure no anchors overlaps each other by setting the correct position and width - getLevel = (m: Doc, placed: { anchorStartTime: number, anchorEndTime: number, level: number }[]) => { + getLevel = ( + m: Doc, + placed: { anchorStartTime: number; anchorEndTime: number; level: number }[] + ) => { const timelineContentWidth = this.props.PanelWidth(); const x1 = this.anchorStart(m); - const x2 = this.anchorEnd(m, x1 + 10 / timelineContentWidth * this.duration); + const x2 = this.anchorEnd( + m, + x1 + (10 / timelineContentWidth) * this.duration + ); let max = 0; - const overlappedLevels = new Set(placed.map(p => { - const y1 = p.anchorStartTime; - const y2 = p.anchorEndTime; - if ((x1 >= y1 && x1 <= y2) || (x2 >= y1 && x2 <= y2) || - (y1 >= x1 && y1 <= x2) || (y2 >= x1 && y2 <= x2)) { - max = Math.max(max, p.level); - return p.level; - } - })); + const overlappedLevels = new Set( + placed.map((p) => { + const y1 = p.anchorStartTime; + const y2 = p.anchorEndTime; + if ( + (x1 >= y1 && x1 <= y2) || + (x2 >= y1 && x2 <= y2) || + (y1 >= x1 && y1 <= x2) || + (y2 >= x1 && y2 <= x2) + ) { + max = Math.max(max, p.level); + return p.level; + } + }) + ); let level = max + 1; for (let j = max; j >= 0; j--) !overlappedLevels.has(j) && (level = j); @@ -235,82 +484,185 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument } dictationHeightPercent = 50; - dictationHeight = () => this.props.PanelHeight() * (100 - this.dictationHeightPercent) / 100; - timelineContentHeight = () => this.props.PanelHeight() * this.dictationHeightPercent / 100; - dictationScreenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, -this.timelineContentHeight()); + dictationHeight = () => + (this.props.PanelHeight() * (100 - this.dictationHeightPercent)) / 100 + timelineContentHeight = () => + (this.props.PanelHeight() * this.dictationHeightPercent) / 100 + dictationScreenToLocalTransform = () => + this.props + .ScreenToLocalTransform() + .translate(0, -this.timelineContentHeight()) @computed get renderDictation() { const dictation = Cast(this.dataDoc[this.props.dictationKey], Doc, null); - return !dictation ? (null) : <div style={{ position: "absolute", height: "100%", top: this.timelineContentHeight(), background: "tan" }}> - <DocumentView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} - Document={dictation} - PanelHeight={this.dictationHeight} - isAnnotationOverlay={true} - isDocumentActive={returnFalse} - select={emptyFunction} - scaling={returnOne} - xMargin={25} - yMargin={10} - ScreenToLocalTransform={this.dictationScreenToLocalTransform} - whenChildContentsActiveChanged={emptyFunction} - removeDocument={returnFalse} - moveDocument={returnFalse} - addDocument={returnFalse} - CollectionView={undefined} - renderDepth={this.props.renderDepth + 1}> - </DocumentView> - </div>; + return !dictation ? null : ( + <div + style={{ + position: "absolute", + height: "100%", + top: this.timelineContentHeight(), + background: Colors.LIGHT_BLUE, + }} + > + <DocumentView + {...OmitKeys(this.props, [ + "NativeWidth", + "NativeHeight", + "setContentView", + ]).omit} + Document={dictation} + PanelHeight={this.dictationHeight} + isAnnotationOverlay={true} + isDocumentActive={returnFalse} + select={emptyFunction} + scaling={returnOne} + xMargin={25} + yMargin={10} + ScreenToLocalTransform={this.dictationScreenToLocalTransform} + whenChildContentsActiveChanged={emptyFunction} + removeDocument={returnFalse} + moveDocument={returnFalse} + addDocument={returnFalse} + CollectionView={undefined} + renderDepth={this.props.renderDepth + 1} + ></DocumentView> + </div> + ); } @computed get renderAudioWaveform() { - return !this.props.mediaPath ? (null) : - <div className="collectionStackedTimeline-waveform" > + return !this.props.mediaPath ? null : ( + <div className="collectionStackedTimeline-waveform"> <AudioWaveform duration={this.duration} mediaPath={this.props.mediaPath} - dataDoc={this.dataDoc} - PanelHeight={this.timelineContentHeight} /> - </div>; + layoutDoc={this.layoutDoc} + PanelHeight={this.timelineContentHeight} + trimming={this.props.trimming} + /> + </div> + ); } + currentTimecode = () => this.currentTime; render() { const timelineContentWidth = this.props.PanelWidth(); - const overlaps: { anchorStartTime: number, anchorEndTime: number, level: number }[] = []; - const drawAnchors = this.childDocs.map(anchor => ({ level: this.getLevel(anchor, overlaps), anchor })); + const overlaps: { + anchorStartTime: number; + anchorEndTime: number; + level: number; + }[] = []; + const drawAnchors = this.childDocs.map((anchor) => ({ + level: this.getLevel(anchor, overlaps), + anchor, + })); const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2; - const isActive = this.props.isContentActive() || this.props.isSelected(false); - return <div className="collectionStackedTimeline" ref={(timeline: HTMLDivElement | null) => this._timeline = timeline} - onClick={e => isActive && StopEvent(e)} onPointerDown={e => isActive && this.onPointerDownTimeline(e)}> - {drawAnchors.map(d => { - const start = this.anchorStart(d.anchor); - const end = this.anchorEnd(d.anchor, start + 10 / timelineContentWidth * this.duration); - const left = start / this.duration * timelineContentWidth; - const top = d.level / maxLevel * this.timelineContentHeight(); - const timespan = end - start; - return this.props.Document.hideAnchors ? (null) : - <div className={"collectionStackedTimeline-marker-timeline"} key={d.anchor[Id]} - style={{ left, top, width: `${timespan / this.duration * timelineContentWidth}px`, height: `${this.timelineContentHeight() / maxLevel}px` }} - onClick={e => { this.props.playFrom(start, this.anchorEnd(d.anchor)); e.stopPropagation(); }} > - <StackedTimelineAnchor {...this.props} - mark={d.anchor} - rangeClickScript={this.rangeClickScript} - rangePlayScript={this.rangePlayScript} - left={left} - top={top} - width={timelineContentWidth * timespan / this.duration} - height={this.timelineContentHeight() / maxLevel} - toTimeline={this.toTimeline} - layoutDoc={this.layoutDoc} - currentTimecode={this.currentTimecode} - _timeline={this._timeline} - stackedTimeline={this} - /> - </div>; - })} - {this.selectionContainer} - {this.renderAudioWaveform} - {this.renderDictation} - - <div className="collectionStackedTimeline-current" style={{ left: `${this.currentTime / this.duration * 100}%` }} /> - </div>; + const isActive = + this.props.isContentActive() || this.props.isSelected(false); + return (<div ref={this.createDashEventsTarget} style={{ pointerEvents: SnappingManager.GetIsDragging() ? "all" : undefined }}> + <div + className="collectionStackedTimeline" + ref={(timeline: HTMLDivElement | null) => (this._timeline = timeline)} + onClick={(e) => isActive && StopEvent(e)} + onPointerDown={(e) => isActive && this.onPointerDownTimeline(e)} + > + {drawAnchors.map((d) => { + + const start = this.anchorStart(d.anchor); + const end = this.anchorEnd( + d.anchor, + start + (10 / timelineContentWidth) * this.duration + ); + const left = this.props.trimming ? + (start / this.duration) * timelineContentWidth + : (start - this.trimStart) / this.props.trimDuration * timelineContentWidth; + const top = (d.level / maxLevel) * this.timelineContentHeight(); + const timespan = end - start; + const width = (timespan / this.props.trimDuration) * timelineContentWidth; + const height = this.timelineContentHeight() / maxLevel; + return this.props.Document.hideAnchors ? null : ( + <div + className={"collectionStackedTimeline-marker-timeline"} + key={d.anchor[Id]} + style={{ + left, + top, + width: `${width}px`, + height: `${height}px`, + }} + onClick={(e) => { + this.props.playFrom(start, this.anchorEnd(d.anchor)); + e.stopPropagation(); + }} + > + <StackedTimelineAnchor + {...this.props} + mark={d.anchor} + rangeClickScript={this.rangeClickScript} + rangePlayScript={this.rangePlayScript} + left={left} + top={top} + width={width} + height={height} + toTimeline={this.toTimeline} + layoutDoc={this.layoutDoc} + currentTimecode={this.currentTimecode} + _timeline={this._timeline} + stackedTimeline={this} + trimStart={this.trimStart} + trimEnd={this.trimEnd} + /> + </div> + ); + })} + {!this.props.trimming && this.selectionContainer} + {this.renderAudioWaveform} + {this.renderDictation} + + <div + className="collectionStackedTimeline-current" + style={{ + left: this.props.trimming + ? `${(this.currentTime / this.duration) * 100}%` + : `${(this.currentTime - this.trimStart) / (this.trimEnd - this.trimStart) * 100}%`, + }} + /> + + {this.props.trimming && ( + <> + <div + className="collectionStackedTimeline-trim-shade" + style={{ width: `${(this.trimStart / this.duration) * 100}%` }} + ></div> + + <div + className="collectionStackedTimeline-trim-controls" + style={{ + left: `${(this.trimStart / this.duration) * 100}%`, + width: `${((this.trimEnd - this.trimStart) / this.duration) * 100 + }%`, + }} + > + <div + className="collectionStackedTimeline-trim-handle" + onPointerDown={this.trimLeft} + ></div> + <div + className="collectionStackedTimeline-trim-handle" + onPointerDown={this.trimRight} + ></div> + </div> + + <div + className="collectionStackedTimeline-trim-shade" + style={{ + left: `${(this.trimEnd / this.duration) * 100}%`, + width: `${((this.duration - this.trimEnd) / this.duration) * 100 + }%`, + }} + ></div> + </> + )} + </div> + </div>); } } @@ -335,6 +687,8 @@ interface StackedTimelineAnchorProps { currentTimecode: () => number; isSelected: (outsideReaction?: boolean) => boolean; stackedTimeline: CollectionStackedTimeline; + trimStart: number; + trimEnd: number; } @observer class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> { @@ -345,22 +699,41 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> this._lastTimecode = this.props.currentTimecode(); } componentDidMount() { - this._disposer = reaction(() => this.props.currentTimecode(), + this._disposer = reaction( + () => this.props.currentTimecode(), (time) => { - const dictationDoc = Cast(this.props.layoutDoc["data-dictation"], Doc, null); - const isDictation = dictationDoc && DocListCast(this.props.mark.links).some(link => Cast(link.anchor1, Doc, null)?.annotationOn === dictationDoc); - if (!LightboxView.LightboxDoc + const dictationDoc = Cast( + this.props.layoutDoc["data-dictation"], + Doc, + null + ); + const isDictation = + dictationDoc && + DocListCast(this.props.mark.links).some( + (link) => + Cast(link.anchor1, Doc, null)?.annotationOn === dictationDoc + ); + if ( + !LightboxView.LightboxDoc && // bcz: when should links be followed? we don't want to move away from the video to follow a link but we can open it in a sidebar/etc. But we don't know that upfront. // for now, we won't follow any links when the lightbox is oepn to avoid "losing" the video. /*(isDictation || !Doc.AreProtosEqual(LightboxView.LightboxDoc, this.props.layoutDoc))*/ - && DocListCast(this.props.mark.links).length && + DocListCast(this.props.mark.links).length && time > NumCast(this.props.mark[this.props.startTag]) && time < NumCast(this.props.mark[this.props.endTag]) && - this._lastTimecode < NumCast(this.props.mark[this.props.startTag])) { - LinkManager.FollowLink(undefined, this.props.mark, this.props as any as DocumentViewProps, false, true); + this._lastTimecode < NumCast(this.props.mark[this.props.startTag]) + ) { + LinkManager.FollowLink( + undefined, + this.props.mark, + this.props as any as DocumentViewProps, + false, + true + ); } this._lastTimecode = time; - }); + } + ); } componentWillUnmount() { this._disposer?.(); @@ -373,57 +746,136 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> return this.props.toTimeline(e.clientX - rect.x, rect.width); }; const changeAnchor = (anchor: Doc, left: boolean, time: number) => { - const timelineOnly = Cast(anchor[this.props.startTag], "number", null) !== undefined; - if (timelineOnly) Doc.SetInPlace(anchor, left ? this.props.startTag : this.props.endTag, time, true); - else left ? anchor._timecodeToShow = time : anchor._timecodeToHide = time; + const timelineOnly = + Cast(anchor[this.props.startTag], "number", null) !== undefined; + if (timelineOnly) { + Doc.SetInPlace( + anchor, + left ? this.props.startTag : this.props.endTag, + time, + true + ); + } + else { + left + ? (anchor._timecodeToShow = time) + : (anchor._timecodeToHide = time); + } return false; }; - setupMoveUpEvents(this, e, + setupMoveUpEvents( + this, + e, (e) => changeAnchor(anchor, left, newTime(e)), (e) => { this.props.setTime(newTime(e)); this.props._timeline?.releasePointerCapture(e.pointerId); }, - emptyFunction); + emptyFunction + ); } - renderInner = computedFn(function (this: StackedTimelineAnchor, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) { + + @action + computeTitle = () => { + const start = Math.max(NumCast(this.props.mark[this.props.startTag]), this.props.trimStart) - this.props.trimStart; + const end = Math.min(NumCast(this.props.mark[this.props.endTag]), this.props.trimEnd) - this.props.trimStart; + return `#${formatTime(start)}-${formatTime(end)}`; + } + + renderInner = computedFn(function ( + this: StackedTimelineAnchor, + mark: Doc, + script: undefined | (() => ScriptField), + doublescript: undefined | (() => ScriptField), + x: number, + y: number, + width: number, + height: number + ) { const anchor = observable({ view: undefined as any }); - const focusFunc = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, docTransform?: Transform) => { + const focusFunc = ( + doc: Doc, + willZoom?: boolean, + scale?: number, + afterFocus?: DocAfterFocusFunc, + docTransform?: Transform + ) => { this.props.playLink(mark); this.props.focus(doc, { willZoom, scale, afterFocus, docTransform }); }; return { - anchor, view: <DocumentView key="view" {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} - ref={action((r: DocumentView | null) => anchor.view = r)} - Document={mark} - DataDoc={undefined} - renderDepth={this.props.renderDepth + 1} - LayoutTemplate={undefined} - LayoutTemplateString={LabelBox.LayoutString("data")} - isDocumentActive={returnFalse} - PanelWidth={() => width} - PanelHeight={() => height} - ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(-x, -y)} - focus={focusFunc} - rootSelected={returnFalse} - onClick={script} - onDoubleClick={this.props.layoutDoc.autoPlayAnchors ? undefined : doublescript} - ignoreAutoHeight={false} - hideResizeHandles={true} - bringToFront={emptyFunction} - scriptContext={this.props.stackedTimeline} /> + anchor, + view: ( + <DocumentView + key="view" + {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} + ref={action((r: DocumentView | null) => (anchor.view = r))} + Document={mark} + DataDoc={undefined} + renderDepth={this.props.renderDepth + 1} + LayoutTemplate={undefined} + LayoutTemplateString={LabelBox.LayoutStringWithTitle(LabelBox, "data", this.computeTitle())} + isDocumentActive={returnFalse} + PanelWidth={() => width} + PanelHeight={() => height} + ScreenToLocalTransform={() => + this.props.ScreenToLocalTransform().translate(-x, -y) + } + focus={focusFunc} + rootSelected={returnFalse} + onClick={script} + onDoubleClick={ + this.props.layoutDoc.autoPlayAnchors ? undefined : doublescript + } + ignoreAutoHeight={false} + hideResizeHandles={true} + bringToFront={emptyFunction} + scriptContext={this.props.stackedTimeline} + /> + ), }; }); + render() { - const inner = this.renderInner(this.props.mark, this.props.rangeClickScript, this.props.rangePlayScript, this.props.left, this.props.top, this.props.width, this.props.height); - return <> - {inner.view} - {!inner.anchor.view || !SelectionManager.IsSelected(inner.anchor.view) ? (null) : - <> - <div key="left" className="collectionStackedTimeline-left-resizer" onPointerDown={e => this.onAnchorDown(e, this.props.mark, true)} /> - <div key="right" className="collectionStackedTimeline-resizer" onPointerDown={e => this.onAnchorDown(e, this.props.mark, false)} /> - </>} - </>; + const inner = this.renderInner( + this.props.mark, + this.props.rangeClickScript, + this.props.rangePlayScript, + this.props.left, + this.props.top, + this.props.width, + this.props.height + ); + return ( + <> + {inner.view} + {!inner.anchor.view || + !SelectionManager.IsSelected(inner.anchor.view) ? null : ( + <> + <div + key="left" + className="collectionStackedTimeline-left-resizer" + onPointerDown={(e) => this.onAnchorDown(e, this.props.mark, true)} + /> + <div + key="right" + className="collectionStackedTimeline-resizer" + onPointerDown={(e) => + this.onAnchorDown(e, this.props.mark, false) + } + /> + </> + )} + </> + ); } } -Scripting.addGlobal(function formatToTime(time: number): any { return formatTime(time); });
\ No newline at end of file +Scripting.addGlobal(function formatToTime(time: number): any { + return formatTime(time); +}); +Scripting.addGlobal(function min(num1: number, num2: number): number { + return Math.min(num1, num2); +}); +Scripting.addGlobal(function max(num1: number, num2: number): number { + return Math.max(num1, num2); +});
\ No newline at end of file diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index 4b123c8b6..2f002736d 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -14,6 +14,30 @@ width: 100%; } +// TODO:glr Turn this into a seperate class +.documentButtonMenu { + position: relative; + height: fit-content; + border-bottom: $standard-border; + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; + align-content: center; + padding: 5px 0 5px 0; + + .documentExplanation { + width: 90%; + margin: 5px; + font-size: 11px; + background-color: $light-blue; + color: $medium-blue; + padding: 10px; + border-radius: 10px; + border: solid 2px $medium-blue; + } +} + .collectionStackingView, .collectionMasonryView { height: 100%; diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 4fedda1bd..648ff5087 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -3,7 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { CursorProperty } from "csstype"; import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { DataSym, Doc, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; +import { DataSym, Doc, HeightSym, Opt, WidthSym, DocListCast } from "../../../fields/Doc"; import { collectionSchema, documentSchema } from "../../../fields/documentSchemas"; import { Id } from "../../../fields/FieldSymbols"; import { List } from "../../../fields/List"; @@ -11,7 +11,7 @@ import { listSpec, makeInterface } from "../../../fields/Schema"; import { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from "../../../fields/Types"; import { TraceMobx } from "../../../fields/util"; -import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, Utils } from "../../../Utils"; +import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, Utils, returnTrue, returnEmptyDoclist, returnEmptyFilter } from "../../../Utils"; import { DocUtils, Docs } from "../../documents/Documents"; import { DragManager, dropActionType } from "../../util/DragManager"; import { SnappingManager } from "../../util/SnappingManager"; @@ -23,12 +23,14 @@ import { EditableView } from "../EditableView"; import { LightboxView } from "../LightboxView"; import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment } from "../nodes/DocumentView"; -import { StyleProp } from "../StyleProvider"; +import { StyleProp, DefaultStyleProvider } from "../StyleProvider"; import { CollectionMasonryViewFieldRow } from "./CollectionMasonryViewFieldRow"; import "./CollectionStackingView.scss"; import { CollectionStackingViewFieldColumn } from "./CollectionStackingViewFieldColumn"; import { CollectionSubView } from "./CollectionSubView"; import { CollectionViewType } from "./CollectionView"; +import { FontIconBox } from "../nodes/button/FontIconBox"; +import { CurrentUserUtils } from "../../util/CurrentUserUtils"; const _global = (window /* browser */ || global /* node */) as any; type StackingDocument = makeInterface<[typeof collectionSchema, typeof documentSchema]>; @@ -234,6 +236,7 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, dontCenter={this.props.childIgnoreNativeSize ? "xy" : undefined} dontRegisterView={dataDoc ? true : BoolCast(this.layoutDoc.childDontRegisterViews, this.props.dontRegisterView)} rootSelected={this.rootSelected} + showTitle={this.props.childShowTitle} dropAction={StrCast(this.layoutDoc.childDropAction) as dropActionType} onClick={this.onChildClickHandler} onDoubleClick={this.onChildDoubleClickHandler} @@ -514,6 +517,47 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, return sections.map((section, i) => this.isStackingView ? this.sectionStacking(section[0], section[1]) : this.sectionMasonry(section[0], section[1], i === 0)); } + @computed get buttonMenu() { + const menuDoc: Doc = Cast(this.rootDoc.buttonMenuDoc, Doc, null); + // TODO:glr Allow support for multiple buttons + if (menuDoc) { + const width: number = NumCast(menuDoc._width, 30); + const height: number = NumCast(menuDoc._height, 30); + console.log(menuDoc.title, width, height); + return (<div className="buttonMenu-docBtn" + style={{ width: width, height: height }}> + <DocumentView + Document={menuDoc} + DataDoc={menuDoc} + isContentActive={this.props.isContentActive} + isDocumentActive={returnTrue} + addDocument={this.props.addDocument} + moveDocument={this.props.moveDocument} + addDocTab={this.props.addDocTab} + pinToPres={emptyFunction} + rootSelected={this.props.isSelected} + removeDocument={this.props.removeDocument} + ScreenToLocalTransform={Transform.Identity} + PanelWidth={() => 35} + PanelHeight={() => 35} + renderDepth={this.props.renderDepth} + focus={emptyFunction} + styleProvider={this.props.styleProvider} + layerProvider={this.props.layerProvider} + docViewPath={returnEmptyDoclist} + whenChildContentsActiveChanged={emptyFunction} + bringToFront={emptyFunction} + docFilters={this.props.docFilters} + docRangeFilters={this.props.docRangeFilters} + searchFilterDocs={this.props.searchFilterDocs} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + /> + </div> + ); + } + } + @computed get nativeWidth() { return this.props.NativeWidth?.() ?? Doc.NativeWidth(this.layoutDoc); } @computed get nativeHeight() { return this.props.NativeHeight?.() ?? Doc.NativeHeight(this.layoutDoc); } @@ -529,34 +573,49 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, SetValue: this.addGroup, contents: "+ ADD A GROUP" }; + const buttonMenu = this.rootDoc.buttonMenu; + const noviceExplainer = this.rootDoc.explainer; return ( - <div className="collectionStackingMasonry-cont" > - <div className={this.isStackingView ? "collectionStackingView" : "collectionMasonryView"} - ref={this.createRef} - style={{ - overflowY: this.props.isContentActive() ? "auto" : "hidden", - background: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor), - pointerEvents: this.backgroundEvents ? "all" : undefined - }} - onScroll={action(e => this._scroll = e.currentTarget.scrollTop)} - onDrop={this.onExternalDrop.bind(this)} - onContextMenu={this.onContextMenu} - onWheel={e => this.props.isContentActive(true) && e.stopPropagation()} > - {this.renderedSections} - {!this.showAddAGroup ? (null) : - <div key={`${this.props.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton" - style={{ width: !this.isStackingView ? "100%" : this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}> - <EditableView {...editableViewProps} /> - </div>} - {/* {this.chromeHidden || !this.props.isSelected() ? (null) : - <Switch - onChange={this.onToggle} - onClick={this.onToggle} - defaultChecked={true} - checkedChildren="edit" - unCheckedChildren="view" - />} */} - </div> </div> + <> + {buttonMenu || noviceExplainer ? <div className="documentButtonMenu"> + {buttonMenu ? this.buttonMenu : null} + {Doc.UserDoc().noviceMode && noviceExplainer ? + <div className="documentExplanation"> + {noviceExplainer} + </div> + : null + } + </div> : null} + <div className="collectionStackingMasonry-cont" > + <div className={this.isStackingView ? "collectionStackingView" : "collectionMasonryView"} + ref={this.createRef} + style={{ + overflowY: this.props.isContentActive() ? "auto" : "hidden", + background: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor), + pointerEvents: this.backgroundEvents ? "all" : undefined + }} + onScroll={action(e => this._scroll = e.currentTarget.scrollTop)} + onDrop={this.onExternalDrop.bind(this)} + onContextMenu={this.onContextMenu} + onWheel={e => this.props.isContentActive(true) && e.stopPropagation()} > + {this.renderedSections} + {!this.showAddAGroup ? (null) : + <div key={`${this.props.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton" + style={{ width: !this.isStackingView ? "100%" : this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}> + <EditableView {...editableViewProps} /> + </div>} + {/* {this.chromeHidden || !this.props.isSelected() ? (null) : + <Switch + onChange={this.onToggle} + onClick={this.onToggle} + defaultChecked={true} + checkedChildren="edit" + unCheckedChildren="view" + />} */} + </div> + </div> + </> + ); } } diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index 47733994b..58289a161 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -111,7 +111,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @action pointerEntered = () => SnappingManager.GetIsDragging() && (this._background = "#b4b4b4"); @action pointerLeave = () => this._background = "inherit"; - textCallback = (char: string) => this.addNewTextDoc("", false, true); + textCallback = (char: string) => this.addNewTextDoc("-typed text-", false, true); @action addNewTextDoc = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => { diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index b70df93da..e662f3ddf 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -216,7 +216,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: const targetDocments = DocListCast(this.dataDoc[this.props.fieldKey]); const someMoved = !docDragData.userDropAction && docDragData.draggedDocuments.some(drag => targetDocments.includes(drag)); if (someMoved) docDragData.droppedDocuments = docDragData.droppedDocuments.map((drop, i) => targetDocments.includes(docDragData.draggedDocuments[i]) ? docDragData.draggedDocuments[i] : drop); - if ((!dropAction || dropAction === "move" || someMoved) && docDragData.moveDocument) { + if ((!dropAction || dropAction === "same" || dropAction === "move" || someMoved) && docDragData.moveDocument) { const movedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] === d); const addedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] !== d); if (movedDocs.length) { @@ -321,12 +321,8 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: } }); } else { - let srcUrl: string | undefined; - let srcWeb: Doc | undefined; - if (SelectionManager.Views().length) { - srcWeb = SelectionManager.Views()[0].props.Document; - srcUrl = (srcWeb.data as WebField).url?.href?.match(/http[s]?:\/\/[^/]*/)?.[0]; - } + const srcWeb = SelectionManager.Docs().lastElement(); + const srcUrl = (srcWeb?.data as WebField).url?.href?.match(/http[s]?:\/\/[^/]*/)?.[0]; const reg = new RegExp(Utils.prepend(""), "g"); const modHtml = srcUrl ? html.replace(reg, srcUrl) : html; const htmlDoc = Docs.Create.HtmlDocument(modHtml, { ...options, title: "-web page-", _width: 300, _height: 300 }); @@ -360,7 +356,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: const batch = UndoManager.StartBatch("youtube upload"); const generatedDocuments: Doc[] = []; - this.slowLoadDocuments((uriList || text).split("v=")[1], options, generatedDocuments, text, completed, e.clientX, e.clientY, addDocument).then(batch.end); + this.slowLoadDocuments((uriList || text).split("v=")[1].split("&")[0], options, generatedDocuments, text, completed, e.clientX, e.clientY, addDocument).then(batch.end); return; } diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 89d42439e..3852987b9 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -8,7 +8,7 @@ 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 { returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, emptyFunction } from '../../../Utils'; import { DocUtils } from '../../documents/Documents'; import { CurrentUserUtils } from '../../util/CurrentUserUtils'; import { DocumentManager } from '../../util/DocumentManager'; @@ -26,6 +26,7 @@ import { CollectionSubView } from "./CollectionSubView"; import "./CollectionTreeView.scss"; import { TreeView } from "./TreeView"; import React = require("react"); +import { Transform } from '../../util/Transform'; const _global = (window /* browser */ || global /* node */) as any; export type collectionTreeViewProps = { @@ -107,7 +108,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => { const dragData = de.complete.docDragData; if (dragData) { - const isInTree = () => dragData.draggedDocuments.some(d => d.context === this.doc && this.childDocs.includes(d)); + const isInTree = () => Doc.AreProtosEqual(dragData.treeViewDoc, this.props.Document) || dragData.draggedDocuments.some(d => d.context === this.doc && this.childDocs.includes(d)); dragData.dropAction = targetAction && !isInTree() ? targetAction : this.doc === dragData?.treeViewDoc ? "same" : dragData.dropAction; } } @@ -221,8 +222,9 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll } 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 })); + const customFilters = Cast(this.doc.childContextMenuFilters, listSpec(ScriptField), []); + const icons = StrListCast(this.doc.childContextMenuIcons); + return StrListCast(this.doc.childContextMenuLabels).map((label, i) => ({ script: customScripts[i], filter: customFilters[i], icon: icons[i], label })); } @computed get treeViewElements() { TraceMobx(); @@ -265,13 +267,50 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll return hideTitle ? (null) : (this.outlineMode ? this.documentTitle : this.editableTitle)(this.treeChildren); } - @computed get renderClearButton() { - return !this.doc.treeViewShowClearButton ? (null) : <div key="toolbar"> - <button className="toolbar-button round-button" title="Empty" onClick={undoBatch(action(() => Doc.GetProto(this.doc)[this.props.fieldKey] = undefined))}> - <FontAwesomeIcon icon={"trash"} size="sm" /> - </button> - </div >; + return35 = () => 35; + @computed get buttonMenu() { + const menuDoc: Doc = Cast(this.rootDoc.buttonMenuDoc, Doc, null); + // To create a multibutton menu add a CollectionLinearView + if (menuDoc) { + + const width: number = NumCast(menuDoc._width, 30); + const height: number = NumCast(menuDoc._height, 30); + console.log(menuDoc.title, width, height); + return (<div className="buttonMenu-docBtn" + style={{ width: width, height: height }}> + <DocumentView + Document={menuDoc} + DataDoc={menuDoc} + isContentActive={this.props.isContentActive} + isDocumentActive={returnTrue} + addDocument={this.props.addDocument} + moveDocument={this.props.moveDocument} + addDocTab={this.props.addDocTab} + pinToPres={emptyFunction} + rootSelected={this.props.isSelected} + removeDocument={this.props.removeDocument} + ScreenToLocalTransform={Transform.Identity} + PanelWidth={this.return35} + PanelHeight={this.return35} + renderDepth={this.props.renderDepth + 1} + focus={emptyFunction} + styleProvider={this.props.styleProvider} + layerProvider={this.props.layerProvider} + docViewPath={returnEmptyDoclist} + whenChildContentsActiveChanged={emptyFunction} + bringToFront={emptyFunction} + docFilters={this.props.docFilters} + docRangeFilters={this.props.docRangeFilters} + searchFilterDocs={this.props.searchFilterDocs} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + /> + </div>); + } } + + @observable _explainerHeight: number = 0; + @computed get nativeWidth() { return Doc.NativeWidth(this.Document, undefined, true); } @computed get nativeHeight() { return Doc.NativeHeight(this.Document, undefined, true); } @@ -295,22 +334,34 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll TraceMobx(); const background = () => this.props.styleProvider?.(this.doc, this.props, StyleProp.BackgroundColor); const pointerEvents = () => !this.props.isContentActive() && !SnappingManager.GetIsDragging() ? "none" : undefined; + const buttonMenu = this.rootDoc.buttonMenu; + const noviceExplainer = this.rootDoc.explainer; return !(this.doc instanceof Doc) || !this.treeChildren ? (null) : - <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`, 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${this.outlineMode ? "-outline" : ""}`} > - {this.treeViewElements} - </ul> - </div > - </div>; + <> + {this.titleBar} + <div className="collectionTreeView-container" + style={this.outlineMode ? { transform: `scale(${this.contentScaling})`, width: `calc(${100 / this.contentScaling}%)` } : {}} + onContextMenu={this.onContextMenu}> + {buttonMenu || noviceExplainer ? <div className="documentButtonMenu" ref={action((r: HTMLDivElement) => r && (this._explainerHeight = r.getBoundingClientRect().height))}> + {buttonMenu ? this.buttonMenu : null} + {Doc.UserDoc().noviceMode && noviceExplainer ? + <div className="documentExplanation"> + {noviceExplainer} + </div> + : null + } + </div> : null} + <div className="collectionTreeView-dropTarget" + style={{ background: background(), height: `calc(100% - ${this._explainerHeight}px)`, 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}> + <ul className={`no-indent${this.outlineMode ? "-outline" : ""}`} > + {this.treeViewElements} + </ul> + </div > + </div> + </>; } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 229e93b9e..38e027fb3 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -24,7 +24,7 @@ import { CollectionCarouselView } from './CollectionCarouselView'; import { CollectionDockingView } from "./CollectionDockingView"; import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; import { CollectionGridView } from './collectionGrid/CollectionGridView'; -import { CollectionLinearView } from './CollectionLinearView'; +import { CollectionLinearView } from './collectionLinear'; import CollectionMapView from './CollectionMapView'; import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView'; import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultirowView'; @@ -71,6 +71,7 @@ export interface CollectionViewProps extends FieldViewProps { childDocuments?: Doc[]; // used to override the documents shown by the sub collection to an explicit list (see LinkBox) 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; + childShowTitle?: () => string; childOpacity?: () => number; childContextMenuItems?: () => { script: ScriptField, label: string }[]; childHideTitle?: () => boolean; // whether to hide the documentdecorations title for children @@ -165,7 +166,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab !Doc.UserDoc().noviceMode && subItems.push({ description: "Map", event: () => func(CollectionViewType.Map), icon: "globe-americas" }); subItems.push({ description: "Grid", event: () => func(CollectionViewType.Grid), icon: "th-list" }); - if (!Doc.IsSystem(this.rootDoc) && !this.rootDoc.annotationOn) { + if (!Doc.IsSystem(this.rootDoc) && !this.rootDoc.isGroup && !this.rootDoc.annotationOn) { const existingVm = ContextMenu.Instance.findByDescription(category); const catItems = existingVm && "subitems" in existingVm ? existingVm.subitems : []; catItems.push({ description: "Add a Perspective...", addDivider: true, noexpand: true, subitems: subItems, icon: "eye" }); @@ -245,12 +246,6 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab childLayoutTemplate = () => this.props.childLayoutTemplate?.() || Cast(this.rootDoc.childLayoutTemplate, Doc, null); @computed get childLayoutString() { return StrCast(this.rootDoc.childLayoutString); } - /** - * 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) && this._subView?.IsFiltered(); - } @observable _subView: any = undefined; @@ -279,12 +274,6 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab style={{ pointerEvents: this.props.layerProvider?.(this.rootDoc) === false ? "none" : undefined }}> {this.showIsTagged()} {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: this.showFilterIcon === "hasFilter" ? '#18c718bd' : "orange", zIndex: 1 }} - onPointerDown={action(e => { this.props.select(false); CurrentUserUtils.propertiesWidth = 250; e.stopPropagation(); })} - /> - : (null)} </div>); } } diff --git a/src/client/views/collections/TabDocView.scss b/src/client/views/collections/TabDocView.scss index a963f1cb9..7f62ecaa0 100644 --- a/src/client/views/collections/TabDocView.scss +++ b/src/client/views/collections/TabDocView.scss @@ -1,3 +1,6 @@ +@import "../global/globalCssVariables.scss"; + + input.lm_title:focus, input.lm_title { max-width: unset !important; @@ -57,12 +60,11 @@ input.lm_title { } } -.miniMap-hidden, .miniMap { position: absolute; overflow: hidden; - right: 10; - bottom: 10; + right: 15; + bottom: 15; border: solid 1px; box-shadow: black 0.4vw 0.4vw 0.8vw; width: 100%; @@ -82,17 +84,21 @@ input.lm_title { } .miniMap-hidden { + cursor: pointer; position: absolute; - bottom: 0; - right: 0; - width: 45px; - height: 45px; - transform: translate(20px, 20px) rotate(45deg); - border-radius: 30px; + bottom: 5; + display: flex; + right: 5; + width: 25px; + height: 25px; + border-radius: 3px; padding: 2px; + justify-content: center; + align-items: center; + align-content: center; + background-color: $light-gray; - >svg { - margin-top: 3px; - transform: translate(0px, 7px); + &:hover { + box-shadow: none; } }
\ No newline at end of file diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index 117dba4de..eb95bb913 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -22,6 +22,7 @@ import { SelectionManager } from '../../util/SelectionManager'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; import { undoBatch, UndoManager } from "../../util/UndoManager"; +import { Colors, Shadows } from '../global/globalEnums'; import { LightboxView } from '../LightboxView'; import { DocFocusOptions, DocumentView, DocumentViewProps } from "../nodes/DocumentView"; import { PinProps, PresBox, PresMovement } from '../nodes/trails'; @@ -32,7 +33,6 @@ import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormV 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 { @@ -123,6 +123,8 @@ export class TabDocView extends React.Component<TabDocViewProps> { tab.element[0].prepend(iconWrap); tab._disposers.layerDisposer = reaction(() => ({ layer: tab.DashDoc.activeLayer, color: this.tabColor }), ({ layer, color }) => { + // console.log("TabDocView: " + this.tabColor); + // console.log("lightOrDark: " + lightOrDark(this.tabColor)); const textColor = lightOrDark(this.tabColor); //not working with StyleProp.Color titleEle.style.color = textColor; titleEle.style.backgroundColor = "transparent"; @@ -131,12 +133,6 @@ export class TabDocView extends React.Component<TabDocViewProps> { 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) => { @@ -390,8 +386,16 @@ export class TabDocView extends React.Component<TabDocViewProps> { background={this.miniMapColor} document={this._document} tabView={this.tabView} /> - <Tooltip style={{ display: this.disableMinimap() ? "none" : undefined }} key="ttip" title={<div className="dash-tooltip">{"toggle minimap"}</div>}> - <div className="miniMap-hidden" onPointerDown={e => e.stopPropagation()} onClick={action(e => { e.stopPropagation(); this._document!.hideMinimap = !this._document!.hideMinimap; })} > + <Tooltip key="ttip" title={<div className="dash-tooltip">{this._document.hideMinimap ? "Open minimap" : "Close minimap"}</div>}> + <div className="miniMap-hidden" + style={{ + display: this.disableMinimap() || this._document._viewType !== "freeform" ? "none" : undefined, + color: this._document.hideMinimap ? Colors.BLACK : Colors.WHITE, + backgroundColor: this._document.hideMinimap ? Colors.LIGHT_GRAY : Colors.MEDIUM_BLUE, + boxShadow: this._document.hideMinimap ? Shadows.STANDARD_SHADOW : undefined + }} + onPointerDown={e => e.stopPropagation()} + onClick={action(e => { e.stopPropagation(); this._document!.hideMinimap = !this._document!.hideMinimap; })} > <FontAwesomeIcon icon={"globe-asia"} size="lg" /> </div> </Tooltip> @@ -434,7 +438,7 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> { case StyleProp.DocContents: const background = doc.type === DocumentType.PDF ? "red" : doc.type === DocumentType.IMG ? "blue" : doc.type === DocumentType.RTF ? "orange" : doc.type === DocumentType.VID ? "purple" : doc.type === DocumentType.WEB ? "yellow" : "gray"; - return doc.type === DocumentType.COL ? + return doc.type === DocumentType.COL || doc.type === DocumentType.INK ? undefined : <div style={{ width: doc[WidthSym](), height: doc[HeightSym](), position: "absolute", display: "block", background }} />; } diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 86fd21677..a3da0e0e4 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -54,7 +54,7 @@ export interface TreeViewProps { indentDocument?: (editTitle: boolean) => void; outdentDocument?: (editTitle: boolean) => void; ScreenToLocalTransform: () => Transform; - contextMenuItems: { script: ScriptField, filter: ScriptField, label: string }[]; + contextMenuItems: { script: ScriptField, filter: ScriptField, icon: string, label: string }[]; dontRegisterView?: boolean; styleProvider?: StyleProviderFunc | undefined; treeViewHideHeaderFields: () => boolean; @@ -154,8 +154,8 @@ export class TreeView extends React.Component<TreeViewProps> { } 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"); + const bestAlias = DocListCast(this.props.document.aliases).find(doc => !doc.context && doc.author === Doc.CurrentUserEmail); + this.props.addDocTab(bestAlias ?? Doc.MakeAlias(this.props.document), "add:right"); } } @@ -240,10 +240,13 @@ export class TreeView extends React.Component<TreeViewProps> { } makeFolder = () => { - const folder = Docs.Create.TreeDocument([], { title: "-folder-", _stayInCollection: true, isFolder: true }); + const folder = Docs.Create.TreeDocument([], { title: "Untitled folder", _stayInCollection: true, isFolder: true }); TreeView._editTitleOnLoad = { id: folder[Id], parent: this.props.parentTreeView }; return this.props.addDocument(folder); } + deleteFolder = () => { + return this.props.removeDoc?.(this.doc); + } preTreeDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => { const dragData = de.complete.docDragData; @@ -478,13 +481,16 @@ export class TreeView extends React.Component<TreeViewProps> { } @computed get validExpandViewTypes() { - if (this.doc.viewType === CollectionViewType.Docking) return [this.fieldKey]; + if (this.props.treeView.dashboardMode && Doc.UserDoc().noviceMode) { + return [this.doc.viewType === CollectionViewType.Docking ? this.fieldKey : "layout"]; + } 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 data = () => this.childDocs ? 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); + const layout = this.doc.viewType === CollectionViewType.Docking ? [] : ["layout"]; + return [data(), ...layout, ...(this.props.treeView.fileSysMode ? [aliases(), links(), annos()] : []), fields()].filter(m => m); } @action expandNextviewType = () => { @@ -512,10 +518,12 @@ export class TreeView extends React.Component<TreeViewProps> { DocumentViewInternal.SelectAfterContextMenu = true; } contextMenuItems = () => { - const makeFolder = { script: ScriptField.MakeFunction(`scriptContext.makeFolder()`, { scriptContext: "any" })!, label: "New Folder" }; - 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] : + const makeFolder = { script: ScriptField.MakeFunction(`scriptContext.makeFolder()`, { scriptContext: "any" })!, icon: "folder-plus", label: "New Folder" }; + const deleteFolder = { script: ScriptField.MakeFunction(`scriptContext.deleteFolder()`, { scriptContext: "any" })!, icon: "folder-plus", label: "Delete Folder" }; + const folderOp = this.childDocs?.length ? makeFolder : deleteFolder; + const openAlias = { script: ScriptField.MakeFunction(`openOnRight(getAlias(self))`)!, icon: "copy", label: "Open Alias" }; + const focusDoc = { script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, icon: "eye", label: "Focus or Open" }; + return [...this.props.contextMenuItems.filter(mi => !mi.filter ? true : mi.filter.script.run({ doc: this.doc })?.result), ... (this.doc.isFolder ? [folderOp] : Doc.IsSystem(this.doc) ? [] : this.props.treeView.fileSysMode && this.doc === Doc.GetProto(this.doc) ? [openAlias, makeFolder] : @@ -525,7 +533,8 @@ export class TreeView extends React.Component<TreeViewProps> { 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 })); + const icons = StrListCast(this.doc.childContextMenuIcons); + return StrListCast(this.doc.childContextMenuLabels).map((label, i) => ({ script: customScripts[i], filter: customFilters[i], icon: icons[i], label })); } onChildClick = () => this.props.onChildClick?.() ?? (this._editTitleScript?.() || ScriptCast(this.doc.treeChildClick)); @@ -835,7 +844,7 @@ export class TreeView extends React.Component<TreeViewProps> { dontRegisterView: boolean | undefined, observerHeight: (ref: any) => void, unobserveHeight: (ref: any) => void, - contextMenuItems: ({ script: ScriptField, filter: ScriptField, label: string }[]) + contextMenuItems: ({ script: ScriptField, filter: ScriptField, label: string, icon: string }[]) ) { const viewSpecScript = Cast(conainerCollection.viewSpecScript, ScriptField); if (viewSpecScript) { @@ -852,6 +861,7 @@ export class TreeView extends React.Component<TreeViewProps> { } const dentDoc = (editTitle: boolean, newParent: Doc, addAfter: Doc | undefined, parent: TreeView | CollectionTreeView | undefined) => { + if (parent instanceof TreeView && parent.props.treeView.fileSysMode && !newParent.isFolder) return; const fieldKey = Doc.LayoutFieldKey(newParent); if (remove && fieldKey && Cast(newParent[fieldKey], listSpec(Doc)) !== undefined) { remove(child); @@ -868,7 +878,7 @@ export class TreeView extends React.Component<TreeViewProps> { const childLayout = Doc.Layout(pair.layout); const rowHeight = () => { const aspect = Doc.NativeAspect(childLayout); - return aspect ? Math.min(childLayout[WidthSym](), rowWidth()) / aspect : childLayout[HeightSym](); + return (aspect ? Math.min(childLayout[WidthSym](), rowWidth()) / aspect : childLayout[HeightSym]()); }; return <TreeView key={child[Id]} ref={r => treeViewRefs.set(child, r ? r : undefined)} document={pair.layout} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index 37444a9dc..9fed82dae 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -7,6 +7,7 @@ import { Cast, NumCast, StrCast } from "../../../../fields/Types"; import { aggregateBounds } from "../../../../Utils"; import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; import React = require("react"); +import { ColorScheme } from "../../../util/SettingsManager"; export interface ViewDefBounds { type: string; @@ -361,7 +362,7 @@ export function computeTimelineLayout( groupNames.push({ type: "text", text: toLabel(Math.ceil(maxTime)), x: Math.ceil(maxTime - minTime) * scaling, y: 0, height: fontHeight, fontSize, payload: undefined }); } - const divider = { type: "div", color: CurrentUserUtils.ActiveDashboard?.darkScheme ? "dimGray" : "black", x: 0, y: 0, width: panelDim[0], height: -1, payload: undefined }; + const divider = { type: "div", color: CurrentUserUtils.ActiveDashboard?.colorScheme === ColorScheme.Dark ? "dimgray" : "black", x: 0, y: 0, width: panelDim[0], height: -1, payload: undefined }; return normalizeResults(panelDim, fontHeight, docMap, poolData, viewDefsToJSX, groupNames, (maxTime - minTime) * scaling, [divider]); function layoutDocsAtTime(keyDocs: Doc[], key: number) { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index 16258404d..bb4cae8c6 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -1,12 +1,13 @@ import { action, computed, IReactionDisposer, observable, reaction } from "mobx"; import { observer } from "mobx-react"; -import { Doc } from "../../../../fields/Doc"; +import { Doc, Field } from "../../../../fields/Doc"; import { Id } from "../../../../fields/FieldSymbols"; import { List } from "../../../../fields/List"; -import { NumCast, StrCast } from "../../../../fields/Types"; +import { NumCast } from "../../../../fields/Types"; import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../Utils'; -import { DocumentType } from "../../../documents/DocumentTypes"; +import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; import { LinkManager } from "../../../util/LinkManager"; +import { ColorScheme } from "../../../util/SettingsManager"; import { SnappingManager } from "../../../util/SnappingManager"; import { DocumentView } from "../../nodes/DocumentView"; import "./CollectionFreeFormLinkView.scss"; @@ -41,10 +42,10 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo const { A, B, LinkDocs } = this.props; const linkDoc = LinkDocs[0]; if (SnappingManager.GetIsDragging() || !A.ContentDiv || !B.ContentDiv) return; - setTimeout(action(() => this._opacity = 1), 0); // since the render code depends on querying the Dom through getBoudndingClientRect, we need to delay triggering render() + setTimeout(action(() => this._opacity = 0.75), 0); // since the render code depends on querying the Dom through getBoudndingClientRect, we need to delay triggering render() setTimeout(action(() => (!LinkDocs.length || !linkDoc.linkDisplay) && (this._opacity = 0.05)), 750); // this will unhighlight the link line. - const acont = A.rootDoc.type === DocumentType.LINK ? A.ContentDiv.getElementsByClassName("linkAnchorBox-cont") : []; - const bcont = B.rootDoc.type === DocumentType.LINK ? B.ContentDiv.getElementsByClassName("linkAnchorBox-cont") : []; + const acont = A.ContentDiv.getElementsByClassName("linkAnchorBox-cont"); + const bcont = B.ContentDiv.getElementsByClassName("linkAnchorBox-cont"); const adiv = acont.length ? acont[0] : A.ContentDiv; const bdiv = bcont.length ? bcont[0] : B.ContentDiv; const a = adiv.getBoundingClientRect(); @@ -69,8 +70,10 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo } else { const m = targetAhyperlink.getBoundingClientRect(); const mp = A.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); - linkDoc.anchor1_x = Math.min(1, mp[0] / A.props.PanelWidth()) * 100; - linkDoc.anchor1_y = Math.min(1, mp[1] / A.props.PanelHeight()) * 100; + const mpx = mp[0] / A.props.PanelWidth(); + const mpy = mp[1] / A.props.PanelHeight(); + if (mpx >= 0 && mpx <= 1) linkDoc.anchor1_x = mpx * 100; + if (mpy >= 0 && mpy <= 1) linkDoc.anchor1_y = mpy * 100; } if (!targetBhyperlink) { if (linkDoc.linkAutoMove) { @@ -80,8 +83,10 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo } else { const m = targetBhyperlink.getBoundingClientRect(); const mp = B.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); - linkDoc.anchor2_x = Math.min(1, mp[0] / B.props.PanelWidth()) * 100; - linkDoc.anchor2_y = Math.min(1, mp[1] / B.props.PanelHeight()) * 100; + const mpx = mp[0] / B.props.PanelWidth(); + const mpy = mp[1] / B.props.PanelHeight(); + if (mpx >= 0 && mpx <= 1) linkDoc.anchor2_x = mpx * 100; + if (mpy >= 0 && mpy <= 1) linkDoc.anchor2_y = mpy * 100; } } @@ -140,8 +145,8 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo if (!A.ContentDiv || !B.ContentDiv || !LinkDocs.length) return undefined; const acont = A.ContentDiv.getElementsByClassName("linkAnchorBox-cont"); const bcont = B.ContentDiv.getElementsByClassName("linkAnchorBox-cont"); - const adiv = (acont.length ? acont[0] : A.ContentDiv); - const bdiv = (bcont.length ? bcont[0] : B.ContentDiv); + const adiv = acont.length ? acont[0] : A.ContentDiv; + const bdiv = bcont.length ? bcont[0] : B.ContentDiv; for (let apdiv = adiv; apdiv; apdiv = apdiv.parentElement as any) if ((apdiv as any).hidden) return; for (let bpdiv = bdiv; bpdiv; bpdiv = bpdiv.parentElement as any) if ((bpdiv as any).hidden) return; const a = adiv.getBoundingClientRect(); @@ -175,18 +180,29 @@ 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 linkRelationship = Field.toString(LinkManager.currentLink?.linkRelationship as any as Field); //get string representing relationship const linkRelationshipList = Doc.UserDoc().linkRelationshipList as List<string>; const linkColorList = Doc.UserDoc().linkColorList as List<string>; + const linkRelationshipSizes = Doc.UserDoc().linkRelationshipSizes as List<number>; + const currRelationshipIndex = linkRelationshipList.indexOf(linkRelationship); + + const linkSize = currRelationshipIndex === -1 || currRelationshipIndex >= linkRelationshipSizes.length ? -1 : linkRelationshipSizes[currRelationshipIndex]; + //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)]; + const stroke = currRelationshipIndex === -1 || currRelationshipIndex >= linkColorList.length ? "black" : linkColorList[currRelationshipIndex]; + + //calculate stroke width/thickness based on the relative importance of the relationshipship (i.e. how many links the relationship has) + //thickness varies linearly from 3px to 12px for increasing link count + const strokeWidth = linkSize === -1 ? "3px" : Math.floor(2 + 10 * (linkSize / Math.max(...linkRelationshipSizes))) + "px"; + return !a.width || !b.width || ((!this.props.LinkDocs[0].linkDisplay) && !aActive && !bActive) ? (null) : (<> - <path className="collectionfreeformlinkview-linkLine" style={{ opacity: this._opacity, strokeDasharray: "2 2", stroke: strokeColor }} + <path className="collectionfreeformlinkview-linkLine" style={{ opacity: this._opacity, /*strokeDasharray: "2 2",*/ stroke, strokeWidth }} 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)} + {Field.toString(this.props.LinkDocs[0].description as any as Field)} </text>} </>); } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index e812064b7..dacbb3508 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -1,30 +1,17 @@ import { computed } from "mobx"; import { observer } from "mobx-react"; -import { Doc } from "../../../../fields/Doc"; import { Id } from "../../../../fields/FieldSymbols"; -import { Utils } from "../../../../Utils"; import { DocumentManager } from "../../../util/DocumentManager"; -import { DocumentView } from "../../nodes/DocumentView"; 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 { @computed get uniqueConnections() { - const connections = DocumentManager.Instance.LinkedDocumentViews - .filter(c => c.a.props.Document.type === DocumentType.LINK || c.b.props.Document.type === DocumentType.LINK) - .reduce((drawnPairs, connection) => { - const matchingPairs = drawnPairs.filter(pair => connection.a === pair.a && connection.b === pair.b); - matchingPairs.forEach(drawnPair => drawnPair.l.add(connection.l)); - if (!matchingPairs.length) drawnPairs.push({ a: connection.a, b: connection.b, l: new Set<Doc>([connection.l]) }); - return drawnPairs; - }, [] as { a: DocumentView, b: DocumentView, l: Set<Doc> }[]); - const set = new Map<Doc, { a: DocumentView, b: DocumentView, l: Doc[] }>(); - connections.map(c => !set.has(Array.from(c.l)[0]) && set.set(Array.from(c.l)[0], { a: c.a, b: c.b, l: Array.from(c.l) })); - return Array.from(set.values()).map(c => <CollectionFreeFormLinkView key={c.l[0][Id]} A={c.a} B={c.b} LinkDocs={c.l} />); + return Array.from(new Set(DocumentManager.Instance.LinkedDocumentViews)).map(c => + <CollectionFreeFormLinkView key={c.l[Id]} A={c.a} B={c.b} LinkDocs={[c.l]} /> + ); } render() { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index fb949a36d..7dcd63b80 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,4 +1,4 @@ -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; +import { action, computed, IReactionDisposer, observable, reaction, runInAction, ObservableMap } from "mobx"; import { observer } from "mobx-react"; import { computedFn } from "mobx-utils"; import { DateField } from "../../../../fields/DateField"; @@ -50,7 +50,7 @@ import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCurso import "./CollectionFreeFormView.scss"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); -import Color = require("color"); +import { ColorScheme } from "../../../util/SettingsManager"; export const panZoomSchema = createSchema({ _panX: "number", @@ -183,7 +183,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P addDocument = (newBox: Doc | Doc[]) => { let retVal = false; if (newBox instanceof Doc) { - if (retVal = this.props.addDocument?.(newBox) || false) { + if (retVal = (this.props.addDocument?.(newBox) || false)) { this.bringToFront(newBox); this.updateCluster(newBox); } @@ -209,25 +209,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P return retVal; } - updateGroupBounds = () => { - if (!this.props.Document._isGroup) return; - const clist = this.childDocs.map(cd => ({ x: NumCast(cd.x), y: NumCast(cd.y), width: cd[WidthSym](), height: cd[HeightSym]() })); - const cbounds = aggregateBounds(clist, 0, 0); - const c = [NumCast(this.layoutDoc.x) + this.layoutDoc[WidthSym]() / 2, NumCast(this.layoutDoc.y) + this.layoutDoc[HeightSym]() / 2]; - const p = [NumCast(this.layoutDoc._panX), NumCast(this.layoutDoc._panY)]; - const pbounds = { - x: (cbounds.x - p[0]) * this.zoomScaling() + c[0], y: (cbounds.y - p[1]) * this.zoomScaling() + c[1], - r: (cbounds.r - p[0]) * this.zoomScaling() + c[0], b: (cbounds.b - p[1]) * this.zoomScaling() + c[1] - }; - - this.layoutDoc._width = (pbounds.r - pbounds.x); - this.layoutDoc._height = (pbounds.b - pbounds.y); - this.layoutDoc._panX = (cbounds.r + cbounds.x) / 2; - this.layoutDoc._panY = (cbounds.b + cbounds.y) / 2; - this.layoutDoc.x = pbounds.x; - this.layoutDoc.y = pbounds.y; - } - isCurrent(doc: Doc) { const dispTime = NumCast(doc._timecodeToShow, -1); const endTime = NumCast(doc._timecodeToHide, dispTime + 1.5); @@ -269,8 +250,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P !StrListCast(d._layerTags).includes(StyleLayers.Background) && (d._raiseWhenDragged === undefined ? Doc.UserDoc()._raiseWhenDragged : d._raiseWhenDragged) && (d.zIndex = zsorted.length + 1 + i); // bringToFront } - this.updateGroupBounds(); - (docDragData.droppedDocuments.length === 1 || de.shiftKey) && this.updateClusterDocs(docDragData.droppedDocuments); return true; } @@ -510,7 +489,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const points = ge.points; const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height); const inkDoc = Docs.Create.InkDocument(ActiveInkColor(), CurrentUserUtils.SelectedTool, ActiveInkWidth(), ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), points, - { title: "ink stroke", x: B.x - Number(ActiveInkWidth()) / 2, y: B.y - Number(ActiveInkWidth()) / 2, _width: B.width + Number(ActiveInkWidth()), _height: B.height + Number(ActiveInkWidth()) }); + { title: "ink stroke", x: B.x - ActiveInkWidth() / 2, y: B.y - ActiveInkWidth() / 2, _width: B.width + ActiveInkWidth(), _height: B.height + ActiveInkWidth() }); this.addDocument(inkDoc); e.stopPropagation(); break; @@ -832,14 +811,14 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P setPan(panX: number, panY: number, panTime: number = 0, clamp: boolean = false) { if (!this.isAnnotationOverlay && clamp) { // this section wraps the pan position, horizontally and/or vertically whenever the content is panned out of the viewing bounds - const docs = this.childLayoutPairs.filter(pair => pair.layout instanceof Doc).map(pair => pair.layout); - const measuredDocs = docs.filter(doc => doc && this.childDataProvider(doc, "") && this.childSizeProvider(doc, "")). - map(doc => ({ ...this.childDataProvider(doc, ""), ...this.childSizeProvider(doc, "") })); + const docs = this.childLayoutPairs.map(pair => pair.layout).filter(doc => doc instanceof Doc); + const measuredDocs = docs.map(doc => ({ pos: this.childPositionProviderUnmemoized(doc, ""), size: this.childSizeProviderUnmemoized(doc, "") })) + .filter(({ pos, size }) => pos && size).map(({ pos, size }) => ({ pos: pos!, size: size! })); if (measuredDocs.length) { - const ranges = measuredDocs.reduce(({ xrange, yrange }, { x, y, width, height }) => // computes range of content + const ranges = measuredDocs.reduce(({ xrange, yrange }, { pos, size }) => // computes range of content ({ - xrange: { min: Math.min(xrange.min, x), max: Math.max(xrange.max, x + width) }, - yrange: { min: Math.min(yrange.min, y), max: Math.max(yrange.max, y + height) } + xrange: { min: Math.min(xrange.min, pos.x), max: Math.max(xrange.max, pos.x + (size.width || 0)) }, + yrange: { min: Math.min(yrange.min, pos.y), max: Math.max(yrange.max, pos.y + (size.height || 0)) } }) , { xrange: { min: Number.MAX_VALUE, max: -Number.MAX_VALUE }, @@ -923,7 +902,9 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P HistoryUtil.pushState(state); } } - SelectionManager.DeselectAll(); + if (SelectionManager.Views().length !== 1 || SelectionManager.Views()[0].Document !== doc) { + SelectionManager.DeselectAll(); + } if (this.props.Document.scrollHeight || this.props.Document.scrollTop !== undefined) { this.props.focus(doc, options); } else { @@ -1052,9 +1033,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P freezeDimensions={this.props.childFreezeDimensions} dropAction={StrCast(this.props.Document.childDropAction) as dropActionType} bringToFront={this.bringToFront} - dontRegisterView={this.props.dontRegisterView} + showTitle={this.props.childShowTitle} + dontRegisterView={this.props.dontRenderDocuments || this.props.dontRegisterView} pointerEvents={this.backgroundActive || this.props.childPointerEvents ? "all" : - (this.props.viewDefDivClick || (engine === "pass" && !this.props.isSelected(true))) ? "none" : undefined} + (this.props.viewDefDivClick || (engine === "pass" && !this.props.isSelected(true))) ? "none" : this.props.pointerEvents} jitterRotation={this.props.styleProvider?.(childLayout, this.props, StyleProp.JitterRotation) || 0} //fitToBox={this.props.fitToBox || BoolCast(this.props.freezeChildDimensions)} // bcz: check this />; @@ -1123,10 +1105,16 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } } + childPositionProviderUnmemoized = (doc: Doc, replica: string) => { + return this._layoutPoolData.get(doc[Id] + (replica || "")); + } childDataProvider = computedFn(function childDataProvider(this: any, doc: Doc, replica: string) { return this._layoutPoolData.get(doc[Id] + (replica || "")); }.bind(this)); + childSizeProviderUnmemoized = (doc: Doc, replica: string) => { + return this._layoutSizeData.get(doc[Id] + (replica || "")); + } childSizeProvider = computedFn(function childSizeProvider(this: any, doc: Doc, replica: string) { return this._layoutSizeData.get(doc[Id] + (replica || "")); }.bind(this)); @@ -1235,6 +1223,30 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P { fireImmediately: true, name: "doLayout" }); this._marqueeRef.current?.addEventListener("dashDragAutoScroll", this.onDragAutoScroll as any); + + this._disposers.groupBounds = reaction(() => { + if (this.props.Document._isGroup && this.childDocs.length === this.childDocList?.length) { + const clist = this.childDocs.map(cd => ({ x: NumCast(cd.x), y: NumCast(cd.y), width: cd[WidthSym](), height: cd[HeightSym]() })); + return aggregateBounds(clist, NumCast(this.layoutDoc._xPadding), NumCast(this.layoutDoc._yPadding)); + } + return undefined; + }, + (cbounds) => { + if (cbounds) { + const c = [NumCast(this.layoutDoc.x) + this.layoutDoc[WidthSym]() / 2, NumCast(this.layoutDoc.y) + this.layoutDoc[HeightSym]() / 2]; + const p = [NumCast(this.layoutDoc._panX), NumCast(this.layoutDoc._panY)]; + const pbounds = { + x: (cbounds.x - p[0]) * this.zoomScaling() + c[0], y: (cbounds.y - p[1]) * this.zoomScaling() + c[1], + r: (cbounds.r - p[0]) * this.zoomScaling() + c[0], b: (cbounds.b - p[1]) * this.zoomScaling() + c[1] + }; + this.layoutDoc._width = (pbounds.r - pbounds.x); + this.layoutDoc._height = (pbounds.b - pbounds.y); + this.layoutDoc._panX = (cbounds.r + cbounds.x) / 2; + this.layoutDoc._panY = (cbounds.b + cbounds.y) / 2; + this.layoutDoc.x = pbounds.x; + this.layoutDoc.y = pbounds.y; + } + }, { fireImmediately: true }); } componentWillUnmount() { @@ -1326,8 +1338,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P !options && ContextMenu.Instance.addItem({ description: "Options...", subitems: optionItems, icon: "eye" }); const mores = ContextMenu.Instance.findByDescription("More..."); const moreItems = mores && "subitems" in mores ? mores.subitems : []; - moreItems.push({ description: "Export collection", icon: "download", event: async () => Doc.Zip(this.props.Document) }); - moreItems.push({ description: "Import exported collection", icon: "upload", event: ({ x, y }) => this.importDocument(x, y) }); + if (!Doc.UserDoc().noviceMode) { + moreItems.push({ description: "Export collection", icon: "download", event: async () => Doc.Zip(this.props.Document) }); + moreItems.push({ description: "Import exported collection", icon: "upload", event: ({ x, y }) => this.importDocument(x, y) }); + } !mores && ContextMenu.Instance.addItem({ description: "More...", subitems: moreItems, icon: "eye" }); } @@ -1399,6 +1413,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } chooseGridSpace = (gridSpace: number): number => { + if (!this.zoomScaling()) return 50; const divisions = this.props.PanelWidth() / this.zoomScaling() / gridSpace + 3; return divisions < 60 ? gridSpace : this.chooseGridSpace(gridSpace * 10); } @@ -1410,6 +1425,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const renderGridSpace = gridSpace * this.zoomScaling(); const w = this.props.PanelWidth() + 2 * renderGridSpace; const h = this.props.PanelHeight() + 2 * renderGridSpace; + const strokeStyle = CurrentUserUtils.ActiveDashboard?.colorScheme === ColorScheme.Dark ? "rgba(255,255,255,0.5)" : "rgba(0, 0,0,0.5)"; return <canvas className="collectionFreeFormView-grid" width={w} height={h} style={{ transform: `translate(${shiftX}px, ${shiftY}px)` }} ref={(el) => { const ctx = el?.getContext('2d'); @@ -1420,7 +1436,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P ctx.setLineDash(gridSpace > 50 ? [3, 3] : [1, 5]); ctx.clearRect(0, 0, w, h); if (ctx) { - ctx.strokeStyle = "rgba(0, 0, 0, 0.5)"; + ctx.strokeStyle = strokeStyle; ctx.beginPath(); for (let x = Cx - renderGridSpace; x <= w - Cx; x += renderGridSpace) { ctx.moveTo(x, Cy - h); @@ -1457,7 +1473,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}> <div ref={this._marqueeRef} style={{ display: this.props.dontRenderDocuments ? "none" : undefined }}> - {this.layoutDoc["_backgroundGrid-show"] ? this.backgroundGrid : (null)} + {this.layoutDoc._backgroundGridShow ? this.backgroundGrid : (null)} <CollectionFreeFormViewPannableContents isAnnotationOverlay={this.isAnnotationOverlay} transform={this.contentTransform} @@ -1483,6 +1499,14 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P return wscale < hscale ? wscale : hscale; } + private groupDropDisposer?: DragManager.DragDropDisposer; + protected createGroupEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view + this.groupDropDisposer?.(); + if (ele) { + this.groupDropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc, this.onInternalPreDrop.bind(this)); + } + } + render() { TraceMobx(); const clientRect = this._mainCont?.getBoundingClientRect(); @@ -1526,7 +1550,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P </div>} {this.props.Document._isGroup && SnappingManager.GetIsDragging() && (this.ChildDrag || this.props.layerProvider?.(this.props.Document) === false) ? - <div className="collectionFreeForm-groupDropper" ref={this.createDashEventsTarget} style={{ + <div className="collectionFreeForm-groupDropper" ref={this.createGroupEventsTarget} style={{ width: this.ChildDrag ? "10000" : "100%", height: this.ChildDrag ? "10000" : "100%", left: this.ChildDrag ? "-5000" : 0, diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx index cedeb1112..1f59f9732 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx @@ -3,20 +3,22 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Tooltip } from "@material-ui/core"; import { observer } from "mobx-react"; import { unimplementedFunction } from "../../../../Utils"; +import { DocumentType } from "../../../documents/DocumentTypes"; +import { SelectionManager } from "../../../util/SelectionManager"; import { AntimodeMenu, AntimodeMenuProps } from "../../AntimodeMenu"; @observer export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> { static Instance: MarqueeOptionsMenu; - public createCollection: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; + public createCollection: (e: KeyboardEvent | React.PointerEvent | undefined, group?: boolean) => void = unimplementedFunction; public delete: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; public summarize: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; public inkToText: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; public showMarquee: () => void = unimplementedFunction; public hideMarquee: () => void = unimplementedFunction; public pinWithView: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; - + public isShown = () => this._opacity > 0; constructor(props: Readonly<{}>) { super(props); @@ -26,42 +28,52 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> { render() { const presPinWithViewIcon = <img src="/assets/pinWithView.png" style={{ margin: "auto", width: 19, transform: 'translate(-2px, -2px)' }} />; const buttons = [ - <Tooltip key="group" title={<><div className="dash-tooltip">Create a Collection</div></>} placement="bottom"> + <Tooltip key="collect" title={<div className="dash-tooltip">Create a Collection</div>} placement="bottom"> <button className="antimodeMenu-button" onPointerDown={this.createCollection}> <FontAwesomeIcon icon="object-group" size="lg" /> </button> </Tooltip>, - <Tooltip key="summarize" title={<><div className="dash-tooltip">Summarize Documents</div></>} placement="bottom"> + <Tooltip key="group" title={<div className="dash-tooltip">Create a Grouping</div>} placement="bottom"> <button className="antimodeMenu-button" - onPointerDown={this.summarize}> - <FontAwesomeIcon icon="compress-arrows-alt" size="lg" /> + onPointerDown={e => this.createCollection(e, true)}> + <FontAwesomeIcon icon="layer-group" size="lg" /> </button> </Tooltip>, - <Tooltip key="delete" title={<><div className="dash-tooltip">Delete Documents</div></>} placement="bottom"> + <Tooltip key="summarize" title={<div className="dash-tooltip">Summarize Documents</div>} placement="bottom"> <button className="antimodeMenu-button" - onPointerDown={this.delete}> - <FontAwesomeIcon icon="trash-alt" size="lg" /> + onPointerDown={this.summarize}> + <FontAwesomeIcon icon="compress-arrows-alt" size="lg" /> </button> </Tooltip>, - <Tooltip key="inkToText" title={<><div className="dash-tooltip">Change to Text</div></>} placement="bottom"> + <Tooltip key="delete" title={<div className="dash-tooltip">Delete Documents</div>} placement="bottom"> <button className="antimodeMenu-button" - onPointerDown={this.inkToText}> - <FontAwesomeIcon icon="font" size="lg" /> + onPointerDown={this.delete}> + <FontAwesomeIcon icon="trash-alt" size="lg" /> </button> </Tooltip>, - <Tooltip key="pinWithView" title={<><div className="dash-tooltip">Pin with selected region</div></>} placement="bottom"> + <Tooltip key="pinWithView" title={<div className="dash-tooltip">Pin with selected region</div>} placement="bottom"> <button className="antimodeMenu-button" onPointerDown={this.pinWithView}> - <>{presPinWithViewIcon}</> + {presPinWithViewIcon} </button> </Tooltip>, ]; + if (false && !SelectionManager.Views().some(v => v.props.Document.type !== DocumentType.INK)) { + buttons.push( + <Tooltip key="inkToText" title={<div className="dash-tooltip">Change to Text</div>} placement="bottom"> + <button + className="antimodeMenu-button" + onPointerDown={this.inkToText}> + <FontAwesomeIcon icon="font" size="lg" /> + </button> + </Tooltip>); + } return this.getElement(buttons); } }
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 81f6307d1..24a7d77e0 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -646,8 +646,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque render() { return <div className="marqueeView" style={{ - overflow: //(!this.props.ContainingCollectionView && this.props.isAnnotationOverlay) ? "visible" : - StrCast(this.props.Document._overflow), + overflow: StrCast(this.props.Document._overflow), cursor: "hand" }} onDragOver={e => e.preventDefault()} diff --git a/src/client/views/collections/collectionFreeForm/index.ts b/src/client/views/collections/collectionFreeForm/index.ts new file mode 100644 index 000000000..702dc8d42 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/index.ts @@ -0,0 +1,7 @@ +export * from "./CollectionFreeFormLayoutEngines"; +export * from "./CollectionFreeFormLinkView"; +export * from "./CollectionFreeFormLinksView"; +export * from "./CollectionFreeFormRemoteCursors"; +export * from "./CollectionFreeFormView"; +export * from "./MarqueeOptionsMenu"; +export * from "./MarqueeView";
\ No newline at end of file diff --git a/src/client/views/collections/collectionGrid/index.ts b/src/client/views/collections/collectionGrid/index.ts new file mode 100644 index 000000000..be5d5667a --- /dev/null +++ b/src/client/views/collections/collectionGrid/index.ts @@ -0,0 +1,2 @@ +export * from "./Grid"; +export * from "./CollectionGridView";
\ No newline at end of file diff --git a/src/client/views/collections/CollectionLinearView.scss b/src/client/views/collections/collectionLinear/CollectionLinearView.scss index 46e40489b..8fe804466 100644 --- a/src/client/views/collections/CollectionLinearView.scss +++ b/src/client/views/collections/collectionLinear/CollectionLinearView.scss @@ -1,15 +1,31 @@ -@import "../global/globalCssVariables"; -@import "../_nodeModuleOverrides"; +@import "../../global/globalCssVariables"; +@import "../../_nodeModuleOverrides"; .collectionLinearView-outer { overflow: visible; height: 100%; pointer-events: none; + &.true { + padding-left: 5px; + padding-right: 5px; + border-left: $standard-border; + background-color: $medium-blue-alt; + } + + >input:not(:checked)~&.true { + background-color: transparent; + } + .collectionLinearView { display: flex; height: 100%; align-items: center; + gap: 5px; + + .collectionView { + overflow: visible !important; + } >span { background: $dark-gray; @@ -41,7 +57,7 @@ } .bottomPopup-descriptions { - cursor:pointer; + cursor: pointer; display: inline; white-space: nowrap; padding-left: 8px; @@ -54,7 +70,7 @@ } .bottomPopup-exit { - cursor:pointer; + cursor: pointer; display: inline; white-space: nowrap; margin-right: 10px; @@ -67,29 +83,26 @@ } >label { - margin-top: "auto"; - margin-bottom: "auto"; - background: $dark-gray; - color: $white; - display: inline-block; - border-radius: 18px; - font-size: 12.5px; - width: 18px; - height: 18px; - margin-top: auto; - margin-bottom: auto; - margin-right: 3px; + pointer-events: all; cursor: pointer; + background-color: $medium-blue; + padding: 5; + border-radius: 2px; + height: 25; + min-width: 25; + margin: 0; + color: $white; + display: flex; + font-weight: 100; + width: fit-content; transition: transform 0.2s; - } - - label p { - padding-left: 5px; - } + align-items: center; + justify-content: center; + transition: 0.2s; - label:hover { - background: $medium-gray; - transform: scale(1.15); + &:hover{ + filter: brightness(0.85); + } } >input { @@ -110,13 +123,11 @@ display: flex; opacity: 1; position: relative; - margin-top: auto; .collectionLinearView-docBtn, .collectionLinearView-docBtn-scalable { position: relative; margin: auto; - margin-left: 3px; transform-origin: center 80%; } diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx new file mode 100644 index 000000000..18a715edf --- /dev/null +++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx @@ -0,0 +1,225 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Tooltip } from '@material-ui/core'; +import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { Doc, HeightSym, Opt, WidthSym } from '../../../../fields/Doc'; +import { documentSchema } from '../../../../fields/documentSchemas'; +import { Id } from '../../../../fields/FieldSymbols'; +import { makeInterface } from '../../../../fields/Schema'; +import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; +import { emptyFunction, returnEmptyDoclist, returnTrue, Utils } from '../../../../Utils'; +import { DragManager } from '../../../util/DragManager'; +import { Transform } from '../../../util/Transform'; +import { Colors, Shadows } from '../../global/globalEnums'; +import { DocumentLinksButton } from '../../nodes/DocumentLinksButton'; +import { DocumentView } from '../../nodes/DocumentView'; +import { LinkDescriptionPopup } from '../../nodes/LinkDescriptionPopup'; +import { StyleProp } from '../../StyleProvider'; +import { CollectionSubView } from '../CollectionSubView'; +import { CollectionViewType } from '../CollectionView'; +import "./CollectionLinearView.scss"; + + +type LinearDocument = makeInterface<[typeof documentSchema,]>; +const LinearDocument = makeInterface(documentSchema); + +@observer +export class CollectionLinearView extends CollectionSubView(LinearDocument) { + @observable public addMenuToggle = React.createRef<HTMLInputElement>(); + @observable private _selectedIndex = -1; + private _dropDisposer?: DragManager.DragDropDisposer; + private _widthDisposer?: IReactionDisposer; + private _selectedDisposer?: IReactionDisposer; + + componentWillUnmount() { + this._dropDisposer?.(); + this._widthDisposer?.(); + this._selectedDisposer?.(); + this.childLayoutPairs.map((pair, ind) => ScriptCast(pair.layout.proto?.onPointerUp)?.script.run({ this: pair.layout.proto }, console.log)); + } + + componentDidMount() { + this._widthDisposer = reaction(() => 5 + (this.layoutDoc.linearViewIsExpanded ? this.childDocs.length * (this.rootDoc[HeightSym]()) : 10), + width => this.childDocs.length && (this.layoutDoc._width = width), + { fireImmediately: true } + ); + + this._selectedDisposer = reaction( + () => NumCast(this.layoutDoc.selectedIndex), + (i) => runInAction(() => { + this._selectedIndex = i; + let selected: any = undefined; + this.childLayoutPairs.map(async (pair, ind) => { + const isSelected = this._selectedIndex === ind; + if (isSelected) { + selected = pair; + } + else { + ScriptCast(pair.layout.proto?.onPointerUp)?.script.run({ this: pair.layout.proto }, console.log); + } + }); + if (selected && selected.layout) { + ScriptCast(selected.layout.proto?.onPointerDown)?.script.run({ this: selected.layout.proto }, console.log); + } + }), + { fireImmediately: true } + ); + } + protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view + this._dropDisposer && this._dropDisposer(); + if (ele) { + this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc); + } + } + + dimension = () => NumCast(this.rootDoc._height); // 2 * the padding + getTransform = (ele: Opt<HTMLDivElement>) => { + if (!ele) return Transform.Identity(); + const { scale, translateX, translateY } = Utils.GetScreenTransform(ele); + return new Transform(-translateX, -translateY, 1); + } + + @action + exitLongLinks = () => { + if (DocumentLinksButton.StartLink) { + if (DocumentLinksButton.StartLink.Document) { + action((e: React.PointerEvent<HTMLDivElement>) => { + Doc.UnBrushDoc(DocumentLinksButton.StartLink?.Document as Doc); + }); + } + } + DocumentLinksButton.StartLink = undefined; + DocumentLinksButton.StartLinkView = undefined; + } + + @action + changeDescriptionSetting = () => { + if (LinkDescriptionPopup.showDescriptions) { + if (LinkDescriptionPopup.showDescriptions === "ON") { + LinkDescriptionPopup.showDescriptions = "OFF"; + LinkDescriptionPopup.descriptionPopup = false; + } else { + LinkDescriptionPopup.showDescriptions = "ON"; + } + } else { + LinkDescriptionPopup.showDescriptions = "OFF"; + LinkDescriptionPopup.descriptionPopup = false; + } + } + + myContextMenu = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + } + + + getDisplayDoc = (doc: Doc) => { + const nested = doc._viewType === CollectionViewType.Linear; + const hidden = doc.hidden === true; + + let dref: Opt<HTMLDivElement>; + const docXf = () => this.getTransform(dref); + // const scalable = pair.layout.onClick || pair.layout.onDragStart; + return hidden ? (null) : <div className={`collectionLinearView-docBtn`} key={doc[Id]} ref={r => dref = r || undefined} + style={{ + pointerEvents: "all", + width: nested ? undefined : NumCast(doc._width), + height: nested ? undefined : NumCast(doc._height), + marginLeft: !nested ? 2.5 : 0, + marginRight: !nested ? 2.5 : 0, + // width: NumCast(pair.layout._width), + // height: NumCast(pair.layout._height), + }} > + <DocumentView + Document={doc} + isContentActive={this.props.isContentActive} + isDocumentActive={returnTrue} + addDocument={this.props.addDocument} + moveDocument={this.props.moveDocument} + addDocTab={this.props.addDocTab} + pinToPres={emptyFunction} + rootSelected={this.props.isSelected} + removeDocument={this.props.removeDocument} + ScreenToLocalTransform={docXf} + PanelWidth={nested ? doc[WidthSym] : this.dimension} + PanelHeight={nested ? doc[HeightSym] : this.dimension} + renderDepth={this.props.renderDepth + 1} + focus={emptyFunction} + styleProvider={this.props.styleProvider} + layerProvider={this.props.layerProvider} + docViewPath={returnEmptyDoclist} + whenChildContentsActiveChanged={emptyFunction} + bringToFront={emptyFunction} + docFilters={this.props.docFilters} + docRangeFilters={this.props.docRangeFilters} + searchFilterDocs={this.props.searchFilterDocs} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} /> + </div>; + } + + render() { + const guid = Utils.GenerateGuid(); // Generate a unique ID to use as the label + const flexDir: any = StrCast(this.Document.flexDirection); // Specify direction of linear view content + const flexGap: number = NumCast(this.Document.flexGap); // Specify the gap between linear view content + const expandable: boolean = BoolCast(this.props.Document.linearViewExpandable); // Specify whether it is expandable or not + const floating: boolean = BoolCast(this.props.Document.linearViewFloating); // Specify whether it is expandable or not + + const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); + const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); + const icon: string = StrCast(this.props.Document.icon); // Menu opener toggle + const menuOpener = <label htmlFor={`${guid}`} + style={{ + boxShadow: floating ? Shadows.STANDARD_SHADOW : undefined, + color: BoolCast(this.layoutDoc.linearViewIsExpanded) ? undefined : Colors.BLACK, + backgroundColor: backgroundColor === color ? "black" : BoolCast(this.layoutDoc.linearViewIsExpanded) ? undefined : Colors.LIGHT_GRAY + }} + onPointerDown={e => e.stopPropagation()} > + <div className="collectionLinearView-menuOpener"> + {BoolCast(this.layoutDoc.linearViewIsExpanded) ? icon ? icon : <FontAwesomeIcon icon={"minus"} /> : icon ? icon : <FontAwesomeIcon icon={"plus"} />} + </div> + </label>; + + return <div className={`collectionLinearView-outer ${this.layoutDoc.linearViewSubMenu}`} style={{ backgroundColor: BoolCast(this.layoutDoc.linearViewIsExpanded) ? undefined : "transparent" }}> + <div className="collectionLinearView" ref={this.createDashEventsTarget} + onContextMenu={this.myContextMenu} > + {!expandable ? (null) : <Tooltip title={<><div className="dash-tooltip">{BoolCast(this.props.Document.linearViewIsExpanded) ? "Close" : "Open"}</div></>} placement="top"> + {menuOpener} + </Tooltip>} + <input id={`${guid}`} type="checkbox" checked={BoolCast(this.props.Document.linearViewIsExpanded)} ref={this.addMenuToggle} + onChange={action(() => this.props.Document.linearViewIsExpanded = this.addMenuToggle.current!.checked)} /> + + <div className="collectionLinearView-content" + style={{ + height: this.dimension(), + flexDirection: flexDir, + gap: flexGap + }}> + {this.childLayoutPairs.map(pair => this.getDisplayDoc(pair.layout))} + </div> + {DocumentLinksButton.StartLink && StrCast(this.layoutDoc.title) === "docked buttons" ? <span className="bottomPopup-background" style={{ + pointerEvents: "all" + }} + onPointerDown={e => e.stopPropagation()} > + <span className="bottomPopup-text" > + 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">{"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 linking mode</div></>} placement="top"> + <span className="bottomPopup-exit" onClick={this.exitLongLinks}> + Stop + </span> + </Tooltip> + + </span> : null} + </div> + </div>; + } +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionLinear/index.ts b/src/client/views/collections/collectionLinear/index.ts new file mode 100644 index 000000000..ff73e14ae --- /dev/null +++ b/src/client/views/collections/collectionLinear/index.ts @@ -0,0 +1 @@ +export * from "./CollectionLinearView";
\ No newline at end of file diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx index 9a3ef1422..a439a7998 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx @@ -120,7 +120,6 @@ export class CollectionSchemaCell extends React.Component<CellProps> { this.props.changeFocusedCellByIndex(this.props.row, this.props.col); this.props.setPreviewDoc(this.props.rowProps.original); - // console.log("click cell"); let url: string; if (url = StrCast(this.props.rowProps.row.href)) { // opens up the the doc in a new window, blurring the old one @@ -338,12 +337,12 @@ export class CollectionSchemaCell extends React.Component<CellProps> { } } const script = CompileScript(inputSansCommas, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); - const changeMade = value.length !== value.length || value.length - 2 !== value.length; + const changeMade = value.length - 2 !== value.length; script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run)); // handle booleans } else if (inputIsBool) { const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); - const changeMade = value.length !== value.length || value.length - 2 !== value.length; + const changeMade = value.length - 2 !== value.length; script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run)); } } diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx index 52865eba6..074074bc5 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx @@ -1,18 +1,17 @@ import React = require("react"); -import { IconProp, library } from "@fortawesome/fontawesome-svg-core"; +import { IconProp } from "@fortawesome/fontawesome-svg-core"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable, runInAction } from "mobx"; +import { action, computed, observable, runInAction, trace } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast, Opt } from "../../../../fields/Doc"; +import { Doc, DocListCast, Opt, StrListCast } from "../../../../fields/Doc"; import { listSpec } from "../../../../fields/Schema"; import { PastelSchemaPalette, SchemaHeaderField } from "../../../../fields/SchemaHeaderField"; import { ScriptField } from "../../../../fields/ScriptField"; import { Cast, StrCast } from "../../../../fields/Types"; import { undoBatch } from "../../../util/UndoManager"; -import { SearchBox } from "../../search/SearchBox"; +import { CollectionView } from "../CollectionView"; import { ColumnType } from "./CollectionSchemaView"; import "./CollectionSchemaView.scss"; -import { CollectionView } from "../CollectionView"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; @@ -27,9 +26,9 @@ export interface AddColumnHeaderProps { export class CollectionSchemaAddColumnHeader extends React.Component<AddColumnHeaderProps> { // the button that allows the user to add a column render() { - return ( - <button className="add-column" onClick={() => this.props.createColumn()}><FontAwesomeIcon icon="plus" size="sm" /></button> - ); + return <button className="add-column" onClick={() => this.props.createColumn()}> + <FontAwesomeIcon icon="plus" size="sm" /> + </button>; } } @@ -191,7 +190,7 @@ export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> {this.renderSorting()} {this.renderColors()} <div className="collectionSchema-headerMenu-group"> - <button onClick={() => this.props.deleteColumn(this.props.columnField.heading)}>Delete Column</button> + <button onClick={() => this.props.deleteColumn(this.props.columnField.heading)}>Hide Column</button> </div> </> } @@ -234,7 +233,7 @@ export interface KeysDropdownProps { @observer export class KeysDropdown extends React.Component<KeysDropdownProps> { @observable private _key: string = this.props.keyValue; - @observable private _searchTerm: string = this.props.keyValue; + @observable private _searchTerm: string = this.props.keyValue + ":"; @observable private _isOpen: boolean = false; @observable private _node: HTMLDivElement | null = null; @observable private _inputRef: React.RefObject<HTMLInputElement> = React.createRef(); @@ -326,11 +325,11 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { || ((!key.startsWith("_") && key[0] === key[0].toUpperCase()) || key[0] === "#")) ? showKeys.add(key) : null); return Array.from(showKeys.keys()).filter(key => !this._searchTerm || key.includes(this._searchTerm)); } - @action - renderOptions = (): JSX.Element[] | JSX.Element => { + + @computed get renderOptions() { if (!this._isOpen) { this.defaultMenuHeight = 0; - return <></>; + return (null); } const options = this.showKeys.map(key => { return <div key={key} className="key-option" style={{ @@ -373,30 +372,25 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { return options; } - docSafe: Doc[] = []; + @computed get docSafe() { return DocListCast(this.props.dataDoc?.[this.props.fieldKey]); } - @action - renderFilterOptions = (): JSX.Element[] | JSX.Element => { + @computed get renderFilterOptions() { if (!this._isOpen || !this.props.dataDoc) { this.defaultMenuHeight = 0; - return <></>; + return (null); } const keyOptions: string[] = []; const colpos = this._searchTerm.indexOf(":"); const temp = this._searchTerm.slice(colpos + 1, this._searchTerm.length); - if (this.docSafe.length === 0) { - this.docSafe = DocListCast(this.props.dataDoc[this.props.fieldKey]); - } - const docs = this.docSafe; - docs.forEach((doc) => { + this.docSafe.forEach(doc => { const key = StrCast(doc[this._key]); if (keyOptions.includes(key) === false && key.includes(temp) && key !== "") { keyOptions.push(key); } }); - const filters = Cast(this.props.Document._docFilters, listSpec("string")); - if (filters === undefined || filters.length === 0 || filters.some(filter => filter.split(":")[0] === this._key) === false) { + const filters = StrListCast(this.props.Document._docFilters); + if (filters.some(filter => filter.split(":")[0] === this._key) === false) { this.props.col.setColor("rgb(241, 239, 235)"); this.closeResultsVisibility = "none"; } @@ -408,24 +402,28 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { const options = keyOptions.map(key => { let bool = false; if (filters !== undefined) { - const ind = filters.findIndex(filter => filter.split(":")[0] === key); + const ind = filters.findIndex(filter => filter.split(":")[1] === key); const fields = ind === -1 ? undefined : filters[ind].split(":"); - bool = fields ? fields[1] === "check" : false; + bool = fields ? fields[2] === "check" : false; } return <div key={key} className="key-option" style={{ - border: "1px solid lightgray", paddingLeft: 5, textAlign: "left", + paddingLeft: 5, textAlign: "left", width: this.props.width, maxWidth: this.props.width, overflowX: "hidden", background: "white", backgroundColor: "white", }} > <input type="checkbox" onPointerDown={e => e.stopPropagation()} onClick={e => e.stopPropagation()} - onChange={(e) => { - 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 ? Doc.setDocFilter(docs[0], this._key, key, "check") : Doc.setDocFilter(docs[0], this._key, key, "remove"); - }} + onChange={action(e => { + if (e.target.checked) { + Doc.setDocFilter(this.props.Document, this._key, key, "check"); + this.closeResultsVisibility = "contents"; + this.props.col.setColor("green"); + } else { + Doc.setDocFilter(this.props.Document, this._key, key, "remove"); + this.updateFilter(); + } + })} checked={bool} /> <span style={{ paddingLeft: 4 }}> @@ -472,11 +470,7 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { removeFilters = (e: React.PointerEvent): void => { const keyOptions: string[] = []; - if (this.docSafe.length === 0 && this.props.dataDoc) { - this.docSafe = DocListCast(this.props.dataDoc[this.props.fieldKey]); - } - const docs = this.docSafe; - docs.forEach((doc) => { + this.docSafe.forEach(doc => { const key = StrCast(doc[this._key]); if (keyOptions.includes(key) === false) { keyOptions.push(key); @@ -489,12 +483,10 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { } render() { return ( - <div style={{ display: "flex" }} ref={this.setNode}> - <FontAwesomeIcon onClick={e => { this.props.openHeader(this.props.col, e.clientX, e.clientY); e.stopPropagation(); }} icon={this.props.icon} size="lg" style={{ display: "inline", paddingBottom: "1px", paddingTop: "4px", cursor: "hand" }} /> - - {/* <FontAwesomeIcon icon={fa.faSearchMinus} size="lg" style={{ display: "inline", paddingBottom: "1px", paddingTop: "4px", cursor: "hand" }} onClick={e => { - runInAction(() => { this._isOpen === undefined ? this._isOpen = true : this._isOpen = !this._isOpen }) - }} /> */} + <div style={{ display: "flex", width: '100%', alignContent: 'center', alignItems: 'center' }} ref={this.setNode}> + <div className="schema-icon" onClick={e => { this.props.openHeader(this.props.col, e.clientX, e.clientY); e.stopPropagation(); }}> + <FontAwesomeIcon icon={this.props.icon} size="lg" style={{ display: "inline" }} /> + </div> <div className="keys-dropdown" style={{ zIndex: 1, width: this.props.width, maxWidth: this.props.width }}> <input className="keys-search" style={{ width: "100%" }} @@ -509,7 +501,7 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { {!this._isOpen ? (null) : <div className="keys-options-wrapper" style={{ width: this.props.width, maxWidth: this.props.width, height: "auto", }}> - {this._searchTerm.includes(":") ? this.renderFilterOptions() : this.renderOptions()} + {this._searchTerm.includes(":") ? this.renderFilterOptions : this.renderOptions} </div>} </div > </div> diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx index f48906ba5..0e19ef3d9 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx @@ -134,9 +134,9 @@ export class MovableRow extends React.Component<MovableRowProps> { <div className="collectionSchema-row-wrapper" onKeyPress={this.onKeyDown} ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> <ReactTableDefaults.TrComponent onKeyPress={this.onKeyDown} > <div className="row-dragger"> - <div className="row-option" style={{ left: 5 }} onClick={undoBatch(() => this.props.removeDoc(this.props.rowInfo.original))}><FontAwesomeIcon icon="trash" size="sm" /></div> - <div className="row-option" style={{ cursor: "grab", left: 25 }} ref={reference} onPointerDown={onItemDown}><FontAwesomeIcon icon="grip-vertical" size="sm" /></div> - <div className="row-option" style={{ left: 40 }} onClick={() => this.props.addDocTab(this.props.rowInfo.original, "add:right")}><FontAwesomeIcon icon="external-link-alt" size="sm" /></div> + <div className="row-option" onClick={undoBatch(() => this.props.removeDoc(this.props.rowInfo.original))}><FontAwesomeIcon icon="trash" size="sm" /></div> + <div className="row-option" style={{ cursor: "grab" }} ref={reference} onPointerDown={onItemDown}><FontAwesomeIcon icon="grip-vertical" size="sm" /></div> + <div className="row-option" onClick={() => this.props.addDocTab(this.props.rowInfo.original, "add:right")}><FontAwesomeIcon icon="external-link-alt" size="sm" /></div> </div> {children} </ReactTableDefaults.TrComponent> diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss index c17b79638..b64e9dac1 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss @@ -108,9 +108,7 @@ } .rt-th { padding: 0; - border: solid lightgray; - border-width: 0 1px; - border-bottom: 2px solid lightgray; + border-left: solid 1px $light-gray; } } .rt-th { @@ -220,6 +218,8 @@ } } + + .collectionSchemaView-header { height: 100%; color: gray; @@ -234,6 +234,15 @@ button.add-column { width: 28px; } +.collectionSchemaView-menuOptions-wrapper { + background: rgb(241, 239, 235); + display: flex; + cursor: default; + height: 100%; + align-content: center; + align-items: center; +} + .collectionSchema-header-menuOptions { color: black; width: 180px; @@ -279,6 +288,9 @@ button.add-column { width: 10px; } } + + + .keys-dropdown { position: relative; //width: 100%; @@ -294,26 +306,7 @@ button.add-column { font-weight: normal; } } - .keys-options-wrapper { - width: 100%; - max-height: 150px; - overflow-y: scroll; - position: absolute; - top: 28px; - box-shadow: 0 10px 16px rgba(0, 0, 0, 0.1); - background-color: white; - .key-option { - background-color: white; - border: 1px solid lightgray; - padding: 2px 3px; - &:not(:first-child) { - border-top: 0; - } - &:hover { - background-color: $light-gray; - } - } - } + } .columnMenu-colors { display: flex; @@ -332,11 +325,53 @@ button.add-column { } } +.schema-icon { + cursor: pointer; + width: 25px; + height: 25px; + display: flex; + align-items: center; + justify-content: center; + align-content: center; + background-color: $medium-blue; + color: white; + margin-right: 5px; + font-size: 10px; + border-radius: 3px; + +} + +.keys-options-wrapper { + position: absolute; + text-align: left; + height: fit-content; + top: 100%; + z-index: 21; + background-color: #ffffff; + box-shadow: 0px 3px 4px rgba(0,0,0,30%); + padding: 1px; + .key-option { + cursor: pointer; + color: #000000; + width: 100%; + height: 25px; + font-weight: 400; + display: flex; + justify-content: left; + align-items: center; + padding-left: 5px; + &:hover { + background-color: $light-gray; + } + } +} + .collectionSchema-row { height: 100%; background-color: white; &.row-focused .rt-td { - background-color: #bfffc0; //$light-gray; + background-color: $light-blue; //$light-gray; + overflow: visible; } &.row-wrapped { .rt-td { @@ -345,39 +380,40 @@ button.add-column { } .row-dragger { display: flex; - justify-content: space-around; - //flex: 50 0 auto; - width: 0; - max-width: 50px; - //height: 100%; + justify-content: space-evenly; + width: 58px; + position: absolute; + /* max-width: 50px; */ min-height: 30px; align-items: center; color: lightgray; background-color: white; transition: color 0.1s ease; .row-option { - // padding: 5px; + color: black; cursor: pointer; - position: absolute; + position: relative; transition: color 0.1s ease; display: flex; flex-direction: column; justify-content: center; z-index: 2; + border-radius: 3px; + padding: 3px; &:hover { - color: gray; + background-color: $light-gray; } } } .collectionSchema-row-wrapper { &.row-above { - border-top: 1px solid red; + border-top: 1px solid $medium-blue; } &.row-below { - border-bottom: 1px solid red; + border-bottom: 1px solid $medium-blue; } &.row-inside { - border: 1px solid red; + border: 2px dashed $medium-blue; } .row-dragging { background-color: blue; @@ -390,28 +426,37 @@ button.add-column { height: unset; } +.collectionSchemaView-cellContents { + width: 100%; +} + .collectionSchemaView-cellWrapper { + display: flex; height: 100%; - padding: 4px; text-align: left; padding-left: 19px; position: relative; + align-items: center; + align-content: center; &:focus { outline: none; } &.editing { padding: 0; + box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3); + transform: scale(1.1); + z-index: 40; input { outline: 0; border: none; - background-color: rgb(255, 217, 217); + background-color: $white; width: 100%; - height: 100%; - padding: 2px 3px; + height: fit-content; min-height: 26px; } } &.focused { + overflow: hidden; &.inactive { border: none; } diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx index fed64b620..b89246489 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx @@ -1,6 +1,6 @@ import React = require("react"); import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable, untracked } from "mobx"; +import { action, computed, observable, untracked, trace } from "mobx"; import { observer } from "mobx-react"; import Measure from "react-measure"; import { Resize } from "react-table"; @@ -337,8 +337,9 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { {this.renderTypes(this._col)} {this.renderColors(this._col)} <div className="collectionSchema-headerMenu-group"> - <button onClick={() => { this.deleteColumn(this._col.heading); }} - >Delete Column</button> + <button onClick={() => { this.deleteColumn(this._col.heading); }}> + Hide Column + </button> </div> </div>; } @@ -353,7 +354,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @action setFocused = (doc: Doc) => this._focusedTable = doc; @action setPreviewDoc = (doc: Opt<Doc>) => { - SelectionManager.SelectSchemaView(this, doc); + SelectionManager.SelectSchemaViewDoc(doc); this._previewDoc = doc; } diff --git a/src/client/views/collections/collectionSchema/SchemaTable.tsx b/src/client/views/collections/collectionSchema/SchemaTable.tsx index 631f623c6..bc5a9559f 100644 --- a/src/client/views/collections/collectionSchema/SchemaTable.tsx +++ b/src/client/views/collections/collectionSchema/SchemaTable.tsx @@ -1,7 +1,7 @@ import React = require("react"); import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable } from "mobx"; +import { action, computed, observable, trace } from "mobx"; import { observer } from "mobx-react"; import ReactTable, { CellInfo, Column, ComponentPropsGetterR, Resize, SortingRule } from "react-table"; import "react-table/react-table.css"; @@ -194,10 +194,10 @@ export class SchemaTable extends React.Component<SchemaTableProps> { const sortIcon = col.desc === undefined ? "caret-right" : col.desc === true ? "caret-down" : "caret-up"; const header = <div className="collectionSchemaView-menuOptions-wrapper" style={{ background: col.color, padding: "2px", display: "flex", cursor: "default", height: "100%", }}> {keysDropdown} - <div onClick={e => this.changeSorting(col)} style={{ width: 21, padding: 1, display: "inline", zIndex: 1, background: "inherit", cursor: "hand" }}> + <div onClick={e => this.changeSorting(col)} style={{ width: 21, padding: 1, display: "inline", zIndex: 1, background: "inherit", cursor: "pointer" }}> <FontAwesomeIcon icon={sortIcon} size="lg" /> </div> - {this.props.Document._chromeHidden || this.props.addDocument === returnFalse ? undefined : <div className="collectionSchemaView-addRow" onClick={this.createRow}>+ new</div>} + {/* {this.props.Document._chromeHidden || this.props.addDocument == returnFalse ? undefined : <div className="collectionSchemaView-addRow" onClick={this.createRow}>+ new</div>} */} </div>; return { @@ -386,13 +386,13 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @undoBatch @action createColumn = () => { - let index = 0; - let found = this.props.columns.findIndex(col => col.heading.toUpperCase() === "New field".toUpperCase()) > -1; - while (found) { - index++; - found = this.props.columns.findIndex(col => col.heading.toUpperCase() === ("New field (" + index + ")").toUpperCase()) > -1; + const newFieldName = (index: number) => `New field${index ? ` (${index})` : ""}`; + for (let index = 0; index < 100; index++) { + if (this.props.columns.findIndex(col => col.heading === newFieldName(index)) === -1) { + this.props.columns.push(new SchemaHeaderField(newFieldName(index), "#f1efeb")); + break; + } } - this.props.columns.push(new SchemaHeaderField(`New field ${index ? "(" + index + ")" : ""}`, "#f1efeb")); } @action @@ -567,7 +567,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { <div className="collectionSchemaView-documentPreview" ref="overlay" style={{ position: "absolute", width: 150, height: 150, - background: "dimGray", display: "block", top: 0, left: 0, + background: "dimgray", display: "block", top: 0, left: 0, transform: `translate(${this._showDocPos[0]}px, ${this._showDocPos[1] - 180}px)` }} > <DocumentView diff --git a/src/client/views/global/globalCssVariables.scss b/src/client/views/global/globalCssVariables.scss index 7556f8b8a..520ac9357 100644 --- a/src/client/views/global/globalCssVariables.scss +++ b/src/client/views/global/globalCssVariables.scss @@ -1,13 +1,16 @@ @import url("https://fonts.googleapis.com/css2?family=Roboto&display=swap"); // colors $white: #ffffff; +$off-white: #fdfdfd; $light-gray: #dfdfdf; $medium-gray: #9f9f9f; $dark-gray: #323232; $black: #000000; $light-blue: #bdddf5; +$light-blue-transparent: #bdddf590; $medium-blue: #4476f7; +$medium-blue-alt: #4476f73d; $pink: #e0217d; $yellow: #f5d747; @@ -15,6 +18,7 @@ $close-red: #e48282; $drop-shadow: "#32323215"; + //padding $minimum-padding: 4px; $medium-padding: 16px; @@ -24,7 +28,8 @@ $large-padding: 32px; $icon-size: 28px; // fonts -$sans-serif: "Roboto", sans-serif; +$sans-serif: "Roboto", +sans-serif; $large-header: 16px; $body-text: 12px; $small-text: 9px; @@ -43,7 +48,14 @@ $radialMenu-zindex: 100000; // context menu shows up over everything // borders $standard-border: solid 1px #9f9f9f; -$searchpanel-height: 32px; +// border radius +$standard-border-radius: 3px; + +// shadow +$standard-box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3); + + +$dashboardselector-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? @@ -52,7 +64,7 @@ $SCHEMA_DIVIDER_WIDTH: 4; $MINIMIZED_ICON_SIZE: 24; $MAX_ROW_HEIGHT: 44px; $DFLT_IMAGE_NATIVE_DIM: 900px; -$MENU_PANEL_WIDTH: 60px; +$LEFT_MENU_WIDTH: 60px; $TREE_BULLET_WIDTH: 20px; :export { @@ -63,8 +75,8 @@ $TREE_BULLET_WIDTH: 20px; MAX_ROW_HEIGHT: $MAX_ROW_HEIGHT; SEARCH_THUMBNAIL_SIZE: $search-thumnail-size; ANTIMODEMENU_HEIGHT: $antimodemenu-height; - SEARCH_PANEL_HEIGHT: $searchpanel-height; + DASHBOARD_SELECTOR_HEIGHT: $dashboardselector-height; DFLT_IMAGE_NATIVE_DIM: $DFLT_IMAGE_NATIVE_DIM; - MENU_PANEL_WIDTH: $MENU_PANEL_WIDTH; + LEFT_MENU_WIDTH: $LEFT_MENU_WIDTH; TREE_BULLET_WIDTH: $TREE_BULLET_WIDTH; -} +}
\ No newline at end of file diff --git a/src/client/views/global/globalCssVariables.scss.d.ts b/src/client/views/global/globalCssVariables.scss.d.ts index 11e62e1eb..59c2b3585 100644 --- a/src/client/views/global/globalCssVariables.scss.d.ts +++ b/src/client/views/global/globalCssVariables.scss.d.ts @@ -7,9 +7,9 @@ interface IGlobalScss { MAX_ROW_HEIGHT: string; SEARCH_THUMBNAIL_SIZE: string; ANTIMODEMENU_HEIGHT: string; - SEARCH_PANEL_HEIGHT: string; + DASHBOARD_SELECTOR_HEIGHT: string; DFLT_IMAGE_NATIVE_DIM: string; - MENU_PANEL_WIDTH: string; + LEFT_MENU_WIDTH: string; TREE_BULLET_WIDTH: string; } declare const globalCssVariables: IGlobalScss; diff --git a/src/client/views/global/globalEnums.tsx b/src/client/views/global/globalEnums.tsx index 2aeb8e338..56779c37c 100644 --- a/src/client/views/global/globalEnums.tsx +++ b/src/client/views/global/globalEnums.tsx @@ -5,6 +5,7 @@ export enum Colors { LIGHT_GRAY = "#DFDFDF", WHITE = "#FFFFFF", MEDIUM_BLUE = "#4476F7", + MEDIUM_BLUE_ALT = "#4476f73d", // REDUCED OPACITY LIGHT_BLUE = "#BDDDF5", PINK = "#E0217D", YELLOW = "#F5D747", @@ -35,4 +36,8 @@ export enum IconSizes { export enum Borders { STANDARD = "solid 1px #9F9F9F" +} + +export enum Shadows { + STANDARD_SHADOW = "0px 3px 4px rgba(0, 0, 0, 0.3)" }
\ No newline at end of file diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx index 240a71c3e..5b5c3cd01 100644 --- a/src/client/views/linking/LinkEditor.tsx +++ b/src/client/views/linking/LinkEditor.tsx @@ -2,13 +2,14 @@ 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, StrListCast } from "../../../fields/Doc"; -import { DateCast, StrCast } from "../../../fields/Types"; +import { Doc, NumListCast, StrListCast, Field } from "../../../fields/Doc"; +import { DateCast, StrCast, Cast } from "../../../fields/Types"; import { LinkManager } from "../../util/LinkManager"; import { undoBatch } from "../../util/UndoManager"; import './LinkEditor.scss'; import { LinkRelationshipSearch } from "./LinkRelationshipSearch"; import React = require("react"); +import { ToString } from "../../../fields/FieldSymbols"; interface LinkEditorProps { @@ -20,7 +21,7 @@ interface LinkEditorProps { @observer export class LinkEditor extends React.Component<LinkEditorProps> { - @observable description = StrCast(LinkManager.currentLink?.description); + @observable description = Field.toString(LinkManager.currentLink?.description as any as Field); @observable relationship = StrCast(LinkManager.currentLink?.linkRelationship); @observable openDropdown: boolean = false; @observable showInfo: boolean = false; @@ -41,14 +42,36 @@ export class LinkEditor extends React.Component<LinkEditorProps> { @undoBatch setRelationshipValue = action((value: string) => { if (LinkManager.currentLink) { + const prevRelationship = LinkManager.currentLink.linkRelationship as string; LinkManager.currentLink.linkRelationship = value; + Doc.GetProto(LinkManager.currentLink).linkRelationship = value; const linkRelationshipList = StrListCast(Doc.UserDoc().linkRelationshipList); + const linkRelationshipSizes = NumListCast(Doc.UserDoc().linkRelationshipSizes); 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)) { + if (!linkRelationshipList?.includes(value)) { linkRelationshipList.push(value); + linkRelationshipSizes.push(1); const randColor = "rgb(" + Math.floor(Math.random() * 255) + "," + Math.floor(Math.random() * 255) + "," + Math.floor(Math.random() * 255) + ")"; - linkColorList.push(randColor); + linkColorList.push(randColor) + // if the relationship is already in the list AND the new rel is different from the prev rel, update the rel sizes + } else if (linkRelationshipList && value != prevRelationship) { + const index = linkRelationshipList.indexOf(value); + //increment size of new relationship size + if (index !== -1 && index < linkRelationshipSizes.length) { + const pvalue = linkRelationshipSizes[index]; + linkRelationshipSizes[index] = (pvalue === undefined || !Number.isFinite(pvalue) ? 1 : pvalue + 1); + } + //decrement the size of the previous relationship if it already exists (i.e. not default 'link' relationship upon link creation) + if (linkRelationshipList.includes(prevRelationship)) { + const pindex = linkRelationshipList.indexOf(prevRelationship); + if (pindex !== -1 && pindex < linkRelationshipSizes.length) { + const pvalue = linkRelationshipSizes[pindex]; + linkRelationshipSizes[pindex] = Math.max(0, (pvalue === undefined || !Number.isFinite(pvalue) ? 1 : pvalue - 1)); + } + } + } this.relationshipButtonColor = "rgb(62, 133, 55)"; setTimeout(action(() => this.relationshipButtonColor = ""), 750); @@ -79,7 +102,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> { @undoBatch setDescripValue = action((value: string) => { if (LinkManager.currentLink) { - LinkManager.currentLink.description = value; + Doc.GetProto(LinkManager.currentLink).description = value; this.buttonColor = "rgb(62, 133, 55)"; setTimeout(action(() => this.buttonColor = ""), 750); return true; @@ -140,6 +163,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> { style={{ width: "100%" }} id="input" value={this.relationship} + autoComplete={"off"} placeholder={"Enter link relationship"} onKeyDown={this.onRelationshipKey} onChange={this.handleRelationshipChange} @@ -168,6 +192,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> { <div className="linkEditor-description-editing"> <input style={{ width: "100%" }} + autoComplete={"off"} id="input" value={this.description} placeholder={"Enter link description"} diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx index cb6571f92..03377ad4e 100644 --- a/src/client/views/linking/LinkMenuGroup.tsx +++ b/src/client/views/linking/LinkMenuGroup.tsx @@ -31,7 +31,6 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { 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; diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index 53a7ae9ab..96cc6d600 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -79,7 +79,9 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { onEdit = (e: React.PointerEvent): void => { LinkManager.currentLink = this.props.linkDoc; setupMoveUpEvents(this, e, e => { - DragManager.StartDocumentDrag([this._editRef.current!], new DragManager.DocumentDragData([this.props.linkDoc]), e.x, e.y); + const dragData = new DragManager.DocumentDragData([this.props.linkDoc], "alias"); + dragData.removeDropProperties = ["hidden"]; + DragManager.StartDocumentDrag([this._editRef.current!], dragData, e.x, e.y); return true; }, emptyFunction, () => this.props.showEditor(this.props.linkDoc)); } @@ -163,19 +165,19 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { <div className="linkMenu-item-buttons" ref={this._buttonRef} > - <Tooltip title={<><div className="dash-tooltip">{this.props.linkDoc.hidden ? "Show Anchor" : "Hide Anchor"}</div></>}> - <div className="button" ref={this._editRef} onPointerDown={this.showAnchor} onClick={e => e.stopPropagation()}> - <FontAwesomeIcon className="fa-icon" icon={this.props.linkDoc.hidden ? "eye-slash" : "eye"} size="sm" /></div> + <Tooltip title={<><div className="dash-tooltip">{this.props.linkDoc.hidden ? "Show Link Anchor" : "Hide Link Anchor"}</div></>}> + <div className="button" ref={this._editRef} style={{ background: this.props.linkDoc.hidden ? "" : "#4476f7" /* $medium-blue */ }} onPointerDown={this.showAnchor} onClick={e => e.stopPropagation()}> + <FontAwesomeIcon className="fa-icon" icon={"eye"} size="sm" /></div> </Tooltip> - <Tooltip title={<><div className="dash-tooltip">{!this.props.linkDoc.linkDisplay ? "Show link" : "Hide link"}</div></>}> - <div className="button" ref={this._editRef} onPointerDown={this.showLink} onClick={e => e.stopPropagation()}> - <FontAwesomeIcon className="fa-icon" icon={!this.props.linkDoc.linkDisplay ? "eye-slash" : "eye"} size="sm" /></div> + <Tooltip title={<><div className="dash-tooltip">{this.props.linkDoc.linkDisplay ? "Hide Link Line" : "Show Link Line"}</div></>}> + <div className="button" ref={this._editRef} style={{ background: this.props.linkDoc.hidden ? "gray" : this.props.linkDoc.linkDisplay ? "#4476f7"/* $medium-blue */ : "" }} onPointerDown={this.showLink} onClick={e => e.stopPropagation()}> + <FontAwesomeIcon className="fa-icon" icon={"project-diagram"} size="sm" /></div> </Tooltip> - <Tooltip title={<><div className="dash-tooltip">{!this.props.linkDoc.linkAutoMove ? "Auto move dot" : "Freeze dot position"}</div></>}> - <div className="button" ref={this._editRef} onPointerDown={this.autoMove} onClick={e => e.stopPropagation()}> - <FontAwesomeIcon className="fa-icon" icon={this.props.linkDoc.linkAutoMove ? "play" : "pause"} size="sm" /></div> + <Tooltip title={<><div className="dash-tooltip">{this.props.linkDoc.linkAutoMove ? "Click to freeze link anchor position" : "Click to auto move link anchor"}</div></>}> + <div className="button" ref={this._editRef} style={{ background: this.props.linkDoc.hidden ? "gray" : !this.props.linkDoc.linkAutoMove ? "" : "#4476f7" /* $medium-blue */ }} onPointerDown={this.autoMove} onClick={e => e.stopPropagation()}> + <FontAwesomeIcon className="fa-icon" icon={"play"} size="sm" /></div> </Tooltip> <Tooltip title={<><div className="dash-tooltip">Edit Link</div></>}> diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss index 3fcb024df..a6494e540 100644 --- a/src/client/views/nodes/AudioBox.scss +++ b/src/client/views/nodes/AudioBox.scss @@ -1,3 +1,6 @@ +@import "../global/globalCssVariables.scss"; + + .audiobox-container, .audiobox-container-interactive { width: 100%; @@ -19,10 +22,12 @@ height: 100%; align-items: center; display: inherit; - background: dimgray; + background: $medium-gray; left: 0px; + color: $dark-gray; + &:hover { - color: white; + color: $black; cursor: pointer; } } @@ -44,9 +49,17 @@ .audiobox-record-interactive, .audiobox-record { pointer-events: all; + cursor: pointer; width: 100%; height: 100%; position: relative; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 10px; + color: white; + font-weight: bold; } .audiobox-record { @@ -61,25 +74,37 @@ position: relative; padding-right: 5px; display: flex; - background-color: red; + flex-direction: row; + justify-content: center; + align-items: center; + gap: 7px; + background-color: $medium-blue; + padding: 10px; .time { position: relative; height: 100%; width: 100%; - font-size: 20; + font-size: 16px; text-align: center; - top: 5; + display: flex; + justify-content: center; + align-items: center; + font-weight: bold; } .buttons { + cursor: pointer; position: relative; margin-top: auto; margin-bottom: auto; width: 25px; + width: 25px; padding: 5px; - &:hover{ - background-color: crimson; + color: $dark-gray; + + &:hover { + color: $black; } } } @@ -89,16 +114,15 @@ height: 100%; position: relative; display: flex; - padding-left: 2px; - background: black; + background: $dark-gray; .audiobox-dictation { position: absolute; - width: 30px; + width: 40px; height: 100%; align-items: center; display: inherit; - background: dimgray; + background: $medium-gray; left: 0px; } @@ -109,20 +133,32 @@ position: relative; padding-right: 5px; display: flex; + flex-direction: column; + justify-content: center; - .audiobox-playhead { + .audiobox-buttons { position: relative; margin-top: auto; margin-bottom: auto; - margin-right: 2px; - height: 25px; - padding: 2px; + width: 30px; + height: 30px; border-radius: 50%; - background-color: black; - color: white; + background-color: $dark-gray; + color: $white; + display: flex; + align-items: center; + justify-content: center; + left: 5px; + &:hover { - background-color: grey; - color: lightgrey; + background-color: $black; + } + + svg { + width: 100%; + position: absolute; + border-width: "thin"; + border-color: "white"; } } @@ -131,30 +167,29 @@ margin-top: auto; margin-bottom: auto; width: 25px; - padding: 2px; align-items: center; display: inherit; - background: dimgray; + background: $medium-gray; } .audiobox-timeline { position: absolute; width: 100%; - border: gray solid 1px; - border-radius: 3px; z-index: 1000; overflow: hidden; + border-right: 5px solid black; } .audioBox-total-time, .audioBox-current-time { position: absolute; - font-size: 8; + font-size: $small-text; top: 100%; - color: white; + color: $white; } + .audioBox-current-time { - left: 30px; + left: 42px; } .audioBox-total-time { @@ -164,7 +199,6 @@ } } - @media only screen and (max-device-width: 480px) { .audiobox-dictation { font-size: 5em; @@ -180,9 +214,9 @@ font-size: 3em; } - .audiobox-container .audiobox-controls .audiobox-player .audiobox-playhead, + .audiobox-container .audiobox-controls .audiobox-player .audiobox-buttons, .audiobox-container .audiobox-controls .audiobox-player .audiobox-dictation, - .audiobox-container-interactive .audiobox-controls .audiobox-player .audiobox-playhead { + .audiobox-container-interactive .audiobox-controls .audiobox-player .audiobox-buttons { width: 70px; } }
\ No newline at end of file diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index faaa887c4..c79828470 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -1,6 +1,13 @@ import React = require("react"); import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; +import { + action, + computed, + IReactionDisposer, + observable, + reaction, + runInAction, +} from "mobx"; import { observer } from "mobx-react"; import { DateField } from "../../../fields/DateField"; import { Doc, DocListCast, Opt } from "../../../fields/Doc"; @@ -9,7 +16,7 @@ import { makeInterface } from "../../../fields/Schema"; import { ComputedField } from "../../../fields/ScriptField"; import { Cast, NumCast } from "../../../fields/Types"; import { AudioField, nullAudio } from "../../../fields/URLField"; -import { emptyFunction, formatTime, Utils } from "../../../Utils"; +import { emptyFunction, formatTime } from "../../../Utils"; import { DocUtils } from "../../documents/Documents"; import { Networking } from "../../Network"; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; @@ -17,23 +24,34 @@ import { SnappingManager } from "../../util/SnappingManager"; import { CollectionStackedTimeline } from "../collections/CollectionStackedTimeline"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; -import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from "../DocComponent"; +import { + ViewBoxAnnotatableComponent, + ViewBoxAnnotatableProps, +} from "../DocComponent"; import "./AudioBox.scss"; -import { FieldView, FieldViewProps } from './FieldView'; +import { FieldView, FieldViewProps } from "./FieldView"; import { LinkDocPreview } from "./LinkDocPreview"; +import { faLessThan } from "@fortawesome/free-solid-svg-icons"; +import { Colors } from "../global/globalEnums"; + declare class MediaRecorder { - constructor(e: any); // whatever MediaRecorder has + constructor(e: any); // whatever MediaRecorder has } type AudioDocument = makeInterface<[typeof documentSchema]>; const AudioDocument = makeInterface(documentSchema); @observer -export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps, AudioDocument>(AudioDocument) { - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(AudioBox, fieldKey); } +export class AudioBox extends ViewBoxAnnotatableComponent< + ViewBoxAnnotatableProps & FieldViewProps, + AudioDocument +>(AudioDocument) { + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(AudioBox, fieldKey); + } public static Enabled = false; - static playheadWidth = 30; // width of playhead - static heightPercent = 80; // height of timeline in percent of height of audioBox. + static playheadWidth = 40; // width of playhead + static heightPercent = 75; // height of timeline in percent of height of audioBox. static Instance: AudioBox; _disposers: { [name: string]: IReactionDisposer } = {}; @@ -47,35 +65,82 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp _stream: MediaStream | undefined; _start: number = 0; _play: any = null; + _ended: boolean = false; @observable static _scrubTime = 0; @observable _markerEnd: number = 0; @observable _position: number = 0; @observable _waveHeight: Opt<number> = this.layoutDoc._height; @observable _paused: boolean = false; - @computed get mediaState(): undefined | "pendingRecording" | "recording" | "paused" | "playing" { return this.dataDoc.mediaState as (undefined | "pendingRecording" | "recording" | "paused" | "playing"); } - set mediaState(value) { this.dataDoc.mediaState = value; } - public static SetScrubTime = action((timeInMillisFrom1970: number) => { AudioBox._scrubTime = 0; AudioBox._scrubTime = timeInMillisFrom1970; }); - @computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); } - @computed get duration() { return NumCast(this.dataDoc[`${this.fieldKey}-duration`]); } - @computed get anchorDocs() { return DocListCast(this.dataDoc[this.annotationKey]); } - @computed get links() { return DocListCast(this.dataDoc.links); } - @computed get pauseTime() { return this._pauseEnd - this._pauseStart; } // total time paused to update the correct time - @computed get heightPercent() { return AudioBox.heightPercent; } + @observable _trimming: boolean = false; + @observable _trimStart: number = NumCast(this.layoutDoc.clipStart) ? NumCast(this.layoutDoc.clipStart) : 0; + @observable _trimEnd: number = NumCast(this.layoutDoc.clipEnd) ? NumCast(this.layoutDoc.clipEnd) + : this.duration; + + @computed get mediaState(): + | undefined + | "pendingRecording" + | "recording" + | "paused" + | "playing" { + return this.dataDoc.mediaState as + | undefined + | "pendingRecording" + | "recording" + | "paused" + | "playing"; + } + set mediaState(value) { + this.dataDoc.mediaState = value; + } + public static SetScrubTime = action((timeInMillisFrom1970: number) => { + AudioBox._scrubTime = 0; + AudioBox._scrubTime = timeInMillisFrom1970; + }); + @computed get recordingStart() { + return Cast( + this.dataDoc[this.props.fieldKey + "-recordingStart"], + DateField + )?.date.getTime(); + } + @computed get duration() { + return NumCast(this.dataDoc[`${this.fieldKey}-duration`]); + } + @computed get trimDuration() { + return this._trimming && this._trimEnd ? this.duration : this._trimEnd - this._trimStart; + } + @computed get anchorDocs() { + return DocListCast(this.dataDoc[this.annotationKey]); + } + @computed get links() { + return DocListCast(this.dataDoc.links); + } + @computed get pauseTime() { + return this._pauseEnd - this._pauseStart; + } // total time paused to update the correct time + @computed get heightPercent() { + return AudioBox.heightPercent; + } constructor(props: Readonly<ViewBoxAnnotatableProps & FieldViewProps>) { super(props); AudioBox.Instance = this; if (this.duration === undefined) { - runInAction(() => this.Document[this.fieldKey + "-duration"] = this.Document.duration); + runInAction( + () => + (this.Document[this.fieldKey + "-duration"] = this.Document.duration) + ); } } getLinkData(l: Doc) { let la1 = l.anchor1 as Doc; let la2 = l.anchor2 as Doc; - const linkTime = this._stackedTimeline.current?.anchorStart(la2) || this._stackedTimeline.current?.anchorStart(la1) || 0; + const linkTime = + this._stackedTimeline.current?.anchorStart(la2) || + this._stackedTimeline.current?.anchorStart(la1) || + 0; if (Doc.AreProtosEqual(la1, this.dataDoc)) { la1 = l.anchor2 as Doc; la2 = l.anchor1 as Doc; @@ -84,16 +149,26 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } getAnchor = () => { - return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, - "_timecodeToShow" /* audioStart */, "_timecodeToHide" /* audioEnd */, this._ele?.currentTime || - Cast(this.props.Document._currentTimecode, "number", null) || (this.mediaState === "recording" ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined)) - || this.rootDoc; + return ( + CollectionStackedTimeline.createAnchor( + this.rootDoc, + this.dataDoc, + this.annotationKey, + "_timecodeToShow" /* audioStart */, + "_timecodeToHide" /* audioEnd */, + this._ele?.currentTime || + Cast(this.props.Document._currentTimecode, "number", null) || + (this.mediaState === "recording" + ? (Date.now() - (this.recordingStart || 0)) / 1000 + : undefined) + ) || this.rootDoc + ); } componentWillUnmount() { - Object.values(this._disposers).forEach(disposer => disposer?.()); + Object.values(this._disposers).forEach((disposer) => disposer?.()); const ind = DocUtils.ActiveRecordings.indexOf(this); - ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1)); + ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1); } @action @@ -102,39 +177,68 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this.mediaState = this.path ? "paused" : undefined; + this.layoutDoc.clipStart = this.layoutDoc.clipStart ? this.layoutDoc.clipStart : 0; + this.layoutDoc.clipEnd = this.layoutDoc.clipEnd ? this.layoutDoc.clipEnd : this.duration ? this.duration : undefined; + + this.path && this.setAnchorTime(NumCast(this.layoutDoc.clipStart)); + this.path && this.timecodeChanged(); + this._disposers.triggerAudio = reaction( - () => !LinkDocPreview.LinkInfo && this.props.renderDepth !== -1 ? NumCast(this.Document._triggerAudio, null) : undefined, - start => start !== undefined && setTimeout(() => { - this.playFrom(start); + () => + !LinkDocPreview.LinkInfo && this.props.renderDepth !== -1 + ? NumCast(this.Document._triggerAudio, null) + : undefined, + (start) => + start !== undefined && setTimeout(() => { - this.Document._currentTimecode = start; - this.Document._triggerAudio = undefined; - }, 10); - }), // wait for mainCont and try again to play + this.playFrom(start); + setTimeout(() => { + this.Document._currentTimecode = start; + this.Document._triggerAudio = undefined; + }, 10); + }), // wait for mainCont and try again to play { fireImmediately: true } ); this._disposers.audioStop = reaction( - () => this.props.renderDepth !== -1 && !LinkDocPreview.LinkInfo ? Cast(this.Document._audioStop, "number", null) : undefined, - audioStop => audioStop !== undefined && setTimeout(() => { - this.Pause(); - setTimeout(() => this.Document._audioStop = undefined, 10); - }), // wait for mainCont and try again to play + () => + this.props.renderDepth !== -1 && !LinkDocPreview.LinkInfo + ? Cast(this.Document._audioStop, "number", null) + : undefined, + (audioStop) => + audioStop !== undefined && + setTimeout(() => { + this.Pause(); + setTimeout(() => (this.Document._audioStop = undefined), 10); + }), // wait for mainCont and try again to play { fireImmediately: true } ); } // for updating the timecode + @action timecodeChanged = () => { const htmlEle = this._ele; if (this.mediaState !== "recording" && htmlEle) { - htmlEle.duration && htmlEle.duration !== Infinity && runInAction(() => this.dataDoc[this.fieldKey + "-duration"] = htmlEle.duration); - this.links.map(l => this.getLinkData(l)).forEach(({ la1, la2, linkTime }) => { - if (linkTime > NumCast(this.layoutDoc._currentTimecode) && linkTime < htmlEle.currentTime) { - Doc.linkFollowHighlight(la1); - } - }); + htmlEle.duration && + htmlEle.duration !== Infinity && + runInAction( + () => (this.dataDoc[this.fieldKey + "-duration"] = htmlEle.duration) + ); + this.layoutDoc.clipEnd = this.layoutDoc.clipEnd ? Math.min(this.duration, NumCast(this.layoutDoc.clipEnd)) : this.duration; + this._trimEnd = this._trimEnd ? Math.min(this.duration, this._trimEnd) : this.duration; + this.links + .map((l) => this.getLinkData(l)) + .forEach(({ la1, la2, linkTime }) => { + if ( + linkTime > NumCast(this.layoutDoc._currentTimecode) && + linkTime < htmlEle.currentTime + ) { + Doc.linkFollowHighlight(la1); + } + }); this.layoutDoc._currentTimecode = htmlEle.currentTime; + } } @@ -146,12 +250,13 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // play audio for documents created during recording playFromTime = (absoluteTime: number) => { - this.recordingStart && this.playFrom((absoluteTime - this.recordingStart) / 1000); + this.recordingStart && + this.playFrom((absoluteTime - this.recordingStart) / 1000); } // play back the audio from time @action - playFrom = (seekTimeInSeconds: number, endTime: number = this.duration) => { + playFrom = (seekTimeInSeconds: number, endTime: number = this._trimEnd, fullPlay: boolean = false) => { clearTimeout(this._play); if (Number.isNaN(this._ele?.duration)) { setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500); @@ -162,12 +267,20 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } else { this.Pause(); } - } else if (seekTimeInSeconds <= this._ele.duration) { - this._ele.currentTime = seekTimeInSeconds; + } else if (this._trimStart <= endTime && seekTimeInSeconds <= this._trimEnd) { + const start = Math.max(this._trimStart, seekTimeInSeconds); + const end = Math.min(this._trimEnd, endTime); + this._ele.currentTime = start; this._ele.play(); - runInAction(() => this.mediaState = "playing"); + runInAction(() => (this.mediaState = "playing")); if (endTime !== this.duration) { - this._play = setTimeout(() => this.Pause(), (endTime - seekTimeInSeconds) * 1000); // use setTimeout to play a specific duration + this._play = setTimeout( + () => { + this._ended = fullPlay ? true : this._ended; + this.Pause(); + }, + (end - start) * 1000 + ); // use setTimeout to play a specific duration } } else { this.Pause(); @@ -182,7 +295,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp if (this._paused) { this._pausedTime += (new Date().getTime() - this._recordStart) / 1000; } else { - this.layoutDoc._currentTimecode = (new Date().getTime() - this._recordStart - this.pauseTime) / 1000; + this.layoutDoc._currentTimecode = + (new Date().getTime() - this._recordStart - this.pauseTime) / 1000; } } } @@ -191,7 +305,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp recordAudioAnnotation = async () => { this._stream = await navigator.mediaDevices.getUserMedia({ audio: true }); this._recorder = new MediaRecorder(this._stream); - this.dataDoc[this.props.fieldKey + "-recordingStart"] = new DateField(new Date()); + this.dataDoc[this.props.fieldKey + "-recordingStart"] = new DateField( + new Date() + ); DocUtils.ActiveRecordings.push(this); this._recorder.ondataavailable = async (e: any) => { const [{ result }] = await Networking.UploadFilesToServer(e.data); @@ -200,7 +316,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } }; this._recordStart = new Date().getTime(); - runInAction(() => this.mediaState = "recording"); + runInAction(() => (this.mediaState = "recording")); setTimeout(this.updateRecordTime, 0); this._recorder.start(); setTimeout(() => this._recorder && this.stopRecording(), 60 * 60 * 1000); // stop after an hour @@ -209,21 +325,49 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // context menu specificContextMenu = (e: React.MouseEvent): void => { const funcs: ContextMenuProps[] = []; - funcs.push({ description: (this.layoutDoc.hideAnchors ? "Don't hide" : "Hide") + " anchors", event: () => this.layoutDoc.hideAnchors = !this.layoutDoc.hideAnchors, icon: "expand-arrows-alt" }); - funcs.push({ description: (this.layoutDoc.dontAutoPlayFollowedLinks ? "" : "Don't") + " play when link is selected", event: () => this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks, icon: "expand-arrows-alt" }); - funcs.push({ description: (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : "Auto play") + " anchors onClick", event: () => this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors, icon: "expand-arrows-alt" }); - ContextMenu.Instance?.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); + funcs.push({ + description: + (this.layoutDoc.hideAnchors ? "Don't hide" : "Hide") + " anchors", + event: () => (this.layoutDoc.hideAnchors = !this.layoutDoc.hideAnchors), + icon: "expand-arrows-alt", + }); + funcs.push({ + description: + (this.layoutDoc.dontAutoPlayFollowedLinks ? "" : "Don't") + + " play when link is selected", + event: () => + (this.layoutDoc.dontAutoPlayFollowedLinks = + !this.layoutDoc.dontAutoPlayFollowedLinks), + icon: "expand-arrows-alt", + }); + funcs.push({ + description: + (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : "Auto play") + + " anchors onClick", + event: () => + (this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors), + icon: "expand-arrows-alt", + }); + ContextMenu.Instance?.addItem({ + description: "Options...", + subitems: funcs, + icon: "asterisk", + }); } // stops the recording stopRecording = action(() => { this._recorder.stop(); this._recorder = undefined; - this.dataDoc[this.fieldKey + "-duration"] = (new Date().getTime() - this._recordStart - this.pauseTime) / 1000; + this.dataDoc[this.fieldKey + "-duration"] = + (new Date().getTime() - this._recordStart - this.pauseTime) / 1000; this.mediaState = "paused"; + this._trimEnd = this.duration; + this.layoutDoc.clipStart = 0; + this.layoutDoc.clipEnd = this.duration; this._stream?.getAudioTracks()[0].stop(); const ind = DocUtils.ActiveRecordings.indexOf(this); - ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1)); + ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1); }); // button for starting and stopping the recording @@ -236,17 +380,37 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // for play button Play = (e?: any) => { - this.playFrom(this._ele!.paused ? this._ele!.currentTime : -1); + let start; + if (this._ended || this._ele!.currentTime === this.duration) { + start = this._trimStart; + this._ended = false; + } + else { + start = this._ele!.currentTime; + } + + this.playFrom(start, this._trimEnd, true); e?.stopPropagation?.(); } // creates a text document for dictation onFile = (e: any) => { - const newDoc = CurrentUserUtils.GetNewTextDoc("", NumCast(this.props.Document.x), NumCast(this.props.Document.y) + NumCast(this.props.Document._height) + 10, - NumCast(this.props.Document._width), 2 * NumCast(this.props.Document._height)); + const newDoc = CurrentUserUtils.GetNewTextDoc( + "", + NumCast(this.props.Document.x), + NumCast(this.props.Document.y) + + NumCast(this.props.Document._height) + + 10, + NumCast(this.props.Document._width), + 2 * NumCast(this.props.Document._height) + ); Doc.GetProto(newDoc).recordingSource = this.dataDoc; - Doc.GetProto(newDoc).recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.props.fieldKey}-recordingStart"]`); - Doc.GetProto(newDoc).mediaState = ComputedField.MakeFunction("self.recordingSource.mediaState"); + Doc.GetProto(newDoc).recordingStart = ComputedField.MakeFunction( + `self.recordingSource["${this.props.fieldKey}-recordingStart"]` + ); + Doc.GetProto(newDoc).mediaState = ComputedField.MakeFunction( + "self.recordingSource.mediaState" + ); this.props.addDocument?.(newDoc); e.stopPropagation(); } @@ -261,7 +425,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // returns the path of the audio file @computed get path() { const field = Cast(this.props.Document[this.props.fieldKey], AudioField); - const path = (field instanceof AudioField) ? field.url.href : ""; + const path = field instanceof AudioField ? field.url.href : ""; return path === nullAudio ? "" : path; } @@ -295,98 +459,256 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp playLink = (link: Doc) => { const stack = this._stackedTimeline.current; if (link.annotationOn === this.rootDoc) { - if (!this.layoutDoc.dontAutoPlayFollowedLinks) this.playFrom(stack?.anchorStart(link) || 0, stack?.anchorEnd(link)); - else this._ele!.currentTime = this.layoutDoc._currentTimecode = (stack?.anchorStart(link) || 0); + if (!this.layoutDoc.dontAutoPlayFollowedLinks) { + this.playFrom(stack?.anchorStart(link) || 0, stack?.anchorEnd(link)); + } else { + this._ele!.currentTime = this.layoutDoc._currentTimecode = + stack?.anchorStart(link) || 0; + } + } else { + this.links + .filter((l) => l.anchor1 === link || l.anchor2 === link) + .forEach((l) => { + const { la1, la2 } = this.getLinkData(l); + const startTime = stack?.anchorStart(la1) || stack?.anchorStart(la2); + const endTime = stack?.anchorEnd(la1) || stack?.anchorEnd(la2); + if (startTime !== undefined) { + if (!this.layoutDoc.dontAutoPlayFollowedLinks) { + endTime + ? this.playFrom(startTime, endTime) + : this.playFrom(startTime); + } else { + this._ele!.currentTime = this.layoutDoc._currentTimecode = + startTime; + } + } + }); } - else { - this.links.filter(l => l.anchor1 === link || l.anchor2 === link).forEach(l => { - const { la1, la2 } = this.getLinkData(l); - const startTime = stack?.anchorStart(la1) || stack?.anchorStart(la2); - const endTime = stack?.anchorEnd(la1) || stack?.anchorEnd(la2); - if (startTime !== undefined) { - if (!this.layoutDoc.dontAutoPlayFollowedLinks) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime); - else this._ele!.currentTime = this.layoutDoc._currentTimecode = startTime; - } - }); + } + + // shows trim controls + @action + startTrim = () => { + if (!this.duration) { + this.timecodeChanged(); } + if (this.mediaState === "playing") { + this.Pause(); + } + this._trimming = true; + } + + // hides trim controls and displays new clip + @action + finishTrim = () => { + if (this.mediaState === "playing") { + this.Pause(); + } + this.layoutDoc.clipStart = this._trimStart; + this.layoutDoc.clipEnd = this._trimEnd; + this._trimming = false; + this.setAnchorTime(Math.max(Math.min(this._trimEnd, this._ele!.currentTime), this._trimStart)); + } + + @action + setStartTrim = (newStart: number) => { + this._trimStart = newStart; + } + + @action + setEndTrim = (newEnd: number) => { + this._trimEnd = newEnd; } isActiveChild = () => this._isAnyChildContentActive; - timelineWhenChildContentsActiveChanged = (isActive: boolean) => this.props.whenChildContentsActiveChanged(runInAction(() => this._isAnyChildContentActive = isActive)); - timelineScreenToLocal = () => this.props.ScreenToLocalTransform().translate(-AudioBox.playheadWidth, -(100 - this.heightPercent) / 200 * this.props.PanelHeight()); - setAnchorTime = (time: number) => this._ele!.currentTime = this.layoutDoc._currentTimecode = time; - timelineHeight = () => this.props.PanelHeight() * this.heightPercent / 100 * this.heightPercent / 100; // panelHeight * heightPercent is player height. * heightPercent is timeline height (as per css inline) + timelineWhenChildContentsActiveChanged = (isActive: boolean) => + this.props.whenChildContentsActiveChanged( + runInAction(() => (this._isAnyChildContentActive = isActive)) + ) + timelineScreenToLocal = () => + this.props + .ScreenToLocalTransform() + .translate( + -AudioBox.playheadWidth, + (-(100 - this.heightPercent) / 200) * this.props.PanelHeight() + ) + setAnchorTime = (time: number) => { + (this._ele!.currentTime = this.layoutDoc._currentTimecode = time); + } + + timelineHeight = () => + (((this.props.PanelHeight() * this.heightPercent) / 100) * + this.heightPercent) / + 100 // panelHeight * heightPercent is player height. * heightPercent is timeline height (as per css inline) timelineWidth = () => this.props.PanelWidth() - AudioBox.playheadWidth; @computed get renderTimeline() { - return <CollectionStackedTimeline ref={this._stackedTimeline} {...this.props} - fieldKey={this.annotationKey} - dictationKey={this.fieldKey + "-dictation"} - mediaPath={this.path} - renderDepth={this.props.renderDepth + 1} - startTag={"_timecodeToShow" /* audioStart */} - endTag={"_timecodeToHide" /* audioEnd */} - focus={DocUtils.DefaultFocus} - bringToFront={emptyFunction} - CollectionView={undefined} - isAnyChildContentActive={this.isAnyChildContentActive} - duration={this.duration} - playFrom={this.playFrom} - setTime={this.setAnchorTime} - playing={this.playing} - whenChildContentsActiveChanged={this.timelineWhenChildContentsActiveChanged} - removeDocument={this.removeDocument} - ScreenToLocalTransform={this.timelineScreenToLocal} - Play={this.Play} - Pause={this.Pause} - playLink={this.playLink} - PanelWidth={this.timelineWidth} - PanelHeight={this.timelineHeight} - />; + return ( + <CollectionStackedTimeline + ref={this._stackedTimeline} + {...this.props} + fieldKey={this.annotationKey} + dictationKey={this.fieldKey + "-dictation"} + mediaPath={this.path} + renderDepth={this.props.renderDepth + 1} + startTag={"_timecodeToShow" /* audioStart */} + endTag={"_timecodeToHide" /* audioEnd */} + focus={DocUtils.DefaultFocus} + bringToFront={emptyFunction} + CollectionView={undefined} + duration={this.duration} + playFrom={this.playFrom} + setTime={this.setAnchorTime} + playing={this.playing} + whenChildContentsActiveChanged={ + this.timelineWhenChildContentsActiveChanged + } + moveDocument={this.moveDocument} + addDocument={this.addDocument} + removeDocument={this.removeDocument} + ScreenToLocalTransform={this.timelineScreenToLocal} + Play={this.Play} + Pause={this.Pause} + isContentActive={this.props.isContentActive} + isAnyChildContentActive={this.isAnyChildContentActive} + playLink={this.playLink} + PanelWidth={this.timelineWidth} + PanelHeight={this.timelineHeight} + trimming={this._trimming} + trimStart={this._trimStart} + trimEnd={this._trimEnd} + trimDuration={this.trimDuration} + setStartTrim={this.setStartTrim} + setEndTrim={this.setEndTrim} + /> + ); } render() { - const interactive = SnappingManager.GetIsDragging() || this.props.isContentActive() ? "-interactive" : ""; - return <div className="audiobox-container" - onContextMenu={this.specificContextMenu} - onClick={!this.path && !this._recorder ? this.recordAudioAnnotation : undefined} - style={{ pointerEvents: this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined }}> - {!this.path ? - <div className="audiobox-buttons"> - <div className="audiobox-dictation" onClick={this.onFile}> - <FontAwesomeIcon style={{ width: "30px", background: !this.layoutDoc.dontAutoPlayFollowedLinks ? "yellow" : "rgba(0,0,0,0)" }} icon="file-alt" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /> - </div> - {this.mediaState === "recording" || this.mediaState === "paused" ? - <div className="recording" onClick={e => e.stopPropagation()}> - <div className="buttons" onClick={this.recordClick}> - <FontAwesomeIcon icon={"stop"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /> + const interactive = + SnappingManager.GetIsDragging() || this.props.isContentActive() + ? "-interactive" + : ""; + return ( + <div + className="audiobox-container" + onContextMenu={this.specificContextMenu} + onClick={ + !this.path && !this._recorder ? this.recordAudioAnnotation : undefined + } + style={{ + pointerEvents: + this.props.layerProvider?.(this.layoutDoc) === false + ? "none" + : undefined, + }} + > + {!this.path ? ( + <div className="audiobox-buttons"> + <div className="audiobox-dictation" onClick={this.onFile}> + <FontAwesomeIcon + style={{ + width: "30px" + }} + icon="file-alt" + size={this.props.PanelHeight() < 36 ? "1x" : "2x"} + /> + </div> + {this.mediaState === "recording" || this.mediaState === "paused" ? ( + <div className="recording" onClick={(e) => e.stopPropagation()}> + <div className="recording-buttons" onClick={this.recordClick}> + <FontAwesomeIcon + icon={"stop"} + size={this.props.PanelHeight() < 36 ? "1x" : "2x"} + /> + </div> + <div + className="recording-buttons" + onClick={this._paused ? this.recordPlay : this.recordPause} + > + <FontAwesomeIcon + icon={this._paused ? "play" : "pause"} + size={this.props.PanelHeight() < 36 ? "1x" : "2x"} + /> + </div> + <div className="time"> + {formatTime( + Math.round(NumCast(this.layoutDoc._currentTimecode)) + )} + </div> </div> - <div className="buttons" onClick={this._paused ? this.recordPlay : this.recordPause}> - <FontAwesomeIcon icon={this._paused ? "play" : "pause"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /> + ) : ( + <div + className={`audiobox-record${interactive}`} + style={{ backgroundColor: Colors.DARK_GRAY }} + > + <FontAwesomeIcon icon="microphone" /> + RECORD + </div> + )} + </div> + ) : ( + <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-buttons" + title={this.mediaState === "paused" ? "play" : "pause"} + onClick={this.mediaState === "paused" ? this.Play : this.Pause} + > + {" "} + <FontAwesomeIcon + icon={this.mediaState === "paused" ? "play" : "pause"} + size={"1x"} + /> + </div> + <div + className="audiobox-buttons" + title={this._trimming ? "finish" : "trim"} + onClick={this._trimming ? this.finishTrim : this.startTrim} + > + <FontAwesomeIcon + icon={this._trimming ? "check" : "cut"} + size={"1x"} + /> + </div> + <div + className="audiobox-timeline" + style={{ + top: 0, + height: `100%`, + left: AudioBox.playheadWidth, + width: `calc(100% - ${AudioBox.playheadWidth}px)`, + background: "white", + }} + > + {this.renderTimeline} + </div> + {this.audio} + <div className="audioBox-current-time"> + {this._trimming ? + formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode))) + : formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode) - NumCast(this._trimStart)))} + </div> + <div className="audioBox-total-time"> + {this._trimming || !this._trimEnd ? + formatTime(Math.round(NumCast(this.duration))) + : formatTime(Math.round(NumCast(this.trimDuration)))} + </div> </div> - <div className="time">{formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode)))}</div> </div> - : - <button className={`audiobox-record${interactive}`} style={{ backgroundColor: "black" }}> - RECORD - </button>} - </div> : - <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> - <div className="audiobox-timeline" style={{ top: 0, height: `100%`, left: AudioBox.playheadWidth, width: `calc(100% - ${AudioBox.playheadWidth}px)`, background: "white" }}> - {this.renderTimeline} - </div> - {this.audio} - <div className="audioBox-current-time"> - {formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode)))} - </div> - <div className="audioBox-total-time"> - {formatTime(Math.round(this.duration))} - </div> - </div> - </div> - } - </div>; + )} + </div> + ); } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 9cc4b1f9a..fe34d6687 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -7,7 +7,7 @@ import { listSpec } from "../../../fields/Schema"; import { ComputedField } from "../../../fields/ScriptField"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { TraceMobx } from "../../../fields/util"; -import { numberRange } from "../../../Utils"; +import { DashColor, numberRange } from "../../../Utils"; import { DocumentManager } from "../../util/DocumentManager"; import { SelectionManager } from "../../util/SelectionManager"; import { Transform } from "../../util/Transform"; @@ -18,7 +18,6 @@ import { StyleProp } from "../StyleProvider"; import "./CollectionFreeFormDocumentView.scss"; import { DocumentView, DocumentViewProps } from "./DocumentView"; 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; @@ -165,7 +164,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF 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); + const mixBlendMode = StrCast(this.layoutDoc.mixBlendMode) as any || (typeof background === "string" && DashColor(background).alpha() !== 1 ? "multiply" : undefined); return <div className={"collectionFreeFormDocumentView-container"} style={{ outline: this.Highlight ? "orange solid 2px" : "", diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss index 44cf5d046..660045a6f 100644 --- a/src/client/views/nodes/ComparisonBox.scss +++ b/src/client/views/nodes/ComparisonBox.scss @@ -6,6 +6,7 @@ position: relative; z-index: 0; pointer-events: none; + display: flex; .clip-div { position: absolute; diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index 6708a08ee..0f962f95c 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -15,6 +15,7 @@ import "./ComparisonBox.scss"; import { DocumentView, DocumentViewProps } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import React = require("react"); +import { DocumentType } from '../../documents/DocumentTypes'; export const comparisonSchema = createSchema({}); @@ -49,7 +50,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl } private registerSliding = (e: React.PointerEvent<HTMLDivElement>, targetWidth: number) => { - setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, action(() => { + e.button !== 2 && setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, action(() => { // on click, animate slider movement to the targetWidth this._animating = "all 200ms"; this.layoutDoc._clipWidth = targetWidth * 100 / this.props.PanelWidth(); @@ -87,14 +88,21 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl </div>; }; const displayDoc = (which: string) => { - const whichDoc = Cast(this.dataDoc[`compareBox-${which}`], Doc, null); + var whichDoc = Cast(this.dataDoc[which], Doc, null); + //if (whichDoc?.type === DocumentType.MARKER) + const targetDoc = Cast(whichDoc?.annotationOn, Doc, null) ?? whichDoc; return whichDoc ? <> - <DocumentView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} + <DocumentView + ref={(r) => { + whichDoc !== targetDoc && r?.focus(targetDoc); + }} + {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} isContentActive={returnFalse} isDocumentActive={returnFalse} styleProvider={this.docStyleProvider} - Document={whichDoc} + Document={targetDoc} DataDoc={undefined} + hideLinkButton={true} pointerEvents={"none"} /> {clearButton(which)} </> : // placeholder image if doc is missing @@ -105,16 +113,16 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl const displayBox = (which: string, index: number, cover: number) => { return <div className={`${which}Box-cont`} key={which} style={{ width: this.props.PanelWidth() }} onPointerDown={e => this.registerSliding(e, cover)} - ref={ele => this.createDropTarget(ele, `compareBox-${which}`, index)} > + ref={ele => this.createDropTarget(ele, which, index)} > {displayDoc(which)} </div>; }; return ( <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)} + {displayBox(this.fieldKey === "data" ? "compareBox-after" : `${this.fieldKey}2`, 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)} + {displayBox(this.fieldKey === "data" ? "compareBox-before" : `${this.fieldKey}1`, 0, 0)} </div> <div className="slide-bar" style={{ left: `calc(${clipWidth} - 0.5px)`, cursor: NumCast(this.layoutDoc._clipWidth) < 5 ? "e-resize" : NumCast(this.layoutDoc._clipWidth) / 100 > (this.props.PanelWidth() - 5) / this.props.PanelWidth() ? "w-resize" : undefined }} diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 3d2cdf5a4..dbab5e762 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -23,7 +23,7 @@ import "./DocumentView.scss"; import { EquationBox } from "./EquationBox"; import { FieldView, FieldViewProps } from "./FieldView"; import { FilterBox } from "./FilterBox"; -import { FontIconBox } from "./FontIconBox"; +import { FontIconBox } from "./button/FontIconBox"; import { FormattedTextBox, FormattedTextBoxProps } from "./formattedText/FormattedTextBox"; import { FunctionPlotBox } from "./FunctionPlotBox"; import { ImageBox } from "./ImageBox"; @@ -113,7 +113,6 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & Fo scaling?: () => number, setHeight: (height: number) => void, layoutKey: string, - hideOnLeave?: boolean, }> { @computed get layout(): string { TraceMobx(); @@ -201,7 +200,8 @@ 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, scale: "number", value: "string" }); + const script = code[1].value.replace(/^‘/, "").replace(/’$/, ""); // ‘’ are not valid quotes in javascript so get rid of them -- they may be present to make it easier to write complex scripts - see headerTemplate in currentUserUtils.ts + return ScriptField.MakeScript(script, { 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 b37b68249..228e1bdcb 100644 --- a/src/client/views/nodes/DocumentLinksButton.scss +++ b/src/client/views/nodes/DocumentLinksButton.scss @@ -50,6 +50,7 @@ width: 80%; height: 80%; font-size: 100%; + font-family: 'Roboto'; transition: 0.2s ease all; &:hover { diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index 7648e866e..93cd02d93 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -2,25 +2,24 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Tooltip } from "@material-ui/core"; import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast, Opt, WidthSym, DocListCastAsync } from "../../../fields/Doc"; -import { emptyFunction, setupMoveUpEvents, returnFalse, Utils, emptyPath } from "../../../Utils"; +import { Doc, DocListCast, DocListCastAsync, Opt, WidthSym } from "../../../fields/Doc"; +import { Id } from "../../../fields/FieldSymbols"; +import { Cast, StrCast } from "../../../fields/Types"; import { TraceMobx } from "../../../fields/util"; -import { DocUtils, Docs } from "../../documents/Documents"; +import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../../Utils"; +import { DocServer } from "../../DocServer"; +import { Docs, DocUtils } from "../../documents/Documents"; import { DragManager } from "../../util/DragManager"; +import { Hypothesis } from "../../util/HypothesisUtils"; import { LinkManager } from "../../util/LinkManager"; import { undoBatch, UndoManager } from "../../util/UndoManager"; +import { Colors } from "../global/globalEnums"; +import { LightboxView } from "../LightboxView"; +import './DocumentLinksButton.scss'; import { DocumentView } from "./DocumentView"; -import { StrCast, Cast } from "../../../fields/Types"; import { LinkDescriptionPopup } from "./LinkDescriptionPopup"; -import { Hypothesis } from "../../util/HypothesisUtils"; -import { Id } from "../../../fields/FieldSymbols"; import { TaskCompletionBox } from "./TaskCompletedBox"; import React = require("react"); -import './DocumentLinksButton.scss'; -import { DocServer } from "../../DocServer"; -import { LightboxView } from "../LightboxView"; -import { cat } from "shelljs"; -import { Colors } from "../global/globalEnums"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; @@ -266,6 +265,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp style={{ backgroundColor: Colors.LIGHT_BLUE, color: Colors.BLACK, + fontSize: "20px", width: btnDim, height: btnDim, }}> diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index 7f164ca48..1ec7bf72a 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -175,6 +175,7 @@ position: absolute; bottom: 0; width: 100%; + overflow-y: scroll; transform-origin: bottom left; opacity: 0.1; transition: opacity 0.5s; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 5bd6049d6..1ccf38de2 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -13,7 +13,7 @@ import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from "../../../fields/Ty import { AudioField } from "../../../fields/URLField"; import { GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util'; import { MobileInterface } from '../../../mobile/MobileInterface'; -import { emptyFunction, hasDescendantTarget, OmitKeys, returnVal, Utils, returnTrue } from "../../../Utils"; +import { emptyFunction, hasDescendantTarget, OmitKeys, returnTrue, returnVal, Utils, lightOrDark, simulateMouseClick, returnEmptyString } from "../../../Utils"; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { Docs, DocUtils } from "../../documents/Documents"; import { DocumentType } from '../../documents/DocumentTypes'; @@ -41,13 +41,15 @@ import { CollectionFreeFormDocumentView } from "./CollectionFreeFormDocumentView import { DocumentContentsView } from "./DocumentContentsView"; import { DocumentLinksButton } from './DocumentLinksButton'; import "./DocumentView.scss"; +import { FormattedTextBox } from "./formattedText/FormattedTextBox"; import { LinkAnchorBox } from './LinkAnchorBox'; import { LinkDocPreview } from "./LinkDocPreview"; -import { PresBox } from './trails/PresBox'; import { RadialMenu } from './RadialMenu'; -import React = require("react"); import { ScriptingBox } from "./ScriptingBox"; -import { FormattedTextBox } from "./formattedText/FormattedTextBox"; +import { PresBox } from './trails/PresBox'; +import React = require("react"); +import { IconProp } from "@fortawesome/fontawesome-svg-core"; +import { ColorScheme } from "../../util/SettingsManager"; const { Howl } = require('howler'); interface Window { @@ -88,9 +90,14 @@ export interface DocComponentView { 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; + Pause?: () => void; setFocus?: () => void; + componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null; fieldKey?: string; annotationKey?: string; + getTitle?: () => string; + getScrollHeight?: () => number; + search?: (str: string, bwd?: boolean, clear?: boolean) => boolean; } export interface DocumentViewSharedProps { renderDepth: number; @@ -111,6 +118,7 @@ export interface DocumentViewSharedProps { docFilters: () => string[]; docRangeFilters: () => string[]; searchFilterDocs: () => Doc[]; + showTitle?: () => string; whenChildContentsActiveChanged: (isActive: boolean) => void; rootSelected: (outsideReaction?: boolean) => boolean; // whether the root of a template has been selected addDocTab: (doc: Doc, where: string) => boolean; @@ -135,7 +143,7 @@ export interface DocumentViewSharedProps { export interface DocumentViewProps extends DocumentViewSharedProps { // properties specific to DocumentViews but not to FieldView freezeDimensions?: boolean; - hideResizeHandles?: boolean; // whether to suppress DocumentDecorations when this document is selected + hideResizeHandles?: boolean; // whether to suppress DocumentDecorations when this document is selected hideTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings hideDecorationTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings treeViewDoc?: Doc; @@ -149,7 +157,7 @@ export interface DocumentViewProps extends DocumentViewSharedProps { NativeWidth?: () => number; NativeHeight?: () => number; LayoutTemplate?: () => Opt<Doc>; - contextMenuItems?: () => { script: ScriptField, label: string }[]; + contextMenuItems?: () => { script: ScriptField, filter?: ScriptField, label: string, icon: string }[]; onClick?: () => ScriptField; onDoubleClick?: () => ScriptField; onPointerDown?: () => ScriptField; @@ -205,7 +213,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @computed get finalLayoutKey() { return StrCast(this.Document.layoutKey, "layout"); } @computed get nativeWidth() { return this.props.NativeWidth(); } @computed get nativeHeight() { return this.props.NativeHeight(); } - @computed get onClickHandler() { return this.props.onClick?.() ?? Cast(this.Document.onfClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null)); } + @computed get onClickHandler() { return this.props.onClick?.() ?? Cast(this.Document.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null)); } @computed get onDoubleClickHandler() { return this.props.onDoubleClick?.() ?? (Cast(this.layoutDoc.onDoubleClick, ScriptField, null) ?? this.Document.onDoubleClick); } @computed get onPointerDownHandler() { return this.props.onPointerDown?.() ?? ScriptCast(this.Document.onPointerDown); } @computed get onPointerUpHandler() { return this.props.onPointerUp?.() ?? ScriptCast(this.Document.onPointerUp); } @@ -429,7 +437,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps }); // after a timeout, the right _componentView should have been created, so call it to update its view spec values setTimeout(() => this._componentView?.setViewSpec?.(anchor, LinkDocPreview.LinkInfo ? true : false)); - const focusSpeed = this._componentView?.scrollFocus?.(anchor, !LinkDocPreview.LinkInfo); // bcz: smooth parameter should really be passed into focus() instead of inferred here + const focusSpeed = this._componentView?.scrollFocus?.(anchor, !LinkDocPreview.LinkInfo); // bcz: smooth parameter should really be passed into focus() instead of inferred here const endFocus = focusSpeed === undefined ? options?.afterFocus : async (moved: boolean) => options?.afterFocus ? options?.afterFocus(true) : ViewAdjustment.doNothing; this.props.focus(options?.docTransform ? anchor : this.rootDoc, { ...options, afterFocus: (didFocus: boolean) => @@ -462,7 +470,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } else if (!Doc.IsSystem(this.rootDoc)) { UndoManager.RunInBatch(() => LightboxView.AddDocTab(this.rootDoc, "lightbox", this.props.LayoutTemplate?.()) - //this.props.addDocTab((this.rootDoc._fullScreenView as Doc) || this.rootDoc, "lightbox") , "double tap"); SelectionManager.DeselectAll(); Doc.UnBrushDoc(this.props.Document); @@ -481,8 +488,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (this.onDoubleClickHandler) { this._timeout = setTimeout(() => { this._timeout = undefined; clickFunc(); }, 350); } else clickFunc(); - } else if (this.Document["onClick-rawScript"] && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) {// bcz: hack? don't edit a script if you're clicking on a scripting box itself - this.props.addDocTab(DocUtils.makeCustomViewClicked(Doc.MakeAlias(this.props.Document), undefined, "onClick"), "add:right"); } else if (this.allLinks && this.Document.type !== DocumentType.LINK && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) { this.allLinks.length && LinkManager.FollowLink(undefined, this.props.Document, this.props, e.altKey); } else { @@ -524,19 +529,20 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps // don't preventDefault anymore. Goldenlayout, PDF text selection and RTF text selection all need it to go though //if (this.props.isSelected(true) && this.rootDoc.type !== DocumentType.PDF && this.layoutDoc._viewType !== CollectionViewType.Docking) e.preventDefault(); } - document.removeEventListener("pointermove", this.onPointerMove); + if (this.props.isDocumentActive?.()) { + document.removeEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointermove", this.onPointerMove); + } document.removeEventListener("pointerup", this.onPointerUp); - document.addEventListener("pointermove", this.onPointerMove); document.addEventListener("pointerup", this.onPointerUp); } } onPointerMove = (e: PointerEvent): void => { + if (e.cancelBubble) return; if ((InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || [InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool))) return; - if (e.cancelBubble && this.props.isDocumentActive?.()) { - document.removeEventListener("pointermove", this.onPointerMove); // stop listening to pointerMove if something else has stopPropagated it (e.g., the MarqueeView) - } - else if (!e.cancelBubble && (this.props.isDocumentActive?.() || this.layoutDoc.onDragStart) && !this.layoutDoc._lockedPosition && !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) { + + if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart) && !this.layoutDoc._lockedPosition && !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) { if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { if (!e.altKey && (!this.topMost || this.layoutDoc.onDragStart || this.onClickHandler) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) { document.removeEventListener("pointermove", this.onPointerMove); @@ -549,10 +555,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } } - onPointerUp = (e: PointerEvent): void => { + cleanupPointerEvents = () => { this.cleanUpInteractions(); document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); + } + + onPointerUp = (e: PointerEvent): void => { + this.cleanupPointerEvents(); if (this.onPointerUpHandler?.script && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { this.onPointerUpHandler.script.run({ self: this.rootDoc, this: this.layoutDoc }, console.log); @@ -571,8 +581,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (this.Document._isLinkButton && !this.onClickHandler) { this.Document.followLinkZoom = zoom; this.Document.followLinkLocation = location; - } else { - this.Document.onClick = this.layoutDoc.onClick = undefined; + } else if (this.Document._isLinkButton && this.onClickHandler) { + this.Document._isLinkButton = false; + this.Document["onClick-rawScript"] = this.dataDoc["onClick-rawScript"] = this.dataDoc.onClick = this.Document.onClick = this.layoutDoc.onClick = undefined; } } @undoBatch @action @@ -603,7 +614,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } @undoBatch deleteClicked = () => this.props.removeDocument?.(this.props.Document); - @undoBatch toggleDetail = () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(self, "${this.Document.layoutKey}")`); + @undoBatch setToggleDetail = () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(documentView, "${StrCast(this.Document.layoutKey).replace("layout_", "")}")`, { documentView: "any" }); @undoBatch @action drop = async (e: Event, de: DragManager.DropEvent) => { @@ -633,7 +644,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps makeIntoPortal = async () => { const portalLink = this.allLinks.find(d => d.anchor1 === this.props.Document); if (!portalLink) { - const portal = Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: NumCast(this.layoutDoc._height), _fitWidth: true, title: StrCast(this.props.Document.title) + ".portal" }); + const portal = Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: NumCast(this.layoutDoc._height), _fitWidth: true, title: StrCast(this.props.Document.title) + " [Portal]" }); DocUtils.MakeLink({ doc: this.props.Document }, { doc: portal }, "portal to"); } this.Document.followLinkLocation = "inPlace"; @@ -646,7 +657,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (e && this.rootDoc._hideContextMenu && Doc.UserDoc().noviceMode) { e.preventDefault(); e.stopPropagation(); - !this.props.isSelected(true) && SelectionManager.SelectView(this.props.DocumentView(), false); + //!this.props.isSelected(true) && SelectionManager.SelectView(this.props.DocumentView(), false); } // the touch onContextMenu is button 0, the pointer onContextMenu is button 2 if (e) { @@ -666,18 +677,35 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const cm = ContextMenu.Instance; if (!cm || (e as any)?.nativeEvent?.SchemaHandled) return; + if (e && !(e.nativeEvent as any).dash) { + const onDisplay = () => setTimeout(() => { + DocumentViewInternal.SelectAfterContextMenu && !this.props.isSelected(true) && SelectionManager.SelectView(this.props.DocumentView(), false); // 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. + setTimeout(() => { + const ele = document.elementFromPoint(e.clientX, e.clientY); + simulateMouseClick(ele, e.clientX, e.clientY, e.screenX, e.screenY); + }); + }); + if (navigator.userAgent.includes("Macintosh")) { + cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15, undefined, undefined, onDisplay); + } + else { + onDisplay(); + } + return; + } + const customScripts = Cast(this.props.Document.contextMenuScripts, listSpec(ScriptField), []); 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" })); + item.label && cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.layoutDoc, scriptContext: this.props.scriptContext, self: this.rootDoc }), icon: item.icon as IconProp })); if (!this.props.Document.isFolder) { const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layoutKey)], Doc, null); const appearance = cm.findByDescription("UI Controls..."); const appearanceItems: ContextMenuProps[] = appearance && "subitems" in appearance ? appearance.subitems : []; !Doc.UserDoc().noviceMode && templateDoc && appearanceItems.push({ description: "Open Template ", event: () => this.props.addDocTab(templateDoc, "add:right"), icon: "eye" }); - appearanceItems.push({ + !Doc.UserDoc().noviceMode && appearanceItems.push({ description: "Add a Field", event: () => { const alias = Doc.MakeAlias(this.rootDoc); alias.layout = FormattedTextBox.LayoutString("newfield"); @@ -701,13 +729,15 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const zorders = cm.findByDescription("ZOrder..."); const zorderItems: ContextMenuProps[] = zorders && "subitems" in zorders ? zorders.subitems : []; - zorderItems.push({ description: "Bring to Front", event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, false)), icon: "expand-arrows-alt" }); - zorderItems.push({ description: "Send to Back", event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, true)), icon: "expand-arrows-alt" }); - zorderItems.push({ description: this.rootDoc._raiseWhenDragged !== false ? "Keep ZIndex when dragged" : "Allow ZIndex to change when dragged", event: undoBatch(action(() => this.rootDoc._raiseWhenDragged = this.rootDoc._raiseWhenDragged === undefined ? false : undefined)), icon: "expand-arrows-alt" }); + if (this.props.bringToFront !== emptyFunction) { + zorderItems.push({ description: "Bring to Front", event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, false)), icon: "expand-arrows-alt" }); + zorderItems.push({ description: "Send to Back", event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, true)), icon: "expand-arrows-alt" }); + zorderItems.push({ description: this.rootDoc._raiseWhenDragged !== false ? "Keep ZIndex when dragged" : "Allow ZIndex to change when dragged", event: undoBatch(action(() => this.rootDoc._raiseWhenDragged = this.rootDoc._raiseWhenDragged === undefined ? false : undefined)), icon: "expand-arrows-alt" }); + } !zorders && cm.addItem({ description: "ZOrder...", subitems: zorderItems, icon: "compass" }); onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" }); - onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(self, "${this.Document.layoutKey}")`), icon: "concierge-bell" }); + !Doc.UserDoc().noviceMode && onClicks.push({ description: "Toggle Detail", event: this.setToggleDetail, icon: "concierge-bell" }); onClicks.push({ description: (this.Document.followLinkZoom ? "Don't" : "") + " zoom following link", event: () => this.Document.followLinkZoom = !this.Document.followLinkZoom, icon: this.Document.ignoreClick ? "unlock" : "lock" }); if (!this.Document.annotationOn) { @@ -770,9 +800,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps 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. } + collectionFilters = () => StrListCast(this.props.Document._docFilters); + collectionRangeDocFilters = () => StrListCast(this.props.Document._docRangeFilters); + @computed get showFilterIcon() { + return this.collectionFilters().length || this.collectionRangeDocFilters().length ? "hasFilter" : + this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f)).length || this.props.docRangeFilters().length ? "inheritsFilter" : undefined; + } rootSelected = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || (this.props.Document.rootDocument && this.props.rootSelected?.(outsideReaction)) || false; panelHeight = () => this.props.PanelHeight() - this.headerMargin; screenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -this.headerMargin); @@ -782,6 +817,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps setContentView = action((view: { getAnchor?: () => Doc, forward?: () => boolean, back?: () => boolean }) => this._componentView = view); isContentActive = (outsideReaction?: boolean) => { return CurrentUserUtils.SelectedTool !== InkTool.None || + SnappingManager.GetIsDragging() || + this.props.rootSelected() || this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this._componentView?.isAnyChildContentActive?.() || @@ -801,7 +838,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps </div>; return <div className="documentView-contentsView" style={{ - pointerEvents: (this.props.contentPointerEvents as any) || (this.isContentActive() ? "all" : "none"), + pointerEvents: this.props.pointerEvents as any ? this.props.pointerEvents as any : (this.rootDoc.type !== DocumentType.INK && ((this.props.contentPointerEvents as any) || (this.isContentActive())) ? "all" : "none"), height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined, }}> <DocumentContentsView key={1} {...this.props} @@ -817,7 +854,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps focus={this.focus} layoutKey={this.finalLayoutKey} /> {this.layoutDoc.hideAllLinks ? (null) : this.allLinkEndpoints} - {this.hideLinkButton ? (null) : + {this.hideLinkButton || this.props.renderDepth === -1 ? (null) : <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>} @@ -838,26 +875,41 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps anchorPanelHeight = () => this.props.PanelHeight() || 1; anchorStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => { switch (property) { + case StyleProp.ShowTitle: return ""; case StyleProp.PointerEvents: return "none"; case StyleProp.LinkSource: return this.props.Document;// pass the LinkSource to the LinkAnchorBox default: return this.props.styleProvider?.(doc, props, property); } } - @computed get directLinks() { TraceMobx(); return LinkManager.Instance.getAllDirectLinks(this.rootDoc); } + // We need to use allrelatedLinks to get not just links to the document as a whole, but links to + // anchors that are not rendered as DocumentViews (marked as 'unrendered' with their 'annotationOn' set to this document). e.g., + // - PDF text regions are rendered as an Annotations without generating a DocumentView, ' + // - RTF selections are rendered via Prosemirror and have a mark which contains the Document ID for the annotation link + // - and links to PDF/Web docs at a certain scroll location never create an explicit view. + // For each of these, we create LinkAnchorBox's on the border of the DocumentView. + @computed get directLinks() { + TraceMobx(); return LinkManager.Instance.getAllRelatedLinks(this.rootDoc).filter(link => + Doc.AreProtosEqual(link.anchor1 as Doc, this.rootDoc) || + Doc.AreProtosEqual(link.anchor2 as Doc, this.rootDoc) || + ((link.anchor1 as Doc).unrendered && Doc.AreProtosEqual((link.anchor1 as Doc).annotationOn as Doc, this.rootDoc)) || + ((link.anchor2 as Doc).unrendered && Doc.AreProtosEqual((link.anchor2 as Doc).annotationOn as Doc, this.rootDoc)) + ); + } @computed get allLinks() { TraceMobx(); return LinkManager.Instance.getAllRelatedLinks(this.rootDoc); } @computed get allLinkEndpoints() { // the small blue dots that mark the endpoints of links TraceMobx(); - if (this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return null; + if (this.layoutDoc.unrendered || 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.directLinks, this.props.docFilters?.() ?? [], []).filter(d => !d.hidden); return filtered.map((link, i) => - <div className="documentView-anchorCont" key={i + 1}> + <div className="documentView-anchorCont" key={link[Id]}> <DocumentView {...this.props} Document={link} PanelWidth={this.anchorPanelWidth} PanelHeight={this.anchorPanelHeight} dontRegisterView={false} + showTitle={returnEmptyString} + hideCaptions={true} fitWidth={returnTrue} styleProvider={this.anchorStyleProvider} removeDocument={this.hideLinkAnchor} @@ -920,35 +972,61 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewInternalProps>, property: string) => this.props?.styleProvider?.(doc, props, property + ":caption"); @computed get innards() { TraceMobx(); + const ffscale = (this.props.DocumentView().props.CollectionFreeFormDocumentView?.().props.ScreenToLocalTransform().Scale || 1); const showTitle = this.ShowTitle?.split(":")[0]; const showTitleHover = this.ShowTitle?.includes(":hover"); const showCaption = !this.props.hideCaptions && this.Document._viewType !== CollectionViewType.Carousel ? StrCast(this.layoutDoc._showCaption) : undefined; const captionView = !showCaption ? (null) : - <div className="documentView-captionWrapper"> + <div className="documentView-captionWrapper" + style={{ + pointerEvents: this.onClickHandler || this.Document.ignoreClick ? "none" : this.isContentActive() || this.props.isDocumentActive?.() ? "all" : undefined, + minWidth: 50 * ffscale, + maxHeight: `max(100%, ${20 * ffscale}px)` + }}> <FormattedTextBox {...OmitKeys(this.props, ['children']).omit} yPadding={10} xPadding={10} fieldKey={showCaption} - fontSize={Math.min(32, 12 * this.props.ScreenToLocalTransform().Scale)} - hideOnLeave={true} + fontSize={12 * Math.max(1, 2 * ffscale / 3)} styleProvider={this.captionStyleProvider} dontRegisterView={true} + noSidebar={true} + dontScale={true} + isContentActive={this.isContentActive} onClick={this.onClickFunc} /> </div>; + const targetDoc = (showTitle?.startsWith("_") ? this.layoutDoc : this.rootDoc); + const background = StrCast(SharingManager.Instance.users.find(users => users.user.email === this.dataDoc.author)?.sharingDoc.userColor, [DocumentType.RTF, DocumentType.COL].includes(this.rootDoc.type as any) ? StrCast(Doc.SharingDoc().userColor) : "rgba(0,0,0,0.4)"); const titleView = !showTitle ? (null) : <div className={`documentView-titleWrapper${showTitleHover ? "-hover" : ""}`} key="title" style={{ position: this.headerMargin ? "relative" : "absolute", height: this.titleHeight, - background: StrCast(SharingManager.Instance.users.find(users => users.user.email === this.dataDoc.author)?.sharingDoc.userColor, this.rootDoc.type === DocumentType.RTF ? StrCast(Doc.SharingDoc().userColor) : "rgba(0,0,0,0.4)"), - pointerEvents: this.onClickHandler || this.Document.ignoreClick ? "none" : undefined, + color: lightOrDark(background), + background, + pointerEvents: this.onClickHandler || this.Document.ignoreClick ? "none" : this.isContentActive() || this.props.isDocumentActive?.() ? "all" : undefined, }}> <EditableView ref={this._titleRef} - contents={showTitle === "title" ? StrCast((this.dataDoc || this.props.Document).title) : showTitle.split(";").map(field => field + ":" + (this.dataDoc || this.props.Document)[field]?.toString()).join(" ")} + contents={showTitle.split(";").map(field => field.trim()).map(field => targetDoc[field]?.toString()).join("\\")} display={"block"} fontSize={10} - GetValue={() => Field.toString((this.dataDoc || this.props.Document)[showTitle.split(";")[0]] as any as Field)} - SetValue={undoBatch((value) => showTitle.includes("Date") ? true : (Doc.GetProto(this.dataDoc || this.props.Document)[showTitle] = value) ? true : true)} + GetValue={() => showTitle.split(";").length === 1 ? showTitle + "=" + Field.toString(targetDoc[showTitle.split(";")[0]] as any as Field) : "#" + showTitle} + SetValue={undoBatch(input => { + if (input?.startsWith("#")) { + if (this.props.showTitle) { + this.rootDoc._showTitle = input?.substring(1) ? input.substring(1) : undefined; + } else { + Doc.UserDoc().showTitle = input?.substring(1) ? input.substring(1) : "creationDate"; + } + return true; + } else { + var value = input.replace(new RegExp(showTitle + "="), ""); + if (showTitle !== "title" && Number(value).toString() === value) value = Number(value); + if (showTitle.includes("Date") || showTitle === "author") return true; + return Doc.SetInPlace(targetDoc, showTitle, value, true) ? true : true; + } + return true; + })} /> </div>; return this.props.hideTitle || (!showTitle && !showCaption) ? @@ -960,12 +1038,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } @computed get renderDoc() { TraceMobx(); + const isButton: boolean = this.props.Document.type === DocumentType.FONTICON; if (!(this.props.Document instanceof Doc) || GetEffectiveAcl(this.props.Document[DataSym]) === AclPrivate || this.hidden) return null; return this.docContents ?? <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} id={this.props.Document[Id]} style={{ - background: this.backgroundColor, + background: isButton ? undefined : this.backgroundColor, opacity: this.opacity, color: StrCast(this.layoutDoc.color, "inherit"), fontFamily: StrCast(this.Document._fontFamily, "inherit"), @@ -980,10 +1059,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps </div>; } render() { + TraceMobx(); 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", "#4476F7", "#4476F7", "yellow", "magenta", "cyan", "orange"])[highlightIndex]; + const highlightColor = ["transparent", "rgb(68, 118, 247)", "rgb(68, 118, 247)", "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; @@ -993,6 +1071,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const internal = PresBox.EffectsProvider(this.layoutDoc, this.renderDoc) || this.renderDoc; const boxShadow = this.props.treeViewDoc ? null : highlighting && this.borderRounding && highlightStyle !== "dashed" ? `0 0 0 ${highlightIndex}px ${highlightColor}` : this.boxShadow || (this.props.Document.isTemplateForField ? "black 0.2vw 0.2vw 0.8vw" : undefined); + + // Return surrounding highlight return <div className={DocumentView.ROOT_DIV} ref={this._mainCont} onContextMenu={this.onContextMenu} onKeyDown={this.onKeyDown} @@ -1021,6 +1101,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps </div> </> } + {this.showFilterIcon ? + <FontAwesomeIcon icon={"filter"} size="lg" + 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>; } } @@ -1061,16 +1147,16 @@ export class DocumentView extends React.Component<DocumentViewProps> { @computed get nativeScaling() { if (this.shouldNotScale) return 1; const minTextScale = this.Document.type === DocumentType.RTF ? 0.1 : 0; - if (this.fitWidth || this.props.PanelHeight() / this.effectiveNativeHeight > this.props.PanelWidth() / this.effectiveNativeWidth) { - return Math.max(minTextScale, this.props.PanelWidth() / this.effectiveNativeWidth); // width-limited or fitWidth + if (this.fitWidth || this.props.PanelHeight() / (this.effectiveNativeHeight || 1) > this.props.PanelWidth() / (this.effectiveNativeWidth || 1)) { + return Math.max(minTextScale, this.props.PanelWidth() / (this.effectiveNativeWidth || 1)); // width-limited or fitWidth } - return Math.max(minTextScale, this.props.PanelHeight() / this.effectiveNativeHeight); // height-limited or unscaled + return Math.max(minTextScale, this.props.PanelHeight() / (this.effectiveNativeHeight || 1)); // height-limited or unscaled } @computed get panelWidth() { return this.effectiveNativeWidth ? this.effectiveNativeWidth * this.nativeScaling : this.props.PanelWidth(); } @computed get panelHeight() { if (this.effectiveNativeHeight) { - return Math.min(this.props.PanelHeight(), Math.max(NumCast(this.layoutDoc.scrollHeight), this.effectiveNativeHeight) * this.nativeScaling); + return Math.min(this.props.PanelHeight(), Math.max(this.ComponentView?.getScrollHeight?.() ?? NumCast(this.layoutDoc.scrollHeight), this.effectiveNativeHeight) * this.nativeScaling); } return this.props.PanelHeight(); } @@ -1087,7 +1173,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { } const xf = (this.docView?.props.ScreenToLocalTransform().scale(this.nativeScaling)).inverse(); const [[left, top], [right, bottom]] = [xf.transformPoint(0, 0), xf.transformPoint(this.panelWidth, this.panelHeight)]; - if (this.docView.props.LayoutTemplateString?.includes("LinkAnchorBox")) { + if (this.docView.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { const docuBox = this.docView.ContentDiv.getElementsByClassName("linkAnchorBox-cont"); if (docuBox.length) return docuBox[0].getBoundingClientRect(); } @@ -1146,21 +1232,22 @@ export class DocumentView extends React.Component<DocumentViewProps> { } componentWillUnmount() { Object.values(this._disposers).forEach(disposer => disposer?.()); - !this.props.dontRegisterView && DocumentManager.Instance.RemoveView(this); + !BoolCast(this.props.Document.dontRegisterView, this.props.dontRegisterView) && DocumentManager.Instance.RemoveView(this); } render() { TraceMobx(); const xshift = () => (this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined); const yshift = () => (this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined); + const isButton: boolean = this.props.Document.type === DocumentType.FONTICON || this.props.Document._viewType === CollectionViewType.Linear; return (<div className="contentFittingDocumentView"> {!this.props.Document || !this.props.PanelWidth() ? (null) : ( <div className="contentFittingDocumentView-previewDoc" ref={this.ContentRef} style={{ position: this.props.Document.isInkMask ? "absolute" : undefined, - transform: `translate(${this.centeringX}px, ${this.centeringY}px)`, - width: xshift() ?? `${100 * (this.props.PanelWidth() - this.Xshift * 2) / this.props.PanelWidth()}%`, - height: yshift() ?? (this.fitWidth ? `${this.panelHeight}px` : + transform: isButton ? undefined : `translate(${this.centeringX}px, ${this.centeringY}px)`, + width: isButton ? "100%" : xshift() ?? `${100 * (this.props.PanelWidth() - this.Xshift * 2) / this.props.PanelWidth()}%`, + height: isButton ? undefined : yshift() ?? (this.fitWidth ? `${this.panelHeight}px` : `${100 * this.effectiveNativeHeight / this.effectiveNativeWidth * this.props.PanelWidth() / this.props.PanelHeight()}%`), }}> <DocumentViewInternal {...this.props} @@ -1176,14 +1263,13 @@ export class DocumentView extends React.Component<DocumentViewProps> { ScreenToLocalTransform={this.screenToLocalTransform} focus={this.props.focus || emptyFunction} bringToFront={emptyFunction} - ref={action((r: DocumentViewInternal | null) => this.docView = r)} /> + ref={action((r: DocumentViewInternal | null) => r && (this.docView = r))} /> </div>)} </div>); } } -Scripting.addGlobal(function toggleDetail(doc: any, layoutKey: string, otherKey: string = "layout") { - const dv = DocumentManager.Instance.getDocumentView(doc); - if (dv?.props.Document.layoutKey === layoutKey) dv?.switchViews(otherKey !== "layout", otherKey.replace("layout_", "")); - else dv?.switchViews(true, layoutKey.replace("layout_", "")); +Scripting.addGlobal(function toggleDetail(dv: DocumentView, detailLayoutKeySuffix: string) { + if (dv.Document.layoutKey === "layout_" + detailLayoutKeySuffix) dv.switchViews(false, "layout"); + else dv.switchViews(true, detailLayoutKeySuffix); });
\ No newline at end of file diff --git a/src/client/views/nodes/EquationBox.scss b/src/client/views/nodes/EquationBox.scss index e69de29bb..6c9d53d10 100644 --- a/src/client/views/nodes/EquationBox.scss +++ b/src/client/views/nodes/EquationBox.scss @@ -0,0 +1,3 @@ +.equationBox-cont { + transform-origin: top left; +}
\ No newline at end of file diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx index dacafcdf4..f1f802c13 100644 --- a/src/client/views/nodes/EquationBox.tsx +++ b/src/client/views/nodes/EquationBox.tsx @@ -84,10 +84,16 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps, EquationDo } render() { TraceMobx(); - return (<div onPointerDown={e => !e.ctrlKey && e.stopPropagation()} + const scale = (this.props.scaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1); + return (<div className="equationBox-cont" + onPointerDown={e => !e.ctrlKey && e.stopPropagation()} style={{ + transform: `scale(${scale})`, + width: `${100 / scale}%`, + height: `${100 / scale}%`, pointerEvents: !this.props.isSelected() ? "none" : undefined, }} + onKeyDown={e => e.stopPropagation()} > <EquationEditor ref={this._ref} value={this.dataDoc.text || "x"} diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx index 7ad03e055..2041c7399 100644 --- a/src/client/views/nodes/FilterBox.tsx +++ b/src/client/views/nodes/FilterBox.tsx @@ -9,7 +9,7 @@ import { List } from "../../../fields/List"; import { RichTextField } from "../../../fields/RichTextField"; import { listSpec, makeInterface } from "../../../fields/Schema"; import { ComputedField, ScriptField } from "../../../fields/ScriptField"; -import { Cast, StrCast } from "../../../fields/Types"; +import { Cast, StrCast, NumCast } from "../../../fields/Types"; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from "../../../Utils"; import { Docs } from "../../documents/Documents"; import { DocumentType } from "../../documents/DocumentTypes"; @@ -79,7 +79,7 @@ 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; + return SelectionManager.Docs().length ? SelectionManager.Docs()[0] : CurrentUserUtils.ActiveDashboard; } @computed static get targetDocChildKey() { if (SelectionManager.Views().length) { @@ -88,10 +88,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc return "data"; } @computed static get targetDocChildren() { - if (SelectionManager.Views().length) { - return DocListCast(FilterBox.targetDoc[FilterBox.targetDocChildKey]); - } - return DocListCast(CurrentUserUtils.ActiveDashboard.data); + return DocListCast(FilterBox.targetDoc?.[FilterBox.targetDocChildKey] || CurrentUserUtils.ActiveDashboard.data); } @observable _loaded = false; @@ -347,7 +344,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc ); } setTreeHeight = (hgt: number) => { - this.layoutDoc._height = hgt + 140; // 50? need to add all the border sizes together. + this.layoutDoc._height = NumCast(this.layoutDoc._autoHeightMargins) + 150; // need to add all the border sizes together. } /** @@ -380,9 +377,9 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc </div> <div className="filterBox-select-bool"> - <select className="filterBox-selection" onChange={this.changeBool}> - <option value="AND" key="AND" selected={(FilterBox.targetDoc.currentFilter as Doc)?.filterBoolean === "AND"}>AND</option> - <option value="OR" key="OR" selected={(FilterBox.targetDoc.currentFilter as Doc)?.filterBoolean === "OR"}>OR</option> + <select className="filterBox-selection" onChange={this.changeBool} defaultValue={StrCast((FilterBox.targetDoc.currentFilter as Doc)?.filterBoolean)}> + <option value="AND" key="AND">AND</option> + <option value="OR" key="OR">OR</option> </select> <div className="filterBox-select-text">filters together</div> </div> diff --git a/src/client/views/nodes/FontIconBox.scss b/src/client/views/nodes/FontIconBox.scss deleted file mode 100644 index 718af2c16..000000000 --- a/src/client/views/nodes/FontIconBox.scss +++ /dev/null @@ -1,103 +0,0 @@ -@import "../global/globalCssVariables"; - -.fontIconBox-label { - color: $white; - margin-right: 4px; - margin-top: 1px; - position: relative; - text-align: center; - font-size: 7px; - letter-spacing: normal; - background-color: inherit; - border-radius: 8px; - margin-top: -8px; - padding: 0; - width: 100%; -} - -.fontIconBadge-container { - position:absolute; - z-index: 1000; - top: 12px; - - .fontIconBadge { - position: absolute; - top: -10px; - right: -10px; - color: $white; - background: $pink; - font-weight: 300; - border-radius: 100%; - width: 25px; - height: 25px; - text-align: center; - padding-top: 4px; - font-size: 12px; - } -} - -.menuButton-circle, -.menuButton-round { - border-radius: 100%; - background-color: $dark-gray; - padding: 0; - - .fontIconBox-label { - //margin-left: -10px; // button padding is 10px; - bottom: 0; - position: absolute; - } - - &:hover { - background-color: $light-gray; - } -} - -.menuButton-square { - padding-top: 3px; - padding-bottom: 3px; - background-color: $dark-gray; - - .fontIconBox-label { - border-radius: 0px; - margin-top: 0px; - border-radius: "inherit"; - } -} - -.menuButton, -.menuButton-circle, -.menuButton-round, -.menuButton-square { - margin-left: -5%; - width: 110%; - height: 100%; - pointer-events: all; - touch-action: none; - - .menuButton-wrap { - touch-action: none; - border-radius: 8px; - width: 100%; - } - - .menuButton-icon-square { - width: auto; - height: 29px; - padding: 4px; - } - - svg { - width: 95% !important; - height: 95%; - } -} -.menuButton-round { - width: 100%; - svg { - width: 50% !important; - height: 50%; - position: relative; - bottom: 2px; - } -}
\ No newline at end of file diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx deleted file mode 100644 index 0d415e238..000000000 --- a/src/client/views/nodes/FontIconBox.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tooltip } from '@material-ui/core'; -import { observer } from 'mobx-react'; -import * as React from 'react'; -import { AclPrivate, Doc, DocListCast } from '../../../fields/Doc'; -import { createSchema, makeInterface } from '../../../fields/Schema'; -import { ScriptField } from '../../../fields/ScriptField'; -import { Cast, StrCast } from '../../../fields/Types'; -import { GetEffectiveAcl } from '../../../fields/util'; -import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../../Utils"; -import { DragManager } from '../../util/DragManager'; -import { ContextMenu } from '../ContextMenu'; -import { DocComponent } from '../DocComponent'; -import { StyleProp } from '../StyleProvider'; -import { FieldView, FieldViewProps } from './FieldView'; -import './FontIconBox.scss'; -import { Colors } from '../global/globalEnums'; -const FontIconSchema = createSchema({ - icon: "string", -}); - -type FontIconDocument = makeInterface<[typeof FontIconSchema]>; -const FontIconDocument = makeInterface(FontIconSchema); -@observer -export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>(FontIconDocument) { - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(FontIconBox, fieldKey); } - showTemplate = (): void => { - const dragFactory = Cast(this.layoutDoc.dragFactory, Doc, null); - dragFactory && this.props.addDocTab(dragFactory, "add:right"); - } - dragAsTemplate = (): void => { this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)'); }; - useAsPrototype = (): void => { this.layoutDoc.onDragStart = ScriptField.MakeFunction('makeDelegate(this.dragFactory, true)'); }; - - specificContextMenu = (): void => { - if (!Doc.UserDoc().noviceMode) { - const cm = ContextMenu.Instance; - cm.addItem({ description: "Show Template", event: this.showTemplate, icon: "tag" }); - cm.addItem({ description: "Use as Render Template", event: this.dragAsTemplate, icon: "tag" }); - cm.addItem({ description: "Use as Prototype", event: this.useAsPrototype, icon: "tag" }); - } - } - - render() { - const label = StrCast(this.rootDoc.label, StrCast(this.rootDoc.title)); - const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); - const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); - const shape = StrCast(this.layoutDoc.iconShape, label ? "round" : "circle"); - const icon = StrCast(this.dataDoc.icon, "user") as any; - const presSize = shape === 'round' ? 25 : 30; - const presTrailsIcon = <img src={`/assets/${"presTrails.png"}`} - style={{ width: presSize, height: presSize, filter: `invert(${color === Colors.DARK_GRAY ? "0%" : "100%"})`, marginBottom: "5px" }} />; - const button = <button className={`menuButton-${shape}`} onContextMenu={this.specificContextMenu} - style={{ backgroundColor: backgroundColor, }}> - <div className="menuButton-wrap"> - {icon === 'pres-trail' ? presTrailsIcon : <FontAwesomeIcon className={`menuButton-icon-${shape}`} icon={icon} color={color} - size={this.layoutDoc.iconShape === "square" ? "sm" : "sm"} />} - {!label ? (null) : <div className="fontIconBox-label" style={{ color, backgroundColor }}> {label} </div>} - <FontIconBadge collection={Cast(this.rootDoc.watchedDocuments, Doc, null)} /> - </div> - </button>; - return !this.layoutDoc.toolTip ? button : - <Tooltip title={<div className="dash-tooltip">{StrCast(this.layoutDoc.toolTip)}</div>}> - {button} - </Tooltip>; - } -} - -interface FontIconBadgeProps { - collection: Doc | undefined; -} - -@observer -export class FontIconBadge extends React.Component<FontIconBadgeProps> { - _notifsRef = React.createRef<HTMLDivElement>(); - - onPointerDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, - (e: PointerEvent) => { - const dragData = new DragManager.DocumentDragData([this.props.collection!]); - DragManager.StartDocumentDrag([this._notifsRef.current!], dragData, e.x, e.y); - return true; - }, - returnFalse, emptyFunction, false); - } - - render() { - if (!(this.props.collection instanceof Doc)) return (null); - const length = DocListCast(this.props.collection.data).filter(d => GetEffectiveAcl(d) !== AclPrivate).length; // Object.keys(d).length).length; // filter out any documents that we can't read - return <div className="fontIconBadge-container" style={{ width: 15, height: 15, top: 12 }} ref={this._notifsRef}> - <div className="fontIconBadge" style={length > 0 ? { "display": "initial" } : { "display": "none" }} - onPointerDown={this.onPointerDown} > - {length} - </div> - </div>; - } -}
\ No newline at end of file diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx index b00f97236..5050fc2d2 100644 --- a/src/client/views/nodes/FunctionPlotBox.tsx +++ b/src/client/views/nodes/FunctionPlotBox.tsx @@ -1,18 +1,16 @@ -import EquationEditor from 'equation-editor-react'; import functionPlot from "function-plot"; +import { action, computed, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; +import { Doc, DocListCast } from '../../../fields/Doc'; import { documentSchema } from '../../../fields/documentSchemas'; -import { createSchema, makeInterface, listSpec } from '../../../fields/Schema'; -import { StrCast, Cast } from '../../../fields/Types'; +import { List } from '../../../fields/List'; +import { createSchema, listSpec, makeInterface } from '../../../fields/Schema'; +import { Cast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; +import { Docs } from '../../documents/Documents'; import { ViewBoxBaseComponent } from '../DocComponent'; import { FieldView, FieldViewProps } from './FieldView'; -import './LabelBox.scss'; -import { DocListCast, Doc } from '../../../fields/Doc'; -import { computed, action, reaction } from 'mobx'; -import { Docs } from '../../documents/Documents'; -import { List } from '../../../fields/List'; const EquationSchema = createSchema({}); diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 2536dbe16..4238f6d29 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -13,6 +13,7 @@ width: 100%; pointer-events: none; mix-blend-mode: multiply; // bcz: makes text fuzzy! + overflow: hidden; } .imageBox-fader { @@ -101,7 +102,6 @@ margin: 0 auto; display: flex; height: 100%; - overflow: auto; .imageBox-fadeBlocker { width: 100%; diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 38deb4a73..89f70985c 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -81,11 +81,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp }), ({ forceFull, scrSize, selected }) => this._curSuffix = forceFull ? "_o" : scrSize < 100 ? "_s" : scrSize < 400 ? "_m" : scrSize < 800 || !selected ? "_l" : "_o", { 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(); - })); this._disposers.path = reaction(() => ({ nativeSize: this.nativeSize, width: this.layoutDoc[WidthSym]() }), ({ nativeSize, width }) => { if (!this.layoutDoc._height) { @@ -144,9 +139,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp const funcs: ContextMenuProps[] = []; funcs.push({ description: "Rotate Clockwise 90", event: this.rotate, icon: "expand-arrows-alt" }); funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? "Dynamic Res" : "Full Res"}`, event: this.resolution, icon: "expand-arrows-alt" }); + funcs.push({ description: "Copy path", event: () => Utils.CopyText(this.choosePath(field.url)), icon: "expand-arrows-alt" }); if (!Doc.UserDoc().noviceMode) { funcs.push({ description: "Export to Google Photos", event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: "caret-square-right" }); - funcs.push({ description: "Copy path", event: () => Utils.CopyText(field.url.href), icon: "expand-arrows-alt" }); const existingAnalyze = ContextMenu.Instance?.findByDescription("Analyzers..."); const modes: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : []; @@ -291,7 +286,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } return <div className="imageBox-cont" key={this.layoutDoc[Id]} ref={this.createDropTarget} onPointerDown={this.marqueeDown}> - <div className="imageBox-fader" > + <div className="imageBox-fader" style={{ overflow: this.props.docViewPath?.().lastElement().fitWidth ? "auto" : undefined }} > <img key="paths" ref={this._imgRef} src={srcpath} style={{ transform, transformOrigin }} @@ -359,15 +354,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} - scaling={returnOne} select={emptyFunction} + scaling={returnOne} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} removeDocument={this.removeDocument} moveDocument={this.moveDocument} diff --git a/src/client/views/nodes/LabelBigText.js b/src/client/views/nodes/LabelBigText.js new file mode 100644 index 000000000..db43551d8 --- /dev/null +++ b/src/client/views/nodes/LabelBigText.js @@ -0,0 +1,241 @@ +/* +Brorlandi/big-text.js v1.0.0, 2017 +Adapted from DanielHoffmann/jquery-bigtext, v1.3.0, May 2014 +And from Jetroid/bigtext.js v1.0.0, September 2016 + +Usage: +BigText("#myElement",{ + rotateText: {Number}, (null) + fontSizeFactor: {Number}, (0.8) + maximumFontSize: {Number}, (null) + limitingDimension: {String}, ("both") + horizontalAlign: {String}, ("center") + verticalAlign: {String}, ("center") + textAlign: {String}, ("center") + whiteSpace: {String}, ("nowrap") +}); + + +Original Projects: +https://github.com/DanielHoffmann/jquery-bigtext +https://github.com/Jetroid/bigtext.js + +Options: + +rotateText: Rotates the text inside the element by X degrees. + +fontSizeFactor: This option is used to give some vertical spacing for letters that overflow the line-height (like 'g', 'Á' and most other accentuated uppercase letters). This does not affect the font-size if the limiting factor is the width of the parent div. The default is 0.8 + +maximumFontSize: maximum font size to use. + +limitingDimension: In which dimension the font size should be limited. Possible values: "width", "height" or "both". Defaults to both. Using this option with values different than "both" overwrites the element parent width or height. + +horizontalAlign: Where to align the text horizontally. Possible values: "left", "center", "right". Defaults to "center". + +verticalAlign: Where to align the text vertically. Possible values: "top", "center", "bottom". Defaults to "center". + +textAlign: Sets the text align of the element. Possible values: "left", "center", "right". Defaults to "center". This option is only useful if there are linebreaks (<br> tags) inside the text. + +whiteSpace: Sets whitespace handling. Possible values: "nowrap", "pre". Defaults to "nowrap". (Can also be set to enable wrapping but this doesn't work well.) + +Bruno Orlandi - 2017 + +Copyright (C) 2013 Daniel Hoffmann Bernardes, Ícaro Technologies +Copyright (C) 2016 Jet Holt + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +function _calculateInnerDimensions(computedStyle) { + //Calculate the inner width and height + var innerWidth; + var innerHeight; + + var width = parseInt(computedStyle.getPropertyValue("width")); + var height = parseInt(computedStyle.getPropertyValue("height")); + var paddingLeft = parseInt(computedStyle.getPropertyValue("padding-left")); + var paddingRight = parseInt(computedStyle.getPropertyValue("padding-right")); + var paddingTop = parseInt(computedStyle.getPropertyValue("padding-top")); + var paddingBottom = parseInt(computedStyle.getPropertyValue("padding-bottom")); + var borderLeft = parseInt(computedStyle.getPropertyValue("border-left-width")); + var borderRight = parseInt(computedStyle.getPropertyValue("border-right-width")); + var borderTop = parseInt(computedStyle.getPropertyValue("border-top-width")); + var borderBottom = parseInt(computedStyle.getPropertyValue("border-bottom-width")); + + //If box-sizing is border-box, we need to subtract padding and border. + var parentBoxSizing = computedStyle.getPropertyValue("box-sizing"); + if (parentBoxSizing == "border-box") { + innerWidth = width - (paddingLeft + paddingRight + borderLeft + borderRight); + innerHeight = height - (paddingTop + paddingBottom + borderTop + borderBottom); + } else { + innerWidth = width; + innerHeight = height; + } + var obj = {}; + obj["width"] = innerWidth; + obj["height"] = innerHeight; + return obj; +} + +export default function BigText(element, options) { + + if (typeof element === 'string') { + element = document.querySelector(element); + } else if (element.length) { + // Support for array based queries (such as jQuery) + element = element[0]; + } + + var defaultOptions = { + rotateText: null, + fontSizeFactor: 0.8, + maximumFontSize: null, + limitingDimension: "both", + horizontalAlign: "center", + verticalAlign: "center", + textAlign: "center", + whiteSpace: "nowrap" + }; + + //Merge provided options and default options + options = options || {}; + for (var opt in defaultOptions) + if (defaultOptions.hasOwnProperty(opt) && !options.hasOwnProperty(opt)) + options[opt] = defaultOptions[opt]; + + //Get variables which we will reference frequently + var style = element.style; + var computedStyle = document.defaultView.getComputedStyle(element); + var parent = element.parentNode; + var parentStyle = parent.style; + var parentComputedStyle = document.defaultView.getComputedStyle(parent); + + //hides the element to prevent "flashing" + style.visibility = "hidden"; + + //Set some properties + style.display = "inline-block"; + style.clear = "both"; + style.float = "left"; + style.fontSize = (1000 * options.fontSizeFactor) + "px"; + style.lineHeight = "1000px"; + style.whiteSpace = options.whiteSpace; + style.textAlign = options.textAlign; + style.position = "relative"; + style.padding = 0; + style.margin = 0; + style.left = "50%"; + style.top = "50%"; + + //Get properties of parent to allow easier referencing later. + var parentPadding = { + top: parseInt(parentComputedStyle.getPropertyValue("padding-top")), + right: parseInt(parentComputedStyle.getPropertyValue("padding-right")), + bottom: parseInt(parentComputedStyle.getPropertyValue("padding-bottom")), + left: parseInt(parentComputedStyle.getPropertyValue("padding-left")), + }; + var parentBorder = { + top: parseInt(parentComputedStyle.getPropertyValue("border-top")), + right: parseInt(parentComputedStyle.getPropertyValue("border-right")), + bottom: parseInt(parentComputedStyle.getPropertyValue("border-bottom")), + left: parseInt(parentComputedStyle.getPropertyValue("border-left")), + }; + + //Calculate the parent inner width and height + var parentInnerDimensions = _calculateInnerDimensions(parentComputedStyle); + var parentInnerWidth = parentInnerDimensions["width"]; + var parentInnerHeight = parentInnerDimensions["height"]; + + var box = { + width: element.offsetWidth, //Note: This is slightly larger than the jQuery version + height: element.offsetHeight, + }; + + + if (options.rotateText !== null) { + if (typeof options.rotateText !== "number") + throw "bigText error: rotateText value must be a number"; + var rotate = "rotate(" + options.rotateText + "deg)"; + style.webkitTransform = rotate; + style.msTransform = rotate; + style.MozTransform = rotate; + style.OTransform = rotate; + style.transform = rotate; + //calculating bounding box of the rotated element + var sine = Math.abs(Math.sin(options.rotateText * Math.PI / 180)); + var cosine = Math.abs(Math.cos(options.rotateText * Math.PI / 180)); + box.width = element.offsetWidth * cosine + element.offsetHeight * sine; + box.height = element.offsetWidth * sine + element.offsetHeight * cosine; + } + + var widthFactor = (parentInnerWidth - parentPadding.left - parentPadding.right) / box.width; + var heightFactor = (parentInnerHeight - parentPadding.top - parentPadding.bottom) / box.height; + var lineHeight; + + if (options.limitingDimension.toLowerCase() === "width") { + lineHeight = Math.floor(widthFactor * 1000); + parentStyle.height = lineHeight + "px"; + } else if (options.limitingDimension.toLowerCase() === "height") { + lineHeight = Math.floor(heightFactor * 1000); + } else if (widthFactor < heightFactor) + lineHeight = Math.floor(widthFactor * 1000); + else if (widthFactor >= heightFactor) + lineHeight = Math.floor(heightFactor * 1000); + + var fontSize = lineHeight * options.fontSizeFactor; + if (options.maximumFontSize !== null && fontSize > options.maximumFontSize) { + fontSize = options.maximumFontSize; + lineHeight = fontSize / options.fontSizeFactor; + } + + style.fontSize = Math.floor(fontSize) + "px"; + style.lineHeight = Math.ceil(lineHeight) + "px"; + style.marginBottom = "0px"; + style.marginRight = "0px"; + + if (options.limitingDimension.toLowerCase() === "height") { + //this option needs the font-size to be set already so computedStyle.getPropertyValue("width") returns the right size + //this +4 is to compensate the rounding erros that can occur due to the calls to Math.floor in the centering code + parentStyle.width = (parseInt(computedStyle.getPropertyValue("width")) + 4) + "px"; + } + + //Calculate the inner width and height + var innerDimensions = _calculateInnerDimensions(computedStyle); + var innerWidth = innerDimensions["width"]; + var innerHeight = innerDimensions["height"]; + + switch (options.verticalAlign.toLowerCase()) { + case "top": + style.top = "0%"; + break; + case "bottom": + style.top = "100%"; + style.marginTop = Math.floor(-innerHeight) + "px"; + break; + default: + style.marginTop = Math.ceil((-innerHeight / 2)) + "px"; + break; + } + + switch (options.horizontalAlign.toLowerCase()) { + case "left": + style.left = "0%"; + break; + case "right": + style.left = "100%"; + style.marginLeft = Math.floor(-innerWidth) + "px"; + break; + default: + style.marginLeft = Math.ceil((-innerWidth / 2)) + "px"; + break; + } + + //shows the element after the work is done + style.visibility = "visible"; + + return element; +} diff --git a/src/client/views/nodes/LabelBox.scss b/src/client/views/nodes/LabelBox.scss index 109a02df4..6a0d651d2 100644 --- a/src/client/views/nodes/LabelBox.scss +++ b/src/client/views/nodes/LabelBox.scss @@ -9,10 +9,10 @@ .labelBox-mainButton { max-width: 100%; - width: fit-content; - height: max-content; + width: 100%; + height: 100%; border-radius: inherit; - letter-spacing: 2px; + //letter-spacing: 2px; // bcz: doesn't work with LabelBigText text-transform: uppercase; overflow: hidden; display: inline-block; @@ -28,5 +28,5 @@ .labelBox-missingParam { width: 100%; background: lightgray; - border: dimGray solid 1px; + border: dimgray solid 1px; }
\ No newline at end of file diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index 6a7793ff0..90b9ce55d 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -1,30 +1,47 @@ -import { action, computed, observable, runInAction } from 'mobx'; +import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast } from '../../../fields/Doc'; import { documentSchema } from '../../../fields/documentSchemas'; import { List } from '../../../fields/List'; import { createSchema, listSpec, makeInterface } from '../../../fields/Schema'; -import { Cast, NumCast, StrCast } from '../../../fields/Types'; +import { Cast, StrCast } from '../../../fields/Types'; import { DragManager } from '../../util/DragManager'; import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxBaseComponent } from '../DocComponent'; +import { StyleProp } from '../StyleProvider'; import { FieldView, FieldViewProps } from './FieldView'; +import BigText from './LabelBigText'; import './LabelBox.scss'; -import { StyleProp } from '../StyleProvider'; const LabelSchema = createSchema({}); type LabelDocument = makeInterface<[typeof LabelSchema, typeof documentSchema]>; const LabelDocument = makeInterface(LabelSchema, documentSchema); +export interface LabelBoxProps { + label?: string; +} + @observer -export class LabelBox extends ViewBoxBaseComponent<FieldViewProps, LabelDocument>(LabelDocument) { +export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxProps), LabelDocument>(LabelDocument) { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(LabelBox, fieldKey); } + public static LayoutStringWithTitle(fieldType: { name: string }, fieldStr: string, label: string) { + return `<${fieldType.name} fieldKey={'${fieldStr}'} label={'${label}'} {...props} />`; //e.g., "<ImageBox {...props} fieldKey={"data} />" + } private dropDisposer?: DragManager.DragDropDisposer; + componentDidMount() { + this.props.setContentView?.(this); + } + + getTitle() { + return this.props.label ? this.props.label : this.rootDoc["title-custom"] ? StrCast(this.rootDoc.title) : + typeof this.rootDoc[this.fieldKey] === "string" ? StrCast(this.rootDoc[this.fieldKey]) : StrCast(this.rootDoc.title); + } + protected createDropTarget = (ele: HTMLDivElement) => { this.dropDisposer?.(); if (ele) { @@ -65,8 +82,8 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps, LabelDocument render() { const params = Cast(this.paramsDoc["onClick-paramFieldKeys"], listSpec("string"), []); const missingParams = params?.filter(p => !this.paramsDoc[p]); - params?.map(p => DocListCast(this.paramsDoc[p])); // bcz: really hacky form of prefetching ... - const label = typeof this.rootDoc[this.fieldKey] === "string" ? StrCast(this.rootDoc[this.fieldKey]) : StrCast(this.rootDoc.title); + params?.map(p => DocListCast(this.paramsDoc[p])); // bcz: really hacky form of prefetching ... + const label = this.getTitle(); return ( <div className="labelBox-outerDiv" onMouseLeave={action(() => this._mouseOver = false)} @@ -75,17 +92,28 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps, LabelDocument style={{ boxShadow: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BoxShadow) }}> <div className="labelBox-mainButton" style={{ backgroundColor: this.hoverColor, - fontSize: StrCast(this.layoutDoc._fontSize) || "inherit", + fontSize: StrCast(this.layoutDoc._fontSize) || Math.min(18, this.props.PanelHeight() / 2), fontFamily: StrCast(this.layoutDoc._fontFamily) || "inherit", letterSpacing: StrCast(this.layoutDoc.letterSpacing), textTransform: StrCast(this.layoutDoc.textTransform) as any, - paddingLeft: NumCast(this.layoutDoc._xPadding), - paddingRight: NumCast(this.layoutDoc._xPadding), - paddingTop: NumCast(this.layoutDoc._yPadding), - paddingBottom: NumCast(this.layoutDoc._yPadding), + width: this.props.PanelWidth(), + height: this.props.PanelHeight(), whiteSpace: this.layoutDoc._singleLine ? "pre" : "pre-wrap" }} > - {label.startsWith("#") ? (null) : label} + <span ref={r => { + if (r) { + BigText(r, { + rotateText: null, + fontSizeFactor: 1, + maximumFontSize: null, + limitingDimension: "both", + horizontalAlign: "center", + verticalAlign: "center", + textAlign: "center", + whiteSpace: "nowrap" + }); + } + }}>{label.startsWith("#") ? (null) : label}</span> </div> <div className="labelBox-fieldKeyParams" > {!missingParams?.length ? (null) : missingParams.map(m => <div key={m} className="labelBox-missingParam">{m}</div>)} diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index 8f9959693..a7bfb93eb 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -3,7 +3,6 @@ import { action, observable } from "mobx"; import { observer } from "mobx-react"; import { Doc } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; -import { Id } from "../../../fields/FieldSymbols"; import { makeInterface } from "../../../fields/Schema"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { TraceMobx } from "../../../fields/util"; @@ -129,7 +128,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch <div className="linkAnchorBoxBox-flyout" title=" " onPointerOver={() => Doc.UnBrushDoc(this.rootDoc)}> <LinkEditor sourceDoc={Cast(this.dataDoc[this.fieldKey], Doc, null)} hideback={true} linkDoc={this.rootDoc} showLinks={action(() => { })} /> {!this._forceOpen ? (null) : <div className="linkAnchorBox-linkCloser" onPointerDown={action(() => this._isOpen = this._editing = this._forceOpen = false)}> - <FontAwesomeIcon color="dimGray" icon={"times"} size={"sm"} /> + <FontAwesomeIcon color="dimgray" icon={"times"} size={"sm"} /> </div>} </div> ); diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index 55ea45bb8..b82d16677 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -2,10 +2,10 @@ import React = require("react"); import { observer } from "mobx-react"; import { documentSchema } from "../../../fields/documentSchemas"; import { makeInterface } from "../../../fields/Schema"; -import { returnFalse } from "../../../Utils"; -import { CollectionTreeView } from "../collections/CollectionTreeView"; +import { emptyFunction, returnFalse } from "../../../Utils"; import { ViewBoxBaseComponent } from "../DocComponent"; import { StyleProp } from "../StyleProvider"; +import { ComparisonBox } from "./ComparisonBox"; import { FieldView, FieldViewProps } from './FieldView'; import "./LinkBox.scss"; @@ -20,22 +20,15 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps, LinkDocument>( if (this.dataDoc.treeViewOpen === undefined) setTimeout(() => this.dataDoc.treeViewOpen = true); return <div className={`linkBox-container${this.isContentActive() ? "-interactive" : ""}`} style={{ background: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor) }} > - <CollectionTreeView {...this.props} - childDocuments={[this.dataDoc]} - treeViewOpen={true} - treeViewExpandedView={"fields"} - treeViewHideTitle={true} - treeViewSkipFields={["treeViewExpandedView", "aliases", "_removeDropProperties", - "treeViewOpen", "aliasNumber", "isPrototype", "creationDate", "author"]} + <ComparisonBox {...this.props} + fieldKey="anchor" + setHeight={emptyFunction} dontRegisterView={true} renderDepth={this.props.renderDepth + 1} - CollectionView={undefined} - isAnyChildContentActive={returnFalse} isContentActive={this.isContentActiveFunc} addDocument={returnFalse} removeDocument={returnFalse} - moveDocument={returnFalse}> - </CollectionTreeView> + moveDocument={returnFalse} /> </div>; } }
\ No newline at end of file diff --git a/src/client/views/nodes/LinkDescriptionPopup.tsx b/src/client/views/nodes/LinkDescriptionPopup.tsx index b62a4dd56..a9d33f161 100644 --- a/src/client/views/nodes/LinkDescriptionPopup.tsx +++ b/src/client/views/nodes/LinkDescriptionPopup.tsx @@ -1,8 +1,9 @@ import React = require("react"); +import { action, observable } from "mobx"; import { observer } from "mobx-react"; -import "./LinkDescriptionPopup.scss"; -import { observable, action } from "mobx"; +import { Doc } from "../../../fields/Doc"; import { LinkManager } from "../../util/LinkManager"; +import "./LinkDescriptionPopup.scss"; import { TaskCompletionBox } from "./TaskCompletedBox"; @@ -25,7 +26,7 @@ export class LinkDescriptionPopup extends React.Component<{}> { onDismiss = (add: boolean) => { LinkDescriptionPopup.descriptionPopup = false; if (add) { - LinkManager.currentLink && (LinkManager.currentLink.description = this.description); + LinkManager.currentLink && (Doc.GetProto(LinkManager.currentLink).description = this.description); } } diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index 126a37eb8..424083dac 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -4,7 +4,7 @@ import { action, computed, observable } from 'mobx'; import { observer } from "mobx-react"; import wiki from "wikijs"; import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocCastAsync } from "../../../fields/Doc"; -import { NumCast, StrCast } from "../../../fields/Types"; +import { NumCast, StrCast, Cast } from "../../../fields/Types"; import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, setupMoveUpEvents, Utils } from "../../../Utils"; import { DocServer } from '../../DocServer'; import { Docs, DocUtils } from "../../documents/Documents"; @@ -14,6 +14,7 @@ import { undoBatch } from '../../util/UndoManager'; import { DocumentView, DocumentViewSharedProps } from "./DocumentView"; import './LinkDocPreview.scss'; import React = require("react"); +import { DocumentType } from '../../documents/DocumentTypes'; interface LinkDocPreviewProps { linkDoc?: Doc; @@ -85,7 +86,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { this._linkDoc = DocListCast(anchor.links)[0]; this._linkSrc = anchor; const linkTarget = LinkManager.getOppositeAnchor(this._linkDoc, this._linkSrc); - this._targetDoc = linkTarget?.annotationOn as Doc ?? linkTarget; + this._targetDoc = linkTarget?.type === DocumentType.MARKER && linkTarget?.annotationOn ? Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget : linkTarget; this._toolTipText = ""; } })); diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss index 72dec6e4c..f44355929 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -1,3 +1,5 @@ +@import "../global/globalCssVariables.scss"; + .pdfBox, .pdfBox-interactive { display: inline-block; @@ -18,12 +20,13 @@ top: 0; left: 0; + // glr: This should really be the same component as text and PDFs .pdfBox-sidebarBtn { - background: #121721; + background: $black; height: 25px; width: 25px; - right: 0; - color: white; + right: 5px; + color: $white; display: flex; position: absolute; align-items: center; @@ -31,6 +34,13 @@ border-radius: 3px; pointer-events: all; z-index: 1; // so it appears on top of the document's title, if shown + + box-shadow: $standard-box-shadow; + transition: 0.2s; + + &:hover{ + filter: brightness(0.85); + } } .pdfBox-pageNums { diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 5e07229c1..30b4dc92a 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, DocListCast } from "../../../fields/Doc"; +import { Doc, DocListCast, Opt, WidthSym, StrListCast } from "../../../fields/Doc"; import { documentSchema } from '../../../fields/documentSchemas'; -import { makeInterface } from "../../../fields/Schema"; -import { Cast, NumCast, StrCast, BoolCast } from '../../../fields/Types'; +import { makeInterface, listSpec } from "../../../fields/Schema"; +import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { PdfField } from "../../../fields/URLField"; import { TraceMobx } from '../../../fields/util'; -import { Utils, setupMoveUpEvents, emptyFunction, returnOne } from '../../../Utils'; +import { emptyFunction, returnOne, setupMoveUpEvents, Utils } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { KeyCodes } from '../../util/KeyCodes'; import { undoBatch } from '../../util/UndoManager'; @@ -17,13 +17,15 @@ import { panZoomSchema } from '../collections/collectionFreeForm/CollectionFreeF import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from "../DocComponent"; +import { Colors } from '../global/globalEnums'; +import { AnchorMenu } from '../pdf/AnchorMenu'; import { PDFViewer } from "../pdf/PDFViewer"; import { SidebarAnnos } from '../SidebarAnnos'; import { FieldView, FieldViewProps } from './FieldView'; import { pageSchema } from "./ImageBox"; import "./PDFBox.scss"; import React = require("react"); -import { AnchorMenu } from '../pdf/AnchorMenu'; +import { CurrentUserUtils } from '../../util/CurrentUserUtils'; type PdfDocument = makeInterface<[typeof documentSchema, typeof panZoomSchema, typeof pageSchema]>; const PdfDocument = makeInterface(documentSchema, panZoomSchema, pageSchema); @@ -78,9 +80,9 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const anchor = AnchorMenu.Instance?.GetAnchor() ?? Docs.Create.TextanchorDocument({ - title: StrCast(this.rootDoc.title + " " + this.layoutDoc._scrollTop), - annotationOn: this.rootDoc, + title: StrCast(this.rootDoc.title + "@" + this.layoutDoc._scrollTop?.toFixed(0)), y: NumCast(this.layoutDoc._scrollTop), + unrendered: true }); this.addDocument(anchor); return anchor; @@ -95,7 +97,17 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps !this.Document._fitWidth && (this.Document._height = this.Document[WidthSym]() * (nh / nw)); } - public search = (string: string, fwd: boolean) => this._pdfViewer?.search(string, fwd); + public search = action((searchString: string, bwd?: boolean, clear: boolean = false) => { + if (!this._searching && !clear) { + this._searching = true; + setTimeout(() => { + this._searchRef.current?.focus(); + this._searchRef.current?.select(); + this._searchRef.current?.setRangeText(searchString); + }); + } + return this._pdfViewer?.search(searchString, bwd, clear) || false; + }); public prevAnnotation = () => this._pdfViewer?.prevAnnotation(); public nextAnnotation = () => this._pdfViewer?.nextAnnotation(); public backPage = () => { this.Document._curPage = (this.Document._curPage || 1) - 1; return true; }; @@ -106,11 +118,6 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps onKeyDown = action((e: KeyboardEvent) => { let processed = false; switch (e.key) { - case "f": if (e.ctrlKey) { - setTimeout(() => this._searchRef.current?.focus(), 100); - this._searching = processed = true; - } - break; case "PageDown": processed = this.forwardPage(); break; case "PageUp": processed = this.backPage(); break; } @@ -183,8 +190,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps <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} - onKeyDown={e => e.keyCode === KeyCodes.ENTER && this.search(this._searchString, !e.shiftKey)} /> - <button className="pdfBox-search" title="Search" onClick={e => this.search(this._searchString, !e.shiftKey)}> + onKeyDown={e => e.keyCode === KeyCodes.ENTER && this.search(this._searchString, e.shiftKey)} /> + <button className="pdfBox-search" title="Search" onClick={e => this.search(this._searchString, e.shiftKey)}> <FontAwesomeIcon icon="search" size="sm" /> </button> <button className="pdfBox-prevIcon" title="Previous Annotation" onClick={this.prevAnnotation} > @@ -195,7 +202,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps </button> </div> <button className="pdfBox-overlayButton" title={searchTitle} - onClick={action(() => { this._searching = !this._searching; this.search("mxytzlaf", true); })} > + onClick={action(() => { this._searching = !this._searching; this.search("", true, true); })} > <div className="pdfBox-overlayButton-arrow" onPointerDown={(e) => e.stopPropagation()} /> <div className="pdfBox-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}> <FontAwesomeIcon icon={this._searching ? "times" : "search"} size="lg" /> @@ -208,11 +215,15 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps onClick={action(() => this._pageControls = !this._pageControls)} /> {this._pageControls ? pageBtns : (null)} </div> - <button className="pdfBox-sidebarBtn" title="Toggle Sidebar" - style={{ display: !this.props.isContentActive() ? "none" : undefined }} + <div className="pdfBox-sidebarBtn" key="sidebar" title="Toggle Sidebar" + style={{ + display: !this.props.isContentActive() ? "none" : undefined, + top: StrCast(this.rootDoc._showTitle) === "title" ? 20 : 5, + backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK + }} onPointerDown={this.sidebarBtnDown} > - <FontAwesomeIcon icon={"chevron-left"} size="sm" /> - </button> + <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={"comment-alt"} size="sm" /> + </div> </div>; } sidebarWidth = () => !this.SidebarShown ? 0 : @@ -221,10 +232,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps specificContextMenu = (e: React.MouseEvent): void => { const funcs: ContextMenuProps[] = []; - funcs.push({ description: "Copy path", event: () => this.pdfUrl && Utils.CopyText(this.pdfUrl.url.pathname), icon: "expand-arrows-alt" }); - funcs.push({ description: "Toggle Fit Width " + (this.Document._fitWidth ? "Off" : "On"), event: () => this.Document._fitWidth = !this.Document._fitWidth, icon: "expand-arrows-alt" }); - funcs.push({ description: "Toggle Annotation View ", event: () => this.Document._showSidebar = !this.Document._showSidebar, icon: "expand-arrows-alt" }); - funcs.push({ description: "Toggle Sidebar ", event: () => this.toggleSidebar(), icon: "expand-arrows-alt" }); + funcs.push({ description: "Copy path", event: () => this.pdfUrl && Utils.CopyText(Utils.prepend("") + this.pdfUrl.url.pathname), icon: "expand-arrows-alt" }); + //funcs.push({ description: "Toggle Sidebar ", event: () => this.toggleSidebar(), icon: "expand-arrows-alt" }); ContextMenu.Instance.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); } @@ -241,9 +250,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @observable _showSidebar = false; @computed get SidebarShown() { return this._showSidebar || this.layoutDoc._showSidebar ? true : false; } - contentScaling = () => { - return 1; - } + contentScaling = () => 1; @computed get renderPdfView() { TraceMobx(); const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss index cdd36eb3b..4871599b8 100644 --- a/src/client/views/nodes/VideoBox.scss +++ b/src/client/views/nodes/VideoBox.scss @@ -14,6 +14,9 @@ height: 100%; position: relative; .videoBox-viewer { + display:flex; + flex-direction: column; + height: 100%; border-radius: inherit; opacity: 0.99; // hack! overcomes some kind of Chrome weirdness where buttons (e.g., snapshot) disappear at some point as the video is resized larger } @@ -26,7 +29,17 @@ .videoBox-stackPanel { z-index: -1; width: 100%; - position: absolute; + position: relative; + } + + .videoBox-annotationLayer { + position: relative; + transform-origin: left top; + top: 0; + width: 100%; + pointer-events: none; + mix-blend-mode: multiply; // bcz: makes text fuzzy! + overflow: hidden; } } diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 484dec7e2..440ccf638 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -30,6 +30,7 @@ import { DragManager } from "../../util/DragManager"; import { DocumentManager } from "../../util/DocumentManager"; import { DocumentType } from "../../documents/DocumentTypes"; import { Tooltip } from "@material-ui/core"; +import { AnchorMenu } from "../pdf/AnchorMenu"; const path = require('path'); type VideoDocument = makeInterface<[typeof documentSchema]>; @@ -74,7 +75,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp getAnchor = () => { const timecode = Cast(this.layoutDoc._currentTimecode, "number", null); - return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow"/* videoStart */, "_timecodeToHide" /* videoEnd */, timecode ? timecode : undefined) || this.rootDoc; + const marquee = AnchorMenu.Instance.GetAnchor?.(); + return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow"/* videoStart */, "_timecodeToHide" /* videoEnd */, timecode ? timecode : undefined, undefined, marquee) || this.rootDoc; } videoLoad = () => { @@ -323,8 +325,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp {!this.audiopath || this.audiopath === field.url.href ? (null) : <audio ref={this.setAudioRef} className={`audiobox-control${this.props.isContentActive() ? "-interactive" : ""}`}> <source src={this.audiopath} type="audio/mpeg" /> - Not supported. - </audio>} + Not supported. + </audio>} </div> </div>; } @@ -537,12 +539,18 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp Pause={this.Pause} playLink={this.playLink} PanelHeight={this.timelineHeight} + trimming={false} + trimStart={0} + trimEnd={this.duration} + trimDuration={this.duration} + setStartTrim={() => { }} + setEndTrim={() => { }} /> </div>; } @computed get annotationLayer() { - return <div className="imageBox-annotationLayer" style={{ transition: this.transition, height: `${this.heightPercent}%` }} ref={this._annotationLayer} />; + return <div className="videoBox-annotationLayer" style={{ transition: this.transition, height: `${this.heightPercent}%` }} ref={this._annotationLayer} />; } marqueeDown = (e: React.PointerEvent) => { @@ -560,10 +568,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this.props.select(true); }); + @computed get fitWidth() { return this.props.docViewPath?.().lastElement().fitWidth; } contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content]; scaling = () => this.props.scaling?.() || 1; - panelWidth = () => this.props.PanelWidth() * this.heightPercent / 100; - panelHeight = () => this.layoutDoc._fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 1) : this.props.PanelHeight() * this.heightPercent / 100; + panelWidth = (): number => this.fitWidth ? this.props.PanelWidth() : (Doc.NativeAspect(this.rootDoc) || 1) * this.panelHeight(); + panelHeight = (): number => this.fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 1) : this.heightPercent / 100 * this.props.PanelHeight(); screenToLocalTransform = () => { const offset = (this.props.PanelWidth() - this.panelWidth()) / 2 / this.scaling(); return this.props.ScreenToLocalTransform().translate(-offset, 0).scale(100 / this.heightPercent); @@ -577,10 +586,17 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp return (<div className="videoBox" onContextMenu={this.specificContextMenu} ref={this._mainCont} style={{ pointerEvents: this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined, - borderRadius + borderRadius, + overflow: this.props.docViewPath?.().lastElement().fitWidth ? "auto" : undefined }} onWheel={e => { e.stopPropagation(); e.preventDefault(); }}> <div className="videoBox-viewer" onPointerDown={this.marqueeDown} > - <div style={{ position: "absolute", transition: this.transition, width: this.panelWidth(), height: this.panelHeight(), top: 0, left: `${(100 - this.heightPercent) / 2}%` }}> + <div style={{ + position: "absolute", transition: this.transition, + width: this.panelWidth(), + height: this.panelHeight(), + top: 0, + left: (this.props.PanelWidth() - this.panelWidth()) / 2 + }}> <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} renderDepth={this.props.renderDepth + 1} fieldKey={this.annotationKey} @@ -600,9 +616,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp {this.contentFunc} </CollectionFreeFormView> </div> - {this.uIButtons} {this.annotationLayer} - {this.renderTimeline} {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) : <MarqueeAnnotator rootDoc={this.rootDoc} @@ -617,6 +631,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} />} + {this.renderTimeline} + {this.uIButtons} </div> </div >); } diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss index 19b69ff5a..79289abaa 100644 --- a/src/client/views/nodes/WebBox.scss +++ b/src/client/views/nodes/WebBox.scss @@ -6,11 +6,97 @@ position: relative; display: flex; + .webBox-ui { + position: absolute; + width: 100%; + height: 100%; + z-index: 1; + pointer-events: none; + top: 0; + left: 0; + overflow: hidden; + + .webBox-overlayButton { + border-bottom-left-radius: 50%; + display: flex; + justify-content: space-evenly; + align-items: center; + height: 20px; + background: none; + padding: 0; + position: absolute; + pointer-events: all; + color: white; + bottom: 0; + right: 0; + + .webBox-overlayButton-arrow { + width: 0; + height: 0; + border-top: 10px solid transparent; + border-bottom: 10px solid transparent; + border-right: 15px solid #121721; + transition: all 0.5s; + } + + .webBox-overlayButton-iconCont { + background: #121721; + height: 20px; + width: 25px; + display: flex; + position: relative; + align-items: center; + justify-content: center; + border-radius: 3px; + pointer-events: all; + } + } + + .webBox-nextIcon, + .webBox-prevIcon { + background: #121721; + color: white; + height: 20px; + width: 25px; + display: flex; + position: relative; + align-items: center; + justify-content: center; + border-radius: 3px; + pointer-events: all; + padding: 0px; + } + + .webBox-overlayButton:hover { + background: none; + } + + + .webBox-overlayCont { + position: absolute; + width: calc(100% - 40px); + height: 20px; + background: #121721; + bottom: 0; + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + transition: left .5s; + pointer-events: all; + + .webBox-searchBar { + width: 70%; + font-size: 14px; + } + } + } + .webBox-overlayButton-sidebar { background: #121721; height: 25px; width: 25px; - right: 0; + right: 5px; display: flex; position: absolute; align-items: center; @@ -18,6 +104,13 @@ border-radius: 3px; pointer-events: all; z-index: 1; // so it appears on top of the document's title, if shown + + box-shadow: $standard-box-shadow; + transition: 0.2s; + + &:hover{ + filter: brightness(0.85); + } } .pdfViewerDash-dragAnnotationBox { diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index e4d4557af..37d716993 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -13,23 +13,27 @@ 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, returnEmptyString, returnEmptyFilter } from "../../../Utils"; +import { emptyFunction, getWordAtPoint, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, Utils } from "../../../Utils"; import { Docs } from "../../documents/Documents"; -import { DocumentType } from '../../documents/DocumentTypes'; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; +import { KeyCodes } from "../../util/KeyCodes"; import { Scripting } from "../../util/Scripting"; import { SnappingManager } from "../../util/SnappingManager"; import { undoBatch } from "../../util/UndoManager"; +import { MarqueeOptionsMenu } from "../collections/collectionFreeForm"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from "../DocComponent"; import { DocumentDecorations } from "../DocumentDecorations"; +import { Colors } from "../global/globalEnums"; import { LightboxView } from "../LightboxView"; import { MarqueeAnnotator } from "../MarqueeAnnotator"; import { AnchorMenu } from "../pdf/AnchorMenu"; import { Annotation } from "../pdf/Annotation"; import { SidebarAnnos } from "../SidebarAnnos"; +import { StyleProp } from "../StyleProvider"; +import { DocumentViewProps } from "./DocumentView"; import { FieldView, FieldViewProps } from './FieldView'; import { LinkDocPreview } from "./LinkDocPreview"; import "./WebBox.scss"; @@ -52,6 +56,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps private _keyInput = React.createRef<HTMLInputElement>(); private _initialScroll: Opt<number>; private _sidebarRef = React.createRef<SidebarAnnos>(); + private _searchRef = React.createRef<HTMLInputElement>(); + private _searchString = ""; + @observable private _searching: boolean = false; + @observable _showSidebar = false; @observable private _scrollTimer: any; @observable private _overlayAnnoInfo: Opt<Doc>; @observable private _marqueeing: number[] | undefined; @@ -59,7 +67,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @observable private _iframeClick: HTMLIFrameElement | undefined = undefined; @observable private _iframe: HTMLIFrameElement | null = null; @observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); - @observable private _scrollHeight = 1500; + @observable private _scrollHeight = NumCast(this.layoutDoc.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; } @@ -69,15 +77,28 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps constructor(props: any) { super(props); - 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); + Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(this.dataDoc) || 850); + Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(this.dataDoc) || this.Document[HeightSym]() / this.Document[WidthSym]() * 850); + } + + @action + search = (searchString: string, bwd?: boolean, clear: boolean = false) => { + if (!this._searching && !clear) { + this._searching = true; + setTimeout(() => { + this._searchRef.current?.focus(); + this._searchRef.current?.select(); + this._searchRef.current?.setRangeText(searchString); + }); + } + if (clear) { + this._iframe?.contentWindow?.getSelection()?.empty(); } - if (this.layoutDoc[this.fieldKey + "-contentWidth"] === undefined) { - this.layoutDoc[this.fieldKey + "-contentWidth"] = Doc.NativeWidth(this.layoutDoc); + if (searchString) { + (this._iframe?.contentWindow as any)?.find(searchString, false, bwd, true); } + return true; } - async 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. @@ -133,6 +154,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps componentWillUnmount() { Object.values(this._disposers).forEach(disposer => disposer?.()); this._iframe?.removeEventListener('wheel', this.iframeWheel, true); + this._iframe?.contentDocument?.removeEventListener("pointerup", this.iframeUp); } @action @@ -187,8 +209,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps 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) + y: NumCast(this.layoutDoc._scrollTop), + unrendered: true }); this.addDocumentWrapper(anchor); return anchor; @@ -196,8 +218,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @action iframeUp = (e: PointerEvent) => { + this.props.docViewPath().lastElement()?.docView?.cleanupPointerEvents(); // pointerup events aren't generated on containing document view, so we have to invoke it here. if (this._iframe?.contentWindow && this._iframe.contentDocument && !this._iframe.contentWindow.getSelection()?.isCollapsed) { - this._iframe.contentDocument.addEventListener("pointerup", this.iframeUp); const mainContBounds = Utils.GetScreenTransform(this._mainCont.current!); const scale = (this.props.scaling?.() || 1) * mainContBounds.scale; const sel = this._iframe.contentWindow.getSelection(); @@ -206,7 +228,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps AnchorMenu.Instance.jumpTo(e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._scrollTop) * scale); } - } else AnchorMenu.Instance.fadeOut(true); + } } @action iframeDown = (e: PointerEvent) => { @@ -218,7 +240,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this._marqueeing = [e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._scrollTop) * scale]; if (word) { - this._iframe?.contentDocument?.addEventListener("pointerup", this.iframeUp); 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. } else { this._iframeClick = this._iframe ?? undefined; @@ -227,8 +248,18 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps e.stopPropagation(); e.preventDefault(); } + + // bcz: hack - iframe grabs all events which messes up how we handle contextMenus. So this super naively simulates the event stack to get the specific menu items and the doc view menu items. + if (e.button === 2 || (e.button === 0 && e.altKey)) { + e.preventDefault(); + e.stopPropagation(); + ContextMenu.Instance.closeMenu(); + ContextMenu.Instance.setIgnoreEvents(true); + } } + getScrollHeight = () => this._scrollHeight; + isFirefox = () => { return "InstallTrigger" in window; // navigator.userAgent.indexOf("Chrome") !== -1; } @@ -238,6 +269,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @action iframeLoaded = (e: any) => { const iframe = this._iframe; + this._iframe?.contentDocument?.addEventListener("pointerup", this.iframeUp); if (iframe?.contentDocument) { iframe?.contentDocument.addEventListener("pointerdown", this.iframeDown); this._scrollHeight = Math.max(this.scrollHeight, iframe?.contentDocument.body.scrollHeight); @@ -321,13 +353,14 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (future === undefined) this.dataDoc[this.fieldKey + "-future"] = new List<string>([this._url]); else this.dataDoc[this.fieldKey + "-future"] = new List<string>([...future, this._url]); this.dataDoc[this.fieldKey] = new WebField(new URL(history.pop()!)); + console.log(this._urlHash); return true; } return false; } static urlHash = (s: string) => { - return s.split('').reduce((a: any, b: any) => { a = ((a << 5) - a) + b.charCodeAt(0); return a & a; }, 0); + return Math.abs(s.split('').reduce((a: any, b: any) => { a = ((a << 5) - a) + b.charCodeAt(0); return a & a; }, 0)); } @action @@ -391,13 +424,22 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ); } - specificContextMenu = (e: React.MouseEvent): void => { + specificContextMenu = (e: React.MouseEvent | PointerEvent): void => { const cm = ContextMenu.Instance; const funcs: ContextMenuProps[] = []; - funcs.push({ description: (this.layoutDoc.useCors ? "Don't Use" : "Use") + " Cors", event: () => this.layoutDoc.useCors = !this.layoutDoc.useCors, icon: "snowflake" }); - funcs.push({ description: (this.layoutDoc[this.fieldKey + "-contentWidth"] ? "Unfreeze" : "Freeze") + " Content Width", event: () => this.layoutDoc[this.fieldKey + "-contentWidth"] = this.layoutDoc[this.fieldKey + "-contentWidth"] ? undefined : Doc.NativeWidth(this.layoutDoc), icon: "snowflake" }); - funcs.push({ description: "Toggle Annotation View ", event: () => this.Document._showSidebar = !this.Document._showSidebar, icon: "expand-arrows-alt" }); - cm.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); + if (!cm.findByDescription("Options...")) { + !Doc.UserDoc().noviceMode && funcs.push({ description: (this.layoutDoc.useCors ? "Don't Use" : "Use") + " Cors", event: () => this.layoutDoc.useCors = !this.layoutDoc.useCors, icon: "snowflake" }); + funcs.push({ + description: (!this.layoutDoc.forceReflow ? "Force" : "Prevent") + " Reflow", event: () => { + const nw = !this.layoutDoc.forceReflow ? undefined : Doc.NativeWidth(this.layoutDoc) - this.sidebarWidth() / (this.props.scaling?.() || 1); + this.layoutDoc.forceReflow = !nw; + if (nw) { + Doc.SetInPlace(this.layoutDoc, this.fieldKey + "-nativeWidth", nw, true); + } + }, icon: "snowflake" + }); + cm.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); + } } @action @@ -410,15 +452,22 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false); } } - @action finishMarquee = (x?: number, y?: number) => { + @action finishMarquee = (x?: number, y?: number, e?: PointerEvent) => { this._marqueeing = undefined; this._isAnnotating = false; this._iframeClick = undefined; - x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false, false); + if (x !== undefined && y !== undefined) { + this._setPreviewCursor?.(x, y, false, false); + ContextMenu.Instance.closeMenu(); + ContextMenu.Instance.setIgnoreEvents(false); + if (e?.button === 2 || e?.altKey) { + this.specificContextMenu(undefined as any); + this.props.docViewPath().lastElement().docView?.onContextMenu(undefined, x, y); + } + } } - @computed - get urlContent() { + @computed get urlContent() { const field = this.dataDoc[this.props.fieldKey]; let view; if (field instanceof HtmlField) { @@ -485,34 +534,63 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps NumCast(this.layoutDoc.nativeWidth) @computed get content() { - 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)}%`, }}> + const interactive = !this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && this.props.isContentActive() && this.props.pointerEvents !== "none" && CurrentUserUtils.SelectedTool === InkTool.None && !DocumentDecorations.Instance?.Interacting; + return <div className={"webBox-cont" + (interactive ? "-interactive" : "")} + style={{ width: !this.layoutDoc.forceReflow ? NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]) || `100%` : "100%", }}> {this.urlContent} </div>; } @computed get annotationLayer() { TraceMobx(); + const pe = this.pointerEvents(); return <div className="webBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}> {this.inlineTextAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map(anno => - <Annotation {...this.props} fieldKey={this.annotationKey} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} />) - } + <Annotation {...this.props} fieldKey={this.annotationKey} pointerEvents={pe} 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; } + @computed get searchUI() { + return <div className="webBox-ui" + onPointerDown={e => e.stopPropagation()} style={{ display: this.props.isContentActive() ? "flex" : "none" }}> + <div className="webBox-overlayCont" onPointerDown={(e) => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}> + <button className="webBox-overlayButton" title={"search"} /> + <input className="webBox-searchBar" placeholder="Search" ref={this._searchRef} onChange={this.searchStringChanged} + onKeyDown={e => e.keyCode === KeyCodes.ENTER && this.search(this._searchString, e.shiftKey)} /> + <button className="webBox-search" title="Search" onClick={e => this.search(this._searchString, e.shiftKey)}> + <FontAwesomeIcon icon="search" size="sm" /> + </button> + </div> + <button className="webBox-overlayButton" title={"search"} + onClick={action(() => { this._searching = !this._searching; this.search("", false, true); })} > + <div className="webBox-overlayButton-arrow" onPointerDown={(e) => e.stopPropagation()} /> + <div className="webBox-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}> + <FontAwesomeIcon icon={this._searching ? "times" : "search"} size="lg" /> + </div> + </button> + </div>; + } + searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => this._searchString = e.currentTarget.value; showInfo = action((anno: Opt<Doc>) => this._overlayAnnoInfo = anno); 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; + basicFilter = () => [...this.props.docFilters(), Utils.PropUnsetFilter("textInlineAnnotations")]; transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()]; opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()]; + childStyleProvider = (doc: (Doc | undefined), props: Opt<DocumentViewProps>, property: string): any => { + if (doc instanceof Doc && property === StyleProp.PointerEvents) { + if (doc.textInlineAnnotations) return "none"; + } + return this.props.styleProvider?.(doc, props, property); + } + pointerEvents = () => this.props.isContentActive() && this.props.pointerEvents !== "none" && !MarqueeOptionsMenu.Instance.isShown() ? "all" : SnappingManager.GetIsDragging() ? undefined : "none"; render() { - const pointerEvents = this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined; + const pointerEvents = this.props.layerProvider?.(this.layoutDoc) === false ? "none" : this.props.pointerEvents ? this.props.pointerEvents as any : undefined; const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; const scale = previewScale * (this.props.scaling?.() || 1); const renderAnnotations = (docFilters?: () => string[]) => @@ -527,7 +605,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ScreenToLocalTransform={this.scrollXf} scaling={returnOne} dropAction={"alias"} - docFilters={docFilters || this.props.docFilters} + docFilters={docFilters || this.basicFilter} dontRenderDocuments={docFilters ? false : true} select={emptyFunction} ContentScaling={returnOne} @@ -536,10 +614,11 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps removeDocument={this.removeDocument} moveDocument={this.moveDocument} addDocument={this.sidebarAddDocument} - childPointerEvents={true} + styleProvider={this.childStyleProvider} pointerEvents={CurrentUserUtils.SelectedTool !== InkTool.None || this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} />; return ( - <div className="webBox" ref={this._mainCont} style={{ pointerEvents: this.props.isContentActive() ? "all" : this.props.isContentActive() || SnappingManager.GetIsDragging() ? undefined : "none" }} > + <div className="webBox" ref={this._mainCont} + style={{ pointerEvents: this.pointerEvents() }} > <div className={`webBox-container`} style={{ pointerEvents }} onContextMenu={this.specificContextMenu}> <base target="_blank" /> <div className={"webBox-outerContent"} ref={this._outerRef} @@ -590,11 +669,16 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps moveDocument={this.moveDocument} removeDocument={this.removeDocument} /> - <button className="webBox-overlayButton-sidebar" key="sidebar" title="Toggle Sidebar" - style={{ display: !this.props.isContentActive() ? "none" : undefined }} + <div className="webBox-overlayButton-sidebar" key="sidebar" title="Toggle Sidebar" + style={{ + display: !this.props.isContentActive() ? "none" : undefined, + top: StrCast(this.rootDoc._showTitle) === "title" ? 20 : 5, + backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK + }} onPointerDown={this.sidebarBtnDown} > - <FontAwesomeIcon style={{ color: "white" }} icon={"chevron-left"} size="sm" /> - </button> + <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={"comment-alt"} size="sm" /> + </div> + {!this.props.isContentActive() ? (null) : this.searchUI} </div>); } } diff --git a/src/client/views/nodes/button/ButtonInterface.ts b/src/client/views/nodes/button/ButtonInterface.ts new file mode 100644 index 000000000..0aa2ac8e1 --- /dev/null +++ b/src/client/views/nodes/button/ButtonInterface.ts @@ -0,0 +1,12 @@ +import { Doc } from "../../../../fields/Doc"; +import { IconProp } from "@fortawesome/fontawesome-svg-core"; +import { ButtonType } from "./FontIconBox"; + +export interface IButtonProps { + type: string | ButtonType; + rootDoc: Doc; + label: any; + icon: IconProp; + color: string; + backgroundColor: string; +}
\ No newline at end of file diff --git a/src/client/views/nodes/button/ButtonScripts.ts b/src/client/views/nodes/button/ButtonScripts.ts new file mode 100644 index 000000000..bb4dd8bc9 --- /dev/null +++ b/src/client/views/nodes/button/ButtonScripts.ts @@ -0,0 +1,14 @@ +import { Scripting } from "../../../util/Scripting"; +import { SelectionManager } from "../../../util/SelectionManager"; + +// toggle: Set overlay status of selected document +Scripting.addGlobal(function changeView(view: string) { + const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined; + selected ? selected.Document._viewType = view : console.log("[FontIconBox.tsx] changeView failed"); +}); + +// toggle: Set overlay status of selected document +Scripting.addGlobal(function toggleOverlay() { + const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined; + selected ? selected.props.CollectionFreeFormDocumentView?.().float() : console.log("failed"); +});
\ No newline at end of file diff --git a/src/client/views/nodes/button/FontIconBadge.scss b/src/client/views/nodes/button/FontIconBadge.scss new file mode 100644 index 000000000..78f506e57 --- /dev/null +++ b/src/client/views/nodes/button/FontIconBadge.scss @@ -0,0 +1,11 @@ +.fontIconBadge { + background: red; + width: 15px; + height: 15px; + top: 8px; + display: block; + position: absolute; + right: 5; + border-radius: 50%; + text-align: center; +}
\ No newline at end of file diff --git a/src/client/views/nodes/button/FontIconBadge.tsx b/src/client/views/nodes/button/FontIconBadge.tsx new file mode 100644 index 000000000..cf86b5e07 --- /dev/null +++ b/src/client/views/nodes/button/FontIconBadge.tsx @@ -0,0 +1,37 @@ +import { observer } from "mobx-react"; +import * as React from "react"; +import { AclPrivate, Doc, DocListCast } from "../../../../fields/Doc"; +import { GetEffectiveAcl } from "../../../../fields/util"; +import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../../../Utils"; +import { DragManager } from "../../../util/DragManager"; +import "./FontIconBadge.scss"; + +interface FontIconBadgeProps { + collection: Doc | undefined; +} + +@observer +export class FontIconBadge extends React.Component<FontIconBadgeProps> { + _notifsRef = React.createRef<HTMLDivElement>(); + + onPointerDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, + (e: PointerEvent) => { + const dragData = new DragManager.DocumentDragData([this.props.collection!]); + DragManager.StartDocumentDrag([this._notifsRef.current!], dragData, e.x, e.y); + return true; + }, + returnFalse, emptyFunction, false); + } + + render() { + if (!(this.props.collection instanceof Doc)) return (null); + const length = DocListCast(this.props.collection.data).filter(d => GetEffectiveAcl(d) !== AclPrivate).length; // Object.keys(d).length).length; // filter out any documents that we can't read + return <div className="fontIconBadge-container" ref={this._notifsRef}> + <div className="fontIconBadge" style={length > 0 ? { "display": "initial" } : { "display": "none" }} + onPointerDown={this.onPointerDown} > + {length} + </div> + </div>; + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/button/FontIconBox.scss b/src/client/views/nodes/button/FontIconBox.scss new file mode 100644 index 000000000..079c767b9 --- /dev/null +++ b/src/client/views/nodes/button/FontIconBox.scss @@ -0,0 +1,411 @@ +@import "../../global/globalCssVariables"; + +.menuButton { + height: 100%; + display: flex; + justify-content: center; + align-items: center; + font-size: 80%; + border-radius: $standard-border-radius; + transition: 0.15s; + + .menuButton-wrap { + grid-column: 1; + justify-content: center; + align-items: center; + text-align: center; + } + + .fontIconBox-label { + color: $white; + position: relative; + text-align: center; + font-size: 7px; + letter-spacing: normal; + background-color: inherit; + margin-top: 5px; + border-radius: 8px; + padding: 0; + width: 100%; + font-family: 'ROBOTO'; + text-transform: uppercase; + font-weight: bold; + transition: 0.15s; + + + } + + .fontIconBox-icon { + width: 80%; + height: 80%; + } + + &.clickBtn { + cursor: pointer; + + &:hover { + background-color: rgba(0, 0, 0, 0.3) !important; + } + + svg { + width: 50% !important; + height: 50%; + } + } + + &.textBtn { + display: grid; + /* grid-row: auto; */ + grid-auto-flow: column; + cursor: pointer; + width: 100%; + justify-content: center; + align-items: center; + justify-items: center; + + &:hover { + filter:brightness(0.85) !important; + } + } + + &.tglBtn { + cursor: pointer; + + &.switch { + //TOGGLE + + .switch { + position: relative; + display: inline-block; + width: 100%; + height: 25px; + margin: 0; + } + + .switch input { + opacity: 0; + width: 0; + height: 0; + } + + .slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: lightgrey; + -webkit-transition: .4s; + transition: .4s; + } + + .slider:before { + position: absolute; + content: ""; + height: 21px; + width: 21px; + left: 2px; + bottom: 2px; + background-color: $white; + -webkit-transition: .4s; + transition: .4s; + } + + input:checked+.slider { + background-color: $medium-blue; + } + + input:focus+.slider { + box-shadow: 0 0 1px $medium-blue; + } + + input:checked+.slider:before { + -webkit-transform: translateX(26px); + -ms-transform: translateX(26px); + transform: translateX(26px); + } + + /* Rounded sliders */ + .slider.round { + border-radius: $standard-border-radius; + } + + .slider.round:before { + border-radius: $standard-border-radius; + } + } + + svg { + width: 50% !important; + height: 50%; + } + + &:hover { + background-color: rgba(0, 0, 0, 0.3); + } + } + + &.toolBtn { + cursor: pointer; + width: 100%; + border-radius: 100%; + + svg { + width: 60% !important; + height: 60%; + } + } + + &.menuBtn { + cursor: pointer !important; + border-radius: 0px; + flex-direction: column; + + svg { + width: 45% !important; + height: 45%; + } + + &:hover{ + filter: brightness(0.85); + } + } + + + + &.colorBtn { + color: black; + cursor: pointer; + flex-direction: column; + background: transparent; + + .colorButton-color { + margin-top: 3px; + width: 80%; + height: 3px; + } + + .menuButton-dropdownBox { + position: absolute; + width: fit-content; + height: fit-content; + color: black; + top: 100%; + left: 0; + z-index: 21; + background-color: #e3e3e3; + box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3); + border-radius: 3px; + } + + &:hover { + background-color: rgba(0, 0, 0, 0.3) !important; + } + } + + &.drpdownList { + width: 100%; + display: grid; + grid-auto-columns: 80px 20px; + justify-items: center; + font-family: 'Roboto'; + white-space: nowrap; + text-overflow: ellipsis; + font-size: 13; + font-weight: 600; + overflow: hidden; + cursor: pointer; + background: transparent; + align-content: center; + align-items: center; + + &:hover { + background-color: rgba(0, 0, 0, 0.3) !important; + } + + .menuButton-dropdownList { + position: absolute; + width: 150px; + height: fit-content; + top: 100%; + z-index: 21; + background-color: $white; + box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3); + padding: 1px; + + .list-item { + color: $black; + width: 100%; + height: 25px; + font-weight: 400; + display: flex; + justify-content: left; + align-items: center; + padding-left: 5px; + } + + .list-item:hover { + background-color: lightgrey; + } + } + } + + &.numBtn { + cursor: pointer; + background: transparent; + + &:hover { + background-color: rgba(0, 0, 0, 0.3) !important; + } + + &.slider { + color: $white; + cursor: pointer; + flex-direction: column; + background: transparent; + + .menu-slider { + width: 100px; + } + + .menuButton-dropdownBox { + position: absolute; + width: fit-content; + height: fit-content; + top: 100%; + z-index: 21; + background-color: #e3e3e3; + box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3); + border-radius: $standard-border-radius; + } + } + + .button { + width: 25%; + display: flex; + align-items: center; + justify-content: center; + + &.number { + width: 50%; + + .button-input { + background: none; + border: none; + text-align: right; + width: 100%; + color: $white; + height: 100%; + text-align: center; + } + + .button-input:focus { + outline: none; + } + } + } + + &.list { + width: 100%; + justify-content: space-around; + border: $standard-border; + + .menuButton-dropdownList { + position: absolute; + width: fit-content; + height: fit-content; + min-width: 50%; + max-height: 50vh; + overflow-y: scroll; + top: 100%; + z-index: 21; + background-color: $white; + box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3); + padding: 1px; + + .list-item { + color: $black; + width: 100%; + height: 25px; + font-weight: 400; + display: flex; + justify-content: center; + align-items: center; + } + + .list-item:hover { + background-color: lightgrey; + } + } + } + } + + &.editableText { + cursor: text; + display: flex; + flex-direction: row; + gap: 5px; + padding-left: 10px; + justify-content: flex-start; + color: black; + background-color: $light-gray; + padding: 5px; + padding-left: 10px; + width: 100%; + height: 100%; + } + + &.drpDownBtn { + cursor: pointer; + background: transparent; + border: solid 0.5px grey; + + &.true { + background: rgba(0, 0, 0, 0.3); + } + + .menuButton-dropdownBox { + position: absolute; + width: 150px; + height: 250px; + top: 100%; + background-color: #e3e3e3; + box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3); + border-radius: $standard-border-radius; + } + } + + .menuButton-dropdown { + display: flex; + justify-content: center; + align-items: center; + font-size: 15px; + grid-column: 2; + border-radius: 0px 7px 7px 0px; + width: 13px; + height: 100%; + right: 0; + } + + .menuButton-dropdown-header { + width: 100%; + font-weight: 300; + padding: 5px; + overflow: hidden; + font-size: 12px; + white-space: nowrap; + text-overflow: ellipsis; + } + + .dropbox-background { + width: 100vw; + height: 100vh; + top: 0; + z-index: 20; + left: 0; + background: transparent; + position: fixed; + } + +}
\ No newline at end of file diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx new file mode 100644 index 000000000..33fa23805 --- /dev/null +++ b/src/client/views/nodes/button/FontIconBox.tsx @@ -0,0 +1,877 @@ +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Tooltip } from '@material-ui/core'; +import { action, computed, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { ColorState, SketchPicker } from 'react-color'; +import { Doc, StrListCast, WidthSym, HeightSym } from '../../../../fields/Doc'; +import { InkTool } from '../../../../fields/InkField'; +import { createSchema, makeInterface } from '../../../../fields/Schema'; +import { ScriptField } from '../../../../fields/ScriptField'; +import { BoolCast, Cast, NumCast, StrCast } from '../../../../fields/Types'; +import { WebField } from '../../../../fields/URLField'; +import { DocumentType } from '../../../documents/DocumentTypes'; +import { Scripting } from "../../../util/Scripting"; +import { SelectionManager } from '../../../util/SelectionManager'; +import { UndoManager, undoBatch } from '../../../util/UndoManager'; +import { CollectionViewType } from '../../collections/CollectionView'; +import { ContextMenu } from '../../ContextMenu'; +import { DocComponent } from '../../DocComponent'; +import { EditableView } from '../../EditableView'; +import { GestureOverlay } from '../../GestureOverlay'; +import { Colors } from '../../global/globalEnums'; +import { SetActiveInkColor, ActiveFillColor, SetActiveFillColor, ActiveInkWidth, ActiveInkColor, SetActiveInkWidth } from '../../InkingStroke'; +import { StyleProp } from '../../StyleProvider'; +import { FieldView, FieldViewProps } from '.././FieldView'; +import { RichTextMenu } from '../formattedText/RichTextMenu'; +import { Utils } from '../../../../Utils'; +import { IButtonProps } from './ButtonInterface'; +import { FontIconBadge } from './FontIconBadge'; +import './FontIconBox.scss'; +import { WebBox } from '../WebBox'; +const FontIconSchema = createSchema({ + icon: "string", +}); + +export enum ButtonType { + TextButton = "textBtn", + MenuButton = "menuBtn", + DropdownList = "drpdownList", + DropdownButton = "drpdownBtn", + ClickButton = "clickBtn", + DoubleButton = "dblBtn", + ToggleButton = "tglBtn", + ColorButton = "colorBtn", + ToolButton = "toolBtn", + NumberButton = "numBtn", + EditableText = "editableText" +} + +export enum NumButtonType { + Slider = "slider", + DropdownOptions = "list", + Inline = "inline" +} + +export interface ButtonProps extends FieldViewProps { + type?: ButtonType; +} + +type FontIconDocument = makeInterface<[typeof FontIconSchema]>; +const FontIconDocument = makeInterface(FontIconSchema); +@observer +export class FontIconBox extends DocComponent<ButtonProps, FontIconDocument>(FontIconDocument) { + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(FontIconBox, fieldKey); } + showTemplate = (): void => { + const dragFactory = Cast(this.layoutDoc.dragFactory, Doc, null); + dragFactory && this.props.addDocTab(dragFactory, "add:right"); + } + dragAsTemplate = (): void => { this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)'); }; + useAsPrototype = (): void => { this.layoutDoc.onDragStart = ScriptField.MakeFunction('makeDelegate(this.dragFactory, true)'); }; + + specificContextMenu = (): void => { + if (!Doc.UserDoc().noviceMode) { + const cm = ContextMenu.Instance; + cm.addItem({ description: "Show Template", event: this.showTemplate, icon: "tag" }); + cm.addItem({ description: "Use as Render Template", event: this.dragAsTemplate, icon: "tag" }); + cm.addItem({ description: "Use as Prototype", event: this.useAsPrototype, icon: "tag" }); + } + } + + // Determining UI Specs + @observable private label = StrCast(this.rootDoc.label, StrCast(this.rootDoc.title)); + @observable private icon = StrCast(this.dataDoc.icon, "user") as any; + @observable private dropdown: boolean = BoolCast(this.rootDoc.dropDownOpen); + @observable private buttonList: string[] = StrListCast(this.rootDoc.btnList); + @observable private type = StrCast(this.rootDoc.btnType); + + /** + * Types of buttons in dash: + * - Main menu button (LHS) + * - Tool button + * - Expandable button (CollectionLinearView) + * - Button inside of CollectionLinearView vs. outside of CollectionLinearView + * - Action button + * - Dropdown button + * - Color button + * - Dropdown list + * - Number button + **/ + + _batch: UndoManager.Batch | undefined = undefined; + /** + * Number button + */ + @computed get numberButton() { + const numBtnType: string = StrCast(this.rootDoc.numBtnType); + const setValue = (value: number) => { + // Script for running the toggle + const script: string = StrCast(this.rootDoc.script) + "(" + value + ")"; + ScriptField.MakeScript(script)?.script.run(); + }; + + // Script for checking the outcome of the toggle + const checkScript: string = StrCast(this.rootDoc.script) + "(0, true)"; + const checkResult: number = ScriptField.MakeScript(checkScript)?.script.run().result || 0; + + + if (numBtnType === NumButtonType.Slider) { + const dropdown = + <div + className="menuButton-dropdownBox" + onPointerDown={e => e.stopPropagation()} + > + <input type="range" step="1" min={NumCast(this.rootDoc.numBtnMin, 0)} max={NumCast(this.rootDoc.numBtnMax, 100)} value={checkResult} + className={"menu-slider"} id="slider" + onPointerDown={() => this._batch = UndoManager.StartBatch("presDuration")} + onPointerUp={() => this._batch?.end()} + onChange={e => { e.stopPropagation(); setValue(Number(e.target.value)); }} + /> + </div>; + return ( + <div + className={`menuButton ${this.type} ${numBtnType}`} + onClick={action(() => this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen)} + > + {checkResult} + {this.rootDoc.dropDownOpen ? dropdown : null} + </div> + ); + } else if (numBtnType === NumButtonType.DropdownOptions) { + const items: number[] = []; + for (let i = 0; i < 100; i++) { + if (i % 2 === 0) { + items.push(i); + } + } + const list = items.map((value) => { + return <div className="list-item" key={`${value}`} + style={{ + backgroundColor: value === checkResult ? Colors.LIGHT_BLUE : undefined + }} + onClick={() => setValue(value)}> + {value} + </div>; + }); + return ( + <div + className={`menuButton ${this.type} ${numBtnType}`} + > + <div className={`button`} onClick={action((e) => setValue(Number(checkResult) - 1))}> + <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={"minus"} /> + </div> + <div + className={`button ${'number'}`} + onPointerDown={(e) => { + e.stopPropagation(); + e.preventDefault(); + }} + onClick={action(() => this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen)} + > + <input + style={{ width: 30 }} + className="button-input" + type="number" + value={checkResult} + onChange={action((e) => setValue(Number(e.target.value)))} + /> + </div> + <div className={`button`} onClick={action((e) => setValue(Number(checkResult) + 1))}> + <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={"plus"} /> + </div> + + {this.rootDoc.dropDownOpen ? + <div> + <div className="menuButton-dropdownList" + style={{ left: "25%" }}> + {list} + </div> + <div className="dropbox-background" onClick={(e) => { e.stopPropagation(); this.rootDoc.dropDownOpen = false; }} /> + </div> : null} + + </div> + ); + } else { + return ( + <div> + + </div> + ); + } + + + } + + /** + * Dropdown button + */ + @computed get dropdownButton() { + const active: string = StrCast(this.rootDoc.dropDownOpen); + const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); + const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); + return ( + <div className={`menuButton ${this.type} ${active}`} + style={{ color: color, backgroundColor: backgroundColor, borderBottomLeftRadius: this.dropdown ? 0 : undefined }} + onClick={action(() => this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen)}> + <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} /> + {!this.label || !Doc.UserDoc()._showLabel ? (null) : <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}> {this.label} </div>} + <div + className="menuButton-dropdown" + style={{ borderBottomRightRadius: this.dropdown ? 0 : undefined }}> + <FontAwesomeIcon icon={'caret-down'} color={color} size="sm" /> + </div> + {this.rootDoc.dropDownOpen ? + <div className="menuButton-dropdownBox"> + {/* DROPDOWN BOX CONTENTS */} + </div> : null} + </div> + ); + } + + /** + * Dropdown list + */ + @computed get dropdownListButton() { + const active: string = StrCast(this.rootDoc.dropDownOpen); + const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); + const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); + + const script: string = StrCast(this.rootDoc.script); + + let noviceList: string[] = []; + let text: string | undefined; + let dropdown = true; + let icon: IconProp = "caret-down"; + + if (script === 'setView') { + const selected = SelectionManager.Docs().lastElement(); + if (selected) { + if (StrCast(selected.type) === DocumentType.COL) { + text = StrCast(selected._viewType); + } else { + dropdown = false; + text = selected.type === DocumentType.RTF ? "Text" : StrCast(selected.type); + icon = Doc.toIcon(selected); + } + } else { + dropdown = false; + icon = "globe-asia"; + text = "User Default"; + } + noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Stacking]; + } else if (script === 'setFont') { + const editorView = RichTextMenu.Instance?.TextView?.EditorView; + text = StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily); + noviceList = ["Roboto", "Times New Roman", "Arial", "Georgia", + "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]; + } + + // Get items to place into the list + const list = this.buttonList.map((value) => { + if (Doc.UserDoc().noviceMode && !noviceList.includes(value)) { + return; + } + const click = () => { + const s = ScriptField.MakeScript(script + '("' + value + '")'); + if (s) { + s.script.run().result; + } + }; + return <div className="list-item" key={`${value}`} + style={{ + fontFamily: script === 'setFont' ? value : undefined, + backgroundColor: value === text ? Colors.LIGHT_BLUE : undefined + }} + onClick={click}> + {value[0].toUpperCase() + value.slice(1)} + </div>; + }); + + const label = !this.label || !Doc.UserDoc()._showLabel ? (null) : + <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor, position: "absolute" }}> + {this.label} + </div>; + + return ( + <div className={`menuButton ${this.type} ${active}`} + style={{ backgroundColor: this.rootDoc.dropDownOpen ? Colors.MEDIUM_BLUE : backgroundColor, color: color, display: dropdown ? undefined : "flex" }} + onClick={dropdown ? () => this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen : undefined}> + {dropdown ? (null) : <FontAwesomeIcon style={{ marginLeft: 5 }} className={`fontIconBox-icon-${this.type}`} icon={icon} color={color} />} + <div className="menuButton-dropdown-header"> + {text && text[0].toUpperCase() + text.slice(1)} + </div> + {label} + {!dropdown ? (null) : <div className="menuButton-dropDown"> + <FontAwesomeIcon icon={icon} color={color} size="sm" /> + </div>} + {this.rootDoc.dropDownOpen ? + <div> + <div className="menuButton-dropdownList" + style={{ left: 0 }}> + {list} + </div> + <div className="dropbox-background" onClick={(e) => { e.stopPropagation(); this.rootDoc.dropDownOpen = false; }} /> + </div> + : null} + </div> + ); + } + + @observable colorPickerClosed: boolean = true; + @computed get colorScript() { + const script = StrCast(this.rootDoc.script); + return ScriptField.MakeScript(script + '(colValue, checkResult)', { colValue: "string", checkResult: "boolean" }); + } + + colorPicker = (curColor: string) => { + const change = (value: ColorState) => { + const s = this.colorScript; + s && undoBatch(() => s.script.run({ colValue: Utils.colorString(value), checkResult: false }).result)(); + }; + const presets = ['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', + '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', + '#FFFFFF', '#f1efeb', "transparent"]; + return <SketchPicker + onChange={change} + color={curColor} + presetColors={presets} />; + } + /** + * Color button + */ + @computed get colorButton() { + const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); + const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); + const curColor = this.colorScript?.script.run({ colValue: undefined, checkResult: true }).result ?? "transparent"; + + const label = !this.label || !Doc.UserDoc()._showLabel ? (null) : + <div className="fontIconBox-label" style={{ color, backgroundColor, position: "absolute" }}> + {this.label} + </div>; + + const dropdownCaret = <div + className="menuButton-dropDown" + style={{ borderBottomRightRadius: this.dropdown ? 0 : undefined }}> + <FontAwesomeIcon icon={'caret-down'} color={color} size="sm" /> + </div>; + setTimeout(() => this.colorPicker(curColor)); // cause an update to the color picker rendered in MainView + return ( + <div className={`menuButton ${this.type} ${this.colorPickerClosed}`} + style={{ color: color, borderBottomLeftRadius: this.dropdown ? 0 : undefined }} + onClick={action(() => this.colorPickerClosed = !this.colorPickerClosed)} + onPointerDown={e => e.stopPropagation()}> + <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} /> + <div className="colorButton-color" style={{ backgroundColor: curColor }} /> + {label} + {/* {dropdownCaret} */} + {this.colorPickerClosed ? (null) : + <div> + <div className="menuButton-dropdownBox" + onPointerDown={e => e.stopPropagation()} + onClick={e => e.stopPropagation()}> + {this.colorPicker(curColor)} + </div> + <div className="dropbox-background" onClick={action((e) => { + e.stopPropagation(); this.colorPickerClosed = true; + })} /> + </div>} + </div> + ); + } + + @computed get toggleButton() { + // Determine the type of toggle button + const switchToggle: boolean = BoolCast(this.rootDoc.switchToggle); + const buttonText: string = StrCast(this.rootDoc.buttonText); + // Colors + const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); + const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); + + // Button label + const label = !this.label || !Doc.UserDoc()._showLabel ? (null) : + <div className="fontIconBox-label" style={{ color, backgroundColor, position: "absolute" }}> + {this.label} + </div>; + + if (switchToggle) { + return ( + <div className={`menuButton ${this.type} ${'switch'}`}> + {buttonText ? buttonText : null} + <label className="switch"> + <input type="checkbox" + checked={backgroundColor === Colors.MEDIUM_BLUE} + /> + <span className="slider round"></span> + </label> + </div> + ); + } else { + return ( + <div className={`menuButton ${this.type}`} + style={{ opacity: 1, backgroundColor, color }}> + <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} /> + {label} + </div> + ); + } + } + + + + /** + * Default + */ + @computed get defaultButton() { + const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); + const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); + const active: string = StrCast(this.rootDoc.dropDownOpen); + return ( + <div className={`menuButton ${this.type}`} onContextMenu={this.specificContextMenu} + style={{ backgroundColor: "transparent", borderBottomLeftRadius: this.dropdown ? 0 : undefined }}> + <div className="menuButton-wrap"> + <FontAwesomeIcon className={`menuButton-icon-${this.type}`} icon={this.icon} color={"black"} size={"sm"} /> + {!this.label || !Doc.UserDoc()._showLabel ? (null) : <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}> {this.label} </div>} + </div> + </div> + ); + } + + @computed get editableText() { + // Script for running the toggle + const script: string = StrCast(this.rootDoc.script); + + // Script for checking the outcome of the toggle + const checkScript: string = StrCast(this.rootDoc.script) + "('', true)"; + + // Function to run the script + const checkResult = ScriptField.MakeScript(checkScript)?.script.run().result; + + const setValue = (value: string, shiftDown?: boolean): boolean => { + ScriptField.MakeScript(script + "('" + value + "')")?.script.run(); + return true; + }; + return ( + <div className="menuButton editableText"> + <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={"lock"} /> + <div style={{ width: "calc(100% - .875em)", paddingLeft: "4px" }}> + <EditableView GetValue={() => checkResult} SetValue={setValue} contents={checkResult} /> + </div> + </div> + ); + } + + + render() { + const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); + const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); + + const label = !this.label || !Doc.UserDoc()._showLabel ? (null) : + <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor, position: "absolute" }}> + {this.label} + </div>; + + const menuLabel = !this.label || !Doc.UserDoc()._showMenuLabel ? (null) : + <div className="fontIconBox-label" style={{ color: color, backgroundColor: "transparent" }}> + {this.label} + </div>; + + const buttonProps: IButtonProps = { + type: this.type, + rootDoc: this.rootDoc, + label: label, + backgroundColor: backgroundColor, + icon: this.icon, + color: color + }; + + const buttonText = StrCast(this.rootDoc.buttonText); + + // TODO:glr Add label of button type + let button = this.defaultButton; + + switch (this.type) { + case ButtonType.TextButton: + button = ( + <div className={`menuButton ${this.type}`} style={{ color: color, backgroundColor: backgroundColor, opacity: 1, gridAutoColumns: `${NumCast(this.rootDoc._height)} auto` }}> + <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} /> + {buttonText ? + <div className="button-text"> + {buttonText} + </div> + : null} + {label} + </div> + ); + // button = <TextButton {...buttonProps}></TextButton> + break; + case ButtonType.EditableText: + button = this.editableText; + break; + case ButtonType.NumberButton: + button = this.numberButton; + break; + case ButtonType.DropdownButton: + button = this.dropdownButton; + break; + case ButtonType.DropdownList: + button = this.dropdownListButton; + break; + case ButtonType.ColorButton: + button = this.colorButton; + break; + case ButtonType.ToolButton: + button = ( + <div className={`menuButton ${this.type}`} style={{ opacity: 1, backgroundColor: backgroundColor, color: color }}> + <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} /> + {label} + </div> + ); + break; + case ButtonType.ToggleButton: + button = this.toggleButton; + // button = <ToggleButton {...buttonProps}></ToggleButton> + break; + case ButtonType.ClickButton: + button = ( + <div className={`menuButton ${this.type}`} style={{ color: color, backgroundColor: backgroundColor, opacity: 1 }}> + <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} /> + {label} + </div> + ); + break; + case ButtonType.MenuButton: + const trailsIcon = <img src={`/assets/${"presTrails.png"}`} + style={{ width: 30, height: 30, filter: `invert(${color === Colors.DARK_GRAY ? "0%" : "100%"})` }} />; + button = ( + <div className={`menuButton ${this.type}`} style={{ color: color, backgroundColor: backgroundColor }}> + {this.icon === "pres-trail" ? trailsIcon : <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />} + {menuLabel} + <FontIconBadge collection={Cast(this.rootDoc.watchedDocuments, Doc, null)} /> + </div > + ); + break; + default: + break; + } + + return !this.layoutDoc.toolTip || this.type === ButtonType.DropdownList || this.type === ButtonType.ColorButton || this.type === ButtonType.NumberButton || this.type === ButtonType.EditableText ? button : + <Tooltip title={<div className="dash-tooltip">{StrCast(this.layoutDoc.toolTip)}</div>}> + {button} + </Tooltip>; + } +} + + +// toggle: Set overlay status of selected document +Scripting.addGlobal(function setView(view: string) { + const selected = SelectionManager.Docs().lastElement(); + selected ? selected._viewType = view : console.log("[FontIconBox.tsx] changeView failed"); +}); + + +// toggle: Set overlay status of selected document +Scripting.addGlobal(function setBackgroundColor(color?: string, checkResult?: boolean) { + const selected = SelectionManager.Docs().lastElement(); + if (checkResult) { + return selected?._backgroundColor ?? "transparent"; + } + if (selected) selected._backgroundColor = color; +}); + +// toggle: Set overlay status of selected document +Scripting.addGlobal(function setHeaderColor(color?: string, checkResult?: boolean) { + if (checkResult) { + return Doc.SharingDoc().userColor; + } + Doc.SharingDoc().userColor = undefined; + Doc.GetProto(Doc.SharingDoc()).userColor = color; + Doc.UserDoc().showTitle = color === "transparent" ? undefined : StrCast(Doc.UserDoc().showTitle, "creationDate"); +}); + +// toggle: Set overlay status of selected document +Scripting.addGlobal(function toggleOverlay(checkResult?: boolean) { + const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined; + if (checkResult && selected) { + if (NumCast(selected.Document.z) >= 1) return Colors.MEDIUM_BLUE; + return "transparent"; + } + selected ? selected.props.CollectionFreeFormDocumentView?.().float() : console.log("[FontIconBox.tsx] toggleOverlay failed"); +}); + +/** TEXT + * setFont + * setFontSize + * toggleBold + * toggleUnderline + * toggleItalic + * setAlignment + * toggleBold + * toggleItalic + * toggleUnderline + **/ + +// toggle: Set overlay status of selected document +Scripting.addGlobal(function setFont(font: string, checkResult?: boolean) { + SelectionManager.Docs().map(doc => doc._fontFamily = font); + const editorView = RichTextMenu.Instance.TextView?.EditorView; + if (checkResult) { + return StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily); + } + if (editorView) RichTextMenu.Instance.setFontFamily(font); + else Doc.UserDoc().fontFamily = font; +}); + +Scripting.addGlobal(function getActiveTextInfo(info: "family" | "size" | "color" | "highlight") { + const editorView = RichTextMenu.Instance.TextView?.EditorView; + const style = editorView?.state && RichTextMenu.Instance.getActiveFontStylesOnSelection(); + switch (info) { + case "family": return style?.activeFamilies[0]; + case "size": return style?.activeSizes[0]; + case "color": return style?.activeColors[0]; + case "highlight": return style?.activeHighlights[0]; + } +}); + +Scripting.addGlobal(function setAlignment(align: "left" | "right" | "center", checkResult?: boolean) { + const editorView = RichTextMenu.Instance?.TextView?.EditorView; + if (checkResult) { + let active: string; + if (editorView) { + active = editorView?.state && RichTextMenu.Instance.getActiveAlignment(); + } else { + active = StrCast(Doc.UserDoc().textAlign); + } + if (active === align) return Colors.MEDIUM_BLUE; + return "transparent"; + } + SelectionManager.Docs().map(doc => doc.textAlign = align); + switch (align) { + case "left": + editorView?.state && RichTextMenu.Instance.alignLeft(editorView, editorView.dispatch); + break; + case "center": + editorView?.state && RichTextMenu.Instance.alignCenter(editorView, editorView.dispatch); + break; + case "right": + editorView?.state && RichTextMenu.Instance.alignRight(editorView, editorView.dispatch); + break; + default: + break; + } + Doc.UserDoc().textAlign = align; +}); + +Scripting.addGlobal(function setBulletList(mapStyle: "bullet" | "decimal", checkResult?: boolean) { + const editorView = RichTextMenu.Instance?.TextView?.EditorView; + if (checkResult) { + const active = editorView?.state && RichTextMenu.Instance.getActiveListStyle(); + if (active === mapStyle) return Colors.MEDIUM_BLUE; + return "transparent"; + } + if (editorView) { + const active = editorView?.state && RichTextMenu.Instance.getActiveListStyle(); + editorView?.state && RichTextMenu.Instance.changeListType( + editorView.state.schema.nodes.ordered_list.create({ mapStyle: active === mapStyle ? "" : mapStyle })); + } +}); + +// toggle: Set overlay status of selected document +Scripting.addGlobal(function setFontColor(color?: string, checkResult?: boolean) { + const editorView = RichTextMenu.Instance.TextView?.EditorView; + + if (checkResult) { + return editorView ? RichTextMenu.Instance.fontColor : Doc.UserDoc().fontColor; + } + + if (editorView) color && RichTextMenu.Instance.setColor(color, editorView, editorView?.dispatch); + else Doc.UserDoc().fontColor = color; +}); + +// toggle: Set overlay status of selected document +Scripting.addGlobal(function setFontHighlight(color?: string, checkResult?: boolean) { + const selected = SelectionManager.Docs().lastElement(); + const editorView = RichTextMenu.Instance.TextView?.EditorView; + + if (checkResult) { + return (selected ?? Doc.UserDoc())._fontHighlight; + } + if (selected) { + selected._fontColor = color; + if (color) { + editorView?.state && RichTextMenu.Instance.setHighlight(color, editorView, editorView?.dispatch); + } + } + Doc.UserDoc()._fontHighlight = color; +}); + +// toggle: Set overlay status of selected document +Scripting.addGlobal(function setFontSize(size: string | number, checkResult?: boolean) { + if (typeof size === "number") size = size.toString(); + if (size && Number(size).toString() === size) size += "px"; + const editorView = RichTextMenu.Instance.TextView?.EditorView; + if (checkResult) { + return (editorView ? RichTextMenu.Instance.fontSize : StrCast(Doc.UserDoc().fontSize, "10px")).replace("px", ""); + } + if (editorView) RichTextMenu.Instance.setFontSize(size); + else Doc.UserDoc()._fontSize = size; +}); + +Scripting.addGlobal(function toggleBold(checkResult?: boolean) { + const editorView = RichTextMenu.Instance?.TextView?.EditorView; + if (checkResult) { + return (editorView ? RichTextMenu.Instance.bold : Doc.UserDoc().fontWeight === "bold") ? Colors.MEDIUM_BLUE : "transparent"; + } + if (editorView) RichTextMenu.Instance?.toggleBold(); + else Doc.UserDoc().fontWeight = Doc.UserDoc().fontWeight === "bold" ? undefined : "bold"; +}); + +Scripting.addGlobal(function toggleUnderline(checkResult?: boolean) { + const editorView = RichTextMenu.Instance?.TextView?.EditorView; + if (checkResult) { + return (editorView ? RichTextMenu.Instance.underline : Doc.UserDoc().textDecoration === "underline") ? Colors.MEDIUM_BLUE : "transparent"; + } + if (editorView) RichTextMenu.Instance?.toggleUnderline(); + else Doc.UserDoc().textDecoration = Doc.UserDoc().textDecoration === "underline" ? undefined : "underline"; +}); + +Scripting.addGlobal(function toggleItalic(checkResult?: boolean) { + const editorView = RichTextMenu.Instance?.TextView?.EditorView; + if (checkResult) { + return (editorView ? RichTextMenu.Instance.italics : Doc.UserDoc().fontStyle === "italics") ? Colors.MEDIUM_BLUE : "transparent"; + } + if (editorView) RichTextMenu.Instance?.toggleItalics(); + else Doc.UserDoc().fontStyle = Doc.UserDoc().fontStyle === "italics" ? undefined : "italics"; +}); + + + + +/** INK + * setActiveInkTool + * setStrokeWidth + * setStrokeColor + **/ + +Scripting.addGlobal(function setActiveInkTool(tool: string, checkResult?: boolean) { + if (checkResult) { + return ((Doc.UserDoc().activeInkTool === tool && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool) ? + Colors.MEDIUM_BLUE : "transparent"; + } + if (["circle", "square", "line"].includes(tool)) { + if (GestureOverlay.Instance.InkShape === tool) { + Doc.UserDoc().activeInkTool = InkTool.None; + GestureOverlay.Instance.InkShape = InkTool.None; + } else { + Doc.UserDoc().activeInkTool = InkTool.Pen; + GestureOverlay.Instance.InkShape = tool; + } + } else if (tool) { // pen + if (Doc.UserDoc().activeInkTool === tool && !GestureOverlay.Instance.InkShape) { + Doc.UserDoc().activeInkTool = InkTool.None; + } else { + Doc.UserDoc().activeInkTool = tool; + GestureOverlay.Instance.InkShape = ""; + } + } else { + Doc.UserDoc().activeInkTool = InkTool.None; + } +}); + +// toggle: Set overlay status of selected document +Scripting.addGlobal(function setFillColor(color?: string, checkResult?: boolean) { + const selected = SelectionManager.Docs().lastElement(); + if (checkResult) { + if (selected?.type === DocumentType.INK) { + return StrCast(selected.fillColor); + } + return ActiveFillColor(); + } + SetActiveFillColor(StrCast(color)); + SelectionManager.Docs().filter(doc => doc.type === DocumentType.INK).map(doc => doc.fillColor = color); +}); + +Scripting.addGlobal(function setStrokeWidth(width: number, checkResult?: boolean) { + if (checkResult) { + const selected = SelectionManager.Docs().lastElement(); + if (selected?.type === DocumentType.INK) { + return NumCast(selected.strokeWidth); + } + return ActiveInkWidth(); + } + SetActiveInkWidth(width.toString()); + SelectionManager.Docs().filter(doc => doc.type === DocumentType.INK).map(doc => doc.strokeWidth = Number(width)); +}); + +// toggle: Set overlay status of selected document +Scripting.addGlobal(function setStrokeColor(color?: string, checkResult?: boolean) { + if (checkResult) { + const selected = SelectionManager.Docs().lastElement(); + if (selected?.type === DocumentType.INK) { + return StrCast(selected.color); + } + return ActiveInkColor(); + } + SetActiveInkColor(StrCast(color)); + SelectionManager.Docs().filter(doc => doc.type === DocumentType.INK).map(doc => doc.color = String(color)); +}); + + +/** WEB + * webSetURL + **/ +Scripting.addGlobal(function webSetURL(url: string, checkResult?: boolean) { + const selected = SelectionManager.Views().lastElement(); + if (selected?.rootDoc.type === DocumentType.WEB) { + if (checkResult) { + return StrCast(selected.rootDoc.data, Cast(selected.rootDoc.data, WebField, null)?.url?.href); + } + (selected.ComponentView as WebBox).submitURL(url); + //selected.rootDoc.data = new WebField(url); + } +}); +Scripting.addGlobal(function webForward() { + const selected = SelectionManager.Views().lastElement(); + if (selected?.rootDoc.type === DocumentType.WEB) { + (selected.ComponentView as WebBox).forward(); + } +}); +Scripting.addGlobal(function webBack() { + const selected = SelectionManager.Views().lastElement(); + if (selected?.rootDoc.type === DocumentType.WEB) { + (selected.ComponentView as WebBox).back(); + } +}); + + +/** Schema + * toggleSchemaPreview + **/ +Scripting.addGlobal(function toggleSchemaPreview(checkResult?: boolean) { + const selected = SelectionManager.Docs().lastElement(); + if (checkResult && selected) { + const result: boolean = NumCast(selected.schemaPreviewWidth) > 0; + if (result) return Colors.MEDIUM_BLUE; + else return "transparent"; + } + else if (selected) { + if (NumCast(selected.schemaPreviewWidth) > 0) { + selected.schemaPreviewWidth = 200; + } else { + selected.schemaPreviewWidth = 0; + } + } +}); + +/** STACK + * groupBy + */ +Scripting.addGlobal(function setGroupBy(key: string, checkResult?: boolean) { + SelectionManager.Docs().map(doc => doc._fontFamily = key); + const editorView = RichTextMenu.Instance.TextView?.EditorView; + if (checkResult) { + return StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily); + } + if (editorView) RichTextMenu.Instance.setFontFamily(key); + else Doc.UserDoc().fontFamily = key; +});
\ No newline at end of file diff --git a/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx b/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx new file mode 100644 index 000000000..235495250 --- /dev/null +++ b/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx @@ -0,0 +1,75 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import React, { Component } from 'react'; +import { BoolCast, StrCast } from '../../../../../fields/Types'; +import { IButtonProps } from '../ButtonInterface'; +import { ColorState, SketchPicker } from 'react-color'; +import { ScriptField } from '../../../../../fields/ScriptField'; +import { Doc } from '../../../../../fields/Doc'; + +export class ColorDropdown extends Component<IButtonProps> { + render() { + const active: string = StrCast(this.props.rootDoc.dropDownOpen); + + const script: string = StrCast(this.props.rootDoc.script); + const scriptCheck: string = script + "(undefined, true)"; + const boolResult = ScriptField.MakeScript(scriptCheck)?.script.run().result; + + const stroke: boolean = false; + // if (script === "setStrokeColor") { + // stroke = true; + // const checkWidth = ScriptField.MakeScript("setStrokeWidth(0, true)")?.script.run().result; + // const width = 20 + (checkWidth / 100) * 70; + // const height = 20 + (checkWidth / 100) * 70; + // strokeIcon = (<div style={{ borderRadius: "100%", width: width + '%', height: height + '%', backgroundColor: boolResult ? boolResult : "#FFFFFF" }} />); + // } + + const colorOptions: string[] = ['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', + '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', + '#FFFFFF', '#f1efeb']; + + const colorBox = (func: (color: ColorState) => void) => <SketchPicker + disableAlpha={!stroke} + onChange={func} color={boolResult ? boolResult : "#FFFFFF"} + presetColors={colorOptions} />; + const label = !this.props.label || !Doc.UserDoc()._showLabel ? (null) : + <div className="fontIconBox-label" style={{ color: this.props.color, backgroundColor: this.props.backgroundColor, position: "absolute" }}> + {this.props.label} + </div>; + + const dropdownCaret = <div + className="menuButton-dropDown" + style={{ borderBottomRightRadius: active ? 0 : undefined }}> + <FontAwesomeIcon icon={'caret-down'} color={this.props.color} size="sm" /> + </div>; + + const click = (value: ColorState) => { + const hex: string = value.hex; + const s = ScriptField.MakeScript(script + '("' + hex + '", false)'); + if (s) { + s.script.run().result; + } + }; + return ( + <div className={`menuButton ${this.props.type} ${active}`} + style={{ color: this.props.color, borderBottomLeftRadius: active ? 0 : undefined }} + onClick={() => this.props.rootDoc.dropDownOpen = !this.props.rootDoc.dropDownOpen} + onPointerDown={e => e.stopPropagation()}> + <FontAwesomeIcon className={`fontIconBox-icon-${this.props.type}`} icon={this.props.icon} color={this.props.color} /> + <div className="colorButton-color" + style={{ backgroundColor: boolResult ? boolResult : "#FFFFFF" }} /> + {label} + {/* {dropdownCaret} */} + {this.props.rootDoc.dropDownOpen ? + <div> + <div className="menuButton-dropdownBox" + onPointerDown={e => e.stopPropagation()} + onClick={e => e.stopPropagation()}> + {colorBox(click)} + </div> + <div className="dropbox-background" onClick={(e) => { e.stopPropagation(); this.props.rootDoc.dropDownOpen = false; }} /> + </div> + : null} + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/button/colorDropdown/index.ts b/src/client/views/nodes/button/colorDropdown/index.ts new file mode 100644 index 000000000..1147d6457 --- /dev/null +++ b/src/client/views/nodes/button/colorDropdown/index.ts @@ -0,0 +1 @@ +export * from './ColorDropdown';
\ No newline at end of file diff --git a/src/client/views/nodes/button/textButton/TextButton.tsx b/src/client/views/nodes/button/textButton/TextButton.tsx new file mode 100644 index 000000000..e18590a95 --- /dev/null +++ b/src/client/views/nodes/button/textButton/TextButton.tsx @@ -0,0 +1,17 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import React, { Component } from 'react'; +import { BoolCast } from '../../../../../fields/Types'; +import { IButtonProps } from '../ButtonInterface'; + +export class TextButton extends Component<IButtonProps> { + render() { + const type = this.props.type; + // Determine the type of toggle button + const buttonText: boolean = BoolCast(this.props.rootDoc.switchToggle); + + return (<div className={`menuButton ${this.props.type}`} style={{ opacity: 1, backgroundColor: this.props.backgroundColor, color: this.props.color }}> + <FontAwesomeIcon className={`fontIconBox-icon-${this.props.type}`} icon={this.props.icon} color={this.props.color} /> + {this.props.label} + </div>); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/button/textButton/index.ts b/src/client/views/nodes/button/textButton/index.ts new file mode 100644 index 000000000..01d62eb7e --- /dev/null +++ b/src/client/views/nodes/button/textButton/index.ts @@ -0,0 +1 @@ +export * from './TextButton';
\ No newline at end of file diff --git a/src/client/views/nodes/button/toggleButton/ToggleButton.tsx b/src/client/views/nodes/button/toggleButton/ToggleButton.tsx new file mode 100644 index 000000000..dca6487d8 --- /dev/null +++ b/src/client/views/nodes/button/toggleButton/ToggleButton.tsx @@ -0,0 +1,34 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import React, { Component } from 'react'; +import { BoolCast } from '../../../../../fields/Types'; +import { Colors } from '../../../global/globalEnums'; +import { IButtonProps } from '../ButtonInterface'; + +export class ToggleButton extends Component<IButtonProps> { + render() { + const type = this.props.type; + // Determine the type of toggle button + const switchToggle: boolean = BoolCast(this.props.rootDoc.switchToggle); + + if (switchToggle) { + return ( + <div className={`menuButton ${type} ${'switch'}`}> + <label className="switch"> + <input type="checkbox" + checked={this.props.backgroundColor === Colors.MEDIUM_BLUE} + /> + <span className="slider round"></span> + </label> + </div> + ); + } else { + return ( + <div className={`menuButton ${type}`} + style={{ opacity: 1, backgroundColor: this.props.backgroundColor, color: this.props.color }}> + <FontAwesomeIcon className={`fontIconBox-icon-${type}`} icon={this.props.icon} color={this.props.color} /> + {this.props.label} + </div> + ); + } + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/button/toggleButton/index.ts b/src/client/views/nodes/button/toggleButton/index.ts new file mode 100644 index 000000000..cdb9c527c --- /dev/null +++ b/src/client/views/nodes/button/toggleButton/index.ts @@ -0,0 +1 @@ +export * from './ToggleButton';
\ No newline at end of file diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx index 8915d7c47..364be461f 100644 --- a/src/client/views/nodes/formattedText/DashDocView.tsx +++ b/src/client/views/nodes/formattedText/DashDocView.tsx @@ -1,7 +1,7 @@ import { IReactionDisposer, reaction, observable, action } from "mobx"; import { NodeSelection } from "prosemirror-state"; import { Doc, HeightSym, WidthSym } from "../../../../fields/Doc"; -import { Cast, StrCast } from "../../../../fields/Types"; +import { Cast, StrCast, NumCast } from "../../../../fields/Types"; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, Utils, returnTransparent } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; import { Docs, DocUtils } from "../../../documents/Documents"; @@ -12,6 +12,7 @@ import { FormattedTextBox } from "./FormattedTextBox"; import React = require("react"); import * as ReactDOM from 'react-dom'; import { observer } from "mobx-react"; +import { ColorScheme } from "../../../util/SettingsManager"; export class DashDocView { _fieldWrapper: HTMLSpanElement; // container for label and value @@ -20,7 +21,7 @@ export class DashDocView { this._fieldWrapper = document.createElement("span"); this._fieldWrapper.style.position = "relative"; this._fieldWrapper.style.textIndent = "0"; - this._fieldWrapper.style.border = "1px solid " + StrCast(tbox.layoutDoc.color, (CurrentUserUtils.ActiveDashboard.darkScheme ? "dimGray" : "lightGray")); + this._fieldWrapper.style.border = "1px solid " + StrCast(tbox.layoutDoc.color, (CurrentUserUtils.ActiveDashboard?.colorScheme === ColorScheme.Dark ? "dimgray" : "lightGray")); this._fieldWrapper.style.width = node.attrs.width; this._fieldWrapper.style.height = node.attrs.height; this._fieldWrapper.style.display = node.attrs.hidden ? "none" : "inline-block"; @@ -69,30 +70,31 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { @observable _finalLayout: any; @observable _resolvedDataDoc: any; - constructor(props: IDashDocViewInternal) { - super(props); - this._textBox = this.props.tbox; - const updateDoc = action((dashDoc: Doc) => { - this._dashDoc = dashDoc; - this._finalLayout = this.props.docid ? dashDoc : Doc.expandTemplateLayout(Doc.Layout(dashDoc), dashDoc, this.props.fieldKey); + updateDoc = action((dashDoc: Doc) => { + this._dashDoc = dashDoc; + this._finalLayout = this.props.docid ? dashDoc : Doc.expandTemplateLayout(Doc.Layout(dashDoc), dashDoc, this.props.fieldKey); - if (this._finalLayout) { - if (!Doc.AreProtosEqual(this._finalLayout, dashDoc)) { - this._finalLayout.rootDocument = dashDoc.aliasOf; - } - this._resolvedDataDoc = Cast(this._finalLayout.resolvedDataDoc, Doc, null); + if (this._finalLayout) { + if (!Doc.AreProtosEqual(this._finalLayout, dashDoc)) { + this._finalLayout.rootDocument = dashDoc.aliasOf; } - if (this.props.width !== (this._dashDoc?._width ?? "") + "px" || this.props.height !== (this._dashDoc?._height ?? "") + "px") { - try { // bcz: an exception will be thrown if two aliases are open at the same time when a doc view comment is made - this.props.view.dispatch(this.props.view.state.tr.setNodeMarkup(this.props.getPos(), null, { - ...this.props.node.attrs, width: (this._dashDoc?._width ?? "") + "px", height: (this._dashDoc?._height ?? "") + "px" - })); - } catch (e) { - console.log("DashDocView:" + e); - } + this._resolvedDataDoc = Cast(this._finalLayout.resolvedDataDoc, Doc, null); + } + if (this.props.width !== (this._dashDoc?._width ?? "") + "px" || this.props.height !== (this._dashDoc?._height ?? "") + "px") { + try { // bcz: an exception will be thrown if two aliases are open at the same time when a doc view comment is made + this.props.view.dispatch(this.props.view.state.tr.setNodeMarkup(this.props.getPos(), null, { + ...this.props.node.attrs, width: (this._dashDoc?._width ?? "") + "px", height: (this._dashDoc?._height ?? "") + "px" + })); + } catch (e) { + console.log("DashDocView:" + e); } - }); + } + }); + + constructor(props: IDashDocViewInternal) { + super(props); + this._textBox = this.props.tbox; DocServer.GetRefField(this.props.docid + this.props.alias).then(async dashDoc => { if (!(dashDoc instanceof Doc)) { @@ -101,15 +103,27 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { const aliasedDoc = Doc.MakeAlias(dashDocBase, this.props.docid + this.props.alias); aliasedDoc.layoutKey = "layout"; this.props.fieldKey && DocUtils.makeCustomViewClicked(aliasedDoc, Docs.Create.StackingDocument, this.props.fieldKey, undefined); - updateDoc(aliasedDoc); + this.updateDoc(aliasedDoc); } }); } else { - updateDoc(dashDoc); + this.updateDoc(dashDoc); } }); } + componentDidMount() { + this._disposers.upater = reaction(() => this._dashDoc && (NumCast(this._dashDoc._height) + NumCast(this._dashDoc._width)), + () => { + if (this._dashDoc) { + this.props.view.dispatch(this.props.view.state.tr.setNodeMarkup(this.props.getPos(), null, { + ...this.props.node.attrs, width: (this._dashDoc?._width ?? "") + "px", height: (this._dashDoc?._height ?? "") + "px" + })); + } + }); + } + + removeDoc = () => { this.props.view.dispatch(this.props.view.state.tr .setSelection(new NodeSelection(this.props.view.state.doc.resolve(this.props.getPos()))) diff --git a/src/client/views/nodes/formattedText/DashFieldView.scss b/src/client/views/nodes/formattedText/DashFieldView.scss index e7dd286a5..c36e6804b 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.scss +++ b/src/client/views/nodes/formattedText/DashFieldView.scss @@ -8,7 +8,7 @@ height: 10px; position: relative; display: inline-block; - background: dimGray; + background: dimgray; } .dashFieldView-fieldCheck { min-width: 12px; diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index 62f65cdae..34908e54b 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -82,7 +82,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna // set the display of the field's value (checkbox for booleans, span of text for strings) @computed get fieldValueContent() { if (this._dashDoc) { - const dashVal = this._dashDoc[DataSym][this._fieldKey] ?? this._dashDoc[this._fieldKey] ?? (this._fieldKey === "PARAMS" ? this._textBoxDoc[this._fieldKey] : ""); + const dashVal = this._dashDoc[this._fieldKey] ?? this._dashDoc[DataSym][this._fieldKey] ?? (this._fieldKey === "PARAMS" ? this._textBoxDoc[this._fieldKey] : ""); const fval = dashVal instanceof List ? dashVal.join(this.multiValueDelimeter) : StrCast(dashVal).startsWith(":=") || dashVal === "" ? Doc.Layout(this._textBoxDoc)[this._fieldKey] : dashVal; const boolVal = Cast(fval, "boolean", null); const strVal = Field.toString(fval as Field) || ""; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index 3cedab1a4..27817f317 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -67,13 +67,27 @@ audiotag:hover { .formattedTextBox-sidebar-handle { position: absolute; top: 0; + left: 17px; //top: calc(50% - 17.5px); // use this to center vertically -- make sure it looks okay for slide views - width: 10px; - height: 100%; - max-height: 35px; - background: lightgray; - border-radius: 20px; + width: 17px; + height: 17px; + font-size: 11px; + border-radius: 3px; + color: white; + background: $medium-gray; + border-radius: 5px; + display: flex; + justify-content: center; + align-items: center; cursor:grabbing; + box-shadow: $standard-box-shadow; + // transition: 0.2s; + opacity: 0.3; + &:hover{ + opacity: 1 !important; + filter: brightness(0.85); + } + } .formattedTextBox-sidebar, @@ -414,12 +428,7 @@ footnote::after { .formattedTextBox-sidebar-handle { position: absolute; - top: 0; - //top: calc(50% - 17.5px); // use this to center vertically -- make sure it looks okay for slide views - width: 10px; - height: 35px; background: lightgray; - border-radius: 20px; cursor: grabbing; } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 4b1d76d00..aa53f751d 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1,6 +1,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { isEqual } from "lodash"; -import { action, computed, IReactionDisposer, reaction, runInAction, observable } from "mobx"; +import { action, computed, IReactionDisposer, reaction, runInAction, observable, trace } from "mobx"; import { observer } from "mobx-react"; import { baseKeymap, selectAll } from "prosemirror-commands"; import { history } from "prosemirror-history"; @@ -63,11 +63,12 @@ import { SummaryView } from "./SummaryView"; import applyDevTools = require("prosemirror-dev-tools"); import React = require("react"); import { SidebarAnnos } from '../../SidebarAnnos'; +import { Colors } from '../../global/globalEnums'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; const translateGoogleApi = require("translate-google-api"); export interface FormattedTextBoxProps { makeLink?: () => Opt<Doc>; // bcz: hack: notifies the text document when the container has made a link. allows the text doc to react and setup a hyeprlink for any selected text - hideOnLeave?: boolean; // used by DocumentView for setting caption's hide on leave (bcz: would prefer to have caption-hideOnLeave field set or something similar) xPadding?: number; // used to override document's settings for xMargin --- see CollectionCarouselView yPadding?: number; noSidebar?: boolean; @@ -213,8 +214,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp @action setupAnchorMenu = () => { AnchorMenu.Instance.Status = "marquee"; + + AnchorMenu.Instance.OnClick = (e: PointerEvent) => { + !this.layoutDoc.showSidebar && this.toggleSidebar(); + this._sidebarRef.current?.anchorMenuClick(this.getAnchor()); + }; AnchorMenu.Instance.Highlight = action((color: string, isLinkButton: boolean) => { - this._editorView?.state && RichTextMenu.Instance.insertHighlight(color, this._editorView.state, this._editorView?.dispatch); + this._editorView?.state && RichTextMenu.Instance.setHighlight(color, this._editorView, this._editorView?.dispatch); return undefined; }); AnchorMenu.Instance.onMakeAnchor = this.getAnchor; @@ -273,8 +279,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp (curText !== Cast(this.dataDoc[this.fieldKey], RichTextField)?.Text) && (this.dataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now()))); if ((!curTemp && !curProto) || curText || json.includes("dash")) { // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended) if (removeSelection(json) !== removeSelection(curLayout?.Data)) { - !curText && tx.storedMarks?.filter(m => m.type.name === "pFontSize").map(m => Doc.UserDoc().fontSize = this.layoutDoc._fontSize = (m.attrs.fontSize + "px")); - !curText && tx.storedMarks?.filter(m => m.type.name === "pFontFamily").map(m => Doc.UserDoc().fontFamily = this.layoutDoc._fontFamily = m.attrs.fontFamily); this.dataDoc[this.props.fieldKey] = new RichTextField(json, curText); this.dataDoc[this.props.fieldKey + "-noTemplate"] = true;//(curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: curText }); @@ -429,9 +433,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark)); } protected createDropTarget = (ele: HTMLDivElement) => { - this.ProseRef = ele; this._dropDisposer?.(); - ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.layoutDoc)); + this.ProseRef = ele; + if (ele) { + this.setupEditor(this.config, this.props.fieldKey); + this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.layoutDoc); + } // if (this.autoHeight) this.tryUpdateScrollHeight(); } @@ -453,10 +460,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const target = dragData.droppedDocuments[0]; target._fitToBox = true; const node = schema.nodes.dashDoc.create({ - width: target[WidthSym](), height: target[HeightSym](), + width: target[WidthSym](), + height: target[HeightSym](), title: "dashDoc", docid: target[Id], - float: "right" + float: "unset" }); const view = this._editorView!; view.dispatch(view.state.tr.insert(view.posAtCoords({ left: de.x, top: de.y })!.pos, node)); @@ -571,14 +579,28 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const cm = ContextMenu.Instance; const changeItems: ContextMenuProps[] = []; - changeItems.push({ description: "plain", event: undoBatch(() => Doc.setNativeView(this.rootDoc)), icon: "eye" }); + changeItems.push({ + description: "plain", event: undoBatch(() => { + Doc.setNativeView(this.rootDoc); + this.layoutDoc.autoHeightMargins = undefined; + }), icon: "eye" + }); + changeItems.push({ + description: "metadata", event: undoBatch(() => { + this.dataDoc.layout_meta = Cast(Doc.UserDoc().emptyHeader, Doc, null)?.layout; + this.rootDoc.layoutKey = "layout_meta"; + setTimeout(() => this.rootDoc._headerHeight = this.rootDoc._autoHeightMargins = 50, 50); + }), icon: "eye" + }); const noteTypesDoc = Cast(Doc.UserDoc()["template-notes"], Doc, null); DocListCast(noteTypesDoc?.data).forEach(note => { + const icon: IconProp = StrCast(note.icon) as IconProp; changeItems.push({ description: StrCast(note.title), event: undoBatch(() => { + this.layoutDoc.autoHeightMargins = undefined; Doc.setNativeView(this.rootDoc); DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.TreeDocument, StrCast(note.title), note); - }), icon: "eye" + }), icon: icon }); }); !Doc.UserDoc().noviceMode && changeItems.push({ description: "FreeForm", event: () => DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.FreeformDocument, "freeform"), icon: "eye" }); @@ -710,7 +732,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const splitter = state.schema.marks.splitter.create({ id: Utils.GenerateGuid() }); 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 anchor = anchorDoc ?? Docs.Create.TextanchorDocument({ title: this._editorView?.state.doc.textBetween(sel.from, sel.to), unrendered: true }); 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) => { @@ -802,11 +824,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this._disposers.breakupDictation = reaction(() => DocumentManager.Instance.RecordingEvent, this.breakupDictation); this._disposers.autoHeight = reaction(() => this.autoHeight, autoHeight => autoHeight && this.tryUpdateScrollHeight()); this._disposers.scrollHeight = reaction(() => ({ scrollHeight: this.scrollHeight, autoHeight: this.autoHeight, width: NumCast(this.layoutDoc._width) }), - ({ width, scrollHeight, autoHeight }) => width && autoHeight && this.resetNativeHeight(scrollHeight) + ({ width, scrollHeight, autoHeight }) => { + width && autoHeight && this.resetNativeHeight(scrollHeight); + }, { fireImmediately: true } ); 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, marginsHeight: this.autoHeightMargins }), - ({ sidebarHeight, textHeight, autoHeight, marginsHeight }) => autoHeight && this.props.setHeight(marginsHeight + Math.max(sidebarHeight, textHeight))); + ({ sidebarHeight, textHeight, autoHeight, marginsHeight }) => { + autoHeight && this.props.setHeight?.(marginsHeight + Math.max(sidebarHeight, textHeight)); + }, { fireImmediately: true }); 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)); @@ -861,18 +887,19 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } ); - this.setupEditor(this.config, this.props.fieldKey); - this._disposers.search = reaction(() => Doc.IsSearchMatch(this.rootDoc), search => search ? this.highlightSearchTerms([Doc.SearchQuery()], search.searchMatch < 0) : this.unhighlightSearchTerms(), { fireImmediately: Doc.IsSearchMatchUnmemoized(this.rootDoc) ? true : false }); this._disposers.selected = reaction(() => this.props.isSelected(), - action((selected) => { + action(selected => { if (RichTextMenu.Instance?.view === this._editorView && !selected) { RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined); } - })); + if (this._editorView && selected) { + RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props); + } + }), { fireImmediately: true }); if (!this.props.dontRegisterView) { this._disposers.record = reaction(() => this._recording, @@ -902,7 +929,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp }, { fireImmediately: true } ); quickScroll = undefined; - this.tryUpdateScrollHeight(); + setTimeout(this.tryUpdateScrollHeight, 10); } pushToGoogleDoc = async () => { @@ -933,7 +960,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp GoogleApiClientUtils.Docs.write({ reference, content, mode }); } }; - UndoManager.AddEvent({ undo, redo }); + UndoManager.AddEvent({ undo, redo, prop: "" }); redo(); }); } @@ -1141,8 +1168,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } selectOnLoad && this._editorView!.focus(); // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet. - if (!this._editorView!.state.storedMarks?.some(mark => mark.type === schema.marks.user_mark)) { - this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ?? []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })]; + if (this._editorView && !this._editorView.state.storedMarks?.some(mark => mark.type === schema.marks.user_mark)) { + this._editorView.state.storedMarks = [...(this._editorView.state.storedMarks ?? []), + schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }), + ...(Doc.UserDoc().fontColor !== "transparent" && Doc.UserDoc().fontColor ? [schema.mark(schema.marks.pFontColor, { color: StrCast(Doc.UserDoc().fontColor) })] : []), + ...(Doc.UserDoc().fontStyle === "italics" ? [schema.mark(schema.marks.em)] : []), + ...(Doc.UserDoc().textDecoration === "underline" ? [schema.mark(schema.marks.underline)] : []), + ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: StrCast(Doc.UserDoc().fontFamily) })] : []), + ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: StrCast(Doc.UserDoc().fontSize, "") })] : []), + ...(Doc.UserDoc().fontWeight === "bold" ? [schema.mark(schema.marks.strong)] : [])]; } } @@ -1161,9 +1195,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if ((e.target as any).tagName === "AUDIOTAG") { e.preventDefault(); e.stopPropagation(); + const timecode = Number((e.target as any)?.dataset?.timecode); DocServer.GetRefField((e.target as any)?.dataset?.audioid || 0).then(anchor => { if (anchor instanceof Doc) { - const timecode = NumCast(anchor.timecodeToShow, 0); + // const timecode = NumCast(anchor.timecodeToShow, 0); const audiodoc = anchor.annotationOn as Doc; const func = () => { const docView = DocumentManager.Instance.getDocumentView(audiodoc); @@ -1357,6 +1392,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this._undoTyping = undefined; return wasUndoing; } + @action onBlur = (e: any) => { FormattedTextBox.Focused === this && (FormattedTextBox.Focused = undefined); @@ -1441,22 +1477,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } } tryUpdateScrollHeight = () => { - if (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath())) { - 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... + 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) { + const proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + Number(getComputedStyle(child).height.replace("px", "")), margins); + const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight); + if (scrollHeight && this.props.renderDepth && !this.props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation + const setScrollHeight = () => this.rootDoc[this.fieldKey + "-scrollHeight"] = scrollHeight; + if (this.rootDoc === this.layoutDoc.doc || this.layoutDoc.resolvedDataDoc) { + setScrollHeight(); + } else { + setTimeout(setScrollHeight, 10); // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template. So set a timeout and hope its arrived... } } } @@ -1481,11 +1512,18 @@ 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.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" : "")) - }} />; + const color = !annotated ? Colors.WHITE : Colors.BLACK; + const backgroundColor = !annotated ? this.sidebarWidth() ? Colors.MEDIUM_BLUE : Colors.BLACK : this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.WidgetColor + (annotated ? ":annotated" : "")); + return (!annotated && (!this.props.isContentActive() || SnappingManager.GetIsDragging())) ? (null) : + <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} + style={{ + left: `max(0px, calc(100% - ${this.sidebarWidthPercent} - 17px))`, + backgroundColor: backgroundColor, + color: color, + opacity: annotated ? 1 : undefined + }} > + <FontAwesomeIcon icon={"comment-alt"} /> + </div>; } @computed get sidebarCollection() { const renderComponent = (tag: string) => { @@ -1538,7 +1576,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp TraceMobx(); const selected = this.props.isSelected(); const active = this.props.isContentActive(); - const scale = this.props.hideOnLeave ? 1 : (this.props.scaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1); + const scale = (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); if (!selected && FormattedTextBoxComment.textBox === this) setTimeout(FormattedTextBoxComment.Hide); @@ -1547,7 +1585,8 @@ 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" : ""; - return ( + const styleFromString = this.styleFromLayoutString(scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' > + return (styleFromString?.height === "0px" ? (null) : <div className="formattedTextBox-cont" onWheel={e => this.props.isContentActive() && e.stopPropagation()} style={{ @@ -1556,7 +1595,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp 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' > + ...styleFromString }}> <div className={`formattedTextBox-cont`} ref={this._ref} style={{ @@ -1565,7 +1604,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp background: this.props.background ? this.props.background : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor), color: this.props.color ? this.props.color : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color), fontSize: this.props.fontSize ? this.props.fontSize : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontSize), - fontWeight: Cast(this.layoutDoc._fontWeight, "number", null), + fontWeight: Cast(this.layoutDoc._fontWeight, "string", null) as any, fontFamily: StrCast(this.layoutDoc._fontFamily, "inherit"), pointerEvents: interactive ? undefined : "none", }} diff --git a/src/client/views/nodes/formattedText/RichTextMenu.scss b/src/client/views/nodes/formattedText/RichTextMenu.scss index c94e93541..8afa0f6b5 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.scss +++ b/src/client/views/nodes/formattedText/RichTextMenu.scss @@ -2,6 +2,7 @@ .button-dropdown-wrapper { position: relative; + display: flex; .dropdown-button { width: 15px; diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 82ad2b7db..bd05af977 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -1,8 +1,7 @@ import React = require("react"); -import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Tooltip } from "@material-ui/core"; -import { action, IReactionDisposer, observable, reaction, runInAction } from "mobx"; +import { action, IReactionDisposer, observable, reaction, runInAction, computed } from "mobx"; import { observer } from "mobx-react"; import { lift, wrapIn } from "prosemirror-commands"; import { Mark, MarkType, Node as ProsNode, NodeType, ResolvedPos } from "prosemirror-model"; @@ -10,10 +9,7 @@ import { wrapInList } from "prosemirror-schema-list"; import { EditorState, NodeSelection, TextSelection } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { Doc } from "../../../../fields/Doc"; -import { DarkPastelSchemaPalette, PastelSchemaPalette } from '../../../../fields/SchemaHeaderField'; import { Cast, StrCast } from "../../../../fields/Types"; -import { TraceMobx } from "../../../../fields/util"; -import { unimplementedFunction, Utils } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; import { LinkManager } from "../../../util/LinkManager"; import { SelectionManager } from "../../../util/SelectionManager"; @@ -29,7 +25,7 @@ const { toggleMark } = require("prosemirror-commands"); @observer export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { - static Instance: RichTextMenu; + @observable static Instance: RichTextMenu; public overMenu: boolean = false; // kind of hacky way to prevent selects not being selectable private _linkToRef = React.createRef<HTMLInputElement>(); @@ -37,29 +33,24 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { public editorProps: FieldViewProps & FormattedTextBoxProps | undefined; public _brushMap: Map<string, Set<Mark>> = new Map(); - private fontSizeOptions: { mark: Mark | null, title: string, label: string, command: any, hidden?: boolean, style?: {} }[]; - private fontFamilyOptions: { mark: Mark | null, title: string, label: string, command: any, hidden?: boolean, style?: {} }[]; - private listTypeOptions: { node: NodeType | any | null, title: string, label: string, command: any, style?: {} }[]; - private fontColors: (string | undefined)[]; - private highlightColors: (string | undefined)[]; @observable private collapsed: boolean = false; - @observable private boldActive: boolean = false; - @observable private italicsActive: boolean = false; - @observable private underlineActive: boolean = false; - @observable private strikethroughActive: boolean = false; - @observable private subscriptActive: boolean = false; - @observable private superscriptActive: boolean = false; - - @observable private activeFontSize: string = ""; - @observable private activeFontFamily: string = ""; + @observable private _boldActive: boolean = false; + @observable private _italicsActive: boolean = false; + @observable private _underlineActive: boolean = false; + @observable private _strikethroughActive: boolean = false; + @observable private _subscriptActive: boolean = false; + @observable private _superscriptActive: boolean = false; + + @observable private _activeFontSize: string = "13px"; + @observable private _activeFontFamily: string = ""; @observable private activeListType: string = ""; @observable private activeAlignment: string = "left"; @observable private brushMarks: Set<Mark> = new Set(); @observable private showBrushDropdown: boolean = false; - @observable private activeFontColor: string = "black"; + @observable private _activeFontColor: string = "black"; @observable private showColorDropdown: boolean = false; @observable private activeHighlightColor: string = "transparent"; @@ -72,74 +63,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { _delayHide = false; constructor(props: Readonly<{}>) { super(props); - RichTextMenu.Instance = this; - this._canFade = false; - //this.Pinned = BoolCast(Doc.UserDoc()["menuRichText-pinned"]); - runInAction(() => this.Pinned = true); - - this.fontSizeOptions = [ - { mark: schema.marks.pFontSize.create({ fontSize: 7 }), title: "Set font size", label: "7px", command: this.changeFontSize }, - { mark: schema.marks.pFontSize.create({ fontSize: 8 }), title: "Set font size", label: "8px", command: this.changeFontSize }, - { mark: schema.marks.pFontSize.create({ fontSize: 9 }), title: "Set font size", label: "9px", command: this.changeFontSize }, - { mark: schema.marks.pFontSize.create({ fontSize: 10 }), title: "Set font size", label: "10px", command: this.changeFontSize }, - { mark: schema.marks.pFontSize.create({ fontSize: 12 }), title: "Set font size", label: "12px", command: this.changeFontSize }, - { mark: schema.marks.pFontSize.create({ fontSize: 14 }), title: "Set font size", label: "14px", command: this.changeFontSize }, - { mark: schema.marks.pFontSize.create({ fontSize: 16 }), title: "Set font size", label: "16px", command: this.changeFontSize }, - { mark: schema.marks.pFontSize.create({ fontSize: 18 }), title: "Set font size", label: "18px", command: this.changeFontSize }, - { mark: schema.marks.pFontSize.create({ fontSize: 20 }), title: "Set font size", label: "20px", command: this.changeFontSize }, - { mark: schema.marks.pFontSize.create({ fontSize: 24 }), title: "Set font size", label: "24px", command: this.changeFontSize }, - { mark: schema.marks.pFontSize.create({ fontSize: 32 }), title: "Set font size", label: "32px", command: this.changeFontSize }, - { mark: schema.marks.pFontSize.create({ fontSize: 48 }), title: "Set font size", label: "48px", command: this.changeFontSize }, - { mark: schema.marks.pFontSize.create({ fontSize: 72 }), title: "Set font size", label: "72px", command: this.changeFontSize }, - { mark: null, title: "", label: "...", command: unimplementedFunction, hidden: true }, - { mark: null, title: "", label: "13px", command: unimplementedFunction, hidden: true }, // this is here because the default size is 13, but there is no actual 13pt option - ]; - - this.fontFamilyOptions = [ - { mark: schema.marks.pFontFamily.create({ family: "Times New Roman" }), title: "Set font family", label: "Times New Roman", command: this.changeFontFamily, style: { fontFamily: "Times New Roman" } }, - { mark: schema.marks.pFontFamily.create({ family: "Arial" }), title: "Set font family", label: "Arial", command: this.changeFontFamily, style: { fontFamily: "Arial" } }, - { mark: schema.marks.pFontFamily.create({ family: "Georgia" }), title: "Set font family", label: "Georgia", command: this.changeFontFamily, style: { fontFamily: "Georgia" } }, - { mark: schema.marks.pFontFamily.create({ family: "Comic Sans MS" }), title: "Set font family", label: "Comic Sans MS", command: this.changeFontFamily, style: { fontFamily: "Comic Sans MS" } }, - { mark: schema.marks.pFontFamily.create({ family: "Tahoma" }), title: "Set font family", label: "Tahoma", command: this.changeFontFamily, style: { fontFamily: "Tahoma" } }, - { mark: schema.marks.pFontFamily.create({ family: "Impact" }), title: "Set font family", label: "Impact", command: this.changeFontFamily, style: { fontFamily: "Impact" } }, - { mark: schema.marks.pFontFamily.create({ family: "Crimson Text" }), title: "Set font family", label: "Crimson Text", command: this.changeFontFamily, style: { fontFamily: "Crimson Text" } }, - { mark: null, title: "", label: "various", command: unimplementedFunction, hidden: true }, - // { mark: null, title: "", label: "default", command: unimplementedFunction, hidden: true }, - ]; - - this.listTypeOptions = [ - { node: schema.nodes.ordered_list.create({ mapStyle: "bullet" }), title: "Set list type", label: ":", command: this.changeListType }, - { node: schema.nodes.ordered_list.create({ mapStyle: "decimal" }), title: "Set list type", label: "1.1", command: this.changeListType }, - { node: schema.nodes.ordered_list.create({ mapStyle: "multi" }), title: "Set list type", label: "A.1", command: this.changeListType }, - { node: schema.nodes.ordered_list.create({ mapStyle: "" }), title: "Set list type", label: "<none>", command: this.changeListType }, - //{ node: undefined, title: "Set list type", label: "Remove", command: this.changeListType }, - ]; - - this.fontColors = [ - DarkPastelSchemaPalette.get("pink2"), - DarkPastelSchemaPalette.get("purple4"), - DarkPastelSchemaPalette.get("bluegreen1"), - DarkPastelSchemaPalette.get("yellow4"), - DarkPastelSchemaPalette.get("red2"), - DarkPastelSchemaPalette.get("bluegreen7"), - DarkPastelSchemaPalette.get("bluegreen5"), - DarkPastelSchemaPalette.get("orange1"), - "#757472", - "#000" - ]; - - this.highlightColors = [ - PastelSchemaPalette.get("pink2"), - PastelSchemaPalette.get("purple4"), - PastelSchemaPalette.get("bluegreen1"), - PastelSchemaPalette.get("yellow4"), - PastelSchemaPalette.get("red2"), - PastelSchemaPalette.get("bluegreen7"), - PastelSchemaPalette.get("bluegreen5"), - PastelSchemaPalette.get("orange1"), - "white", - "transparent" - ]; + runInAction(() => { + RichTextMenu.Instance = this; + this._canFade = false; + //this.Pinned = BoolCast(Doc.UserDoc()["menuRichText-pinned"]); + this.Pinned = true; + }); } componentDidMount() { @@ -150,6 +79,14 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this._reaction?.(); } + @computed get bold() { return this._boldActive; } + @computed get underline() { return this._underlineActive; } + @computed get italics() { return this._italicsActive; } + @computed get strikeThrough() { return this._strikethroughActive; } + @computed get fontColor() { return this._activeFontColor; } + @computed get fontFamily() { return this._activeFontFamily; } + @computed get fontSize() { return this._activeFontSize; } + public delayHide = () => this._delayHide = true; @action @@ -179,10 +116,10 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.activeListType = this.getActiveListStyle(); this.activeAlignment = this.getActiveAlignment(); - this.activeFontFamily = !activeFamilies.length ? "Arial" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various"; - this.activeFontSize = !activeSizes.length ? "13px" : activeSizes.length === 1 ? String(activeSizes[0]) : "..."; - this.activeFontColor = !activeColors.length ? "black" : activeColors.length === 1 ? String(activeColors[0]) : "..."; - this.activeHighlightColor = !activeHighlights.length ? "" : activeHighlights.length === 1 ? String(activeHighlights[0]) : "..."; + this._activeFontFamily = !activeFamilies.length ? "Arial" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various"; + this._activeFontSize = !activeSizes.length ? "13px" : activeSizes[0]; + this._activeFontColor = !activeColors.length ? "black" : activeColors.length > 0 ? String(activeColors[0]) : "..."; + this.activeHighlightColor = !activeHighlights.length ? "" : activeHighlights.length > 0 ? String(activeHighlights[0]) : "..."; // update link in current selection this.getTextLinkTargetTitle().then(targetTitle => this.setCurrentLink(targetTitle)); @@ -194,7 +131,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { if (node?.type === schema.nodes.ordered_list) { let attrs = node.attrs; if (mark.type === schema.marks.pFontFamily) attrs = { ...attrs, fontFamily: mark.attrs.family }; - if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, fontSize: `${mark.attrs.fontSize}px` }; + if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, fontSize: mark.attrs.fontSize }; if (mark.type === schema.marks.pFontColor) attrs = { ...attrs, fontColor: mark.attrs.color }; const tr = updateBullets(state.tr.setNodeMarkup(state.selection.from, node.type, attrs), state.schema); dispatch(tr.setSelection(new NodeSelection(tr.doc.resolve(state.selection.from)))); @@ -251,25 +188,22 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { if (this.TextView.props.isSelected(true)) { const state = this.view.state; const pos = this.view.state.selection.$from; - const ref_node = this.reference_node(pos); - if (ref_node && ref_node !== this.view.state.doc && ref_node.isText) { - const marks = Array.from(ref_node.marks); - marks.push(...(this.view.state.storedMarks as any)); - marks.forEach(m => { - m.type === state.schema.marks.pFontFamily && activeFamilies.push(m.attrs.family); - m.type === state.schema.marks.pFontColor && activeColors.push(m.attrs.color); - m.type === state.schema.marks.pFontSize && activeSizes.push(String(m.attrs.fontSize) + "px"); - m.type === state.schema.marks.marker && activeHighlights.push(String(m.attrs.highlight)); + const marks: Mark<any>[] = [...(state.storedMarks ?? [])]; + if (state.selection.empty) { + const ref_node = this.reference_node(pos); + marks.push(...(ref_node !== this.view.state.doc && ref_node?.isText ? Array.from(ref_node.marks) : [])); + } else { + state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { + node.marks?.filter(mark => !mark.isInSet(marks)).map(mark => marks.push(mark)); }); } - !activeFamilies.length && (activeFamilies.push(StrCast(this.TextView.layoutDoc._fontFamily, StrCast(Doc.UserDoc().fontFamily)))); - !activeSizes.length && (activeSizes.push(StrCast(this.TextView.layoutDoc._fontSize, StrCast(Doc.UserDoc().fontSize)))); - !activeColors.length && (activeColors.push(StrCast(this.TextView.layoutDoc.color, StrCast(Doc.UserDoc().fontColor)))); + marks.forEach(m => { + m.type === state.schema.marks.pFontFamily && activeFamilies.push(m.attrs.family); + m.type === state.schema.marks.pFontColor && activeColors.push(m.attrs.color); + m.type === state.schema.marks.pFontSize && activeSizes.push(m.attrs.fontSize); + m.type === state.schema.marks.marker && activeHighlights.push(String(m.attrs.highlight)); + }); } - !activeFamilies.length && (activeFamilies.push(StrCast(Doc.UserDoc().fontFamily))); - !activeSizes.length && (activeSizes.push(StrCast(Doc.UserDoc().fontSize))); - !activeColors.length && (activeColors.push(StrCast(Doc.UserDoc().fontColor, "black"))); - !activeHighlights.length && (activeHighlights.push(StrCast(Doc.UserDoc().fontHighlight, ""))); return { activeFamilies, activeSizes, activeColors, activeHighlights }; } @@ -309,11 +243,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { return []; } activeMarks = markGroup.filter(mark_type => { - if (mark_type === state.schema.marks.pFontSize) { - return ref_node.marks.some(m => m.type.name === state.schema.marks.pFontSize.name); - } + // if (mark_type === state.schema.marks.pFontSize) { + // return mark.isINSet + // ref_node.marks.some(m => m.type.name === state.schema.marks.pFontSize.name); + // } const mark = state.schema.mark(mark_type); - return ref_node.marks.includes(mark); + return mark.isInSet(ref_node.marks); }); } } @@ -328,123 +263,86 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { setActiveMarkButtons(activeMarks: MarkType[] | undefined) { if (!activeMarks) return; - this.boldActive = false; - this.italicsActive = false; - this.underlineActive = false; - this.strikethroughActive = false; - this.subscriptActive = false; - this.superscriptActive = false; + this._boldActive = false; + this._italicsActive = false; + this._underlineActive = false; + this._strikethroughActive = false; + this._subscriptActive = false; + this._superscriptActive = false; activeMarks.forEach(mark => { switch (mark.name) { - case "strong": this.boldActive = true; break; - case "em": this.italicsActive = true; break; - case "underline": this.underlineActive = true; break; - case "strikethrough": this.strikethroughActive = true; break; - case "subscript": this.subscriptActive = true; break; - case "superscript": this.superscriptActive = true; break; + case "strong": this._boldActive = true; break; + case "em": this._italicsActive = true; break; + case "underline": this._underlineActive = true; break; + case "strikethrough": this._strikethroughActive = true; break; + case "subscript": this._subscriptActive = true; break; + case "superscript": this._superscriptActive = true; break; } }); } - createButton(faIcon: string, title: string, isActive: boolean = false, command?: any, onclick?: any) { - const self = this; - function onClick(e: React.PointerEvent) { - e.preventDefault(); - e.stopPropagation(); - 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); - }, "rich text menu command"); - self.setActiveMarkButtons(self.getActiveMarksOnSelection()); + toggleBold = () => { + if (this.view) { + const mark = this.view.state.schema.mark(this.view.state.schema.marks.strong); + this.setMark(mark, this.view.state, this.view.dispatch, false); + this.view.focus(); } - - return ( - <Tooltip title={<div className="dash-tooltip">{title}</div>} key={title} placement="bottom"> - <button className={"antimodeMenu-button" + (isActive ? " active" : "")} onPointerDown={onClick}> - <FontAwesomeIcon icon={faIcon as IconProp} size="lg" /> - </button> - </Tooltip> - ); } - createMarksDropdown(activeOption: string, options: { mark: Mark | null, title: string, label: string, command: (mark: Mark, view: EditorView) => void, hidden?: boolean, style?: {} }[], key: string, setter: (val: string) => void): JSX.Element { - const items = options.map(({ title, label, hidden, style }) => { - if (hidden) { - return <option value={label} title={title} key={label} style={style ? style : {}} hidden>{label}</option>; - } - return <option value={label} title={title} key={label} style={style ? style : {}}>{label}</option>; - }); - - const self = this; - function onChange(e: React.ChangeEvent<HTMLSelectElement>) { - e.stopPropagation(); - e.preventDefault(); - self.TextView?.endUndoTypingBatch(); - UndoManager.RunInBatch(() => { - options.forEach(({ label, mark, command }) => { - if (e.target.value === label && mark) { - if (!self.TextView?.props.isSelected(true)) { - switch (mark.type) { - case schema.marks.pFontFamily: setter(Doc.UserDoc().fontFamily = mark.attrs.family); break; - case schema.marks.pFontSize: setter(Doc.UserDoc().fontSize = mark.attrs.fontSize.toString() + "px"); break; - } - } - else self.view && mark && command(mark, self.view); - } - }); - }, "text mark dropdown"); + toggleUnderline = () => { + if (this.view) { + const mark = this.view.state.schema.mark(this.view.state.schema.marks.underline); + this.setMark(mark, this.view.state, this.view.dispatch, false); + this.view.focus(); } + } - return <Tooltip key={key} title={<div className="dash-tooltip">{key}</div>} placement="bottom"> - <select onChange={onChange} value={activeOption}>{items}</select> - </Tooltip>; + toggleItalics = () => { + if (this.view) { + const mark = this.view.state.schema.mark(this.view.state.schema.marks.em); + this.setMark(mark, this.view.state, this.view.dispatch, false); + this.view.focus(); + } } - createNodesDropdown(activeMap: string, options: { node: NodeType | any | null, title: string, label: string, command: (node: NodeType | any) => void, hidden?: boolean, style?: {} }[], key: string, setter: (val: string) => {}): JSX.Element { - const activeOption = activeMap === "bullet" ? ":" : activeMap === "decimal" ? "1.1" : activeMap === "multi" ? "A.1" : "<none>"; - const items = options.map(({ title, label, hidden, style }) => { - if (hidden) { - return <option value={label} title={title} key={label} style={style ? style : {}} hidden>{label}</option>; - } - return <option value={label} title={title} key={label} style={style ? style : {}}>{label}</option>; - }); - const self = this; - function onChange(val: string) { - self.TextView.endUndoTypingBatch(); - options.forEach(({ label, node, command }) => { - if (val === label && node) { - if (self.TextView.props.isSelected(true)) { - UndoManager.RunInBatch(() => self.view && node && command(node), "nodes dropdown"); - setter(val); - } - } - }); + setFontSize = (fontSize: string) => { + if (this.view) { + const fmark = this.view.state.schema.marks.pFontSize.create({ fontSize }); + this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true); + this.view.focus(); + this.updateMenu(this.view, undefined, this.props); } + } - return <Tooltip key={key} title={<div className="dash-tooltip">{key}</div>} placement="bottom"> - <select value={activeOption} onChange={e => onChange(e.target.value)}>{items}</select> - </Tooltip>; + setFontFamily = (family: string) => { + if (this.view) { + const fmark = this.view.state.schema.marks.pFontFamily.create({ family: family }); + this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true); + this.view.focus(); + this.updateMenu(this.view, undefined, this.props); + } } - changeFontSize = (mark: Mark, view: EditorView) => { - const fmark = view.state.schema.marks.pFontSize.create({ fontSize: mark.attrs.fontSize }); - this.setMark(fmark, view.state, (tx: any) => view.dispatch(tx.addStoredMark(fmark)), true); + setHighlight(color: String, view: EditorView, dispatch: any) { + const highlightMark = view.state.schema.mark(view.state.schema.marks.marker, { highlight: color }); + if (view.state.selection.empty) return false; view.focus(); - this.updateMenu(view, undefined, this.props); + this.setMark(highlightMark, view.state, dispatch, false); } - changeFontFamily = (mark: Mark, view: EditorView) => { - const fmark = view.state.schema.marks.pFontFamily.create({ family: mark.attrs.family }); - this.setMark(fmark, view.state, (tx: any) => view.dispatch(tx.addStoredMark(fmark)), true); - view.focus(); - this.updateMenu(view, undefined, this.props); + setColor(color: String, view: EditorView, dispatch: any) { + if (this.view) { + const colorMark = view.state.schema.mark(view.state.schema.marks.pFontColor, { color }); + this.setMark(colorMark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(colorMark)), true); + view.focus(); + this.updateMenu(this.view, undefined, this.props); + } } // TODO: remove doesn't work - //remove all node type and apply the passed-in one to the selected text + // remove all node type and apply the passed-in one to the selected text changeListType = (nodeType: Node | undefined) => { if (!this.view || (nodeType as any)?.attrs.mapStyle === "") return; @@ -490,25 +388,27 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { dispatch?.(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark)); return true; } - alignCenter = (state: EditorState<any>, dispatch: any) => { - return this.TextView.props.isSelected(true) && this.alignParagraphs(state, "center", dispatch); + alignCenter = (view: EditorView, dispatch: any) => { + return this.TextView.props.isSelected(true) && this.alignParagraphs(view, "center", dispatch); } - alignLeft = (state: EditorState<any>, dispatch: any) => { - return this.TextView.props.isSelected(true) && this.alignParagraphs(state, "left", dispatch); + alignLeft = (view: EditorView, dispatch: any) => { + return this.TextView.props.isSelected(true) && this.alignParagraphs(view, "left", dispatch); } - alignRight = (state: EditorState<any>, dispatch: any) => { - return this.TextView.props.isSelected(true) && this.alignParagraphs(state, "right", dispatch); + alignRight = (view: EditorView, dispatch: any) => { + return this.TextView.props.isSelected(true) && this.alignParagraphs(view, "right", dispatch); } - alignParagraphs(state: EditorState<any>, align: "left" | "right" | "center", dispatch: any) { - var tr = state.tr; - state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { + alignParagraphs(view: EditorView, align: "left" | "right" | "center", dispatch: any) { + var tr = view.state.tr; + view.state.doc.nodesBetween(view.state.selection.from, view.state.selection.to, (node, pos, parent, index) => { if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) { tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, align }, node.marks); return false; } + view.focus(); return true; }); + view.focus(); dispatch?.(tr); return true; } @@ -597,47 +497,6 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } _brushNameRef = React.createRef<HTMLInputElement>(); - createBrushButton() { - const self = this; - const onBrushClick = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - self.TextView.endUndoTypingBatch(); - UndoManager.RunInBatch(() => self.view && self.fillBrush(self.view.state, self.view.dispatch), "rt brush"); - }; - - let label = "Stored marks: "; - if (this.brushMarks && this.brushMarks.size > 0) { - this.brushMarks.forEach((mark: Mark) => { - const markType = mark.type; - label += markType.name; - label += ", "; - }); - label = label.substring(0, label.length - 2); - } else { - label = "No marks are currently stored"; - } - - //onPointerDown={onBrushClick} - - const button = <Tooltip title={<div className="dash-tooltip">style brush</div>} placement="bottom"> - <button className="antimodeMenu-button" onClick={onBrushClick} style={this.brushMarks?.size > 0 ? { backgroundColor: "121212" } : {}}> - <FontAwesomeIcon icon="paint-roller" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.brushMarks?.size > 0 ? 45 : 0}deg)` }} /> - </button> - </Tooltip>; - - const dropdownContent = - <div className="dropdown"> - <p>{label}</p> - <button onPointerDown={this.clearBrush}>Clear brush</button> - <input placeholder="-brush name-" ref={this._brushNameRef} onKeyPress={this.onBrushNameKeyPress} /> - </div>; - - return ( - <ButtonDropdown view={this.view} key={"brush dropdown"} button={button} openDropdownOnButton={false} dropdownContent={dropdownContent} /> - ); - } - @action clearBrush() { RichTextMenu.Instance.brushMarks = new Set(); @@ -666,123 +525,14 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } } - @action toggleColorDropdown() { this.showColorDropdown = !this.showColorDropdown; } - @action setActiveColor(color: string) { this.activeFontColor = color; } get TextView() { return (this.view as any)?.TextView as FormattedTextBox; } get TextViewFieldKey() { return this.TextView?.props.fieldKey; } - createColorButton() { - const self = this; - function onColorClick(e: React.PointerEvent) { - e.preventDefault(); - e.stopPropagation(); - self.TextView.endUndoTypingBatch(); - if (self.view) { - UndoManager.RunInBatch(() => self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch), "rt menu color"); - self.view.focus(); - self.updateMenu(self.view, undefined, self.props); - } - } - function changeColor(e: React.PointerEvent, color: string) { - e.preventDefault(); - e.stopPropagation(); - self.setActiveColor(color); - self.TextView.endUndoTypingBatch(); - if (self.view) { - UndoManager.RunInBatch(() => self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch), "rt menu color"); - self.view.focus(); - self.updateMenu(self.view, undefined, self.props); - } - } - // onPointerDown={onColorClick} - const button = <Tooltip title={<div className="dash-tooltip">set font color</div>} placement="bottom"> - <button className="antimodeMenu-button color-preview-button"> - <FontAwesomeIcon icon="palette" size="lg" /> - <div className="color-preview" style={{ backgroundColor: this.activeFontColor }}></div> - </button> - </Tooltip>; - const dropdownContent = - <div className="dropdown" > - <p>Change font color:</p> - <div className="color-wrapper"> - {this.fontColors.map(color => { - if (color) { - return this.activeFontColor === color ? - <button className="color-button active" key={"active" + color} style={{ backgroundColor: color }} onPointerDown={e => changeColor(e, color)}></button> : - <button className="color-button" key={"other" + color} style={{ backgroundColor: color }} onPointerDown={e => changeColor(e, color)}></button>; - } - })} - </div> - </div>; - - return ( - <ButtonDropdown view={this.view} key={"color dropdown"} button={button} dropdownContent={dropdownContent} openDropdownOnButton={true} /> - ); - } - - public insertColor(color: String, state: EditorState<any>, dispatch: any) { - const colorMark = state.schema.mark(state.schema.marks.pFontColor, { color: color }); - if (state.selection.empty) { - dispatch(state.tr.addStoredMark(colorMark)); - return false; - } - this.setMark(colorMark, state, dispatch, true); - } - - @action toggleHighlightDropdown() { this.showHighlightDropdown = !this.showHighlightDropdown; } @action setActiveHighlight(color: string) { this.activeHighlightColor = color; } - createHighlighterButton() { - const self = this; - function onHighlightClick(e: React.PointerEvent) { - e.preventDefault(); - e.stopPropagation(); - self.TextView.endUndoTypingBatch(); - UndoManager.RunInBatch(() => self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch), "rt highligher"); - } - function changeHighlight(e: React.PointerEvent, color: string) { - e.preventDefault(); - e.stopPropagation(); - self.setActiveHighlight(color); - self.TextView.endUndoTypingBatch(); - UndoManager.RunInBatch(() => self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch), "rt highlighter"); - } - - //onPointerDown={onHighlightClick} - const button = <Tooltip title={<div className="dash-tooltip">set highlight color</div>} placement="bottom"> - <button className="antimodeMenu-button color-preview-button" key="highilghter-button" > - <FontAwesomeIcon icon="highlighter" size="lg" /> - <div className="color-preview" style={{ backgroundColor: this.activeHighlightColor }}></div> - </button> - </Tooltip>; - - const dropdownContent = - <div className="dropdown"> - <p>Change highlight color:</p> - <div className="color-wrapper"> - {this.highlightColors.map(color => { - if (color) { - return this.activeHighlightColor === color ? - <button className="color-button active" key={`active ${color}`} style={{ backgroundColor: color }} onPointerDown={e => changeHighlight(e, color)}>{color === "transparent" ? "X" : ""}</button> : - <button className="color-button" key={`inactive ${color}`} style={{ backgroundColor: color }} onPointerDown={e => changeHighlight(e, color)}>{color === "transparent" ? "X" : ""}</button>; - } - })} - </div> - </div>; - - return ( - <ButtonDropdown view={this.view} key={"highlighter"} button={button} dropdownContent={dropdownContent} openDropdownOnButton={true} /> - ); - } - - insertHighlight(color: String, state: EditorState<any>, dispatch: any) { - if (state.selection.empty) return false; - toggleMark(state.schema.marks.marker, { highlight: color })(state, dispatch); - } - @action toggleLinkDropdown() { this.showLinkDropdown = !this.showLinkDropdown; } @action setCurrentLink(link: string) { this.currentLink = link; } createLinkButton() { @@ -828,7 +578,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { if (linkDoc instanceof Doc) { const anchor1 = await Cast(linkDoc.anchor1, Doc); const anchor2 = await Cast(linkDoc.anchor2, Doc); - const currentDoc = SelectionManager.Views().length && SelectionManager.Views()[0].props.Document; + const currentDoc = SelectionManager.Docs().lastElement(); if (currentDoc && anchor1 && anchor2) { if (Doc.AreProtosEqual(currentDoc, anchor1)) { return StrCast(anchor2.title); @@ -852,7 +602,6 @@ 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 @@ -921,95 +670,70 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { return ref_node; } - @action onPointerEnter(e: React.PointerEvent) { RichTextMenu.Instance.overMenu = false; } - @action onPointerLeave(e: React.PointerEvent) { RichTextMenu.Instance.overMenu = false; } - - @action - toggleMenuPin = (e: React.MouseEvent) => { - Doc.UserDoc()["menuRichText-pinned"] = this.Pinned = !this.Pinned; - if (!this.Pinned) { - this.fadeOut(true); - } - } - - @action - protected toggleCollapse = (e: React.MouseEvent) => { - this.collapsed = !this.collapsed; - setTimeout(() => { - const x = Math.min(this._left, window.innerWidth - RichTextMenu.Instance.width); - RichTextMenu.Instance.jumpTo(x, this._top, true); - }, 0); - } - render() { - TraceMobx(); - const row1 = <div className="antimodeMenu-row" key="row 1" style={{ display: this.collapsed ? "none" : undefined }}>{[ - //!this.collapsed ? this.getDragger() : (null), - // !this.Pinned ? (null) : <div key="frag1"> {[ - // this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)), - // this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)), - // this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)), - // this.createButton("strikethrough", "Strikethrough", this.strikethroughActive, toggleMark(schema.marks.strikethrough)), - // this.createButton("superscript", "Superscript", this.superscriptActive, toggleMark(schema.marks.superscript)), - // this.createButton("subscript", "Subscript", this.subscriptActive, toggleMark(schema.marks.subscript)), - // <div className="richTextMenu-divider" key="divider" /> - // ]}</div>, - this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)), - this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)), - this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)), - this.createButton("strikethrough", "Strikethrough", this.strikethroughActive, toggleMark(schema.marks.strikethrough)), - this.createButton("superscript", "Superscript", this.superscriptActive, toggleMark(schema.marks.superscript)), - this.createButton("subscript", "Subscript", this.subscriptActive, toggleMark(schema.marks.subscript)), - this.createColorButton(), - this.createHighlighterButton(), - this.createLinkButton(), - this.createBrushButton(), - <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), - this.createButton("indent", "Inset More", undefined, this.insetParagraph), - this.createButton("outdent", "Inset Less", undefined, this.outsetParagraph), - this.createButton("hand-point-left", "Hanging Indent", undefined, this.hangingIndentParagraph), - this.createButton("hand-point-right", "Indent", undefined, this.indentParagraph), - ]}</div>; - - const row2 = <div className="antimodeMenu-row row-2" key="row2"> - {this.collapsed ? this.getDragger() : (null)} - <div key="row 2" style={{ display: this.collapsed ? "none" : undefined }}> - <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); - })), - this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family", action((val: string) => { - this.activeFontFamily = val; - SelectionManager.Views().map(dv => dv.props.Document._fontFamily = val); - })), - <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> - {/* <div key="collapser"> - {<div key="collapser"> - <button className="antimodeMenu-button" key="collapse menu" title="Collapse menu" onClick={this.toggleCollapse} style={{ backgroundColor: this.collapsed ? "#121212" : "", width: 25 }}> - <FontAwesomeIcon icon="chevron-left" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.3s", transform: `rotate(${this.collapsed ? 180 : 0}deg)` }} /> - </button> - </div> } - <button className="antimodeMenu-button" key="pin menu" title="Pin menu" onClick={this.toggleMenuPin} style={{ backgroundColor: this.Pinned ? "#121212" : "", display: this.collapsed ? "none" : undefined }}> - <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.Pinned ? 45 : 0}deg)` }} /> - </button> - </div> */} - </div>; - - return ( - <div className="richTextMenu" onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave} > - {this.getElementWithRows([row1, row2], 2, false)} - </div> - ); + return null; + // TraceMobx(); + // const row1 = <div className="antimodeMenu-row" key="row 1" style={{ display: this.collapsed ? "none" : undefined }}>{[ + // //!this.collapsed ? this.getDragger() : (null), + // // !this.Pinned ? (null) : <div key="frag1"> {[ + // // this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)), + // // this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)), + // // this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)), + // // this.createButton("strikethrough", "Strikethrough", this.strikethroughActive, toggleMark(schema.marks.strikethrough)), + // // this.createButton("superscript", "Superscript", this.superscriptActive, toggleMark(schema.marks.superscript)), + // // this.createButton("subscript", "Subscript", this.subscriptActive, toggleMark(schema.marks.subscript)), + // // <div className="richTextMenu-divider" key="divider" /> + // // ]}</div>, + // this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)), + // this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)), + // this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)), + // this.createButton("strikethrough", "Strikethrough", this.strikethroughActive, toggleMark(schema.marks.strikethrough)), + // this.createButton("superscript", "Superscript", this.superscriptActive, toggleMark(schema.marks.superscript)), + // this.createButton("subscript", "Subscript", this.subscriptActive, toggleMark(schema.marks.subscript)), + // this.createColorButton(), + // this.createHighlighterButton(), + // this.createLinkButton(), + // this.createBrushButton(), + // <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), + // this.createButton("indent", "Inset More", undefined, this.insetParagraph), + // this.createButton("outdent", "Inset Less", undefined, this.outsetParagraph), + // this.createButton("hand-point-left", "Hanging Indent", undefined, this.hangingIndentParagraph), + // this.createButton("hand-point-right", "Indent", undefined, this.indentParagraph), + // ]}</div>; + + // const row2 = <div className="antimodeMenu-row row-2" key="row2"> + // {this.collapsed ? this.getDragger() : (null)} + // <div key="row 2" style={{ display: this.collapsed ? "none" : undefined }}> + // <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); + // })), + // this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family", action((val: string) => { + // this.activeFontFamily = val; + // SelectionManager.Views().map(dv => dv.props.Document._fontFamily = val); + // })), + // <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> + // {/* <div key="collapser"> + // {<div key="collapser"> + // <button className="antimodeMenu-button" key="collapse menu" title="Collapse menu" onClick={this.toggleCollapse} style={{ backgroundColor: this.collapsed ? "#121212" : "", width: 25 }}> + // <FontAwesomeIcon icon="chevron-left" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.3s", transform: `rotate(${this.collapsed ? 180 : 0}deg)` }} /> + // </button> + // </div> } + // <button className="antimodeMenu-button" key="pin menu" title="Pin menu" onClick={this.toggleMenuPin} style={{ backgroundColor: this.Pinned ? "#121212" : "", display: this.collapsed ? "none" : undefined }}> + // <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.Pinned ? 45 : 0}deg)` }} /> + // </button> + // </div> */} + // </div>; } } diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index 3fd7d61fa..711136469 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -282,7 +282,7 @@ export class RichTextRules { if (rstate) { this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3)))); } - const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: rawdocid, _width: 500, _height: 500, }, docid); + const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: rawdocid.replace(/^:/, ""), _width: 500, _height: 500, }, docid); DocUtils.MakeLink({ doc: this.TextBox.getAnchor() }, { doc: target }, "portal to", undefined); const fstate = this.TextBox.EditorView?.state; @@ -290,7 +290,7 @@ export class RichTextRules { this.TextBox.EditorView?.dispatch(fstate.tr.setSelection(new TextSelection(fstate.doc.resolve(selection)))); } }); - return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 2); + return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 3); } return state.tr; } diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index 655ee7e44..6103a28d6 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -61,13 +61,13 @@ export const marks: { [index: string]: MarkSpec } = { /** FONT SIZES */ pFontSize: { - attrs: { fontSize: { default: 10 } }, + attrs: { fontSize: { default: "10px" } }, parseDOM: [{ tag: "span", getAttrs(dom: any) { - return { fontSize: dom.style.fontSize ? Number(dom.style.fontSize.replace("px", "")) : "" }; + return { fontSize: dom.style.fontSize ? dom.style.fontSize.toString() : "" }; } }], - toDOM: (node) => node.attrs.fontSize ? ['span', { style: `font-size: ${node.attrs.fontSize}px;` }] : ['span', 0] + toDOM: (node) => node.attrs.fontSize ? ['span', { style: `font-size: ${node.attrs.fontSize};` }] : ['span', 0] }, /* FONTS */ diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index cfc3e75cc..2a153f256 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -30,8 +30,7 @@ import { LightboxView } from "../../LightboxView"; import { CollectionFreeFormDocumentView } from "../CollectionFreeFormDocumentView"; import { FieldView, FieldViewProps } from '../FieldView'; import "./PresBox.scss"; -import Color = require("color"); -import { PresEffect, PresStatus, PresMovement } from "./PresEnums"; +import { PresEffect, PresMovement, PresStatus } from "./PresEnums"; export class PinProps { audioRange?: boolean; @@ -166,8 +165,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> this.layoutDoc._gridGap = 0; this.layoutDoc._yMargin = 0; this.turnOffEdit(true); - DocListCastAsync((Doc.UserDoc().myPresentations as Doc).data).then(pres => - !pres?.includes(this.rootDoc) && Doc.AddDocToList(Doc.UserDoc().myPresentations as Doc, "data", this.rootDoc)); + DocListCastAsync((Doc.UserDoc().myTrails as Doc).data).then(pres => + !pres?.includes(this.rootDoc) && Doc.AddDocToList(Doc.UserDoc().myTrails as Doc, "data", this.rootDoc)); this._disposers.selection = reaction(() => SelectionManager.Views(), views => views.some(view => view.props.Document === this.rootDoc) && this.updateCurrentPresentation()); } @@ -401,11 +400,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> LightboxView.SetLightboxDoc(targetDoc); } else if (curDoc.presMovement === PresMovement.Pan && targetDoc) { LightboxView.SetLightboxDoc(undefined); - await DocumentManager.Instance.jumpToDocument(targetDoc, false, openInTab, srcContext, undefined, undefined, undefined, includesDoc || tab ? undefined : resetSelection); // documents open in new tab instead of on right + await DocumentManager.Instance.jumpToDocument(targetDoc, false, openInTab, srcContext, undefined, undefined, undefined, includesDoc || tab ? undefined : resetSelection, undefined, true); // documents open in new tab instead of on right } else if ((curDoc.presMovement === PresMovement.Zoom || curDoc.presMovement === PresMovement.Jump) && targetDoc) { LightboxView.SetLightboxDoc(undefined); //awaiting jump so that new scale can be found, since jumping is async - await DocumentManager.Instance.jumpToDocument(targetDoc, true, openInTab, srcContext, undefined, undefined, undefined, includesDoc || tab ? undefined : resetSelection); // documents open in new tab instead of on right + await DocumentManager.Instance.jumpToDocument(targetDoc, true, openInTab, srcContext, undefined, undefined, undefined, includesDoc || tab ? undefined : resetSelection, undefined, true, NumCast(curDoc.presZoom)); // documents open in new tab instead of on right } // After navigating to the document, if it is added as a presPinView then it will // adjust the pan and scale to that of the pinView when it was added. @@ -413,8 +412,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> // if targetDoc is not displayed but one of its aliases is, then we need to modify that alias, not the original target this.navigateToView(targetDoc, activeItem); } - // TODO: Add progressivize for navigating web (storing websites for given frames) - } /** @@ -1029,6 +1026,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> Array.from(this._selectedArray.keys()).forEach((doc) => doc.presTransition = timeInMS); } + // Converts seconds to ms and updates presTransition + setZoom = (number: String, change?: number) => { + let scale = Number(number) / 100; + if (change) scale += change; + if (scale < 0.01) scale = 0.01; + if (scale > 1.5) scale = 1.5; + Array.from(this._selectedArray.keys()).forEach((doc) => doc.presZoom = scale); + } + // Converts seconds to ms and updates presDuration setDurationTime = (number: String, change?: number) => { let timeInMS = Number(number) * 1000; @@ -1146,11 +1152,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @computed get transitionDropdown() { const activeItem: Doc = this.activeItem; const targetDoc: Doc = this.targetDoc; - const type = targetDoc.type; const isPresCollection: boolean = (targetDoc === this.layoutDoc.presCollection); const isPinWithView: boolean = BoolCast(activeItem.presPinView); if (activeItem && targetDoc) { + const type = targetDoc.type; const transitionSpeed = activeItem.presTransition ? NumCast(activeItem.presTransition) / 1000 : 0.5; + const zoom = activeItem.presZoom ? NumCast(activeItem.presZoom) * 100 : 75; let duration = activeItem.presDuration ? NumCast(activeItem.presDuration) / 1000 : 2; if (activeItem.type === DocumentType.AUDIO) duration = NumCast(activeItem.duration); const effect = targetDoc.presEffect ? targetDoc.presEffect : 'None'; @@ -1175,6 +1182,31 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> </div> </div> } + <div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === PresMovement.Zoom ? "inline-flex" : "none" }}> + <div className="presBox-subheading">Zoom (% screen filled)</div> + <div className="ribbon-property"> + <input className="presBox-input" + type="number" value={zoom} + onChange={action((e) => this.setZoom(e.target.value))} />% + </div> + <div className="ribbon-propertyUpDown"> + <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), 0.1))}> + <FontAwesomeIcon icon={"caret-up"} /> + </div> + <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), -0.1))}> + <FontAwesomeIcon icon={"caret-down"} /> + </div> + </div> + </div> + <input type="range" step="1" min="0" max="150" value={zoom} + className={`toolbar-slider ${activeItem.presMovement === PresMovement.Zoom ? "" : "none"}`} + id="toolbar-slider" + onPointerDown={() => this._batch = UndoManager.StartBatch("presZoom")} + onPointerUp={() => this._batch?.end()} + onChange={(e: React.ChangeEvent<HTMLInputElement>) => { + e.stopPropagation(); + this.setZoom(e.target.value); + }} /> <div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? "inline-flex" : "none" }}> <div className="presBox-subheading">Movement Speed</div> <div className="ribbon-property"> @@ -1220,7 +1252,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <input className="presBox-input" type="number" value={duration} onChange={action((e) => this.setDurationTime(e.target.value))} /> s - </div> + </div> <div className="ribbon-propertyUpDown"> <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setDurationTime(String(duration), 1000))}> <FontAwesomeIcon icon={"caret-up"} /> @@ -2229,7 +2261,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> const isMini: boolean = this.toolbarWidth <= 100; return ( <div className="presBox-buttons" style={{ display: !this.rootDoc._chromeHidden ? "none" : undefined }}> - {isMini ? (null) : <select className="presBox-viewPicker" + {isMini || Doc.UserDoc().noviceMode ? (null) : <select className="presBox-viewPicker" style={{ display: this.layoutDoc.presStatus === "edit" ? "block" : "none" }} onPointerDown={e => e.stopPropagation()} onChange={this.viewChanged} diff --git a/src/client/views/pdf/AnchorMenu.scss b/src/client/views/pdf/AnchorMenu.scss index b7afb26a5..6990bdcf1 100644 --- a/src/client/views/pdf/AnchorMenu.scss +++ b/src/client/views/pdf/AnchorMenu.scss @@ -4,6 +4,35 @@ padding: 5px; grid-template-columns: 90px 20px 90px; } +.anchorMenu-highlighter { + padding-right: 5px; + .antimodeMenu-button { + padding: 0; + padding: 0; + padding-right: 0px; + padding-left: 0px; + width: 5px; + } +} +.anchor-color-preview-button { + width: 25px !important; + .anchor-color-preview { + display: flex; + flex-direction: column; + padding-right: 3px; + width: unset !important; + .color-preview { + width: 60%; + top: 80%; + height: 4px; + position: relative; + top: unset; + width: 15px; + margin-top: 5px; + display: block; + } + } +} .color-wrapper { display: flex; diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx index 17979ef4b..ad3afb775 100644 --- a/src/client/views/pdf/AnchorMenu.tsx +++ b/src/client/views/pdf/AnchorMenu.tsx @@ -41,7 +41,6 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { @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" | "" = ""; @@ -85,6 +84,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { if (!this.Highlight(this.highlightColor, false) && this.Pinned) { this.Highlighting = !this.Highlighting; } + AnchorMenu.Instance.fadeOut(true); } @action @@ -96,9 +96,11 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { @computed get highlighter() { const button = - <button className="antimodeMenu-button color-preview-button" title="" key="highlighter-button" onClick={this.highlightClicked}> - <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} /> - <div className="color-preview" style={{ backgroundColor: this.highlightColor }}></div> + <button className="antimodeMenu-button anchor-color-preview-button" title="" key="highlighter-button" onClick={this.highlightClicked}> + <div className="anchor-color-preview" > + <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} /> + <div className="color-preview" style={{ backgroundColor: this.highlightColor }}></div> + </div> </button>; const dropdownContent = @@ -116,7 +118,9 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { </div>; return ( <Tooltip key="highlighter" title={<div className="dash-tooltip">{"Click to Highlight"}</div>}> - <ButtonDropdown key={"highlighter"} button={button} dropdownContent={dropdownContent} pdf={true} /> + <div className="anchorMenu-highlighter"> + <ButtonDropdown key={"highlighter"} button={button} dropdownContent={dropdownContent} pdf={true} /> + </div> </Tooltip> ); } @@ -151,9 +155,10 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { </button> </Tooltip>, //NOTE: link popup is currently in progress - <Tooltip key="link" title={<div className="dash-tooltip">{"Link selected text to document"}</div>}> + <Tooltip key="link" title={<div className="dash-tooltip">{"Find document to link to selected text"}</div>}> <button className="antimodeMenu-button link" onPointerDown={this.toggleLinkPopup} style={{}}> - <FontAwesomeIcon icon="link" size="lg" /> + <FontAwesomeIcon style={{ position: "absolute", transform: "scale(1.5)" }} icon={"search"} size="lg" /> + <FontAwesomeIcon style={{ position: "absolute", transform: "scale(0.5)", transformOrigin: "top left", top: 12, left: 12 }} icon={"link"} size="lg" /> </button> </Tooltip>, <LinkPopup key="popup" showPopup={this._showLinkPopup} linkFrom={this.onMakeAnchor} /> diff --git a/src/client/views/pdf/Annotation.scss b/src/client/views/pdf/Annotation.scss index e98f071c3..1de60ffed 100644 --- a/src/client/views/pdf/Annotation.scss +++ b/src/client/views/pdf/Annotation.scss @@ -4,4 +4,7 @@ position: absolute; background-color: rgba(146, 245, 95, 0.467); transition: opacity 0.5s; + &:hover { + cursor: pointer; + } }
\ No newline at end of file diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index 07b734405..b1d1d8293 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -16,17 +16,19 @@ interface IAnnotationProps extends FieldViewProps { dataDoc: Doc; fieldKey: string; showInfo: (anno: Opt<Doc>) => void; + pointerEvents?: string; } @observer export class Annotation extends React.Component<IAnnotationProps> { render() { - return DocListCast(this.props.anno.textInlineAnnotations).map(a => <RegionAnnotation {...this.props} document={a} key={a[Id]} />); + return DocListCast(this.props.anno.textInlineAnnotations).map(a => <RegionAnnotation pointerEvents={this.props.pointerEvents} {...this.props} document={a} key={a[Id]} />); } } interface IRegionAnnotationProps extends IAnnotationProps { document: Doc; + pointerEvents?: string; } @observer class RegionAnnotation extends React.Component<IRegionAnnotationProps> { @@ -96,6 +98,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { width: NumCast(this.props.document._width), height: NumCast(this.props.document._height), opacity: this._brushed ? 0.5 : undefined, + pointerEvents: this.props.pointerEvents as any, backgroundColor: this._brushed ? "orange" : StrCast(this.props.document.backgroundColor), }} > </div>); diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 02010e123..3f7f38bdf 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -2,14 +2,13 @@ import { action, computed, IReactionDisposer, observable, ObservableMap, reactio import { observer } from "mobx-react"; import * as Pdfjs from "pdfjs-dist"; import "pdfjs-dist/web/pdf_viewer.css"; -import { DataSym, Doc, DocListCast, Field, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; +import { Doc, DocListCast, Field, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; import { Id } from "../../../fields/FieldSymbols"; import { InkTool } from "../../../fields/InkField"; -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, returnEmptyString, returnEmptyFilter } from "../../../Utils"; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, OmitKeys, smoothScroll, Utils } from "../../../Utils"; import { DocUtils } from "../../documents/Documents"; import { Networking } from "../../Network"; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; @@ -17,10 +16,13 @@ import { CompiledScript, CompileScript } from "../../util/Scripting"; import { SelectionManager } from "../../util/SelectionManager"; import { SharingManager } from "../../util/SharingManager"; import { SnappingManager } from "../../util/SnappingManager"; +import { MarqueeOptionsMenu } from "../collections/collectionFreeForm"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; import { MarqueeAnnotator } from "../MarqueeAnnotator"; +import { DocumentViewProps } from "../nodes/DocumentView"; import { FieldViewProps } from "../nodes/FieldView"; import { LinkDocPreview } from "../nodes/LinkDocPreview"; +import { StyleProp } from "../StyleProvider"; import { AnchorMenu } from "./AnchorMenu"; import { Annotation } from "./Annotation"; import "./PDFViewer.scss"; @@ -126,12 +128,6 @@ export class PDFViewer extends React.Component<IViewerProps> { } }); - this._disposers.searchMatch = reaction(() => Doc.IsSearchMatch(this.props.rootDoc), - m => { - if (m) (this._lastSearch = true) && this.search(Doc.SearchQuery(), m.searchMatch > 0); - else !(this._lastSearch = false) && setTimeout(() => !this._lastSearch && this.search("", false, true), 200); - }, { fireImmediately: true }); - this._disposers.selected = reaction(() => this.props.isSelected(), selected => { // if (!selected) { @@ -186,7 +182,7 @@ export class PDFViewer extends React.Component<IViewerProps> { let focusSpeed: Opt<number>; 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); + const scrollTo = doc.unrendered ? NumCast(doc.y) : Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.props.layoutDoc._scrollTop), windowHeight, .1 * windowHeight); if (scrollTo !== undefined) { focusSpeed = 500; @@ -338,10 +334,10 @@ export class PDFViewer extends React.Component<IViewerProps> { } @action - search = (searchString: string, fwd: boolean, clear: boolean = false) => { + search = (searchString: string, bwd?: boolean, clear: boolean = false) => { const findOpts = { caseSensitive: false, - findPrevious: !fwd, + findPrevious: bwd, highlightAll: true, phraseSearch: true, query: searchString @@ -349,7 +345,7 @@ export class PDFViewer extends React.Component<IViewerProps> { if (clear) { this._pdfViewer?.findController.executeCommand('reset', { query: "" }); } else if (!searchString) { - fwd ? this.nextAnnotation() : this.prevAnnotation(); + bwd ? this.prevAnnotation() : this.nextAnnotation(); } else if (this._pdfViewer?.pageViewsReady) { this._pdfViewer.findController.executeCommand('findagain', findOpts); } @@ -358,6 +354,7 @@ export class PDFViewer extends React.Component<IViewerProps> { this._mainCont.current.addEventListener("pagesloaded", executeFind); this._mainCont.current.addEventListener("pagerendered", executeFind); } + return true; } @action @@ -487,11 +484,12 @@ export class PDFViewer extends React.Component<IViewerProps> { } } + pointerEvents = () => this.props.isContentActive() && this.props.pointerEvents !== "none" && !MarqueeOptionsMenu.Instance.isShown() ? "all" : SnappingManager.GetIsDragging() ? undefined : "none"; @computed get annotationLayer() { + const pe = this.pointerEvents(); return <div className="pdfViewerDash-annotationLayer" style={{ height: Doc.NativeHeight(this.props.Document), transform: `scale(${this._zoomed})` }} ref={this._annotationLayer}> {this.inlineTextAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map(anno => - <Annotation {...this.props} fieldKey={this.props.fieldKey + "-annotations"} showInfo={this.showInfo} dataDoc={this.props.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} />) - } + <Annotation {...this.props} fieldKey={this.props.fieldKey + "-annotations"} pointerEvents={pe} showInfo={this.showInfo} dataDoc={this.props.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} />)} </div>; } @@ -508,8 +506,16 @@ 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); + basicFilter = () => [...this.props.docFilters(), Utils.PropUnsetFilter("textInlineAnnotations")]; transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()]; opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()]; + childStyleProvider = (doc: (Doc | undefined), props: Opt<DocumentViewProps>, property: string): any => { + if (doc instanceof Doc && property === StyleProp.PointerEvents) { + if (doc.textInlineAnnotations) return "none"; + return "all"; + } + return this.props.styleProvider?.(doc, props, property); + } @computed get overlayLayer() { const renderAnnotations = (docFilters?: () => string[]) => <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} @@ -522,12 +528,12 @@ export class PDFViewer extends React.Component<IViewerProps> { select={emptyFunction} ContentScaling={this.contentZoom} bringToFront={emptyFunction} - docFilters={docFilters || this.props.docFilters} + docFilters={docFilters || this.basicFilter} + styleProvider={this.childStyleProvider} dontRenderDocuments={docFilters ? false : true} CollectionView={undefined} ScreenToLocalTransform={this.overlayTransform} - renderDepth={this.props.renderDepth + 1} - childPointerEvents={true} />; + renderDepth={this.props.renderDepth + 1} />; return <div> <div className={`pdfViewerDash-overlay${CurrentUserUtils.SelectedTool !== InkTool.None || SnappingManager.GetIsDragging() ? "-inking" : ""}`} style={{ @@ -549,7 +555,7 @@ export class PDFViewer extends React.Component<IViewerProps> { </div>; } @computed get pdfViewerDiv() { - return <div className={"pdfViewerDash-text" + (this._textSelecting && (this.props.isSelected() || this.props.isContentActive()) ? "-selected" : "")} ref={this._viewer} />; + return <div className={"pdfViewerDash-text" + (this.props.pointerEvents !== "none" && this._textSelecting && (this.props.isSelected() || this.props.isContentActive()) ? "-selected" : "")} ref={this._viewer} />; } @computed get contentScaling() { return this.props.ContentScaling?.() || 1; } @computed get standinViews() { @@ -562,7 +568,7 @@ export class PDFViewer extends React.Component<IViewerProps> { render() { TraceMobx(); return <div className="pdfViewer-content"> - <div className={`pdfViewerDash${this.props.isContentActive() ? "-interactive" : ""}`} ref={this._mainCont} + <div className={`pdfViewerDash${this.props.isContentActive() && this.props.pointerEvents !== "none" ? "-interactive" : ""}`} ref={this._mainCont} onScroll={this.onScroll} onWheel={this.onZoomWheel} onPointerDown={this.onPointerDown} onClick={this.onClick} style={{ overflowX: this._zoomed !== 1 ? "scroll" : undefined, diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss index 2586ef2ee..e8865b918 100644 --- a/src/client/views/search/SearchBox.scss +++ b/src/client/views/search/SearchBox.scss @@ -78,8 +78,10 @@ .searchBox-result-title { display: relative; float: left; - width: calc(100% - 60px); + width: calc(100% - 45px); text-align: left; + overflow: hidden; + text-overflow: ellipsis; } .searchBox-result-type { @@ -87,7 +89,7 @@ margin-top: 6px; display: relative; float: right; - width: 60px; + width: 45px; text-align: right; color: #222; } diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index 260ddfc90..3612bd7c4 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -13,6 +13,7 @@ import { FieldView, FieldViewProps } from '../nodes/FieldView'; import "./SearchBox.scss"; import { DocumentManager } from '../../util/DocumentManager'; import { DocUtils } from '../../documents/Documents'; +import { Tooltip } from "@material-ui/core"; export const searchSchema = createSchema({ Document: Doc @@ -103,9 +104,9 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc * 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); + onResultClick = action(async (doc: Doc) => { this._selectedResult = doc; + this.selectElement(doc, () => DocumentManager.Instance.getFirstDocumentView(doc)?.ComponentView?.search?.(this._searchString, undefined, false)); }); makeLink = action((linkTo: Doc) => { @@ -114,7 +115,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc const linkFrom = this.props.linkFrom(); if (linkFrom) { console.log(linkFrom.title); - DocUtils.MakeLink({ doc: linkFrom }, { doc: linkTo }); + DocUtils.MakeLink({ doc: linkFrom }, { doc: linkTo }, "Link"); } } }); @@ -268,8 +269,8 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc * 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); + selectElement = async (doc: Doc, finishFunc: () => void) => { + await DocumentManager.Instance.jumpToDocument(doc, true, undefined, undefined, undefined, undefined, undefined, finishFunc); } /** @@ -299,20 +300,30 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc className += " searchBox-results-scroll-view-result-selected"; } + const formattedType = SearchBox.formatType(StrCast(result[0].type)); + const title = result[0].title; + 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-result-type"> - {SearchBox.formatType(StrCast(result[0].type))} + <Tooltip key={result[0][Id]} placement={"right"} title={<><div className="dash-tooltip">{title}</div></>}> + <div onClick={isLinkSearch ? + () => this.makeLink(result[0]) : + e => { + this.onResultClick(result[0]); + e.stopPropagation(); + }} className={className}> + <div className="searchBox-result-title"> + {title} + </div> + <div className="searchBox-result-type"> + {formattedType} + </div> + <div className="searchBox-result-keys"> + {result[1].join(", ")} + </div> </div> - <div className="searchBox-result-keys"> - {result[1].join(", ")} - </div> - </div> + </Tooltip> ); } @@ -327,7 +338,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc {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} /> + <input defaultValue={""} autoComplete="off" onChange={this.onInputChange} onKeyPress={e => e.key === "Enter" ? this.submitSearch() : null} type="text" placeholder="Search..." id="search-input" className="searchBox-input" style={{ width: isLinkSearch ? "100%" : undefined, borderRadius: isLinkSearch ? "5px" : undefined }} ref={this._inputRef} /> </div > <div className="searchBox-results-container"> <div className="searchBox-results-count"> diff --git a/src/client/views/topbar/TopBar.scss b/src/client/views/topbar/TopBar.scss index d04ae8a80..923f1892e 100644 --- a/src/client/views/topbar/TopBar.scss +++ b/src/client/views/topbar/TopBar.scss @@ -22,7 +22,7 @@ .topBar-icon { cursor: pointer; - font-size: 12px; + font-size: 12.5px; font-family: 'Roboto'; width: fit-content; display: flex; @@ -31,18 +31,20 @@ align-items: center; justify-self: center; align-self: center; - border-radius: 5px; + border-radius: $standard-border-radius; padding: 5px; - transition: linear 0.1s; + transition: linear 0.2s; color: $black; background-color: $light-gray; - } - .topBar-icon:hover { - background-color: $light-blue; + &:hover { + background-color: darken($color: $light-gray, $amount: 20); + } } - + + + .topbar-center { grid-column: 2; display: inline-flex; @@ -59,7 +61,7 @@ .topbar-lozenge-dashboard { display: flex; - + .topbar-dashSelect { border: none; @@ -156,9 +158,9 @@ } &.topbar-input { - margin:5px; - border-radius:20px; - border:$dark-gray; + margin: 5px; + border-radius: 20px; + border: $dark-gray; display: block; width: 130px; -webkit-transition: width 0.4s; diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx index 05edb975c..d5254e315 100644 --- a/src/client/views/topbar/TopBar.tsx +++ b/src/client/views/topbar/TopBar.tsx @@ -28,7 +28,7 @@ export class TopBar extends React.Component { {`${Doc.CurrentUserEmail}`} </div> <div className="topbar-icon" onClick={() => window.location.assign(Utils.prepend("/logout"))}> - {"Sign out"} + {"Log out"} </div> </div> <div className="topbar-center" > @@ -51,7 +51,8 @@ export class TopBar extends React.Component { </div> </div> <div className="topbar-right" > - <div className="topbar-icon"> + <div className="topbar-icon" onClick={() => window.open( + "https://brown-dash.github.io/Dash-Documentation/", "_blank")}> {"Help"}<FontAwesomeIcon icon="question-circle"></FontAwesomeIcon> </div> <div className="topbar-icon" onClick={() => SettingsManager.Instance.open()}> diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index d24175ecd..42d93c60c 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -1,15 +1,17 @@ +import { IconProp } from "@fortawesome/fontawesome-svg-core"; import { saveAs } from "file-saver"; import { action, computed, observable, ObservableMap, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; import { alias, map, serializable } from "serializr"; import { DocServer } from "../client/DocServer"; import { DocumentType } from "../client/documents/DocumentTypes"; +import { CurrentUserUtils } from "../client/util/CurrentUserUtils"; import { LinkManager } from "../client/util/LinkManager"; import { Scripting, scriptingGlobal } from "../client/util/Scripting"; import { SelectionManager } from "../client/util/SelectionManager"; import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from "../client/util/SerializationHelper"; import { UndoManager } from "../client/util/UndoManager"; -import { intersectRect, Utils } from "../Utils"; +import { DashColor, intersectRect, Utils } from "../Utils"; import { DateField } from "./DateField"; import { Copy, HandleUpdate, Id, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update } from "./FieldSymbols"; import { List } from "./List"; @@ -23,9 +25,6 @@ import { Cast, FieldValue, NumCast, StrCast, ToConstructor } from "./Types"; import { AudioField, ImageField, PdfField, VideoField, WebField } from "./URLField"; 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 { @@ -80,6 +79,7 @@ export function DocListCastAsync(field: FieldResult, defaultValue?: Doc[]) { export async function DocCastAsync(field: FieldResult): Promise<Opt<Doc>> { return Cast(field, Doc); } +export function NumListCast(field: FieldResult) { return Cast(field, listSpec("number"), []); } export function StrListCast(field: FieldResult) { return Cast(field, listSpec("string"), []); } export function DocListCast(field: FieldResult) { return Cast(field, listSpec(Doc), []).filter(d => d instanceof Doc) as Doc[]; } export function DocListCastOrNull(field: FieldResult) { return Cast(field, listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[] | undefined; } @@ -1089,7 +1089,7 @@ 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); + const isTransparent = (color: string) => color !== "" && (DashColor(color).alpha() !== 1); return isTransparent(StrCast(doc[key])); } if (typeof value === "string") { @@ -1136,14 +1136,14 @@ export namespace Doc { // filters document in a container collection: // 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) { + export function setDocFilter(container: Opt<Doc>, key: string, value: any, modifiers: "remove" | "match" | "check" | "x" | "exists" | "unset", toggle?: boolean, fieldSuffix?: string, append: boolean = true) { if (!container) return; const filterField = "_" + (fieldSuffix ? fieldSuffix + "-" : "") + "docFilters"; const docFilters = Cast(container[filterField], listSpec("string"), []); runInAction(() => { for (let i = 0; i < docFilters.length; i++) { const fields = docFilters[i].split(":"); // split key:value:modifier - if (fields[0] === key && (fields[1] === value || modifiers === "match" || modifiers === "remove")) { + if (fields[0] === key && (fields[1] === value || modifiers === "match")) { if (fields[2] === modifiers && modifiers && fields[1] === value) { if (toggle) modifiers = "remove"; else return; diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts index 1270a2dab..f16e143d8 100644 --- a/src/fields/InkField.ts +++ b/src/fields/InkField.ts @@ -1,8 +1,8 @@ +import { createSimpleSchema, list, object, serializable } from "serializr"; +import { Scripting } from "../client/util/Scripting"; import { Deserializable } from "../client/util/SerializationHelper"; -import { serializable, custom, createSimpleSchema, list, object, map } from "serializr"; +import { Copy, ToScriptString, ToString } from "./FieldSymbols"; import { ObjectField } from "./ObjectField"; -import { Copy, ToScriptString, ToString, Update } from "./FieldSymbols"; -import { Scripting } from "../client/util/Scripting"; // Helps keep track of the current ink tool in use. export enum InkTool { diff --git a/src/fields/Proxy.ts b/src/fields/Proxy.ts index 62734d3d2..f01b502c9 100644 --- a/src/fields/Proxy.ts +++ b/src/fields/Proxy.ts @@ -79,7 +79,7 @@ export class ProxyField<T extends RefField> extends ObjectField { return field; })); } - return this.promise as any; + return DocServer.GetCachedRefField(this.fieldId) ?? (this.promise as any); } promisedValue(): string { return !this.cache && !this.failed && !this.promise ? this.fieldId : ""; } setPromise(promise: any) { diff --git a/src/fields/RichTextUtils.ts b/src/fields/RichTextUtils.ts index 0b5f14d74..a19be5df9 100644 --- a/src/fields/RichTextUtils.ts +++ b/src/fields/RichTextUtils.ts @@ -2,7 +2,7 @@ import { AssertionError } from "assert"; import { docs_v1 } from "googleapis"; import { Fragment, Mark, Node } from "prosemirror-model"; import { sinkListItem } from "prosemirror-schema-list"; -import { Utils } from "../Utils"; +import { Utils, DashColor } from "../Utils"; import { Docs, DocUtils } from "../client/documents/Documents"; import { schema } from "../client/views/nodes/formattedText/schema_rts"; import { GooglePhotos } from "../client/apis/google_docs/GooglePhotosClientUtils"; @@ -482,7 +482,7 @@ export namespace RichTextUtils { } const fromHex = (color: string): docs_v1.Schema$OptionalColor => { - const c = Color(color); + const c = DashColor(color); return fromRgb.convert(c.red(), c.green(), c.blue()); }; diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts index f17a390a6..db2c6ca5b 100644 --- a/src/fields/documentSchemas.ts +++ b/src/fields/documentSchemas.ts @@ -15,7 +15,6 @@ export const documentSchema = createSchema({ // "Location" properties in a very general sense _curPage: "number", // current page of a page based document _currentFrame: "number", // current frame of a frame based collection (e.g., a progressive slide) - _fullScreenView: Doc, // alias to display when double-clicking to open document in a full-screen view lastFrame: "number", // last frame of a frame based collection (e.g., a progressive slide) activeFrame: "number", // the active frame of a frame based animated document _currentTimecode: "number", // current play back time of a temporal document (video / audio) diff --git a/src/fields/util.ts b/src/fields/util.ts index 439c4d333..c708affe3 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -98,13 +98,12 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number } else { DocServer.registerDocWithCachedUpdate(receiver, prop as string, curValue); } - !receiver[Initializing] && (!receiver[UpdatingFromServer] || receiver[ForceServerWrite]) && UndoManager.AddEvent({ - redo: () => receiver[prop] = value, - undo: () => { - // console.log("Undo: " + prop + " = " + curValue); // bcz: uncomment to log undo - receiver[prop] = curValue; - } - }); + !receiver[Initializing] && (!receiver[UpdatingFromServer] || receiver[ForceServerWrite]) && + UndoManager.AddEvent({ + redo: () => receiver[prop] = value, + undo: () => receiver[prop] = curValue, + prop: prop?.toString() + }); return true; } return false; @@ -191,7 +190,8 @@ let HierarchyMapping: Map<symbol, number> | undefined; function getEffectiveAcl(target: any, user?: string): symbol { const targetAcls = target[AclSym]; const userChecked = user || Doc.CurrentUserEmail; // if the current user is the author of the document / the current user is a member of the admin group - if (userChecked === (target.__fields?.author || target.author)) return AclAdmin; // target may be a Doc of Proxy, so check __fields.author and .author + const targetAuthor = (target.__fields?.author || target.author); // target may be a Doc of Proxy, so check __fields.author and .author + if (userChecked === targetAuthor || !targetAuthor) return AclAdmin; if (SnappingManager.GetCachedGroupByName("Admin")) return AclAdmin; if (targetAcls && Object.keys(targetAcls).length) { @@ -405,7 +405,8 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any ind !== -1 && receiver[prop].splice(ind, 1); }); lastValue = ObjectField.MakeCopy(receiver[prop]); - }) + }), + prop: "" } : diff?.op === "$remFromSet" ? { @@ -423,7 +424,8 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any ind !== -1 && receiver[prop].indexOf(item.value ? item.value() : item) === -1 && receiver[prop].splice(ind, 0, item); }); lastValue = ObjectField.MakeCopy(receiver[prop]); - } + }, + prop: "" } : { redo: () => { @@ -434,7 +436,8 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any // console.log("undo list: " + prop, receiver[prop]) // bcz: uncomment to log undo receiver[prop] = ObjectField.MakeCopy(prevValue as List<any>); lastValue = ObjectField.MakeCopy(receiver[prop]); - } + }, + prop: "" }); } target[Update](op); diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx index 404e828ea..652804126 100644 --- a/src/mobile/MobileInterface.tsx +++ b/src/mobile/MobileInterface.tsx @@ -17,7 +17,7 @@ import { Docs, DocumentOptions, DocUtils } from '../client/documents/Documents'; import { DocumentType } from "../client/documents/DocumentTypes"; import { CurrentUserUtils } from '../client/util/CurrentUserUtils'; import { Scripting } from '../client/util/Scripting'; -import { SettingsManager } from '../client/util/SettingsManager'; +import { SettingsManager, ColorScheme } from '../client/util/SettingsManager'; import { Transform } from '../client/util/Transform'; import { UndoManager } from "../client/util/UndoManager"; import { TabDocView } from '../client/views/collections/TabDocView'; @@ -403,7 +403,7 @@ export class MobileInterface extends React.Component { const freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions); const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: `Dashboard ${dashboardCount}` }, id, "row"); - const toggleTheme = ScriptField.MakeScript(`self.darkScheme = !self.darkScheme`); + const toggleTheme = ScriptField.MakeScript(`self.colorScheme = self.colorScheme ? undefined: ${ColorScheme.Dark}}`); const toggleComic = ScriptField.MakeScript(`toggleComicMode()`); const cloneDashboard = ScriptField.MakeScript(`cloneDashboard()`); dashboardDoc.contextMenuScripts = new List<ScriptField>([toggleTheme!, toggleComic!, cloneDashboard!]); diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index a617571ae..7b83d09ef 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -56,12 +56,12 @@ export namespace DashUploadUtils { const size = "content-length"; const type = "content-type"; - const { imageFormats, videoFormats, applicationFormats, audioFormats } = AcceptableMedia; + const { imageFormats, videoFormats, applicationFormats, audioFormats } = AcceptableMedia; //TODO:glr export function uploadYoutube(videoId: string): Promise<Upload.FileResponse> { console.log("UPLOAD " + videoId); return new Promise<Upload.FileResponse<Upload.FileInformation>>((res, rej) => { - exec('/usr/local/bin/youtube-dl -o ' + (videoId + ".mp4") + ' https://www.youtube.com/watch?v=' + videoId + ' -f `/usr/local/bin/youtube-dl https://www.youtube.com/watch?v=' + videoId + ' -F | grep "(best)" | sed -e "s/ .*//"`', + exec('youtube-dl -o ' + (videoId + ".mp4") + ' https://www.youtube.com/watch?v=' + videoId + ' -f "best[filesize<50M]"', (error: any, stdout: any, stderr: any) => { if (error) console.log(`error: ${error.message}`); else if (stderr) console.log(`stderr: ${stderr}`); @@ -242,18 +242,23 @@ export namespace DashUploadUtils { const { headers } = (await new Promise<any>((resolve, reject) => { request.head(resolvedUrl, (error, res) => error ? reject(error) : resolve(res)); }).catch(error => console.error(error))); - // Compute the native width and height ofthe image with an npm module - const { width: nativeWidth, height: nativeHeight }: RequestedImageSize = await requestImageSize(resolvedUrl); - // Bundle up the information into an object - return { - source, - contentSize: parseInt(headers[size]), - contentType: headers[type], - nativeWidth, - nativeHeight, - filename, - ...results - }; + try { + // Compute the native width and height ofthe image with an npm module + const { width: nativeWidth, height: nativeHeight } = await requestImageSize(resolvedUrl); + // Bundle up the information into an object + return { + source, + contentSize: parseInt(headers[size]), + contentType: headers[type], + nativeWidth, + nativeHeight, + filename, + ...results + }; + } catch (e) { + console.log(e); + return e; + } }; /** diff --git a/src/server/index.ts b/src/server/index.ts index 9687c3b23..f8c32103b 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -16,6 +16,7 @@ import UserManager from './ApiManagers/UserManager'; import UtilManager from './ApiManagers/UtilManager'; import { GoogleCredentialsLoader, SSL } from './apis/google/CredentialsLoader'; import { GoogleApiServerUtils } from "./apis/google/GoogleApiServerUtils"; +import { DashSessionAgent } from "./DashSession/DashSessionAgent"; import { AppliedSessionAgent } from "./DashSession/Session/agents/applied_session_agent"; import { DashUploadUtils } from './DashUploadUtils'; import { Database } from './database'; @@ -23,7 +24,6 @@ import { Logger } from "./ProcessFactory"; import RouteManager, { Method, PublicHandler } from './RouteManager'; import RouteSubscriber from './RouteSubscriber'; import initializeServer, { resolvedPorts } from './server_Initialization'; -import { DashSessionAgent } from "./DashSession/DashSessionAgent"; export const AdminPriviliges: Map<string, boolean> = new Map(); export const onWindows = process.platform === "win32"; |