diff options
Diffstat (limited to 'src')
71 files changed, 2083 insertions, 784 deletions
diff --git a/src/client/ClientRecommender.tsx b/src/client/ClientRecommender.tsx index 3f875057e..1d4653471 100644 --- a/src/client/ClientRecommender.tsx +++ b/src/client/ClientRecommender.tsx @@ -40,8 +40,6 @@ const fieldkey = "data"; @observer export class ClientRecommender extends React.Component<RecommenderProps> { - - static Instance: ClientRecommender; private mainDoc?: RecommenderDocument; private docVectors: Set<RecommenderDocument> = new Set(); diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 337c3e928..219890945 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -164,6 +164,7 @@ export class DocumentOptions { version?: string; // version identifier for a document label?: string; 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. toolTip?: string; // tooltip to display on hover @@ -171,7 +172,7 @@ export class DocumentOptions { description?: string; // added for links layout?: string | Doc; // default layout string for a document contentPointerEvents?: string; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents - childLimitHeight?: number; // whether to limit the height of colleciton children. 0 - means height can be no bigger than width + childLimitHeight?: number; // whether to limit the height of collection children. 0 - means height can be no bigger than width childLayoutTemplate?: Doc; // template for collection to use to render its children (see PresBox or Buxton layout in tree view) childLayoutString?: string; // template string for collection to use to render its children childDontRegisterViews?: boolean; @@ -248,6 +249,7 @@ export class DocumentOptions { treeViewHideHeaderFields?: boolean; // whether to hide the drop down options for tree view items. treeViewShowClearButton?: boolean; // whether a clear button should be displayed 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 treeViewExpandedView?: string; // which field/thing is displayed when this item is opened in tree view treeViewExpandedViewLock?: boolean; // whether the expanded view can be changed @@ -668,7 +670,7 @@ export namespace Docs { viewProps["acl-Override"] = "None"; viewProps["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Add; const viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewProps, true, true); - ![DocumentType.LINK, DocumentType.TEXTANCHOR, DocumentType.LABEL].includes(viewDoc.type as any) && DocUtils.MakeLinkToActiveAudio(viewDoc); + ![DocumentType.LINK, DocumentType.TEXTANCHOR, DocumentType.LABEL].includes(viewDoc.type as any) && DocUtils.MakeLinkToActiveAudio(() => viewDoc); !Doc.IsSystem(dataDoc) && ![DocumentType.HTMLANCHOR, DocumentType.KVP, DocumentType.LINK, DocumentType.LINKANCHOR, DocumentType.TEXTANCHOR].includes(proto.type as any) && !dataDoc.isFolder && !dataProps.annotationOn && Doc.AddDocToList(Cast(Doc.UserDoc().myFileOrphans, Doc, null), "data", dataDoc); @@ -702,8 +704,8 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.WEBCAM), "", options); } - export function ScreenshotDocument(url: string, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.SCREENSHOT), "", options); + export function ScreenshotDocument(title: string, options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.SCREENSHOT), "", { ...options, title }); } export function ComparisonDocument(options: DocumentOptions = { title: "Comparison Box" }) { @@ -947,7 +949,14 @@ export namespace DocUtils { } return false; } - export function FilterDocs(docs: Doc[], docFilters: string[], docRangeFilters: string[], viewSpecScript?: ScriptField) { + /** + * @param docs + * @param docFilters + * @param docRangeFilters + * @param viewSpecScript + * Given a list of docs and docFilters, @returns the list of Docs that match those filters + */ + export function FilterDocs(docs: Doc[], docFilters: string[], docRangeFilters: string[], viewSpecScript?: ScriptField, parentCollection?: Doc) { const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; if (!docFilters?.length && !docRangeFilters?.length) { return childDocs.filter(d => !d.cookies); // remove documents that need a cookie if there are no filters to provide one @@ -971,11 +980,20 @@ export namespace DocUtils { if (d.cookies && (!filterFacets.cookies || !Object.keys(filterFacets.cookies).some(key => d.cookies === key))) { return false; } + for (const facetKey of Object.keys(filterFacets).filter(fkey => fkey !== "cookies")) { const facet = filterFacets[facetKey]; + + // facets that match some value in the field of the document (e.g. some text field) const matches = Object.keys(facet).filter(value => value !== "cookies" && facet[value] === "match"); + + // facets that have a check next to them const checks = Object.keys(facet).filter(value => facet[value] === "check"); + + // 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; 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 satisfiesMatchFacets = !matches.length ? true : matches.some(value => { @@ -987,11 +1005,17 @@ export namespace DocUtils { } return Field.toString(d[facetKey] as Field).includes(value); }); - if (!satisfiesCheckFacets || !satisfiesMatchFacets || failsNotEqualFacets) { - return false; + // 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 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; } + } - return true; + return (parentCollection?.currentFilter as Doc)?.filterBoolean === "OR" ? false : true; }) : childDocs; const rangeFilteredDocs = filteredDocs.filter(d => { for (let i = 0; i < docRangeFilters.length; i += 3) { @@ -1046,10 +1070,10 @@ export namespace DocUtils { export let ActiveRecordings: { props: FieldViewProps, getAnchor: () => Doc }[] = []; - export function MakeLinkToActiveAudio(doc: Doc, broadcastEvent = true) { + export function MakeLinkToActiveAudio(getSourceDoc: () => Doc, broadcastEvent = true) { broadcastEvent && runInAction(() => DocumentManager.Instance.RecordingEvent = DocumentManager.Instance.RecordingEvent + 1); return DocUtils.ActiveRecordings.map(audio => - DocUtils.MakeLink({ doc: doc }, { doc: audio.getAnchor() || audio.props.Document }, "recording link", "recording timeline")); + DocUtils.MakeLink({ doc: getSourceDoc() }, { doc: audio.getAnchor() || audio.props.Document }, "recording link", "recording timeline")); } export function MakeLink(source: { doc: Doc }, target: { doc: Doc }, linkRelationship: string = "", description: string = "", id?: string, allowParCollectionLink?: boolean, showPopup?: number[]) { @@ -1317,7 +1341,7 @@ export namespace DocUtils { } } - export function LeavePushpin(doc: Doc) { + export function LeavePushpin(doc: Doc, annotationField: string) { if (doc.isPushpin) return undefined; const context = Cast(doc.context, Doc, null) ?? Cast(doc.annotationOn, Doc, null); const hasContextAnchor = DocListCast(doc.links). @@ -1330,7 +1354,7 @@ export namespace DocUtils { icon: "map-pin", x: Cast(doc.x, "number", null), y: Cast(doc.y, "number", null), backgroundColor: "#ACCEF7", _width: 15, _height: 15, _xPadding: 0, _isLinkButton: true, _timecodeToShow: Cast(doc._timecodeToShow, "number", null) }); - Doc.AddDocToList(context, Doc.LayoutFieldKey(context) + "-annotations", pushpin); + Doc.AddDocToList(context, annotationField, pushpin); const pushpinLink = DocUtils.MakeLink({ doc: pushpin }, { doc: doc }, "pushpin", ""); doc._timecodeToShow = undefined; return pushpin; diff --git a/src/client/util/CaptureManager.scss b/src/client/util/CaptureManager.scss new file mode 100644 index 000000000..71539ee1e --- /dev/null +++ b/src/client/util/CaptureManager.scss @@ -0,0 +1,175 @@ +@import "../views/globalCssVariables"; + +.capture-interface { + //background-color: whitesmoke !important; + width: 450px; + + button { + background: #315a96; + outline: none; + border-radius: 5px; + border: 0px; + color: #fcfbf7; + text-transform: uppercase; + letter-spacing: 2px; + font-size: 75%; + padding: 10px; + margin: 10px; + transition: transform 0.2s; + margin: 2px; + } +} + +.capture-t1 { + display: flex; + justify-content: left; + align-items: center; + font-size: 20px; + font-family: 'Roboto'; + font-weight: 600; + color: black; + + .recordButtonOutline { + border-radius: 100%; + width: 25px; + height: 25px; + margin-right: 10px; + border: solid 1px #a94442; + display: flex; + align-items: center; + justify-content: center; + } + + .recordButtonInner { + border-radius: 100%; + width: 70%; + height: 70%; + background: #a94442; + } +} + +.capture-t2 { + font-size: 12px; + padding-right: 15px; + color: black; + margin-top: 10px; + margin-bottom: 10px; + /* right: 135; */ + // position: absolute; + // left: 243; +} + +.capture-block { + display: block; + padding-bottom: 8px; + padding-top: 6px; + + .capture-block-title { + font-size: 16; + font-weight: bold; + text-align: left; + color: black; + width: 80; + margin-right: 50px; + margin-bottom: 5px; + } + + .capture-block-list { + height: 135px; + width: calc(100% + 15px); + overflow: scroll; + } + + .capture-block-radio { + font-size: 12; + display: block; + font-weight: normal; + + .radio-container { + display: flex; + justify-content: left; + align-items: center; + font-size: 13px; + font-family: 'Roboto'; + } + } + + .list-item { + display: flex; + height: 25px; + font-family: 'Roboto'; + font-size: 13px; + + .number { + width: 20px; + height: 20px; + display: flex; + justify-content: center; + align-items: center; + background-color: #BDDBE8; + border-radius: 100%; + font-weight: 800; + margin-right: 5px; + } + } + + .buttons { + display: flex; + position: absolute; + bottom: 0; + right: 15; + justify-content: flex-end; + align-items: center; + height: 60px; + + .save { + cursor: pointer; + width: 80px; + height: 40px; + font-size: 14px; + display: flex; + font-weight: bold; + justify-content: center; + align-items: center; + background: #337ab7; + color: whitesmoke; + border-radius: 5px; + text-transform: uppercase; + } + + .cancel { + cursor: pointer; + width: 80px; + height: 40px; + font-size: 14px; + display: flex; + font-weight: 100; + justify-content: center; + align-items: center; + background: #ccc; + color: black; + border-radius: 5px; + text-transform: uppercase; + margin-left: 10px; + } + } +} + +.close-button { + position: absolute; + top: 10; + right: 10; + background:transparent; + border-radius:100%; + width: 25px; + height: 25px; + display: flex; + align-items: center; + justify-content: center; + transition: 0.2s; +} + +.close-button:hover { + background: rgba(0,0,0,0.1); +} + diff --git a/src/client/util/CaptureManager.tsx b/src/client/util/CaptureManager.tsx new file mode 100644 index 000000000..c38337c91 --- /dev/null +++ b/src/client/util/CaptureManager.tsx @@ -0,0 +1,140 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, computed, observable, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import * as React from "react"; +import { convertToObject } from "typescript"; +import { Doc, DocListCast } from "../../fields/Doc"; +import { BoolCast, StrCast, Cast } from "../../fields/Types"; +import { addStyleSheet, addStyleSheetRule, Utils } from "../../Utils"; +import { LightboxView } from "../views/LightboxView"; +import { MainViewModal } from "../views/MainViewModal"; +import "./CaptureManager.scss"; +import { SelectionManager } from "./SelectionManager"; +import { undoBatch } from "./UndoManager"; +const higflyout = require("@hig/flyout"); +export const { anchorPoints } = higflyout; +export const Flyout = higflyout.default; + +@observer +export class CaptureManager extends React.Component<{}> { + public static Instance: CaptureManager; + static _settingsStyle = addStyleSheet(); + @observable _document: any; + @observable isOpen: boolean = false; // whether the CaptureManager is to be displayed or not. + + + constructor(props: {}) { + super(props); + CaptureManager.Instance = this; + } + + public close = action(() => this.isOpen = false); + public open = action((doc: Doc) => { + this.isOpen = true; + this._document = doc; + }); + + + @computed get visibilityContent() { + + return <div className="capture-block"> + <div className="capture-block-title">Visibility</div> + <div className="capture-block-radio"> + <div className="radio-container"> + <input type="radio" value="private" name="visibility" style={{ margin: 0, marginRight: 5 }} /> Private + </div> + <div className="radio-container"> + <input type="radio" value="public" name="visibility" style={{ margin: 0, marginRight: 5 }} /> Public + </div> + </div> + </div>; + } + + @computed get linksContent() { + const doc = this._document; + const order: JSX.Element[] = []; + if (doc) { + console.log('title', doc.title); + console.log('links', doc.links); + const linkDocs = DocListCast(doc.links); + const firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor1 as Doc, doc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, doc)); // link docs where 'doc' is anchor1 + const secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor2 as Doc, doc) || Doc.AreProtosEqual((linkDoc.anchor2 as Doc).annotationOn as Doc, doc)); // link docs where 'doc' is anchor2 + linkDocs.forEach((l, i) => { + if (l) { + console.log(i, (l.anchor1 as Doc).title); + console.log(i, (l.anchor2 as Doc).title); + order.push( + <div className="list-item"> + <div className="number">{i}</div> + {(l.anchor1 as Doc).title} + </div> + ); + } + }); + } + + return <div className="capture-block"> + <div className="capture-block-title">Links</div> + <div className="capture-block-list"> + {order} + </div> + </div>; + } + + @computed get closeButtons() { + return <div className="capture-block"> + <div className="buttons"> + <div className="save" onClick={() => { + LightboxView.SetLightboxDoc(this._document); + this.close(); + }}> + Save + </div> + <div className="cancel" onClick={() => { + const selected = SelectionManager.Views().slice(); + SelectionManager.DeselectAll(); + selected.map(dv => dv.props.removeDocument?.(dv.props.Document)); + this.close(); + }}> + Cancel + </div> + </div> + </div> + } + + + + private get captureInterface() { + return <div className="capture-interface"> + <div className="capture-t1"> + <div className="recordButtonOutline" style={{}}> + <div className="recordButtonInner" style={{}}> + </div> + </div> + Conversation Capture + </div> + <div className="capture-t2"> + + </div> + {this.visibilityContent} + {this.linksContent} + <div className="close-button" onClick={this.close}> + <FontAwesomeIcon icon={"times"} color="black" size={"lg"} /> + </div> + {this.closeButtons} + </div>; + + } + + render() { + return <MainViewModal + contents={this.captureInterface} + isDisplayed={this.isOpen} + interactive={true} + closeOnExternalClick={this.close} + dialogueBoxStyle={{ width: "500px", height: "350px", border: "none", background: "whitesmoke" }} + overlayStyle={{ background: "black" }} + overlayDisplayedOpacity={0.6} + /> + } +}
\ No newline at end of file diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 0dc8cc5c0..14bb87e89 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -423,7 +423,7 @@ export class CurrentUserUtils { ((doc.emptyScript as Doc).proto as Doc)["dragFactory-count"] = 0; } if (doc.emptyScreenshot === undefined) { - doc.emptyScreenshot = Docs.Create.ScreenshotDocument("", { _fitWidth: true, _width: 400, _height: 200, title: "screen snapshot", system: true, cloneFieldFilter: new List<string>(["system"]) }); + doc.emptyScreenshot = Docs.Create.ScreenshotDocument("empty screenshot", { _fitWidth: true, _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"]) }); @@ -516,13 +516,13 @@ export class CurrentUserUtils { static async menuBtnDescriptions(doc: Doc) { return [ { title: "Dashboards", target: Cast(doc.myDashboards, Doc, null), icon: "desktop", click: 'selectMainMenu(self)' }, - { title: "Recently Closed", target: Cast(doc.myRecentlyClosedDocs, Doc, null), icon: "archive", click: 'selectMainMenu(self)' }, + { title: "My Files", target: Cast(doc.myFilesystem, Doc, null), icon: "file", click: 'selectMainMenu(self)' }, + { title: "Tools", target: Cast(doc.myTools, Doc, null), icon: "wrench", click: 'selectMainMenu(self)' }, { title: "Import", target: Cast(doc.myImportPanel, Doc, null), icon: "upload", click: 'selectMainMenu(self)' }, + { title: "Recently Closed", target: Cast(doc.myRecentlyClosedDocs, Doc, null), icon: "archive", click: 'selectMainMenu(self)' }, { title: "Sharing", target: Cast(doc.mySharedDocs, Doc, null), icon: "users", click: 'selectMainMenu(self)', watchedDocuments: doc.mySharedDocs as Doc }, - { title: "Tools", target: Cast(doc.myTools, Doc, null), icon: "wrench", click: 'selectMainMenu(self)' }, - { title: "Filter", target: Cast(doc.myFilter, Doc, null), icon: "filter", click: 'selectMainMenu(self)' }, + // { title: "Filter", target: Cast(doc.currentFilter, Doc, null), icon: "filter", click: 'selectMainMenu(self)' }, { title: "Pres. Trails", target: Cast(doc.myPresentations, Doc, null), icon: "pres-trail", click: 'selectMainMenu(self)' }, - { title: "My Files", target: Cast(doc.myFilesystem, Doc, null), icon: "file", 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)' }, @@ -808,20 +808,19 @@ export class CurrentUserUtils { } } static setupFilterDocs(doc: Doc) { - // setup Recently Closed library item - doc.myFilter === undefined; - if (doc.myFilter === undefined) { - doc.myFilter = new PrefetchProxy(Docs.Create.FilterDocument({ - title: "FilterDoc", + // setup Filter item + if (doc.currentFilter === undefined) { + doc.currentFilter = Docs.Create.FilterDocument({ + 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 - })); + _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true, _autoHeight: true, _fitWidth: true + }); + const clearAll = ScriptField.MakeScript(`getProto(self).data = new List([])`); + (doc.currentFilter as Doc).contextMenuScripts = new List<ScriptField>([clearAll!]); + (doc.currentFilter as Doc).contextMenuLabels = new List<string>(["Clear All"]); + (doc.currentFilter as Doc).filterBoolean = "AND"; } - const clearAll = ScriptField.MakeScript(`getProto(self).data = new List([]); scriptContext._docFilters = scriptContext._docRangeFilters = undefined;`, { scriptContext: Doc.name }); - (doc.myFilter as any as Doc).contextMenuScripts = new List<ScriptField>([clearAll!]); - (doc.myFilter as any as Doc).contextMenuLabels = new List<string>(["Clear All"]); - } static setupUserDoc(doc: Doc) { @@ -853,7 +852,7 @@ export class CurrentUserUtils { CurrentUserUtils.setupPresentations(doc); CurrentUserUtils.setupFilesystem(doc); CurrentUserUtils.setupRecentlyClosedDocs(doc); - CurrentUserUtils.setupFilterDocs(doc); + // CurrentUserUtils.setupFilterDocs(doc); CurrentUserUtils.setupUserDoc(doc); } @@ -1019,6 +1018,8 @@ export class CurrentUserUtils { doc["constants-snapThreshold"] = NumCast(doc["constants-snapThreshold"], 10); // doc["constants-dragThreshold"] = NumCast(doc["constants-dragThreshold"], 4); // Utils.DRAG_THRESHOLD = NumCast(doc["constants-dragThreshold"]); + doc.savedFilters = new List<Doc>(); + 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); @@ -1170,10 +1171,10 @@ export class CurrentUserUtils { CurrentUserUtils.openDashboard(userDoc, copy); } - public static createNewDashboard = (userDoc: Doc, id?: string) => { - const myPresentations = userDoc.myPresentations as Doc; + 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 = Cast(userDoc.myDashboards, Doc) as Doc; + const dashboards = await Cast(userDoc.myDashboards, Doc) as Doc; const dashboardCount = DocListCast(dashboards.data).length + 1; const emptyPane = Cast(userDoc.emptyPane, Doc, null); emptyPane["dragFactory-count"] = NumCast(emptyPane["dragFactory-count"]) + 1; @@ -1246,3 +1247,5 @@ Scripting.addGlobal(function links(doc: any) { return new List(LinkManager.Insta "returns all the links to the document or its annotations", "(doc: any)"); Scripting.addGlobal(function importDocument() { return CurrentUserUtils.importDocument(); }, "imports files from device directly into the import sidebar"); +Scripting.addGlobal(function toggleComicMode() { Doc.UserDoc().renderStyle = Doc.UserDoc().renderStyle === "comic" ? undefined : "comic"; }, + "toggle between regular rendeing and an informal sketch/comic style"); diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index c1235163b..d8c2f913e 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -207,7 +207,7 @@ export namespace DragManager { ) { const addAudioTag = (dropDoc: any) => { dropDoc && !dropDoc.creationDate && (dropDoc.creationDate = new DateField); - dropDoc instanceof Doc && DocUtils.MakeLinkToActiveAudio(dropDoc); + dropDoc instanceof Doc && DocUtils.MakeLinkToActiveAudio(() => dropDoc); return dropDoc; }; const finishDrag = (e: DragCompleteEvent) => { diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx index 68af67a3b..62d656c5d 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -1,5 +1,5 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable, runInAction } from "mobx"; +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import * as React from "react"; import Select from 'react-select'; @@ -284,8 +284,7 @@ export class GroupManager extends React.Component<{}> { placeholder="Group name" onChange={action(() => this.buttonColour = this.inputRef.current?.value ? "black" : "#979797")} /> <Select - isMulti={true} - isSearchable={true} + isMulti options={this.options} onChange={this.handleChange} placeholder={"Select users"} diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index c52127edd..ca5ef75d2 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -66,7 +66,7 @@ export namespace SelectionManager { manager.SelectSchemaView(colSchema, document); } - const IsSelectedCache = computedFn(function isSelected(doc: DocumentView) { // wraapping get() in a computedFn only generates mobx() invalidations when the return value of the function for the specific get parameters has changed + 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 return manager.SelectedViews.get(doc) ? true : false; }); // computed functions, such as used in IsSelected generate errors if they're called outside of a diff --git a/src/client/util/SettingsManager.scss b/src/client/util/SettingsManager.scss index 5ca54517c..d8342ea56 100644 --- a/src/client/util/SettingsManager.scss +++ b/src/client/util/SettingsManager.scss @@ -75,8 +75,8 @@ width: 130; color: black; border-radius: 5px; - padding:7px; - + padding: 7px; + } } @@ -169,7 +169,7 @@ } } -.prefs-content{ +.prefs-content { text-align: left; } @@ -210,6 +210,7 @@ .preferences-font-controls { display: flex; justify-content: space-between; + width: 130%; } .font-select { @@ -429,11 +430,12 @@ font-size: 16px; font-weight: bold; margin-bottom: 16px; - } + } - .tab-column-title, .tab-column-content { - padding-left: 16px; - } + .tab-column-title, + .tab-column-content { + padding-left: 16px; + } } @@ -462,4 +464,4 @@ .settings-interface .settings-heading { font-size: 25; } -} +}
\ No newline at end of file diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index ded56d1da..5a247730e 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -7,7 +7,7 @@ import Select from "react-select"; import * as RequestPromise from "request-promise"; import { AclAddonly, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, DataSym, Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc"; import { List } from "../../fields/List"; -import { Cast, StrCast } from "../../fields/Types"; +import { Cast, NumCast, StrCast } from "../../fields/Types"; import { distributeAcls, GetEffectiveAcl, normalizeEmail, SharingPermissions, TraceMobx } from "../../fields/util"; import { Utils } from "../../Utils"; import { DocServer } from "../DocServer"; @@ -78,7 +78,16 @@ export class SharingManager extends React.Component<{}> { @observable private showGroupOptions: boolean = false; // // whether to show groups as options when sharing (in the react-select component) private populating: boolean = false; // whether the list of users is populating or not @observable private layoutDocAcls: boolean = false; // whether the layout doc or data doc's acls are to be used - @observable private myDocAcls: boolean = false; + @observable private myDocAcls: boolean = false; // whether the My Docs checkbox is selected or not + + // maps acl symbols to SharingPermissions + private AclMap = new Map<symbol, string>([ + [AclPrivate, SharingPermissions.None], + [AclReadonly, SharingPermissions.View], + [AclAddonly, SharingPermissions.Add], + [AclEdit, SharingPermissions.Edit], + [AclAdmin, SharingPermissions.Admin] + ]); // private get linkVisible() { // return this.sharingDoc ? this.sharingDoc[PublicKey] !== SharingPermissions.None : false; @@ -154,6 +163,7 @@ export class SharingManager extends React.Component<{}> { }); } } + /** * Shares the document with a user. */ @@ -164,10 +174,16 @@ export class SharingManager extends React.Component<{}> { const myAcl = `acl-${Doc.CurrentUserEmailNormalized}`; const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document); - - // ! ensures it returns true if document has been shared successfully, false otherwise return !docs.map(doc => { doc.author === Doc.CurrentUserEmail && !doc[myAcl] && distributeAcls(myAcl, SharingPermissions.Admin, doc); + + if (permission === SharingPermissions.None) { + if (doc[acl] && doc[acl] !== SharingPermissions.None) doc.numUsersShared = NumCast(doc.numUsersShared, 1) - 1; + } + else { + if (!doc[acl] || doc[acl] === SharingPermissions.None) doc.numUsersShared = NumCast(doc.numUsersShared, 0) + 1; + } + distributeAcls(acl, permission as SharingPermissions, doc); if (permission !== SharingPermissions.None) return Doc.AddDocToList(sharingDoc, storage, doc); @@ -191,6 +207,14 @@ export class SharingManager extends React.Component<{}> { // ! ensures it returns true if document has been shared successfully, false otherwise return !docs.map(doc => { doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmailNormalized}`] && distributeAcls(`acl-${Doc.CurrentUserEmailNormalized}`, SharingPermissions.Admin, doc); + + if (permission === SharingPermissions.None) { + if (doc[acl] && doc[acl] !== SharingPermissions.None) doc.numGroupsShared = NumCast(doc.numGroupsShared, 1) - 1; + } + else { + if (!doc[acl] || doc[acl] === SharingPermissions.None) doc.numGroupsShared = NumCast(doc.numGroupsShared, 0) + 1; + } + distributeAcls(acl, permission as SharingPermissions, doc); if (group instanceof Doc) { @@ -400,20 +424,12 @@ export class SharingManager extends React.Component<{}> { } distributeOverCollection = (targetDoc?: Doc) => { - const AclMap = new Map<symbol, string>([ - [AclPrivate, SharingPermissions.None], - [AclReadonly, SharingPermissions.View], - [AclAddonly, SharingPermissions.Add], - [AclEdit, SharingPermissions.Edit], - [AclAdmin, SharingPermissions.Admin] - ]); - const target = targetDoc || this.targetDoc!; const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document); docs.forEach(doc => { for (const [key, value] of Object.entries(doc[AclSym])) { - distributeAcls(key, AclMap.get(value)! as SharingPermissions, target); + distributeAcls(key, this.AclMap.get(value)! as SharingPermissions, target); } }); } @@ -474,7 +490,8 @@ export class SharingManager extends React.Component<{}> { const targetDoc = docs[0]; // tslint:disable-next-line: no-unnecessary-callback-wrapper - const admin = this.myDocAcls ? Boolean(docs.length) : docs.map(doc => GetEffectiveAcl(doc)).every(acl => acl === AclAdmin); // if the user has admin access to all selected docs + const effectiveAcls = docs.map(doc => GetEffectiveAcl(doc)); + const admin = this.myDocAcls ? Boolean(docs.length) : effectiveAcls.every(acl => acl === AclAdmin); // users in common between all docs const commonKeys = intersection(...docs.map(doc => this.layoutDocAcls ? doc?.[AclSym] && Object.keys(doc[AclSym]) : doc?.[DataSym]?.[AclSym] && Object.keys(doc[DataSym][AclSym]))); @@ -538,7 +555,7 @@ export class SharingManager extends React.Component<{}> { <span className={"padding"}>Me</span> <div className="edit-actions"> <div className={"permissions-dropdown"}> - {targetDoc?.[`acl-${Doc.CurrentUserEmailNormalized}`]} + {effectiveAcls.every(acl => acl === effectiveAcls[0]) ? this.AclMap.get(effectiveAcls[0])! : "-multiple-"} </div> </div> </div> @@ -548,7 +565,7 @@ export class SharingManager extends React.Component<{}> { // the list of groups shared with const groupListMap: (Doc | { title: string })[] = groups.filter(({ title }) => docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(StrCast(title))}`) : true); - groupListMap.unshift({ title: "Public" }, { title: "Override" }); + groupListMap.unshift({ title: "Public" });//, { title: "Override" }); const groupListContents = groupListMap.map(group => { const groupKey = `acl-${StrCast(group.title)}`; const uniform = docs.every(doc => this.layoutDocAcls ? doc?.[AclSym]?.[groupKey] === docs[0]?.[AclSym]?.[groupKey] : doc?.[DataSym]?.[AclSym]?.[groupKey] === docs[0]?.[DataSym]?.[AclSym]?.[groupKey]); @@ -600,9 +617,10 @@ export class SharingManager extends React.Component<{}> { {<div className="share-container"> <div className="share-setup"> <Select - className={"user-search"} - placeholder={"Enter user or group name..."} + className="user-search" + placeholder="Enter user or group name..." isMulti + isSearchable closeMenuOnSelect={false} options={options} onChange={this.handleUsersChange} @@ -626,16 +644,10 @@ export class SharingManager extends React.Component<{}> { </div> <div className="acl-container"> - <div className="myDocs-acls"> - <input type="checkbox" onChange={action(() => this.myDocAcls = !this.myDocAcls)} checked={this.myDocAcls} /> <label>My Docs</label> - </div> {Doc.UserDoc().noviceMode ? (null) : <div className="layoutDoc-acls"> <input type="checkbox" onChange={action(() => this.layoutDocAcls = !this.layoutDocAcls)} checked={this.layoutDocAcls} /> <label>Layout</label> </div>} - <button className="distribute-button" onClick={() => this.distributeOverCollection()}> - Distribute - </button> </div> </div> } diff --git a/src/client/util/type_decls.d b/src/client/util/type_decls.d index 3b23d5d56..ac0bea46a 100644 --- a/src/client/util/type_decls.d +++ b/src/client/util/type_decls.d @@ -215,5 +215,6 @@ declare const Docs: { StackingDocument(documents: Doc[], options?: DocumentOptions): Doc; }; +declare function idToDoc(id:string):any; declare function assignDoc(doc:Doc, field:any, id:any):string; declare function d(...args:any[]):any; diff --git a/src/client/views/AntimodeMenu.scss b/src/client/views/AntimodeMenu.scss index c9b5e7658..a275901be 100644 --- a/src/client/views/AntimodeMenu.scss +++ b/src/client/views/AntimodeMenu.scss @@ -7,7 +7,7 @@ height: $antimodemenu-height; background: #323232; box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25); - border-radius: 0px 6px 6px 6px; + // border-radius: 0px 6px 6px 6px; z-index: 1001; display: flex; @@ -24,6 +24,16 @@ background-color: transparent; width: 35px; height: 35px; + padding: 5; + text-align: center; + display: flex; + justify-content: center; + align-items: center; + position: relative; + + .svg { + margin: 0; + } &.active { background-color: #121212; @@ -31,7 +41,7 @@ } .antimodeMenu-button:hover { - background-color: #121212; + background-color: rgba(0, 0, 0, 0.4); } .antimodeMenu-dragger { diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx index 5acb3e4c8..fe6d39ca4 100644 --- a/src/client/views/AntimodeMenu.tsx +++ b/src/client/views/AntimodeMenu.tsx @@ -140,7 +140,7 @@ export abstract class AntimodeMenu<T extends AntimodeMenuProps> extends React.Co left: this._left, top: this._top, opacity: this._opacity, transitionProperty: this._transitionProperty, transitionDuration: this._transitionDuration, transitionDelay: this._transitionDelay, position: this.Pinned ? "unset" : undefined }}> - <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: "20px" }} /> + {/* {this.getDragger} */} {buttons} </div> ); diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx index 1f7abf61a..d96de72e3 100644 --- a/src/client/views/ContextMenu.tsx +++ b/src/client/views/ContextMenu.tsx @@ -15,6 +15,7 @@ export class ContextMenu extends React.Component { @observable private _pageY: number = 0; @observable private _display: boolean = false; @observable private _searchString: string = ""; + @observable private _showSearch: boolean = false; // afaik displaymenu can be called before all the items are added to the menu, so can't determine in displayMenu what the height of the menu will be @observable private _yRelativeToTop: boolean = true; @observable selectedIndex = -1; @@ -137,11 +138,13 @@ export class ContextMenu extends React.Component { return y; } + @action - displayMenu = (x: number, y: number, initSearch = "") => { + displayMenu = (x: number, y: number, initSearch = "", showSearch = false) => { //maxX and maxY will change if the UI/font size changes, but will work for any amount //of items added to the menu + this._showSearch = showSearch; this._pageX = x; this._pageY = y; this._searchString = initSearch; @@ -215,11 +218,15 @@ export class ContextMenu extends React.Component { @computed get menuItems() { if (!this._searchString) { - return this._items.map(item => <ContextMenuItem {...item} key={item.description} closeMenu={this.closeMenu} />); + return this._items.map(item => <ContextMenuItem {...item} noexpand={this.itemsNeedSearch ? true : (item as any).noexpand} key={item.description} closeMenu={this.closeMenu} />); } return this.filteredViews; } + @computed get itemsNeedSearch() { + return this._showSearch ? 1 : this._items.reduce((p, mi) => p + ((mi as any).noexpand ? 1 : (mi as any).subitems?.length || 1), 0) > 15; + } + render() { if (!this._display) { return null; @@ -229,12 +236,13 @@ export class ContextMenu extends React.Component { const contents = ( <> - <span className={"search-icon"}> - <span className="icon-background"> - <FontAwesomeIcon icon="search" size="lg" /> - </span> - <input className="contextMenu-item contextMenu-description search" type="text" placeholder="Filter Menu..." value={this._searchString} onKeyDown={this.onKeyDown} onChange={this.onChange} autoFocus /> - </span> + {!this.itemsNeedSearch ? (null) : + <span className={"search-icon"}> + <span className="icon-background"> + <FontAwesomeIcon icon="search" size="lg" /> + </span> + <input className="contextMenu-item contextMenu-description search" type="text" placeholder="Filter Menu..." value={this._searchString} onKeyDown={this.onKeyDown} onChange={this.onChange} autoFocus /> + </span>} {this.menuItems} </> ); @@ -244,8 +252,7 @@ export class ContextMenu extends React.Component { <div className="contextMenu-cont" style={style} ref={measureRef}> {contents} </div> - ) - } + )} </Measure> ); } diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx index e63631161..6fe2abd21 100644 --- a/src/client/views/ContextMenuItem.tsx +++ b/src/client/views/ContextMenuItem.tsx @@ -4,7 +4,6 @@ import { observer } from "mobx-react"; import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { UndoManager } from "../util/UndoManager"; -import { NumberLiteralType } from "typescript"; export interface OriginalMenuProps { description: string; @@ -33,8 +32,8 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select constructor(props: ContextMenuProps | SubmenuProps) { super(props); - if ("subitems" in this.props) { - this.props.subitems?.forEach(i => this._items.push(i)); + if ((this.props as SubmenuProps).subitems) { + (this.props as SubmenuProps).subitems?.forEach(i => this._items.push(i)); } } @@ -107,7 +106,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select }}> {this._items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} />)} </div>; - if (!("noexpand" in this.props)) { + if (!(this.props as SubmenuProps).noexpand) { return <div className="contextMenu-inlineMenu"> {this._items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} />)} </div>; diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 5ff96ac8d..a878a7afb 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -124,8 +124,8 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: "number" })?.script.run({ self: this.rootDoc, this: this.layoutDoc, scale }).result as string || ""; }; divKeys.map((prop: string) => { - const p = (this.props as any)[prop] as string; - p && (style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer)); + const p = (this.props as any)[prop]; + typeof p === "string" && (style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer)); }); return style; } @@ -151,7 +151,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T if (toRemove.length !== 0) { const recent = Cast(Doc.UserDoc().myRecentlyClosedDocs, Doc) as Doc; toRemove.forEach(doc => { - leavePushpin && DocUtils.LeavePushpin(doc); + leavePushpin && DocUtils.LeavePushpin(doc, annotationKey ?? this.annotationKey); Doc.RemoveDocFromList(targetDataDoc, annotationKey ?? this.annotationKey, doc); doc.context = undefined; recent && Doc.AddDocToList(recent, "data", doc, undefined, true, true); @@ -182,7 +182,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T 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))) { + docs.find(doc => Doc.AreProtosEqual(doc, this.props.Document) && Doc.LayoutField(doc) === Doc.LayoutField(this.props.Document))) { return false; } const targetDataDoc = this.props.Document[DataSym]; @@ -216,7 +216,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T else { added.filter(doc => [AclAdmin, AclEdit].includes(GetEffectiveAcl(doc))).map(doc => { // only make a pushpin if we have acl's to edit the document this.props.layerProvider?.(doc, true); - DocUtils.LeavePushpin(doc); + //DocUtils.LeavePushpin(doc); doc._stayInCollection = undefined; doc.context = this.props.Document; if (annotationKey ?? this._annotationKey) Doc.GetProto(doc).annotationOn = this.props.Document; diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index ce7ff1bbd..24ca3a8fa 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -8,7 +8,7 @@ import { Document } from '../../fields/documentSchemas'; import { HtmlField } from '../../fields/HtmlField'; import { InkField } from "../../fields/InkField"; import { ScriptField } from '../../fields/ScriptField'; -import { Cast, NumCast } from "../../fields/Types"; +import { Cast, NumCast, StrCast } from "../../fields/Types"; import { GetEffectiveAcl } from '../../fields/util'; import { setupMoveUpEvents, emptyFunction, returnFalse } from "../../Utils"; import { Docs, DocUtils } from "../documents/Documents"; @@ -26,6 +26,7 @@ import { InkStrokeProperties } from './InkStrokeProperties'; import { LightboxView } from './LightboxView'; import { DocumentView } from "./nodes/DocumentView"; import React = require("react"); +import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; @observer export class DocumentDecorations extends React.Component<{ boundsLeft: number, boundsTop: number }, { value: string }> { @@ -83,7 +84,21 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b UndoManager.RunInBatch(() => titleFieldKey && SelectionManager.Views().forEach(d => { titleFieldKey === "title" && (d.dataDoc["title-custom"] = !this._accumulatedTitle.startsWith("-")); //@ts-ignore - Doc.SetInPlace(d.rootDoc, titleFieldKey, +this._accumulatedTitle == this._accumulatedTitle ? +this._accumulatedTitle : this._accumulatedTitle, true); + const titleField = (+this._accumulatedTitle === this._accumulatedTitle ? +this._accumulatedTitle : this._accumulatedTitle); + Doc.SetInPlace(d.rootDoc, titleFieldKey, titleField, true); + + if (d.rootDoc.syncLayoutFieldWithTitle) { + const title = titleField.toString(); + const curKey = Doc.LayoutFieldKey(d.rootDoc); + if (curKey !== title && d.dataDoc[title] === undefined) { + d.rootDoc.layout = FormattedTextBox.LayoutString(title); + setTimeout(() => { + const val = d.dataDoc[curKey]; + d.dataDoc[curKey] = undefined; + d.dataDoc[title] = val; + }); + } + } }), "title blur"); } } diff --git a/src/client/views/EditableView.scss b/src/client/views/EditableView.scss index e912bc85a..5dc0c1962 100644 --- a/src/client/views/EditableView.scss +++ b/src/client/views/EditableView.scss @@ -8,6 +8,8 @@ } .editableView-container-editing-oneLine { + width: 100%; + span { white-space: nowrap; overflow: hidden; diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 671c0c507..cbaa706e0 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -29,6 +29,7 @@ import { DocumentLinksButton } from "./nodes/DocumentLinksButton"; import { AnchorMenu } from "./pdf/AnchorMenu"; import { SearchBox } from "./search/SearchBox"; import { CurrentUserUtils } from "../util/CurrentUserUtils"; +import { SettingsManager } from "../util/SettingsManager"; const modifiers = ["control", "meta", "shift", "alt"]; type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise<KeyControlInfo>; @@ -135,6 +136,7 @@ export class KeyManager { // DictationManager.Controls.stop(); GoogleAuthenticationManager.Instance.cancel(); SharingManager.Instance.close(); + if (!GroupManager.Instance.isOpen) SettingsManager.Instance.close(); GroupManager.Instance.close(); CollectionFreeFormViewChrome.Instance?.clearKeepPrimitiveMode(); window.getSelection()?.empty(); diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx index e33b3b35e..ce36d9182 100644 --- a/src/client/views/LightboxView.tsx +++ b/src/client/views/LightboxView.tsx @@ -14,7 +14,7 @@ import { Transform } from '../util/Transform'; import { TabDocView } from './collections/TabDocView'; import "./LightboxView.scss"; import { DocumentView, ViewAdjustment } from './nodes/DocumentView'; -import { DefaultStyleProvider } from './StyleProvider'; +import { DefaultStyleProvider, wavyBorderPath } from './StyleProvider'; interface LightboxViewProps { PanelWidth: number; @@ -26,6 +26,8 @@ interface LightboxViewProps { export class LightboxView extends React.Component<LightboxViewProps> { @computed public static get LightboxDoc() { return this._doc; } + private static LightboxDocTemplate = () => LightboxView._layoutTemplate; + @observable private static _layoutTemplate: Opt<Doc>; @observable private static _doc: Opt<Doc>; @observable private static _docTarget: Opt<Doc>; @observable private static _docFilters: string[] = []; // filters @@ -35,7 +37,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { private static _future: Opt<Doc[]> = []; private static _docView: Opt<DocumentView>; static path: { doc: Opt<Doc>, target: Opt<Doc>, history: Opt<{ doc: Doc, target?: Doc }[]>, future: Opt<Doc[]>, saved: Opt<{ panX: Opt<number>, panY: Opt<number>, scale: Opt<number>, scrollTop: Opt<number> }> }[] = []; - @action public static SetLightboxDoc(doc: Opt<Doc>, target?: Doc, future?: Doc[]) { + @action public static SetLightboxDoc(doc: Opt<Doc>, target?: Doc, future?: Doc[], layoutTemplate?: Doc) { if (this.LightboxDoc && this.LightboxDoc !== doc && this._savedState) { this.LightboxDoc._panX = this._savedState.panX; this.LightboxDoc._panY = this._savedState.panY; @@ -62,6 +64,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { this._future = future.slice().sort((a, b) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow)).sort((a, b) => DocListCast(a.links).length - DocListCast(b.links).length); } this._doc = doc; + this._layoutTemplate = layoutTemplate; this._docTarget = target || doc; this._tourMap = DocListCast(doc?.links).map(link => { const opp = LinkManager.getOppositeAnchor(link, doc!); @@ -101,13 +104,13 @@ export class LightboxView extends React.Component<LightboxViewProps> { this._docFilters = (f => this._docFilters ? [this._docFilters.push(f) as any, this._docFilters][1] : [f])(`cookies:${cookie}:provide`); } } - public static AddDocTab = (doc: Doc, location: string) => { + public static AddDocTab = (doc: Doc, location: string, layoutTemplate?: Doc) => { SelectionManager.DeselectAll(); return LightboxView.SetLightboxDoc(doc, undefined, [...DocListCast(doc[Doc.LayoutFieldKey(doc)]), ...DocListCast(doc[Doc.LayoutFieldKey(doc) + "-annotations"]), ...(LightboxView._future ?? []) - ].sort((a: Doc, b: Doc) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow))); + ].sort((a: Doc, b: Doc) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow)), layoutTemplate); } docFilters = () => LightboxView._docFilters || []; addDocTab = LightboxView.AddDocTab; @@ -116,7 +119,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { const target = LightboxView._docTarget = LightboxView._future?.pop(); const targetDocView = target && DocumentManager.Instance.getLightboxDocumentView(target); if (targetDocView && target) { - const l = DocUtils.MakeLinkToActiveAudio(targetDocView.ComponentView?.getAnchor?.() || target).lastElement(); + const l = DocUtils.MakeLinkToActiveAudio(() => targetDocView.ComponentView?.getAnchor?.() || target).lastElement(); l && (Cast(l.anchor2, Doc, null).backgroundColor = "lightgreen"); targetDocView.focus(target, { originalTarget: target, willZoom: true, scale: 0.9 }); if (LightboxView._history?.lastElement().target !== target) LightboxView._history?.push({ doc, target }); @@ -214,7 +217,8 @@ export class LightboxView extends React.Component<LightboxViewProps> { left: this.leftBorder, top: this.topBorder, width: this.lightboxWidth(), - height: this.lightboxHeight() + height: this.lightboxHeight(), + clipPath: `path('${Doc.UserDoc().renderStyle === "comic" ? wavyBorderPath(this.lightboxWidth(), this.lightboxHeight()) : undefined}')` }}> <DocumentView ref={action((r: DocumentView | null) => { LightboxView._docView = r !== null ? r : undefined; @@ -229,6 +233,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { })} Document={LightboxView.LightboxDoc} DataDoc={undefined} + LayoutTemplate={LightboxView.LightboxDocTemplate} addDocument={undefined} fitContentsToDoc={this.fitToBox} isDocumentActive={returnFalse} @@ -282,7 +287,7 @@ interface LightboxTourBtnProps { export class LightboxTourBtn extends React.Component<LightboxTourBtnProps> { render() { return this.props.navBtn("50%", 0, 0, "chevron-down", - () => LightboxView.LightboxDoc && this.props.future()?.length ? "" : "none", e => { + () => LightboxView.LightboxDoc /*&& this.props.future()?.length*/ ? "" : "none", e => { e.stopPropagation(); this.props.stepInto(); }, diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss index 8ccb64744..3f04a0f3a 100644 --- a/src/client/views/MainView.scss +++ b/src/client/views/MainView.scss @@ -358,9 +358,6 @@ .mainView-libraryFlyout-out { transition: width .25s; box-shadow: rgb(156, 147, 150) 0.2vw 0.2vw 0.2vw; - .mainView-docButtons { - left: 0; - } } diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 204ec370f..e54d84a6c 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -17,6 +17,7 @@ import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, return import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; import { DocServer } from '../DocServer'; import { Docs, DocUtils } from '../documents/Documents'; +import { CaptureManager } from '../util/CaptureManager'; import { CurrentUserUtils } from '../util/CurrentUserUtils'; import { DocumentManager } from '../util/DocumentManager'; import { GroupManager } from '../util/GroupManager'; @@ -45,6 +46,7 @@ import { InkStrokeProperties } from './InkStrokeProperties'; import { LightboxView } from './LightboxView'; import { LinkMenu } from './linking/LinkMenu'; import "./MainView.scss"; +import "./collections/TreeView.scss"; import { AudioBox } from './nodes/AudioBox'; import { DocumentLinksButton } from './nodes/DocumentLinksButton'; import { DocumentView, DocumentViewProps, DocAfterFocusFunc } from './nodes/DocumentView'; @@ -60,7 +62,7 @@ import { AnchorMenu } from './pdf/AnchorMenu'; import { PreviewCursor } from './PreviewCursor'; import { PropertiesView } from './PropertiesView'; import { SearchBox } from './search/SearchBox'; -import { DefaultStyleProvider, StyleProp } from './StyleProvider'; +import { DefaultStyleProvider, DashboardStyleProvider, StyleProp } from './StyleProvider'; const _global = (window /* browser */ || global /* node */) as any; @observer @@ -166,7 +168,8 @@ export class MainView extends React.Component { fa.faWindowMinimize, fa.faWindowRestore, fa.faTextWidth, fa.faTextHeight, fa.faClosedCaptioning, fa.faInfoCircle, fa.faTag, fa.faSyncAlt, fa.faPhotoVideo, 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.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); this.initAuthenticationRouters(); } @@ -272,7 +275,11 @@ 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.menuPanelWidth() + this.propertiesWidth()}px)` }}> + style={{ + minWidth: `calc(100% - ${this._flyoutWidth + this.menuPanelWidth() + this.propertiesWidth()}px)`, + transform: LightboxView.LightboxDoc ? "scale(0.0001)" : undefined, + width: `calc(100% - ${this._flyoutWidth + this.menuPanelWidth() + this.propertiesWidth()}px)` + }}> {!this.mainContainer ? (null) : this.mainDocView} </div>; } @@ -619,6 +626,7 @@ export class MainView extends React.Component { <DictationOverlay /> <SharingManager /> <SettingsManager /> + <CaptureManager /> <GroupManager /> <GoogleAuthenticationManager /> <DocumentDecorations boundsLeft={this.leftOffset} boundsTop={this.topOffset} /> @@ -696,5 +704,4 @@ export class MainView extends React.Component { } } -Scripting.addGlobal(function selectMainMenu(doc: Doc, title: string) { MainView.Instance.selectMenu(doc); }); -Scripting.addGlobal(function toggleComicMode() { Doc.UserDoc().fontFamily = "Comic Sans MS"; Doc.UserDoc().renderStyle = Doc.UserDoc().renderStyle === "comic" ? undefined : "comic"; });
\ No newline at end of file +Scripting.addGlobal(function selectMainMenu(doc: Doc, title: string) { MainView.Instance.selectMenu(doc); });
\ No newline at end of file diff --git a/src/client/views/MainViewModal.tsx b/src/client/views/MainViewModal.tsx index 7f91c0079..55dee005d 100644 --- a/src/client/views/MainViewModal.tsx +++ b/src/client/views/MainViewModal.tsx @@ -34,7 +34,7 @@ export class MainViewModal extends React.Component<MainViewOverlayProps> { <div className="overlay" onClick={this.props?.closeOnExternalClick} style={{ - backgroundColor: "gainsboro", + backgroundColor: "black", ...(p.overlayStyle || {}), opacity: p.isDisplayed ? overlayOpacity : 0 }} diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss index 1365725cb..fa45a065d 100644 --- a/src/client/views/PropertiesView.scss +++ b/src/client/views/PropertiesView.scss @@ -157,6 +157,66 @@ } } + .propertiesView-filters { + //border-bottom: 1px solid black; + //padding: 8.5px; + + .propertiesView-filters-title { + font-weight: bold; + font-size: 12.5px; + padding: 4px; + display: flex; + color: white; + padding-left: 8px; + background-color: rgb(51, 51, 51); + + &:hover { + cursor: pointer; + } + + .propertiesView-filters-title-icon { + float: right; + justify-items: right; + align-items: flex-end; + margin-left: auto; + margin-right: 9px; + + &:hover { + cursor: pointer; + } + } + } + + .propertiesView-filters-content { + font-size: 10px; + padding: 10px; + margin-left: 5px; + // max-height: 40%;// overflow-y: scroll; + + .propertiesView-buttonContainer { + float: right; + display: flex; + + button { + width: 15; + height: 15; + padding: 0; + margin-top: -5; + } + } + + button { + width: 5; + height: 5; + } + + input { + width: 100%; + } + } + } + + .propertiesView-appearance { //border-bottom: 1px solid black; //padding: 8.5px; @@ -332,6 +392,7 @@ } } } + .propertiesView-fields { //border-bottom: 1px solid black; //padding: 8.5px; diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index c8ce8bfeb..bb0ad4c66 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -2,10 +2,10 @@ import React = require("react"); import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Checkbox, Tooltip } from "@material-ui/core"; import { intersection } from "lodash"; -import { action, computed, observable } from "mobx"; +import { action, autorun, computed, Lambda, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { ColorState, SketchPicker } from "react-color"; -import { AclAddonly, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, AclUnset, DataSym, Doc, Field, HeightSym, WidthSym } from "../../fields/Doc"; +import { AclAddonly, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, AclUnset, DataSym, Doc, Field, HeightSym, Opt, WidthSym } from "../../fields/Doc"; import { Id } from "../../fields/FieldSymbols"; import { InkField } from "../../fields/InkField"; import { ComputedField } from "../../fields/ScriptField"; @@ -29,6 +29,8 @@ 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; @@ -46,15 +48,14 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @computed get MAX_EMBED_HEIGHT() { return 200; } - @computed get selectedDoc() { return SelectionManager.SelectedSchemaDoc() || this.selectedDocumentView?.rootDoc; } + @computed get selectedDoc() { return SelectionManager.SelectedSchemaDoc() || this.selectedDocumentView?.rootDoc || CurrentUserUtils.ActiveDashboard; } @computed get selectedDocumentView() { if (SelectionManager.Views().length) return SelectionManager.Views()[0]; if (PresBox.Instance?._selectedArray.size) return DocumentManager.Instance.getDocumentView(PresBox.Instance.rootDoc); return undefined; } @computed get isPres(): boolean { - if (this.selectedDoc?.type === DocumentType.PRES) return true; - return false; + return this.selectedDoc?.type === DocumentType.PRES; } @computed get dataDoc() { return this.selectedDoc?.[DataSym]; } @@ -67,6 +68,13 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @observable openContexts: boolean = true; @observable openAppearance: boolean = true; @observable openTransform: boolean = true; + @observable openFilters: boolean = true; // should be false + + /** + * autorun to set up the filter doc of a collection if that collection has been selected and the filters panel is open + */ + private selectedDocListenerDisposer: Opt<Lambda>; + // @observable selectedUser: string = ""; // @observable addButtonPressed: boolean = false; @observable layoutDocAcls: boolean = false; @@ -81,6 +89,15 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @observable _controlBtn: boolean = false; @observable _lock: boolean = false; + componentDidMount() { + this.selectedDocListenerDisposer?.(); + this.selectedDocListenerDisposer = autorun(() => this.openFilters && this.selectedDoc && this.checkFilterDoc()); + } + + componentWillUnmount() { + this.selectedDocListenerDisposer?.(); + } + @computed get isInk() { return this.selectedDoc?.type === DocumentType.INK; } rtfWidth = () => { @@ -149,6 +166,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { rows.push(<div className="propertiesView-field" key={"newKeyValue"} style={{ marginTop: "3px" }}> <EditableView key="editableView" + oneLine contents={"add key:value or #tags"} height={13} fontSize={10} @@ -205,6 +223,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { rows.push(<div className="propertiesView-field" key={"newKeyValue"} style={{ marginTop: "3px" }}> <EditableView key="editableView" + oneLine contents={"add key:value or #tags"} height={13} fontSize={10} @@ -400,7 +419,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { const showAdmin = effectiveAcls.every(acl => acl === AclAdmin); // users in common between all docs - const commonKeys = intersection(...docs.map(doc => this.layoutDocAcls ? doc?.[AclSym] && Object.keys(doc[AclSym]) : doc?.[DataSym][AclSym] && Object.keys(doc[DataSym][AclSym]))); + const commonKeys: string[] = intersection(...docs.map(doc => this.layoutDocAcls ? doc?.[AclSym] && Object.keys(doc[AclSym]) : doc?.[DataSym][AclSym] && Object.keys(doc[DataSym][AclSym]))); const tableEntries = []; @@ -417,7 +436,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { const ownerSame = Doc.CurrentUserEmail !== target.author && docs.filter(doc => doc).every(doc => doc.author === docs[0].author); // shifts the current user, owner, public to the top of the doc. - tableEntries.unshift(this.sharingItem("Override", showAdmin, docs.filter(doc => doc).every(doc => doc["acl-Override"] === docs[0]["acl-Override"]) ? (AclMap.get(target[AclSym]?.["acl-Override"]) || "None") : "-multiple-")); + // tableEntries.unshift(this.sharingItem("Override", showAdmin, docs.filter(doc => doc).every(doc => doc["acl-Override"] === docs[0]["acl-Override"]) ? (AclMap.get(target[AclSym]?.["acl-Override"]) || "None") : "-multiple-")); tableEntries.unshift(this.sharingItem("Public", showAdmin, docs.filter(doc => doc).every(doc => doc["acl-Public"] === docs[0]["acl-Public"]) ? (AclMap.get(target[AclSym]?.["acl-Public"]) || SharingPermissions.None) : "-multiple-")); tableEntries.unshift(this.sharingItem("Me", showAdmin, docs.filter(doc => doc).every(doc => doc.author === Doc.CurrentUserEmail) ? "Owner" : effectiveAcls.every(acl => acl === effectiveAcls[0]) ? AclMap.get(effectiveAcls[0])! : "-multiple-", !ownerSame)); if (ownerSame) tableEntries.unshift(this.sharingItem(StrCast(target.author), showAdmin, "Owner")); @@ -444,13 +463,15 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { const titles = new Set<string>(); SelectionManager.Views().forEach(dv => titles.add(StrCast(dv.rootDoc.title))); const title = Array.from(titles.keys()).length > 1 ? "--multiple selected--" : StrCast(this.selectedDoc?.title); - return <div className="editable-title"><EditableView - key="editableView" - contents={title} - height={25} - fontSize={14} - GetValue={() => title} - SetValue={this.setTitle} /> </div>; + return <div className="editable-title"> + <EditableView + key="editableView" + contents={title} + height={25} + fontSize={14} + GetValue={() => title} + SetValue={this.setTitle} /> + </div>; } @undoBatch @@ -514,8 +535,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { } } - - @computed get controlPointsButton() { const formatInstance = InkStrokeProperties.Instance; @@ -831,6 +850,232 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div>; } + @computed get optionsSubMenu() { + return <div className="propertiesView-settings" onPointerEnter={action(() => this.inOptions = true)} + onPointerLeave={action(() => this.inOptions = false)}> + <div className="propertiesView-settings-title" + onPointerDown={action(() => this.openOptions = !this.openOptions)} + style={{ backgroundColor: this.openOptions ? "black" : "" }}> + Options + <div className="propertiesView-settings-title-icon"> + <FontAwesomeIcon icon={this.openOptions ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + {!this.openOptions ? (null) : + <div className="propertiesView-settings-content"> + <PropertiesButtons /> + </div>} + </div>; + } + + @computed get sharingSubMenu() { + return <div className="propertiesView-sharing"> + <div className="propertiesView-sharing-title" + onPointerDown={action(() => this.openSharing = !this.openSharing)} + style={{ backgroundColor: this.openSharing ? "black" : "" }}> + Sharing {"&"} Permissions + <div className="propertiesView-sharing-title-icon"> + <FontAwesomeIcon icon={this.openSharing ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + {!this.openSharing ? (null) : + <div className="propertiesView-sharing-content"> + <div className="propertiesView-buttonContainer"> + {!Doc.UserDoc().noviceMode ? (<div className="propertiesView-acls-checkbox"> + <Checkbox + color="primary" + onChange={action(() => this.layoutDocAcls = !this.layoutDocAcls)} + checked={this.layoutDocAcls} + /> + <div className="propertiesView-acls-checkbox-text">Layout</div> + </div>) : (null)} + {/* <Tooltip title={<><div className="dash-tooltip">{"Re-distribute sharing settings"}</div></>}> + <button onPointerDown={() => SharingManager.Instance.distributeOverCollection(this.selectedDoc!)}> + <FontAwesomeIcon icon="redo-alt" color="white" size="1x" /> + </button> + </Tooltip> */} + </div> + {this.sharingTable} + </div>} + </div>; + } + + /** + * Checks if a currentFilter (FilterDoc) exists on the current collection (if the Properties Panel + Filters submenu are open). + * If it doesn't exist, it creates it. + */ + checkFilterDoc() { + if (this.selectedDoc.type === DocumentType.COL && !this.selectedDoc.currentFilter) CurrentUserUtils.setupFilterDocs(this.selectedDoc); + } + + /** + * Creates a new currentFilter for this.filterDoc, + */ + createNewFilterDoc = () => { + const currentDocFilters = this.selectedDoc._docFilters; + const currentDocRangeFilters = this.selectedDoc._docRangeFilters; + this.selectedDoc._docFilters = new List<string>(); + this.selectedDoc._docRangeFilters = new List<string>(); + (this.selectedDoc.currentFilter as Doc)._docFiltersList = currentDocFilters; + (this.selectedDoc.currentFilter as Doc)._docRangeFiltersList = currentDocRangeFilters; + this.selectedDoc.currentFilter = undefined; + CurrentUserUtils.setupFilterDocs(this.selectedDoc); + } + + /** + * Updates this.filterDoc's currentFilter and saves the docFilters on the currentFilter + */ + updateFilterDoc = (doc: Doc) => { + if (doc === this.selectedDoc.currentFilter) return; // causes problems if you try to reapply the same doc + const savedDocFilters = doc._docFiltersList; + const currentDocFilters = this.selectedDoc._docFilters; + this.selectedDoc._docFilters = new List<string>(); + (this.selectedDoc.currentFilter as Doc)._docFiltersList = currentDocFilters; + this.selectedDoc.currentFilter = doc; + doc._docFiltersList = new List<string>(); + this.selectedDoc._docFilters = savedDocFilters; + + const savedDocRangeFilters = doc._docRangeFiltersList; + const currentDocRangeFilters = this.selectedDoc._docRangeFilters; + this.selectedDoc._docRangeFilters = new List<string>(); + (this.selectedDoc.currentFilter as Doc)._docRangeFiltersList = currentDocRangeFilters; + this.selectedDoc.currentFilter = doc; + doc._docRangeFiltersList = new List<string>(); + this.selectedDoc._docRangeFilters = savedDocRangeFilters; + } + + @computed get filtersSubMenu() { + return !(this.selectedDoc?.currentFilter instanceof Doc) ? (null) : <div className="propertiesView-filters"> + <div className="propertiesView-filters-title" + onPointerDown={action(() => this.openFilters = !this.openFilters)} + style={{ backgroundColor: this.openFilters ? "black" : "" }}> + Filters + <div className="propertiesView-filters-title-icon"> + <FontAwesomeIcon icon={this.openFilters ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + { + !this.openFilters ? (null) : + <div className="propertiesView-filters-content" style={{ height: this.selectedDoc.currentFilter[HeightSym]() + 15 }}> + <DocumentView + Document={this.selectedDoc.currentFilter} + DataDoc={undefined} + addDocument={undefined} + addDocTab={returnFalse} + pinToPres={emptyFunction} + rootSelected={returnTrue} + removeDocument={returnFalse} + ScreenToLocalTransform={this.getTransform} + PanelWidth={() => this.props.width} + PanelHeight={this.selectedDoc.currentFilter[HeightSym]} + renderDepth={0} + scriptContext={this.selectedDoc.currentFilter} + focus={emptyFunction} + styleProvider={DefaultStyleProvider} + isContentActive={returnTrue} + whenChildContentsActiveChanged={emptyFunction} + bringToFront={emptyFunction} + docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + createNewFilterDoc={this.createNewFilterDoc} + updateFilterDoc={this.updateFilterDoc} + docViewPath={returnEmptyDoclist} + layerProvider={undefined} + dontCenter="y" + /> + </div> + } + </div >; + } + + @computed get inkSubMenu() { + return <> + {!this.isInk ? (null) : + <div className="propertiesView-appearance"> + <div className="propertiesView-appearance-title" + onPointerDown={action(() => this.openAppearance = !this.openAppearance)} + style={{ backgroundColor: this.openAppearance ? "black" : "" }}> + Appearance + <div className="propertiesView-appearance-title-icon"> + <FontAwesomeIcon icon={this.openAppearance ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + {!this.openAppearance ? (null) : + <div className="propertiesView-appearance-content"> + {this.appearanceEditor} + </div>} + </div>} + + {this.isInk ? <div className="propertiesView-transform"> + <div className="propertiesView-transform-title" + onPointerDown={action(() => this.openTransform = !this.openTransform)} + style={{ backgroundColor: this.openTransform ? "black" : "" }}> + Transform + <div className="propertiesView-transform-title-icon"> + <FontAwesomeIcon icon={this.openTransform ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + {this.openTransform ? <div className="propertiesView-transform-content"> + {this.transformEditor} + </div> : null} + </div> : null} + </>; + } + + @computed get fieldsSubMenu() { + return <div className="propertiesView-fields"> + <div className="propertiesView-fields-title" + onPointerDown={action(() => this.openFields = !this.openFields)} + style={{ backgroundColor: this.openFields ? "black" : "" }}> + Fields {"&"} Tags + <div className="propertiesView-fields-title-icon"> + <FontAwesomeIcon icon={this.openFields ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + {!Doc.UserDoc().noviceMode && this.openFields ? <div className="propertiesView-fields-checkbox"> + {this.fieldsCheckbox} + <div className="propertiesView-fields-checkbox-text">Layout</div> + </div> : null} + {!this.openFields ? (null) : + <div className="propertiesView-fields-content"> + {Doc.UserDoc().noviceMode ? this.noviceFields : this.expandedField} + </div>} + </div>; + } + + @computed get contextsSubMenu() { + return <div className="propertiesView-contexts"> + <div className="propertiesView-contexts-title" + onPointerDown={action(() => this.openContexts = !this.openContexts)} + style={{ backgroundColor: this.openContexts ? "black" : "" }}> + Contexts + <div className="propertiesView-contexts-title-icon"> + <FontAwesomeIcon icon={this.openContexts ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + {this.openContexts ? <div className="propertiesView-contexts-content" >{this.contexts}</div> : null} + </div>; + } + + @computed get layoutSubMenu() { + return <div className="propertiesView-layout"> + <div className="propertiesView-layout-title" + onPointerDown={action(() => this.openLayout = !this.openLayout)} + style={{ backgroundColor: this.openLayout ? "black" : "" }}> + Layout + <div className="propertiesView-layout-title-icon"> + <FontAwesomeIcon icon={this.openLayout ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + {this.openLayout ? <div className="propertiesView-layout-content" >{this.layoutPreview}</div> : null} + </div>; + } + + + /** * Handles adding and removing members from the sharing panel */ @@ -851,8 +1096,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div>; } else { - const novice = Doc.UserDoc().noviceMode; - if (this.selectedDoc && !this.isPres) { return <div className="propertiesView" style={{ width: this.props.width, @@ -865,121 +1108,19 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <div className="propertiesView-name"> {this.editableTitle} </div> - <div className="propertiesView-settings" onPointerEnter={action(() => this.inOptions = true)} - onPointerLeave={action(() => this.inOptions = false)}> - <div className="propertiesView-settings-title" - onPointerDown={action(() => this.openOptions = !this.openOptions)} - style={{ backgroundColor: this.openOptions ? "black" : "" }}> - Options - <div className="propertiesView-settings-title-icon"> - <FontAwesomeIcon icon={this.openOptions ? "caret-down" : "caret-right"} size="lg" color="white" /> - </div> - </div> - {!this.openOptions ? (null) : - <div className="propertiesView-settings-content"> - <PropertiesButtons /> - </div>} - </div> - <div className="propertiesView-sharing"> - <div className="propertiesView-sharing-title" - onPointerDown={action(() => this.openSharing = !this.openSharing)} - style={{ backgroundColor: this.openSharing ? "black" : "" }}> - Sharing {"&"} Permissions - <div className="propertiesView-sharing-title-icon"> - <FontAwesomeIcon icon={this.openSharing ? "caret-down" : "caret-right"} size="lg" color="white" /> - </div> - </div> - {!this.openSharing ? (null) : - <div className="propertiesView-sharing-content"> - <div className="propertiesView-buttonContainer"> - {!novice ? (<div className="propertiesView-acls-checkbox"> - <Checkbox - color="primary" - onChange={action(() => this.layoutDocAcls = !this.layoutDocAcls)} - checked={this.layoutDocAcls} - /> - <div className="propertiesView-acls-checkbox-text">Layout</div> - </div>) : (null)} - <Tooltip title={<><div className="dash-tooltip">{"Re-distribute sharing settings"}</div></>}> - <button onPointerDown={() => SharingManager.Instance.distributeOverCollection(this.selectedDoc!)}> - <FontAwesomeIcon icon="redo-alt" color="white" size="1x" /> - </button> - </Tooltip> - </div> - {this.sharingTable} - </div>} - </div> + {this.optionsSubMenu} - {!this.isInk ? (null) : - <div className="propertiesView-appearance"> - <div className="propertiesView-appearance-title" - onPointerDown={action(() => this.openAppearance = !this.openAppearance)} - style={{ backgroundColor: this.openAppearance ? "black" : "" }}> - Appearance - <div className="propertiesView-appearance-title-icon"> - <FontAwesomeIcon icon={this.openAppearance ? "caret-down" : "caret-right"} size="lg" color="white" /> - </div> - </div> - {!this.openAppearance ? (null) : - <div className="propertiesView-appearance-content"> - {this.appearanceEditor} - </div>} - </div>} + {this.sharingSubMenu} - {this.isInk ? <div className="propertiesView-transform"> - <div className="propertiesView-transform-title" - onPointerDown={action(() => this.openTransform = !this.openTransform)} - style={{ backgroundColor: this.openTransform ? "black" : "" }}> - Transform - <div className="propertiesView-transform-title-icon"> - <FontAwesomeIcon icon={this.openTransform ? "caret-down" : "caret-right"} size="lg" color="white" /> - </div> - </div> - {this.openTransform ? <div className="propertiesView-transform-content"> - {this.transformEditor} - </div> : null} - </div> : null} + {this.filtersSubMenu} - <div className="propertiesView-fields"> - <div className="propertiesView-fields-title" - onPointerDown={action(() => this.openFields = !this.openFields)} - style={{ backgroundColor: this.openFields ? "black" : "" }}> - Fields {"&"} Tags - <div className="propertiesView-fields-title-icon"> - <FontAwesomeIcon icon={this.openFields ? "caret-down" : "caret-right"} size="lg" color="white" /> - </div> - </div> - {!novice && this.openFields ? <div className="propertiesView-fields-checkbox"> - {this.fieldsCheckbox} - <div className="propertiesView-fields-checkbox-text">Layout</div> - </div> : null} - {!this.openFields ? (null) : - <div className="propertiesView-fields-content"> - {novice ? this.noviceFields : this.expandedField} - </div>} - </div> - <div className="propertiesView-contexts"> - <div className="propertiesView-contexts-title" - onPointerDown={action(() => this.openContexts = !this.openContexts)} - style={{ backgroundColor: this.openContexts ? "black" : "" }}> - Contexts - <div className="propertiesView-contexts-title-icon"> - <FontAwesomeIcon icon={this.openContexts ? "caret-down" : "caret-right"} size="lg" color="white" /> - </div> - </div> - {this.openContexts ? <div className="propertiesView-contexts-content" >{this.contexts}</div> : null} - </div> - <div className="propertiesView-layout"> - <div className="propertiesView-layout-title" - onPointerDown={action(() => this.openLayout = !this.openLayout)} - style={{ backgroundColor: this.openLayout ? "black" : "" }}> - Layout - <div className="propertiesView-layout-title-icon"> - <FontAwesomeIcon icon={this.openLayout ? "caret-down" : "caret-right"} size="lg" color="white" /> - </div> - </div> - {this.openLayout ? <div className="propertiesView-layout-content" >{this.layoutPreview}</div> : null} - </div> + {this.inkSubMenu} + + {this.fieldsSubMenu} + + {this.contextsSubMenu} + + {this.layoutSubMenu} </div>; } if (this.isPres) { diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx index 6c3eb1e95..bff4c95fc 100644 --- a/src/client/views/SidebarAnnos.tsx +++ b/src/client/views/SidebarAnnos.tsx @@ -113,8 +113,6 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { NativeHeight={returnZero} PanelHeight={this.panelHeight} PanelWidth={this.panelWidth} - xMargin={0} - yMargin={0} styleProvider={this.sidebarStyleProvider} docFilters={this.docFilters} scaleField={this.sidebarKey() + "-scale"} diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 2f0bbc4a8..96f9eab97 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -14,14 +14,18 @@ import { CollectionViewType } from './collections/CollectionView'; import { MainView } from './MainView'; import { DocumentViewProps } from "./nodes/DocumentView"; import "./StyleProvider.scss"; +import "./collections/TreeView.scss"; +import "./nodes/FilterBox.scss"; import React = require("react"); import Color = require('color'); +import { FieldViewProps } from './nodes/FieldView'; export enum StyleLayers { Background = "background" } export enum StyleProp { + TreeViewIcon = "treeViewIcon", DocContents = "docContents", // when specified, the JSX returned will replace the normal rendering of the document view Opacity = "opacity", // opacity of the document view Hidden = "hidden", // whether the document view should not be isplayed @@ -37,6 +41,8 @@ export enum StyleProp { HeaderMargin = "headerMargin", // margin at top of documentview, typically for displaying a title -- doc contents will start below that TitleHeight = "titleHeight", // Height of Title area ShowTitle = "showTitle", // whether to display a title on a Document (optional :hover suffix) + JitterRotation = "jitterRotation", // whether documents should be randomly rotated + BorderPath = "customBorder", // border path for document view } function darkScheme() { return BoolCast(CurrentUserUtils.ActiveDashboard?.darkScheme); } @@ -56,21 +62,28 @@ export function testDocProps(toBeDetermined: any): toBeDetermined is DocumentVie return (toBeDetermined?.isContentActive) ? toBeDetermined : undefined; } -// +export function wavyBorderPath(pw: number, ph: number, inset: number = 0.05) { + return `M ${pw * .5} ${ph * inset} C ${pw * .6} ${ph * inset} ${pw * (1 - 2 * inset)} 0 ${pw * (1 - inset)} ${ph * inset} C ${pw} ${ph * (2 * inset)} ${pw * (1 - inset)} ${ph * .25} ${pw * (1 - inset)} ${ph * .3} C ${pw * (1 - inset)} ${ph * .4} ${pw} ${ph * (1 - 2 * inset)} ${pw * (1 - inset)} ${ph * (1 - inset)} C ${pw * (1 - 2 * inset)} ${ph} ${pw * .6} ${ph * (1 - inset)} ${pw * .5} ${ph * (1 - inset)} C ${pw * .3} ${ph * (1 - inset)} ${pw * (2 * inset)} ${ph} ${pw * inset} ${ph * (1 - inset)} C 0 ${ph * (1 - 2 * inset)} ${pw * inset} ${ph * .8} ${pw * inset} ${ph * .75} C ${pw * inset} ${ph * .7} 0 ${ph * (2 * inset)} ${pw * inset} ${ph * inset} C ${pw * (2 * inset)} 0 ${pw * .25} ${ph * inset} ${pw * .5} ${ph * inset}`; +} + // a preliminary implementation of a dash style sheet for setting rendering properties of documents nested within a Tab // export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any { const docProps = testDocProps(props) ? props : undefined; + const fieldKey = (props as any)?.fieldKey ? (props as any).fieldKey + "-" : ""; const selected = property.includes(":selected"); const isCaption = property.includes(":caption"); const isAnchor = property.includes(":anchor"); const isAnnotated = property.includes(":annotated"); + const isOpen = property.includes(":open"); + const comicStyle = () => doc && !Doc.IsSystem(doc) && Doc.UserDoc().renderStyle === "comic"; const isBackground = () => StrListCast(doc?._layerTags).includes(StyleLayers.Background); const backgroundCol = () => props?.styleProvider?.(doc, props, StyleProp.BackgroundColor); const opacity = () => props?.styleProvider?.(doc, props, StyleProp.Opacity); const showTitle = () => props?.styleProvider?.(doc, props, StyleProp.ShowTitle); - + const random = (min: number, max: number, x: number, y: number) => { /* min should not be equal to max */ return min + ((Math.abs(x * y) * 9301 + 49297) % 233280 / 233280) * (max - min); } 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.Opacity: return Cast(doc?._opacity, "number", Cast(doc?.opacity, "number", null)); @@ -79,21 +92,24 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps !Doc.IsSystem(doc) && doc.type === DocumentType.RTF ? (doc.author === Doc.CurrentUserEmail ? StrCast(Doc.UserDoc().showTitle) : "author;creationDate") : "") || ""; case StyleProp.Color: + const docColor: Opt<string> = StrCast(doc?.[fieldKey + "color"], StrCast(doc?._color)); + if (docColor) return docColor; if (isCaption) return "white"; - const backColor = backgroundCol() || "black"; + const backColor = backgroundCol();// || (darkScheme() ? "black" : "white"); + if (!backColor) return undefined; const col = Color(backColor).rgb(); const colsum = (col.red() + col.green() + col.blue()); if (colsum / col.alpha() > 400 || col.alpha() < 0.25) return "black"; return "white"; case StyleProp.Hidden: return BoolCast(doc?._hidden); - case StyleProp.BorderRounding: return StrCast(doc?._borderRounding); + case StyleProp.BorderRounding: return StrCast(doc?.[fieldKey + "borderRounding"], StrCast(doc?._borderRounding)); case StyleProp.TitleHeight: return 15; + case StyleProp.BorderPath: return comicStyle() && props?.renderDepth ? { path: wavyBorderPath(props?.PanelWidth?.() || 0, props?.PanelHeight?.() || 0), fill: wavyBorderPath(props?.PanelWidth?.() || 0, props?.PanelHeight?.() || 0, .08), width: 3 } : { path: undefined, width: 0 }; + case StyleProp.JitterRotation: return comicStyle() ? random(-1, 1, NumCast(doc?.x), NumCast(doc?.y)) * ((props?.PanelWidth() || 0) > (props?.PanelHeight() || 0) ? 5 : 10) : 0; case StyleProp.HeaderMargin: return ([CollectionViewType.Stacking, CollectionViewType.Masonry].includes(doc?._viewType as any) || doc?.type === DocumentType.RTF) && showTitle() && !StrCast(doc?.showTitle).includes(":hover") ? 15 : 0; case StyleProp.BackgroundColor: { - if (isCaption) return "rgba(0,0,0 ,0.4)"; - if (Doc.UserDoc().renderStyle === "comic") return "transparent"; - let docColor: Opt<string> = StrCast(doc?._backgroundColor); + let docColor: Opt<string> = StrCast(doc?.[fieldKey + "backgroundColor"], StrCast(doc?._backgroundColor, isCaption ? "rbga(0,0,0,0.4)" : "")); if (MainView.Instance.LastButton === doc) return darkScheme() ? "dimgrey" : "lightgrey"; switch (doc?.type) { case DocumentType.PRESELEMENT: docColor = docColor || (darkScheme() ? "" : ""); break; @@ -124,7 +140,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps Doc.UserDoc().activeCollectionBackground)); break; //if (doc._viewType !== CollectionViewType.Freeform && doc._viewType !== CollectionViewType.Time) return "rgb(62,62,62)"; - default: docColor = darkScheme() ? "black" : "white"; break; + default: docColor = docColor || (darkScheme() ? "black" : "white"); break; } if (docColor && (!doc || props?.layerProvider?.(doc) === false)) docColor = Color(docColor.toLowerCase()).fade(0.5).toString(); return docColor; @@ -158,6 +174,8 @@ 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)) ? @@ -169,6 +187,60 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps } } + +function toggleHidden(e: React.MouseEvent, doc: Doc) { + UndoManager.RunInBatch(() => runInAction(() => { + e.stopPropagation(); + doc.hidden = doc.hidden ? undefined : true; + }), "toggleHidden"); +} + +function toggleLock(e: React.MouseEvent, doc: Doc) { + UndoManager.RunInBatch(() => runInAction(() => { + e.stopPropagation(); + doc.lockedPosition = doc.lockedPosition ? undefined : true; + }), "toggleHidden"); +} + +/** + * add lock and hide button decorations for the "Dashboards" flyout TreeView + */ +export function DashboardStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps | DocumentViewProps>, property: string) { + switch (property.split(":")[0]) { + case StyleProp.Decorations: + if (doc) { + const hidden = doc.hidden; + const locked = doc.lockedPosition; + return doc._viewType === CollectionViewType.Docking || (Doc.IsSystem(doc) && Doc.UserDoc().noviceMode) ? (null) : + <> + <div className={`styleProvider-treeView-hide${hidden ? "-active" : ""}`} onClick={(e) => toggleHidden(e, doc)}> + <FontAwesomeIcon icon={hidden ? "eye-slash" : "eye"} size="sm" /> + </div> + <div className={`styleProvider-treeView-lock${locked ? "-active" : ""}`} onClick={(e) => toggleLock(e, doc)}> + <FontAwesomeIcon icon={locked ? "lock" : "unlock"} size="sm" /> + </div> + </>; + } + default: return DefaultStyleProvider(doc, props, property); + + } +} + +function changeFilterBool(e: any, doc: Doc) { + UndoManager.RunInBatch(() => runInAction(() => { + //e.stopPropagation(); + //doc.lockedPosition = doc.lockedPosition ? undefined : true; + }), "changeFilterBool"); +} + +function closeFilter(e: React.MouseEvent, doc: Doc) { + UndoManager.RunInBatch(() => runInAction(() => { + e.stopPropagation(); + //doc.lockedPosition = doc.lockedPosition ? undefined : true; + }), "closeFilter"); +} + + // // a preliminary semantic-"layering/grouping" mechanism for determining interactive properties of documents // currently, the provider tests whether the docuemnt's layer field matches the activeLayer field of the tab. diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx index b2ae441d6..2251b60e5 100644 --- a/src/client/views/collections/CollectionCarousel3DView.tsx +++ b/src/client/views/collections/CollectionCarousel3DView.tsx @@ -13,6 +13,7 @@ import { DragManager } from '../../util/DragManager'; import { DocumentView } from '../nodes/DocumentView'; import "./CollectionCarousel3DView.scss"; import { CollectionSubView } from './CollectionSubView'; +import { StyleProp } from '../StyleProvider'; type Carousel3DDocument = makeInterface<[typeof documentSchema, typeof collectionSchema]>; const Carousel3DDocument = makeInterface(documentSchema, collectionSchema); @@ -40,11 +41,8 @@ export class CollectionCarousel3DView extends CollectionSubView(Carousel3DDocume @computed get content() { const currentIndex = NumCast(this.layoutDoc._itemIndex); const displayDoc = (childPair: { layout: Doc, data: Doc }) => { - const script = ScriptField.MakeScript("this._showCaption = 'caption'", { this: Doc.name }); - const onChildClick = script && (() => script); - return <DocumentView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} + return <DocumentView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "childLayoutTemplate", "childLayoutString"]).omit} onDoubleClick={this.onChildDoubleClick} - onClick={onChildClick} renderDepth={this.props.renderDepth + 1} LayoutTemplate={this.props.childLayoutTemplate} LayoutTemplateString={this.props.childLayoutString} @@ -52,7 +50,6 @@ export class CollectionCarousel3DView extends CollectionSubView(Carousel3DDocume DataDoc={childPair.data} PanelWidth={this.panelWidth} PanelHeight={this.panelHeight} - ScreenToLocalTransform={this.props.ScreenToLocalTransform} bringToFront={returnFalse} />; }; @@ -104,27 +101,6 @@ export class CollectionCarousel3DView extends CollectionSubView(Carousel3DDocume }, 1500); } - _downX = 0; - _downY = 0; - onPointerDown = (e: React.PointerEvent) => { - this._downX = e.clientX; - this._downY = e.clientY; - document.addEventListener("pointerup", this.onpointerup); - } - private _lastTap: number = 0; - private _doubleTap = false; - onpointerup = (e: PointerEvent) => { - this._doubleTap = (Date.now() - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2); - this._lastTap = Date.now(); - } - - onClick = (e: React.MouseEvent) => { - if (this._doubleTap) { - e.stopPropagation(); - this.props.Document.isLightboxOpen = true; - } - } - @computed get buttons() { if (!this.props.isContentActive()) return null; return <div className="arrow-buttons" > @@ -167,7 +143,11 @@ export class CollectionCarousel3DView extends CollectionSubView(Carousel3DDocume const index = NumCast(this.layoutDoc._itemIndex); const translateX = this.panelWidth() * (1 - index); - return <div className="collectionCarousel3DView-outer" onClick={this.onClick} onPointerDown={this.onPointerDown} ref={this.createDashEventsTarget}> + return <div className="collectionCarousel3DView-outer" ref={this.createDashEventsTarget} + style={{ + background: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor), + color: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color), + }} > <div className="carousel-wrapper" style={{ transform: `translateX(${translateX}px)` }}> {this.content} </div> diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss index a9a1898f5..8660113cd 100644 --- a/src/client/views/collections/CollectionCarouselView.scss +++ b/src/client/views/collections/CollectionCarouselView.scss @@ -1,13 +1,10 @@ .collectionCarouselView-outer { - background: gray; height : 100%; .collectionCarouselView-caption { - margin-left: 10%; - margin-right: 10%; height: 50; display: inline-block; - width: 80%; + width: 100%; } .collectionCarouselView-image { height: calc(100% - 50px); @@ -19,13 +16,17 @@ .carouselView-back, .carouselView-fwd { position: absolute; display: flex; - top: 50%; + top: 42.5%; width: 30; - height: 30; + height: 15%; align-items: center; border-radius: 5px; justify-content: center; - background : rgba(255, 255, 255, 0.46); + color: rgba(255,255,255,0.5); + background : rgba(0,0,0, 0.1); + &:hover { + color:white; + } } .carouselView-fwd { right: 0; diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index cc90b9134..cd81a99c9 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -2,17 +2,17 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { computed } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc } from '../../../fields/Doc'; +import { Doc, Opt } from '../../../fields/Doc'; import { collectionSchema, documentSchema } from '../../../fields/documentSchemas'; import { makeInterface } from '../../../fields/Schema'; import { NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { OmitKeys, returnFalse } from '../../../Utils'; import { DragManager } from '../../util/DragManager'; -import { DocumentView } from '../nodes/DocumentView'; +import { DocumentView, DocumentViewProps } from '../nodes/DocumentView'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import { StyleProp } from '../StyleProvider'; import "./CollectionCarouselView.scss"; -import { CollectionSubView } from './CollectionSubView'; +import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; type CarouselDocument = makeInterface<[typeof documentSchema, typeof collectionSchema]>; const CarouselDocument = makeInterface(documentSchema, collectionSchema); @@ -38,77 +38,70 @@ export class CollectionCarouselView extends CollectionSubView(CarouselDocument) e.stopPropagation(); this.layoutDoc._itemIndex = (NumCast(this.layoutDoc._itemIndex) - 1 + this.childLayoutPairs.length) % this.childLayoutPairs.length; } - panelHeight = () => this.props.PanelHeight() - 50; + captionStyleProvider = (doc: (Doc | undefined), props: Opt<DocumentViewProps>, property: string): any => { + const captionProps = { ...this.props, fieldKey: "caption" }; + return this.props.styleProvider?.(doc, props, property) || this.props.styleProvider?.(this.layoutDoc, captionProps, property); + } + panelHeight = () => this.props.PanelHeight() - (StrCast(this.layoutDoc._showCaption) ? 50 : 0); onContentDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick); onContentClick = () => ScriptCast(this.layoutDoc.onChildClick); @computed get content() { const index = NumCast(this.layoutDoc._itemIndex); const curDoc = this.childLayoutPairs?.[index]; + const captionProps = { ...this.props, fieldKey: "caption" }; + const marginX = NumCast(this.layoutDoc["caption-xMargin"]); + const marginY = NumCast(this.layoutDoc["caption-yMargin"]); return !(curDoc?.layout instanceof Doc) ? (null) : <> <div className="collectionCarouselView-image" key="image"> - <DocumentView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} + <DocumentView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "childLayoutTemplate", "childLayoutString"]).omit} onDoubleClick={this.onContentDoubleClick} onClick={this.onContentClick} renderDepth={this.props.renderDepth + 1} + ContainingCollectionView={this} LayoutTemplate={this.props.childLayoutTemplate} LayoutTemplateString={this.props.childLayoutString} Document={curDoc.layout} - DataDoc={curDoc.data} + DataDoc={curDoc.layout.resolvedDataDoc as Doc} PanelHeight={this.panelHeight} - ScreenToLocalTransform={this.props.ScreenToLocalTransform} bringToFront={returnFalse} /> </div> <div className="collectionCarouselView-caption" key="caption" style={{ - background: StrCast(this.layoutDoc._captionBackgroundColor, this.props.styleProvider?.(this.props.Document, this.props, StyleProp.BackgroundColor)), - color: StrCast(this.layoutDoc._captionColor, StrCast(this.layoutDoc.color)), - borderRadius: StrCast(this.layoutDoc._captionBorderRounding), + display: StrCast(this.layoutDoc._showCaption) ? undefined : "none", + borderRadius: this.props.styleProvider?.(this.layoutDoc, captionProps, StyleProp.BorderRounding), + marginRight: marginX, marginLeft: marginX, + width: `calc(100% - ${marginX * 2}px)` }}> - <FormattedTextBox key={index} {...this.props} - xMargin={NumCast(this.layoutDoc["_carousel-caption-xMargin"])} - yMargin={NumCast(this.layoutDoc["_carousel-caption-yMargin"])} - Document={curDoc.layout} DataDoc={undefined} fieldKey={"caption"} /> + <FormattedTextBox key={index} {...captionProps} + styleProvider={this.captionStyleProvider} + Document={curDoc.layout} DataDoc={undefined} + fontSize={NumCast(this.layoutDoc["caption-fontSize"])} + xPadding={NumCast(this.layoutDoc["caption-xPadding"])} + yPadding={NumCast(this.layoutDoc["caption-yPadding"])} /> </div> </>; } @computed get buttons() { return <> - <div key="back" className="carouselView-back" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} onClick={this.goback}> - <FontAwesomeIcon icon={"caret-left"} size={"2x"} /> + <div key="back" className="carouselView-back" onClick={this.goback}> + <FontAwesomeIcon icon={"chevron-left"} size={"2x"} /> </div> - <div key="fwd" className="carouselView-fwd" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} onClick={this.advance}> - <FontAwesomeIcon icon={"caret-right"} size={"2x"} /> + <div key="fwd" className="carouselView-fwd" onClick={this.advance}> + <FontAwesomeIcon icon={"chevron-right"} size={"2x"} /> </div> </>; } - _downX = 0; - _downY = 0; - onPointerDown = (e: React.PointerEvent) => { - this._downX = e.clientX; - this._downY = e.clientY; - document.addEventListener("pointerup", this.onpointerup); - } - private _lastTap: number = 0; - private _doubleTap = false; - onpointerup = (e: PointerEvent) => { - this._doubleTap = (Date.now() - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2); - this._lastTap = Date.now(); - } - - onClick = (e: React.MouseEvent) => { - if (this._doubleTap) { - e.stopPropagation(); - this.props.Document.isLightboxOpen = true; - } - } - render() { - return <div className="collectionCarouselView-outer" onClick={this.onClick} onPointerDown={this.onPointerDown} ref={this.createDashEventsTarget}> + return <div className="collectionCarouselView-outer" ref={this.createDashEventsTarget} + style={{ + background: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor), + color: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color), + }}> {this.content} - {!this.props.Document._chromeHidden ? (null) : this.buttons} + {this.props.Document._chromeHidden ? (null) : this.buttons} </div>; } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionMenu.scss b/src/client/views/collections/CollectionMenu.scss index 2c81d727e..dc5231a3a 100644 --- a/src/client/views/collections/CollectionMenu.scss +++ b/src/client/views/collections/CollectionMenu.scss @@ -14,35 +14,53 @@ top: 0; width: 100%; - .antimodeMenu-button { - padding: 0; - width: 30px; + .recordButtonOutline { + border-radius: 100%; + width: 18px; + height: 18px; + border: solid 1px #f5f5f5; display: flex; + align-items: center; + justify-content: center; + } - svg { - margin: auto; - } + .recordButtonInner { + border-radius: 100%; + width: 70%; + height: 70%; + background: white; } .collectionMenu { display: flex; - padding-bottom: 1px; - height: 32px; - border-bottom: .5px solid rgb(180, 180, 180); + height: 100%; overflow: visible; z-index: 901; border: unset; + .collectionMenu-divider { + height: 85%; + margin-left: 3px; + margin-right: 3px; + width: 1.5px; + background-color: #656060; + } + .collectionViewBaseChrome { display: flex; + align-items: center; .collectionViewBaseChrome-viewPicker { font-size: 75%; - background: #323232; outline-color: black; color: white; border: none; - border-right: solid gray 1px; + background: #323232; + } + + .collectionViewBaseChrome-viewPicker:focus { + outline: none; + border: none; } .collectionViewBaseChrome-viewPicker:active { @@ -65,18 +83,26 @@ margin-left: 3px; margin-right: 0px; font-size: 75%; - background: #323232; + text-transform: capitalize; color: white; border: none; - border-right: solid gray 1px; + background: #323232; + } + + .collectionViewBaseChrome-cmdPicker:focus { + border: none; + outline: none; } .commandEntry-outerDiv { pointer-events: all; - background-color: #323232; + background-color: transparent; display: flex; flex-direction: row; - height: 30px; + align-items: center; + justify-content: center; + height: 100%; + overflow: hidden; .commandEntry-drop { color: white; @@ -86,6 +112,15 @@ } } + .commandEntry-outerDiv:hover{ + background-color: rgba(0,0,0,0.2); + + .collectionViewBaseChrome-viewPicker, + .collectionViewBaseChrome-cmdPicker{ + background: rgb(41,41,41); + } + } + .collectionViewBaseChrome-collapse { transition: all .5s, opacity 0.3s; position: absolute; @@ -103,11 +138,12 @@ .collectionViewBaseChrome-template, .collectionViewBaseChrome-viewModes { - display: grid; - background: rgb(238, 238, 238); + align-items: center; + height: 100%; + display: flex; + background: transparent; color: grey; - margin-top: auto; - margin-bottom: auto; + justify-content: center; } .collectionViewBaseChrome-viewSpecs { @@ -263,27 +299,52 @@ .collectionTreeViewChrome-pivotField-cont, .collection3DCarouselViewChrome-scrollSpeed-cont { justify-self: right; + align-items: center; display: flex; + grid-auto-columns: auto; font-size: 75%; letter-spacing: 2px; .collectionStackingViewChrome-pivotField-label, .collectionTreeViewChrome-pivotField-label, .collection3DCarouselViewChrome-scrollSpeed-label { - vertical-align: center; - padding-left: 10px; - margin: auto; + grid-column: 1; + margin-right: 7px; + user-select: none; + font-family: 'Roboto'; + 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: rgba(0,0,0,0.2); } .collectionStackingViewChrome-pivotField, .collectionTreeViewChrome-pivotField, .collection3DCarouselViewChrome-scrollSpeed { color: white; - width: 100%; + grid-column: 2; + grid-row: 1; + width: 90%; min-width: 100px; display: flex; + height: 80%; + border-radius: 7px; align-items: center; - background: rgb(238, 238, 238); + background: #eeeeee; .editable-view-input, input, @@ -378,20 +439,13 @@ display: inline-flex; position: relative; align-items: center; - margin-left: 10px; - - .antimodeMenu-button { - text-align: center; - display: block; - position: relative; - } + height: 100%; .color-previewI { - width: 80%; - height: 20%; - bottom: 0; + width: 60%; + top: 80%; position: absolute; - margin-left: 2px; + height: 4px; } .color-previewII { diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index 623e05b33..ab4f197a6 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -34,6 +34,10 @@ 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"; @observer export class CollectionMenu extends AntimodeMenu<AntimodeMenuProps> { @@ -257,6 +261,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp case CollectionViewType.Schema: return (<CollectionSchemaViewChrome key="collchrome" {...this.props} />); case CollectionViewType.Tree: return (<CollectionTreeViewChrome key="collchrome" {...this.props} />); case CollectionViewType.Masonry: return (<CollectionStackingViewChrome key="collchrome" {...this.props} />); + case CollectionViewType.Carousel: case CollectionViewType.Carousel3D: return (<Collection3DCarouselViewChrome key="collchrome" {...this.props} />); case CollectionViewType.Grid: return (<CollectionGridViewChrome key="collchrome" {...this.props} />); case CollectionViewType.Docking: return (<CollectionDockingChrome key="collchrome" {...this.props} />); @@ -388,7 +393,29 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp return !targetDoc ? (null) : <Tooltip key="pin" title={<div className="dash-tooltip">{Doc.isDocPinned(targetDoc) ? "Unpin from presentation" : "Pin to presentation"}</div>} placement="top"> <button className="antimodeMenu-button" style={{ backgroundColor: isPinned ? "121212" : undefined, borderLeft: "1px solid gray" }} onClick={e => TabDocView.PinDoc(targetDoc, { unpin: isPinned })}> - <FontAwesomeIcon className="documentdecorations-icon" size="lg" icon="map-pin" /> + <FontAwesomeIcon className="colMenu-icon" size="lg" icon="map-pin" /> + </button> + </Tooltip>; + } + + @undoBatch + @action + startRecording = () => { + const doc = Docs.Create.ScreenshotDocument("screen recording", { _fitWidth: true, _width: 400, _height: 200, mediaState: "pendingRecording" }); + //Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), undefined, doc); + CollectionDockingView.AddSplit(doc, "right"); + } + + @computed + get recordButton() { + const targetDoc = this.selectedDoc; + return <Tooltip key="record" title={<div className="dash-tooltip">{"Capture screen"}</div>} placement="top"> + <button className="antimodeMenu-button" + onClick={e => this.startRecording()}> + <div className="recordButtonOutline" style={{}}> + <div className="recordButtonInner" style={{}}> + </div> + </div> </button> </Tooltip>; } @@ -478,7 +505,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp const targetDoc = this.selectedDoc; return !targetDoc || targetDoc.type === DocumentType.PRES ? (null) : <Tooltip title={<div className="dash-tooltip">{"Tap or Drag to create an alias"}</div>} placement="top"> <button className="antimodeMenu-button" onPointerDown={this.onAliasButtonDown} onClick={this.onAlias} style={{ cursor: "drag" }}> - <FontAwesomeIcon className="documentdecorations-icon" icon="copy" size="lg" /> + <FontAwesomeIcon className="colMenu-icon" icon="copy" size="lg" /> </button> </Tooltip>; } @@ -486,37 +513,47 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp @computed get lightboxButton() { const targetDoc = this.selectedDoc; return !targetDoc ? (null) : <Tooltip title={<div className="dash-tooltip">{"View in Lightbox"}</div>} placement="top"> - <button className="antimodeMenu-button" style={{ borderRight: "1px solid gray", justifyContent: 'center' }} onPointerDown={() => { + <button className="antimodeMenu-button" onPointerDown={() => { const docs = DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]); LightboxView.SetLightboxDoc(targetDoc, undefined, docs); }}> - <FontAwesomeIcon className="documentdecorations-icon" icon="desktop" size="lg" /> + <FontAwesomeIcon className="colMenu-icon" icon="desktop" size="lg" /> </button> </Tooltip>; } + @computed get toggleOverlayButton() { + return <> + <Tooltip title={<div className="dash-tooltip">Toggle Overlay Layer</div>} placement="bottom"> + <button className={"antimodeMenu-button"} key="float" + style={{ + backgroundColor: this.props.docView.layoutDoc.z ? "121212" : undefined, + pointerEvents: this.props.docView.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform ? "none" : undefined, + color: this.props.docView.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform ? "dimgrey" : undefined + }} + onClick={undoBatch(() => this.props.docView.props.CollectionFreeFormDocumentView?.().float())}> + <FontAwesomeIcon icon={["fab", "buffer"]} size={"lg"} /> + </button> + </Tooltip> + </> + } + render() { return ( <div className="collectionMenu-cont" > <div className="collectionMenu"> <div className="collectionViewBaseChrome"> + {this.notACollection || this.props.type === CollectionViewType.Invalid ? (null) : this.viewModes} + <div className="collectionMenu-divider" key="divider1"></div> {this.aliasButton} {/* {this.pinButton} */} + {this.toggleOverlayButton} {this.pinWithViewButton} - {this.lightboxButton} - <Tooltip title={<div className="dash-tooltip">Toggle Overlay Layer</div>} placement="bottom"> - <button className={"antimodeMenu-button"} key="float" - style={{ - backgroundColor: this.props.docView.layoutDoc.z ? "121212" : undefined, borderRight: "1px solid gray", - pointerEvents: this.props.docView.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform ? "none" : undefined, - color: this.props.docView.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform ? "dimgrey" : undefined - }} - onClick={undoBatch(() => this.props.docView.props.CollectionFreeFormDocumentView?.().float())}> - <FontAwesomeIcon icon={["fab", "buffer"]} size={"lg"} /> - </button> - </Tooltip> + <div className="collectionMenu-divider" key="divider2"></div> {this.subChrome} - {this.notACollection || this.props.type === CollectionViewType.Invalid ? (null) : this.viewModes} + <div className="collectionMenu-divider" key="divider3"></div> + {this.lightboxButton} + {this.recordButton} {!this._buttonizableCommands ? (null) : this.templateChrome} </div> </div> @@ -986,6 +1023,7 @@ export class Collection3DCarouselViewChrome extends React.Component<CollectionMe return ( <div className="collection3DCarouselViewChrome-cont"> <div className="collection3DCarouselViewChrome-scrollSpeed-cont"> + {FormattedTextBox.Focused ? <RichTextMenu /> : (null)} <div className="collectionStackingViewChrome-scrollSpeed-label"> AUTOSCROLL SPEED: </div> diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx index 6f6cdd5d2..6baf633dd 100644 --- a/src/client/views/collections/CollectionPileView.tsx +++ b/src/client/views/collections/CollectionPileView.tsx @@ -2,7 +2,7 @@ import { action, computed } from "mobx"; import { observer } from "mobx-react"; import { Doc, HeightSym, WidthSym } from "../../../fields/Doc"; import { NumCast, StrCast } from "../../../fields/Types"; -import { emptyFunction, setupMoveUpEvents } from "../../../Utils"; +import { emptyFunction, setupMoveUpEvents, returnTrue } from "../../../Utils"; import { DocUtils } from "../../documents/Documents"; import { SelectionManager } from "../../util/SelectionManager"; import { SnappingManager } from "../../util/SnappingManager"; @@ -31,9 +31,16 @@ export class CollectionPileView extends CollectionSubView(doc => doc) { // returns the contents of the pileup in a CollectionFreeFormView @computed get contents() { + const isStarburst = this.layoutEngine() === "starburst"; const draggingSelf = this.props.isSelected(); - return <div className="collectionPileView-innards" style={{ pointerEvents: this.layoutEngine() === "starburst" || (SnappingManager.GetIsDragging() && !draggingSelf) ? undefined : "none", zIndex: this.layoutEngine() === "starburst" && !SnappingManager.GetIsDragging() ? -10 : "auto" }} > - <CollectionFreeFormView {...this.props} layoutEngine={this.layoutEngine} + return <div className="collectionPileView-innards" + style={{ + pointerEvents: isStarburst || (SnappingManager.GetIsDragging() && !draggingSelf) ? undefined : "none", + zIndex: isStarburst && !SnappingManager.GetIsDragging() ? -10 : "auto" + }} > + <CollectionFreeFormView {...this.props} + layoutEngine={this.layoutEngine} + childDocumentsActive={isStarburst ? returnTrue : undefined} addDocument={undoBatch((doc: Doc | Doc[]) => { (doc instanceof Doc ? [doc] : doc).map((d) => DocUtils.iconify(d)); return this.props.addDocument?.(doc) || false; diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss index 19ad5cefa..e456c0664 100644 --- a/src/client/views/collections/CollectionStackedTimeline.scss +++ b/src/client/views/collections/CollectionStackedTimeline.scss @@ -62,6 +62,7 @@ .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 6a1242f20..ab9c4aa2c 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -234,8 +234,9 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument return level; } - dictationHeight = () => this.props.PanelHeight() / 3; - timelineContentHeight = () => this.props.PanelHeight() * 2 / 3; + dictationHeightPercent = 50; + dictationHeight = () => `${this.dictationHeightPercent}%`; + 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); @@ -348,7 +349,11 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> (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 ((isDictation || !Doc.AreProtosEqual(LightboxView.LightboxDoc, this.props.layoutDoc)) && DocListCast(this.props.mark.links).length && + 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 && 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])) { diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 8d8c69fd5..30f8e0112 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -55,10 +55,10 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, @computed get chromeHidden() { return this.props.chromeHidden || BoolCast(this.layoutDoc.chromeHidden); } @computed get columnHeaders() { return Cast(this.layoutDoc._columnHeaders, listSpec(SchemaHeaderField), null); } @computed get pivotField() { return StrCast(this.layoutDoc._pivotField); } - @computed get filteredChildren() { return this.childLayoutPairs.filter(pair => pair.layout instanceof Doc).map(pair => pair.layout); } + @computed get filteredChildren() { return this.childLayoutPairs.filter(pair => (pair.layout instanceof Doc) && !pair.layout.hidden).map(pair => pair.layout); } @computed get headerMargin() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin); } @computed get xMargin() { return NumCast(this.layoutDoc._xMargin, 2 * Math.min(this.gridGap, .05 * this.props.PanelWidth())); } - @computed get yMargin() { return this.props.yMargin || NumCast(this.layoutDoc._yMargin, 5); } // 2 * this.gridGap)); } + @computed get yMargin() { return this.props.yPadding || NumCast(this.layoutDoc._yMargin, 5); } // 2 * this.gridGap)); } @computed get gridGap() { return NumCast(this.layoutDoc._gridGap, 10); } @computed get isStackingView() { return (this.props.viewType ?? this.layoutDoc._viewType) === CollectionViewType.Stacking; } @computed get numGroupColumns() { return this.isStackingView ? Math.max(1, this.Sections.size + (this.showAddAGroup ? 1 : 0)) : 1; } diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index bee7aeb87..47733994b 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -235,7 +235,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC } }); const pt = this.props.screenToLocalTransform().inverse().transformPoint(x, y); - ContextMenu.Instance.displayMenu(x, y); + ContextMenu.Instance.displayMenu(x, y, undefined, true); } @computed get innards() { TraceMobx(); diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index a5d679df0..a5d62acb4 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -74,7 +74,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: const { Document, DataDoc } = this.props; const validPairs = this.childDocs.map(doc => Doc.GetLayoutDataDocPair(Document, !this.props.isAnnotationOverlay ? DataDoc : undefined, doc)). filter(pair => { // filter out any documents that have a proto that we don't have permissions to (which we determine by not having any keys - return pair.layout && !pair.layout.hidden && (!pair.layout.proto || (pair.layout.proto instanceof Doc && GetEffectiveAcl(pair.layout.proto) !== AclPrivate));// Object.keys(pair.layout.proto).length)); + return pair.layout && /*!pair.layout.hidden &&*/ (!pair.layout.proto || (pair.layout.proto instanceof Doc && GetEffectiveAcl(pair.layout.proto) !== AclPrivate));// Object.keys(pair.layout.proto).length)); }); return validPairs.map(({ data, layout }) => ({ data: data as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types } @@ -90,7 +90,8 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: searchFilterDocs = () => { return [...this.props.searchFilterDocs(), ...DocListCast(this.props.Document._searchFilterDocs)]; } - @computed get childDocs() { + @computed.struct get childDocs() { + TraceMobx(); let rawdocs: (Doc | Promise<Doc>)[] = []; if (this.dataField instanceof Doc) { // if collection data is just a document, then promote it to a singleton list; rawdocs = [this.dataField]; @@ -114,10 +115,14 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: return childDocs.filter(cd => !cd.cookies); // remove any documents that require a cookie if there are no filters to provide one } + // console.log(CurrentUserUtils.ActiveDashboard._docFilters); + // if (!this.props.Document._docFilters && this.props.Document.currentFilter) { + // (this.props.Document.currentFilter as Doc).filterBoolean = (this.props.ContainingCollectionDoc?.currentFilter as Doc)?.filterBoolean; + // } const docsforFilter: Doc[] = []; childDocs.forEach((d) => { - if (DocUtils.Excluded(d, docFilters)) return; - let notFiltered = d.z || ((!searchDocs.length || searchDocs.includes(d)) && (DocUtils.FilterDocs([d], docFilters, docRangeFilters, viewSpecScript).length > 0)); + // if (DocUtils.Excluded(d, docFilters)) return; + let notFiltered = d.z || Doc.IsSystem(d) || ((!searchDocs.length || searchDocs.includes(d)) && (DocUtils.FilterDocs([d], docFilters, docRangeFilters, viewSpecScript, this.props.Document).length > 0)); const fieldKey = Doc.LayoutFieldKey(d); const annos = !Field.toString(Doc.LayoutField(d) as Field).includes("CollectionView"); const data = d[annos ? fieldKey + "-annotations" : fieldKey]; @@ -125,13 +130,13 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: let subDocs = DocListCast(data); if (subDocs.length > 0) { let newarray: Doc[] = []; - notFiltered = notFiltered || (!searchDocs.length && DocUtils.FilterDocs(subDocs, docFilters, docRangeFilters, viewSpecScript).length); + notFiltered = notFiltered || (!searchDocs.length && DocUtils.FilterDocs(subDocs, docFilters, docRangeFilters, viewSpecScript, d).length); while (subDocs.length > 0 && !notFiltered) { newarray = []; subDocs.forEach((t) => { const fieldKey = Doc.LayoutFieldKey(t); const annos = !Field.toString(Doc.LayoutField(t) as Field).includes("CollectionView"); - notFiltered = notFiltered || ((!searchDocs.length || searchDocs.includes(t)) && ((!docFilters.length && !docRangeFilters.length) || DocUtils.FilterDocs([t], docFilters, docRangeFilters, viewSpecScript).length)); + notFiltered = notFiltered || ((!searchDocs.length || searchDocs.includes(t)) && ((!docFilters.length && !docRangeFilters.length) || DocUtils.FilterDocs([t], docFilters, docRangeFilters, viewSpecScript, d).length)); DocListCast(t[annos ? fieldKey + "-annotations" : fieldKey]).forEach((newdoc) => newarray.push(newdoc)); }); subDocs = newarray; @@ -474,5 +479,6 @@ import { CollectionView, CollectionViewType, CollectionViewProps } from "./Colle import { SelectionManager } from "../../util/SelectionManager"; import { OverlayView } from "../OverlayView"; import { Hypothesis } from "../../util/HypothesisUtils"; -import { GetEffectiveAcl } from "../../../fields/util"; +import { GetEffectiveAcl, TraceMobx } from "../../../fields/util"; +import { FilterBox } from "../nodes/FilterBox"; diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 5a4864d2d..82c8a9114 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -49,7 +49,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll @computed get doc() { return this.props.Document; } @computed get dataDoc() { return this.props.DataDoc || this.doc; } @computed get treeViewtruncateTitleWidth() { return NumCast(this.doc.treeViewTruncateTitleWidth, this.panelWidth()); } - @computed get treeChildren() { return this.props.childDocuments || this.childDocs; } + @computed get treeChildren() { TraceMobx(); return this.props.childDocuments || this.childDocs; } @computed get outlineMode() { return this.doc.treeViewType === "outline"; } @computed get fileSysMode() { return this.doc.treeViewType === "fileSystem"; } @@ -67,19 +67,23 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll Object.values(this._disposers).forEach(disposer => disposer?.()); } - componentWillMount() { + componentDidMount() { this._disposers.autoheight = reaction(() => this.rootDoc.autoHeight, auto => auto && this.computeHeight(), - { fireImmediately: true }) + { fireImmediately: true }); } refList: Set<any> = new Set(); observer: any; computeHeight = () => { - this.rootDoc._height = this.paddingTop() + 26/* bcz: ugh: title bar height hack ... get ref and compute instead */ + + const hgt = this.paddingTop() + 26/* bcz: ugh: title bar height hack ... get ref and compute instead */ + Array.from(this.refList).reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), 0); + this.props.setHeight(hgt); + } + unobserveHeight = (ref: any) => { + this.refList.delete(ref); + this.rootDoc.autoHeight && this.computeHeight(); } - unobserveHeight = (ref: any) => this.refList.delete(ref); observerHeight = (ref: any) => { if (ref) { this.refList.add(ref); @@ -111,15 +115,14 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll const targetDataDoc = this.doc[DataSym]; const value = DocListCast(targetDataDoc[this.props.fieldKey]); const result = value.filter(v => !docs.includes(v)); - SelectionManager.DeselectAll(); + if ((doc instanceof Doc ? [doc] : doc).some(doc => SelectionManager.Views().some(dv => Doc.AreProtosEqual(dv.rootDoc, doc)))) SelectionManager.DeselectAll(); if (result.length !== value.length) { const ind = targetDataDoc[this.props.fieldKey].indexOf(doc); const prev = ind && targetDataDoc[this.props.fieldKey][ind - 1]; - targetDataDoc[this.props.fieldKey] = new List<Doc>(result); + this.props.removeDocument?.(doc); if (ind > 0) { FormattedTextBox.SelectOnLoad = prev[Id]; - const prevView = DocumentManager.Instance.getDocumentView(prev, this.props.CollectionView); - prevView?.select(false); + DocumentManager.Instance.getDocumentView(prev, this.props.CollectionView)?.select(false); } return true; } diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 21dd4b206..85ae66fdc 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -1,4 +1,4 @@ -import { action, computed, observable } from 'mobx'; +import { computed, observable } from 'mobx'; import { observer } from "mobx-react"; import * as React from 'react'; import 'react-image-lightbox-with-rotate/style.css'; // This only needs to be imported once in your app @@ -66,6 +66,7 @@ export interface CollectionViewProps extends FieldViewProps { // property overrides for child documents children?: never | (() => JSX.Element[]) | React.ReactNode; 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; childOpacity?: () => number; childHideTitle?: () => boolean; // whether to hide the documentdecorations title for children diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index f6aecbb14..7e2f7811e 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -9,7 +9,7 @@ import * as ReactDOM from 'react-dom'; import { DataSym, Doc, DocListCast, DocListCastAsync, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; import { Id } from '../../../fields/FieldSymbols'; import { FieldId } from "../../../fields/RefField"; -import { Cast, NumCast, StrCast } from "../../../fields/Types"; +import { Cast, NumCast, StrCast, BoolCast } from "../../../fields/Types"; import { TraceMobx } from '../../../fields/util'; import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents, Utils } from "../../../Utils"; import { DocServer } from "../../DocServer"; @@ -92,7 +92,8 @@ export class TabDocView extends React.Component<TabDocViewProps> { }; tab.element[0].style.borderTopRightRadius = "8px"; tab.element[0].children[1].appendChild(toggle); - tab._disposers.layerDisposer = reaction(() => ({ layer: tab.DashDoc.activeLayer, color: this.tabColor }), + 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 @@ -298,6 +299,8 @@ export class TabDocView extends React.Component<TabDocViewProps> { PanelHeight = () => this._panelHeight; miniMapColor = () => this.tabColor; tabView = () => this._view; + disableMinimap = () => !this._document || (this._document.layout !== CollectionView.LayoutString(Doc.LayoutFieldKey(this._document)) || this._document?._viewType !== CollectionViewType.Freeform); + hideMinimap = () => this.disableMinimap() || BoolCast(this._document?.hideMinimap); @computed get layerProvider() { return this._document && DefaultLayerProvider(this._document); } @computed get docView() { @@ -329,13 +332,14 @@ export class TabDocView extends React.Component<TabDocViewProps> { bringToFront={emptyFunction} pinToPres={TabDocView.PinDoc} /> <TabMinimapView key="minimap" + hideMinimap={this.hideMinimap} addDocTab={this.addDocTab} PanelHeight={this.PanelHeight} PanelWidth={this.PanelWidth} background={this.miniMapColor} document={this._document} tabView={this.tabView} /> - <Tooltip style={{ display: this._document.layout !== CollectionView.LayoutString(Doc.LayoutFieldKey(this._document)) || this._document?._viewType !== CollectionViewType.Freeform ? "none" : undefined }} key="ttip" title={<div className="dash-tooltip">{"toggle minimap"}</div>}> + <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; })} > <FontAwesomeIcon icon={"globe-asia"} size="lg" /> </div> @@ -346,7 +350,10 @@ export class TabDocView extends React.Component<TabDocViewProps> { render() { this.tab && CollectionDockingView.Instance.tabMap.delete(this.tab); return ( - <div className="collectionDockingView-content" style={{ height: "100%", width: "100%" }} ref={ref => { + <div className="collectionDockingView-content" style={{ + fontFamily: Doc.UserDoc().renderStyle === "comic" ? "Comic Sans MS" : undefined, + height: "100%", width: "100%" + }} ref={ref => { if (this._mainCont = ref) { (this._mainCont as any).InitTab = (tab: any) => this.init(tab, this._document); DocServer.GetRefField(this.props.documentId).then(action(doc => doc instanceof Doc && (this._document = doc) && this.tab && this.init(this.tab, this._document))); @@ -360,6 +367,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { interface TabMinimapViewProps { document: Doc; + hideMinimap: () => boolean; tabView: () => DocumentView | undefined; addDocTab: (doc: Doc, where: string) => boolean; PanelWidth: () => number; @@ -410,7 +418,7 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> { const miniLeft = 50 + (NumCast(this.props.document._panX) - this.renderBounds.cx) / this.renderBounds.dim * 100 - miniWidth / 2; const miniTop = 50 + (NumCast(this.props.document._panY) - this.renderBounds.cy) / this.renderBounds.dim * 100 - miniHeight / 2; const miniSize = this.returnMiniSize(); - return this.props.document.hideMinimap ? (null) : + return this.props.hideMinimap() ? (null) : <div className="miniMap" style={{ width: miniSize, height: miniSize, background: this.props.background() }}> <CollectionFreeFormView Document={this.props.document} diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss index bf896b8d3..5b0c04f33 100644 --- a/src/client/views/collections/TreeView.scss +++ b/src/client/views/collections/TreeView.scss @@ -8,6 +8,7 @@ width: 100%; overflow: hidden; } + .treeView-container, .treeView-container-active { .bullet-outline { @@ -21,21 +22,26 @@ .treeView-bulletIcons { width: $TREE_BULLET_WIDTH; + .treeView-expandIcon { display: none; left: -10px; position: absolute; } + .treeView-checkIcon { - left: -10px; + left: 3.5px; + top: 2px; position: absolute; } + &:hover { .treeView-expandIcon { display: unset; } } } + .bullet { position: relative; width: $TREE_BULLET_WIDTH; @@ -46,9 +52,11 @@ border-radius: 4px; } } + .treeView-container-active { z-index: 100; position: relative; + .formattedTextbox-sidebar { background-color: #ffff001f !important; height: 500px !important; @@ -71,7 +79,8 @@ display: flex; overflow: hidden; } -.treeView-border{ + +.treeView-border { border-left: dashed 1px #00000042; } @@ -79,15 +88,20 @@ .treeView-header { border: transparent 1px solid; display: flex; + //align-items: center; ::-webkit-scrollbar { - display: none; + display: none; } + .formattedTextBox-cont { - .formattedTextbox-sidebar, .formattedTextbox-sidebar-inking { + + .formattedTextbox-sidebar, + .formattedTextbox-sidebar-inking { overflow: visible !important; border-left: unset; } + overflow: visible !important; } @@ -105,12 +119,18 @@ margin-left: 0.25rem; opacity: 0.75; - >svg, .styleProvider-treeView-lock, .styleProvider-treeView-hide, .styleProvider-treeView-lock-active, .styleProvider-treeView-hide-active { + >svg, + .styleProvider-treeView-lock, + .styleProvider-treeView-hide, + .styleProvider-treeView-lock-active, + .styleProvider-treeView-hide-active { margin-left: 0.25rem; - margin-right: 0.25rem; + margin-right: 0.25rem; } - - >svg, .styleProvider-treeView-lock, .styleProvider-treeView-hide { + + >svg, + .styleProvider-treeView-lock, + .styleProvider-treeView-hide { display: none; } } @@ -135,7 +155,10 @@ } .right-buttons-container { - >svg, .styleProvider-treeView-lock, .styleProvider-treeView-hide { + + >svg, + .styleProvider-treeView-lock, + .styleProvider-treeView-hide { display: inherit; } } diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 2c3a6c0d7..e1fd78270 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -21,7 +21,7 @@ import { Transform } from '../../util/Transform'; import { undoBatch, UndoManager } from '../../util/UndoManager'; import { EditableView } from "../EditableView"; import { TREE_BULLET_WIDTH } from '../globalCssVariables.scss'; -import { DocumentView, DocumentViewProps, StyleProviderFunc } from '../nodes/DocumentView'; +import { DocumentView, DocumentViewProps, StyleProviderFunc, DocumentViewInternal } from '../nodes/DocumentView'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import { RichTextMenu } from '../nodes/formattedText/RichTextMenu'; import { KeyValueBox } from '../nodes/KeyValueBox'; @@ -150,9 +150,9 @@ export class TreeView extends React.Component<TreeViewProps> { if (this.props.document.isFolder || Doc.IsSystem(this.props.document)) { this.treeViewOpen = !this.treeViewOpen; } else { + // choose an appropriate alias or make one. --- choose the first alias that (1) user owns, (2) has no context field ... otherwise make a new alias this.props.addDocTab(this.props.document, "add:right"); } - docView?.select(false); } constructor(props: any) { super(props); @@ -174,7 +174,7 @@ export class TreeView extends React.Component<TreeViewProps> { componentWillUnmount() { this._selDisposer?.(); - this._treeEle && this.props.unobserveHeight(this._treeEle) + this._treeEle && this.props.unobserveHeight(this._treeEle); document.removeEventListener("pointermove", this.onDragMove, true); document.removeEventListener("pointermove", this.onDragUp, true); } @@ -442,7 +442,7 @@ export class TreeView extends React.Component<TreeViewProps> { @computed get renderBullet() { TraceMobx(); - const iconType = this.doc.isFolder ? (this.treeViewOpen ? "chevron-down" : "chevron-right") : Doc.toIcon(this.doc); + const iconType = this.props.treeView.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.TreeViewIcon + (this.treeViewOpen ? ":open" : "")) || "question"; const checked = this.onCheckedClick ? (this.doc.treeViewChecked ?? "unchecked") : undefined; return <div className={`bullet${this.props.treeView.outlineMode ? "-outline" : ""}`} key={"bullet"} title={this.childDocs?.length ? `click to see ${this.childDocs?.length} items` : "view fields"} @@ -483,22 +483,29 @@ export class TreeView extends React.Component<TreeViewProps> { } @computed get headerElements() { - return this.props.treeViewHideHeaderFields() || Doc.IsSystem(this.doc) ? (null) + return this.props.treeViewHideHeaderFields() || this.doc.treeViewHideHeaderFields ? (null) : <> - <FontAwesomeIcon key="bars" icon="bars" size="sm" onClick={e => { this.showContextMenu(e); e.stopPropagation(); }} /> - {this.doc.treeViewExpandedViewLock ? (null) : + {this.doc.hideContextMenu ? (null) : <FontAwesomeIcon key="bars" icon="bars" size="sm" onClick={e => { this.showContextMenu(e); e.stopPropagation(); }} />} + {this.doc.treeViewExpandedViewLock || Doc.IsSystem(this.doc) ? (null) : <span className="collectionTreeView-keyHeader" key={this.treeViewExpandedView} onPointerDown={this.expandNextviewType}> {this.treeViewExpandedView} </span>} </>; } - showContextMenu = (e: React.MouseEvent) => simulateMouseClick(this._docRef?.ContentDiv, e.clientX, e.clientY + 30, e.screenX, e.screenY + 30); - contextMenuItems = () => Doc.IsSystem(this.doc) ? [] : this.doc.isFolder ? - [{ script: ScriptField.MakeFunction(`scriptContext.makeFolder()`, { scriptContext: "any" })!, label: "New Folder" }] : - this.props.treeView.fileSysMode && this.doc === Doc.GetProto(this.doc) ? - [{ script: ScriptField.MakeFunction(`openOnRight(getAlias(self))`)!, label: "Open Alias" }] : - [{ script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, label: "Focus or Open" }] + showContextMenu = (e: React.MouseEvent) => { + DocumentViewInternal.SelectAfterContextMenu = false; + simulateMouseClick(this._docRef?.ContentDiv, e.clientX, e.clientY + 30, e.screenX, e.screenY + 30); + DocumentViewInternal.SelectAfterContextMenu = true; + } + contextMenuItems = () => { + const makeFolder = { script: ScriptField.MakeFunction(`scriptContext.makeFolder()`, { scriptContext: "any" })!, label: "New Folder" }; + return this.doc.isFolder ? [makeFolder] : + Doc.IsSystem(this.doc) ? [] : + this.props.treeView.fileSysMode && this.doc === Doc.GetProto(this.doc) ? + [{ script: ScriptField.MakeFunction(`openOnRight(getAlias(self))`)!, label: "Open Alias" }, makeFolder] : + [{ script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, label: "Focus or Open" }]; + } onChildClick = () => this.props.onChildClick?.() ?? (this._editTitleScript?.() || ScriptCast(this.doc.treeChildClick)); onChildDoubleClick = () => (!this.props.treeView.outlineMode && this._openScript?.()) || ScriptCast(this.doc.treeChildDoubleClick); @@ -522,7 +529,6 @@ export class TreeView extends React.Component<TreeViewProps> { }}> {StrCast(doc?.title)} </div>; - case StyleProp.Decorations: return (null); default: return this.props?.treeView?.props.styleProvider?.(doc, props, property); } } @@ -542,7 +548,7 @@ export class TreeView extends React.Component<TreeViewProps> { } } } - titleWidth = () => Math.max(20, Math.min(this.props.treeView.truncateTitleWidth(), this.props.panelWidth() - treeBulletWidth())); + titleWidth = () => Math.max(20, Math.min(this.props.treeView.truncateTitleWidth(), this.props.panelWidth() - 2 * treeBulletWidth())); /** * Renders the EditableView title element for placement into the tree. @@ -726,7 +732,7 @@ export class TreeView extends React.Component<TreeViewProps> { return this.props.renderedIds.indexOf(this.doc[Id]) !== -1 ? "<" + this.doc.title + ">" : // just print the title of documents we've previously rendered in this hierarchical path to avoid cycles <div className={`treeView-container${this.props.isContentActive() ? "-active" : ""}`} ref={this.createTreeDropTarget} - onPointerDown={e => this.props.isContentActive(true) && SelectionManager.DeselectAll()} + //onPointerDown={e => this.props.isContentActive(true) && SelectionManager.DeselectAll()} // bcz: this breaks entering a text filter in a filterBox since it deselects the filter's target document onKeyDown={this.onKeyDown}> <li className="collection-child"> {hideTitle && this.doc.type !== DocumentType.RTF ? diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index c623ce653..407216217 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1032,7 +1032,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P docRangeFilters={this.freeformRangeDocFilters} searchFilterDocs={this.searchFilterDocs} isContentActive={this.isAnnotationOverlay ? this.props.isContentActive : returnFalse} - isDocumentActive={this.isContentActive} + isDocumentActive={this.props.childDocumentsActive ? this.props.isDocumentActive : this.isContentActive} focus={this.focusDocument} addDocTab={this.addDocTab} addDocument={this.props.addDocument} @@ -1051,7 +1051,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P dontRegisterView={this.props.dontRegisterView} pointerEvents={this.backgroundActive || this.props.childPointerEvents ? "all" : (this.props.viewDefDivClick || (engine === "pass" && !this.props.isSelected(true))) ? "none" : undefined} - jitterRotation={NumCast(this.props.Document._jitterRotation) || ((Doc.UserDoc().renderStyle === "comic" ? 10 : 0))} + jitterRotation={this.props.styleProvider?.(childLayout, this.props, StyleProp.JitterRotation) || 0} //fitToBox={this.props.fitToBox || BoolCast(this.props.freezeChildDimensions)} // bcz: check this />; } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index d14b68fa7..b1f2750c3 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -101,7 +101,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque else if (e.key === ":") { DocUtils.addDocumentCreatorMenuItems(this.props.addLiveTextDocument, this.props.addDocument || returnFalse, x, y); - cm.displayMenu(this._downX, this._downY); + cm.displayMenu(this._downX, this._downY, undefined, true); e.stopPropagation(); } else if (e.key === "a" && (e.ctrlKey || e.metaKey)) { e.preventDefault(); @@ -155,7 +155,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } else if (!e.ctrlKey && !e.metaKey && SelectionManager.Views().length < 2) { FormattedTextBox.SelectOnLoadChar = Doc.UserDoc().defaultTextLayout && !this.props.childLayoutString ? e.key : ""; FormattedTextBox.LiveTextUndo = UndoManager.StartBatch("live text batch"); - this.props.addLiveTextDocument(CurrentUserUtils.GetNewTextDoc("-typed text-", x, y, 200, 100, this.props.xMargin === 0)); + this.props.addLiveTextDocument(CurrentUserUtils.GetNewTextDoc("-typed text-", x, y, 200, 100, this.props.xPadding === 0)); e.stopPropagation(); } } diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx index 8b5c02b75..f3a39a262 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -49,6 +49,11 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu return this.childLayoutPairs.map(pair => pair.layout).filter(layout => StrCast(layout._dimUnit, "*") === DimUnit.Ratio); } + @computed + private get minimumDim() { + return Math.min(...this.ratioDefinedDocs.filter(layout => layout._dimMagnitude).map(layout => NumCast(layout._dimMagnitude))); + } + /** * This loops through all childLayoutPairs and extracts the values for _dimUnit * and _dimMagnitude, ignoring any that are malformed. Additionally, it then @@ -63,7 +68,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu const widthSpecifiers: WidthSpecifier[] = []; this.childLayoutPairs.map(pair => { const unit = StrCast(pair.layout._dimUnit, "*"); - const magnitude = NumCast(pair.layout._dimMagnitude, 1); + const magnitude = NumCast(pair.layout._dimMagnitude, this.minimumDim); if (unit && magnitude && magnitude > 0 && resolvedUnits.includes(unit)) { (unit === DimUnit.Ratio) && (starSum += magnitude); widthSpecifiers.push({ magnitude, unit }); @@ -80,15 +85,15 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu * themselves to drift toward zero. Thus, whenever we change any of the values, * we normalize everything (dividing by the smallest magnitude). */ - setTimeout(() => { - const { ratioDefinedDocs } = this; - if (this.childLayoutPairs.length) { - const minimum = Math.min(...ratioDefinedDocs.map(doc => NumCast(doc._dimMagnitude, 1))); - if (minimum !== 0) { - ratioDefinedDocs.forEach(layout => layout._dimMagnitude = NumCast(layout._dimMagnitude, 1) / minimum, 1); - } - } - }); + // setTimeout(() => { + // const { ratioDefinedDocs } = this; + // if (this.childLayoutPairs.length) { + // const minimum = this.minimumDim; + // if (minimum !== 0) { + // ratioDefinedDocs.forEach(layout => layout._dimMagnitude = NumCast(layout._dimMagnitude, 1) / minimum, 1); + // } + // } + // }); return { widthSpecifiers, starSum }; } @@ -161,7 +166,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu if (columnUnitLength === undefined) { return 0; // we're still waiting on promises to resolve } - let width = NumCast(layout._dimMagnitude, 1); + let width = NumCast(layout._dimMagnitude, this.minimumDim); if (StrCast(layout._dimUnit, "*") === DimUnit.Ratio) { width *= columnUnitLength; } @@ -273,6 +278,8 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu <ResizeBar width={resizerWidth} key={"resizer" + i} + styleProvider={this.props.styleProvider} + isContentActive={this.props.isContentActive} select={this.props.select} columnUnitLength={this.getColumnUnitLength} toLeft={layout} diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx index 2c5e40d02..29cb3511a 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx @@ -49,6 +49,11 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument) return this.childLayoutPairs.map(pair => pair.layout).filter(layout => StrCast(layout._dimUnit, "*") === DimUnit.Ratio); } + @computed + private get minimumDim() { + return Math.min(...this.ratioDefinedDocs.filter(layout => layout._dimMagnitude).map(layout => NumCast(layout._dimMagnitude))); + } + /** * This loops through all childLayoutPairs and extracts the values for _dimUnit * and _dimUnit, ignoring any that are malformed. Additionally, it then @@ -63,7 +68,7 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument) const heightSpecifiers: HeightSpecifier[] = []; this.childLayoutPairs.map(pair => { const unit = StrCast(pair.layout._dimUnit, "*"); - const magnitude = NumCast(pair.layout._dimMagnitude, 1); + const magnitude = NumCast(pair.layout._dimMagnitude, this.minimumDim); if (unit && magnitude && magnitude > 0 && resolvedUnits.includes(unit)) { (unit === DimUnit.Ratio) && (starSum += magnitude); heightSpecifiers.push({ magnitude, unit }); @@ -80,15 +85,15 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument) * themselves to drift toward zero. Thus, whenever we change any of the values, * we normalize everything (dividing by the smallest magnitude). */ - setTimeout(() => { - const { ratioDefinedDocs } = this; - if (this.childLayoutPairs.length) { - const minimum = Math.min(...ratioDefinedDocs.map(layout => NumCast(layout._dimMagnitude, 1))); - if (minimum !== 0) { - ratioDefinedDocs.forEach(layout => layout._dimMagnitude = NumCast(layout._dimMagnitude, 1) / minimum); - } - } - }); + // setTimeout(() => { + // const { ratioDefinedDocs } = this; + // if (this.childLayoutPairs.length) { + // const minimum = Math.min(...ratioDefinedDocs.map(layout => NumCast(layout._dimMagnitude, 1))); + // if (minimum !== 0) { + // ratioDefinedDocs.forEach(layout => layout._dimMagnitude = NumCast(layout._dimMagnitude, 1) / minimum); + // } + // } + // }); return { heightSpecifiers, starSum }; } @@ -161,7 +166,7 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument) if (rowUnitLength === undefined) { return 0; // we're still waiting on promises to resolve } - let height = NumCast(layout._dimMagnitude, 1); + let height = NumCast(layout._dimMagnitude, this.minimumDim); if (StrCast(layout._dimUnit, "*") === DimUnit.Ratio) { height *= rowUnitLength; } @@ -261,12 +266,16 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument) const height = () => this.lookupPixels(layout); const width = () => PanelWidth() - 2 * NumCast(Document._xMargin) - (BoolCast(Document.showWidthLabels) ? 20 : 0); collector.push( - <div className={"document-wrapper"} key={"wrapper" + i} > + <div className={"document-wrapper"} + style={{ height: height() }} + key={"wrapper" + i} > {this.getDisplayDoc(layout, dxf, width, height)} <HeightLabel layout={layout} collectionDoc={Document} /> </div>, <ResizeBar height={resizerHeight} + styleProvider={this.props.styleProvider} + isContentActive={this.props.isContentActive} key={"resizer" + i} columnUnitLength={this.getRowUnitLength} toTop={layout} diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx index 734915a93..9fe18d118 100644 --- a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx +++ b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx @@ -5,9 +5,13 @@ import { Doc } from "../../../../fields/Doc"; import { NumCast, StrCast } from "../../../../fields/Types"; import { DimUnit } from "./CollectionMulticolumnView"; import { UndoManager } from "../../../util/UndoManager"; +import { StyleProviderFunc } from "../../nodes/DocumentView"; +import { StyleProp } from "../../StyleProvider"; interface ResizerProps { width: number; + styleProvider?: StyleProviderFunc; + isContentActive?: () => boolean; columnUnitLength(): number | undefined; toLeft?: Doc; toRight?: Doc; @@ -85,19 +89,16 @@ export default class ResizeBar extends React.Component<ResizerProps> { } render() { - return ( - <div - className={"multiColumnResizer"} - style={{ - width: this.props.width, - opacity: this.isActivated && this.isHoverActive ? resizerOpacity : 0 - }} - onPointerEnter={action(() => this.isHoverActive = true)} - onPointerLeave={action(() => !this.isResizingActive && (this.isHoverActive = false))} - > - <div className={"multiColumnResizer-hdl"} onPointerDown={e => this.registerResizing(e)} /> - </div> - ); + return <div className="multiColumnResizer" + style={{ + width: this.props.width, + backgroundColor: !this.props.isContentActive?.() ? "" : this.props.styleProvider?.(undefined, undefined, StyleProp.WidgetColor) + }} + onPointerEnter={action(() => this.isHoverActive = true)} + onPointerLeave={action(() => !this.isResizingActive && (this.isHoverActive = false))} + > + <div className={"multiColumnResizer-hdl"} onPointerDown={e => this.registerResizing(e)} /> + </div>; } }
\ No newline at end of file diff --git a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx index d0bc4d01c..5478bf709 100644 --- a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx +++ b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx @@ -5,9 +5,13 @@ import { Doc } from "../../../../fields/Doc"; import { NumCast, StrCast } from "../../../../fields/Types"; import { DimUnit } from "./CollectionMultirowView"; import { UndoManager } from "../../../util/UndoManager"; +import { StyleProp } from "../../StyleProvider"; +import { StyleProviderFunc } from "../../nodes/DocumentView"; interface ResizerProps { height: number; + styleProvider?: StyleProviderFunc; + isContentActive?: () => boolean; columnUnitLength(): number | undefined; toTop?: Doc; toBottom?: Doc; @@ -83,19 +87,16 @@ export default class ResizeBar extends React.Component<ResizerProps> { } render() { - return ( - <div - className={"multiRowResizer"} - style={{ - height: this.props.height, - opacity: this.isActivated && this.isHoverActive ? resizerOpacity : 0 - }} - onPointerEnter={action(() => this.isHoverActive = true)} - onPointerLeave={action(() => !this.isResizingActive && (this.isHoverActive = false))} - > - <div className={"multiRowResizer-hdl"} onPointerDown={e => this.registerResizing(e)} /> - </div> - ); + return <div className="multiRowResizer" + style={{ + height: this.props.height, + backgroundColor: !this.props.isContentActive?.() ? "" : this.props.styleProvider?.(undefined, undefined, StyleProp.WidgetColor) + }} + onPointerEnter={action(() => this.isHoverActive = true)} + onPointerLeave={action(() => !this.isResizingActive && (this.isHoverActive = false))} + > + <div className={"multiRowResizer-hdl"} onPointerDown={e => this.registerResizing(e)} /> + </div>; } }
\ No newline at end of file diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 85899578c..a2e36f12e 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -53,7 +53,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @observable _position: number = 0; @observable _waveHeight: Opt<number> = this.layoutDoc._height; @observable _paused: boolean = false; - @computed get mediaState(): undefined | "recording" | "paused" | "playing" { return this.dataDoc.mediaState as (undefined | "recording" | "paused" | "playing"); } + @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(); } diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 1fda4cc5e..092823603 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -37,10 +37,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF public static animFields = ["_height", "_width", "x", "y", "_scrollTop", "opacity"]; // fields that are configured to be animatable using animation frames @observable _animPos: number[] | undefined = undefined; @observable _contentView: DocumentView | undefined | null; - random(min: number, max: number) { /* min should not be equal to max */ return min + ((Math.abs(this.X * this.Y) * 9301 + 49297) % 233280 / 233280) * (max - min); } get displayName() { return "CollectionFreeFormDocumentView(" + this.rootDoc.title + ")"; } // this makes mobx trace() statements more descriptive get maskCentering() { return this.props.Document.isInkMask ? InkingStroke.MaskDim / 2 : 0; } - get transform() { return `translate(${this.X - this.maskCentering}px, ${this.Y - this.maskCentering}px) rotate(${this.random(-1, 1) * this.props.jitterRotation}deg)`; } + get transform() { return `translate(${this.X - this.maskCentering}px, ${this.Y - this.maskCentering}px) rotate(${this.props.jitterRotation}deg)`; } get X() { return this.dataProvider ? this.dataProvider.x : (this.Document.x || 0); } get Y() { return this.dataProvider ? this.dataProvider.y : (this.Document.y || 0); } get ZInd() { return this.dataProvider ? this.dataProvider.zIndex : (this.Document.zIndex || 0); } @@ -156,7 +155,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF returnThis = () => this; render() { TraceMobx(); - const backgroundColor = () => this.props.styleProvider?.(this.Document, this.props, StyleProp.BackgroundColor); const divProps: DocumentViewProps = { ...this.props, CollectionFreeFormDocumentView: this.returnThis, @@ -175,17 +173,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF transition: this.props.dataTransition ? this.props.dataTransition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.dataTransition), zIndex: this.ZInd, mixBlendMode: StrCast(this.layoutDoc.mixBlendMode) as any, - display: this.ZInd === -99 ? "none" : undefined, + display: this.ZInd === -99 ? "none" : undefined }} > - - {Doc.UserDoc().renderStyle !== "comic" ? (null) : - <div style={{ width: "100%", height: "100%", position: "absolute" }}> - <svg style={{ transform: `scale(1,${this.props.PanelHeight() / this.props.PanelWidth()})`, transformOrigin: "top left", overflow: "visible" }} viewBox="0 0 12 14"> - <path d="M 7 0 C 9 -1 13 1 12 4 C 11 10 13 12 10 12 C 6 12 7 13 2 12 Q -1 11 0 8 C 1 4 0 4 0 2 C 0 0 1 0 1 0 C 3 0 3 1 7 0" - style={{ stroke: "black", fill: backgroundColor(), strokeWidth: 0.2 }} /> - </svg> - </div>} - <DocumentView {...divProps} ref={action((r: DocumentView | null) => this._contentView = r)} /> </div>; } diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index 749ffa7fd..bdbece621 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -3,6 +3,14 @@ .documentView-effectsWrapper { border-radius: inherit; } + +.documentView-customBorder { + width:100%; + height: 100%; + position: absolute; + top: 0; +} + .documentView-node, .documentView-node-topmost { position: inherit; @@ -37,12 +45,13 @@ overflow-y: scroll; height: calc(100% - 20px); } + .documentView-linkAnchorBoxAnchor { - display:flex; + display: flex; overflow: hidden; .documentView-node { - width:10px !important; + width: 10px !important; } } @@ -64,6 +73,7 @@ top: 15%; } } + .documentView-treeView { max-height: 1.5em; text-overflow: ellipsis; @@ -71,7 +81,8 @@ white-space: pre; width: 100%; overflow: hidden; - > .documentView-node { + + >.documentView-node { position: absolute; } } @@ -80,14 +91,33 @@ border-radius: inherit; width: 100%; height: 100%; + + .sharingIndicator { + height: 30px; + width: 30px; + border-radius: 50%; + position: absolute; + right: -15; + opacity: 0.9; + pointer-events: auto; + background-color: #9dca96; + letter-spacing: 2px; + font-size: 10px; + transition: transform 0.2s; + text-align: center; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + } } .documentView-anchorCont { position: absolute; - top: 0; - left: 0; + top: 0; + left: 0; width: 100%; - height: 100%; + height: 100%; display: inline-block; pointer-events: none; } @@ -100,6 +130,7 @@ top: 0; left: 0; } + .documentView-styleWrapper { position: absolute; display: inline-block; @@ -113,7 +144,8 @@ position: absolute; } - .documentView-titleWrapper, .documentView-titleWrapper-hover { + .documentView-titleWrapper, + .documentView-titleWrapper-hover { overflow: hidden; color: white; transform-origin: top left; @@ -126,8 +158,9 @@ white-space: pre; position: absolute; } + .documentView-titleWrapper-hover { - display:none; + display: none; } .documentView-searchHighlight { @@ -150,18 +183,21 @@ } -.documentView-node:hover, .documentView-node-topmost:hover { - > .documentView-styleWrapper { - > .documentView-titleWrapper-hover { - display:inline-block; +.documentView-node:hover, +.documentView-node-topmost:hover { + >.documentView-styleWrapper { + >.documentView-titleWrapper-hover { + display: inline-block; } } - > .documentView-styleWrapper { - > .documentView-captionWrapper { + + >.documentView-styleWrapper { + >.documentView-captionWrapper { opacity: 1; } } } + .contentFittingDocumentView { position: relative; display: flex; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 26cf52f17..7f6905042 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -11,7 +11,7 @@ import { listSpec } from "../../../fields/Schema"; import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from "../../../fields/Types"; import { AudioField } from "../../../fields/URLField"; -import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; +import { GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util'; import { MobileInterface } from '../../../mobile/MobileInterface'; import { emptyFunction, hasDescendantTarget, OmitKeys, returnVal, Utils } from "../../../Utils"; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; @@ -47,6 +47,7 @@ import { PresBox } from './PresBox'; import { RadialMenu } from './RadialMenu'; import React = require("react"); import { ScriptingBox } from "./ScriptingBox"; +import { FormattedTextBox } from "./formattedText/FormattedTextBox"; const { Howl } = require('howler'); interface Window { @@ -124,6 +125,8 @@ export interface DocumentViewSharedProps { disableDocBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over. pointerEvents?: string; scriptContext?: any; // can be assigned anything and will be passed as 'scriptContext' to any OnClick script that executes on this document + createNewFilterDoc?: () => void; + updateFilterDoc?: (doc: Doc) => void; } export interface DocumentViewProps extends DocumentViewSharedProps { // properties specific to DocumentViews but not to FieldView @@ -160,6 +163,7 @@ export interface DocumentViewInternalProps extends DocumentViewProps { @observer export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps, Document>(Document) { + public static SelectAfterContextMenu = true; // whether a document should be selected after it's contextmenu is triggered. @observable _animateScalingTo = 0; @observable _mediaState = 0; @observable _pendingDoubleClick = false; @@ -197,7 +201,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.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null)); } + @computed get onClickHandler() { return this.props.onClick?.() ?? Cast(this.Document.onfClick, 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); } @@ -450,10 +454,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps }, console.log); UndoManager.RunInBatch(() => func().result?.select === true ? this.props.select(false) : "", "on double click"); } else if (!Doc.IsSystem(this.rootDoc)) { - if (this.props.Document.type !== DocumentType.LABEL) { - UndoManager.RunInBatch(() => this.props.addDocTab((this.rootDoc._fullScreenView as Doc) || this.rootDoc, "lightbox"), "double tap"); - SelectionManager.DeselectAll(); - } + 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); } } else if (this.onClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes(ScriptingBox.name)) { // bcz: hack? don't execute script if you're clicking on a scripting box itself @@ -505,6 +510,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps // if this is part of a template, let the event go up to the tempalte root unless right/ctrl clicking !(this.props.Document.rootDocument && !(e.ctrlKey || e.button > 0))) { if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart) && + !this.Document.ignoreClick && !e.ctrlKey && (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) && !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) { @@ -599,7 +605,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (this.props.Document === CurrentUserUtils.ActiveDashboard) { alert((e.target as any)?.closest?.("*.lm_content") ? "You can't perform this move most likely because you don't have permission to modify the destination." : - "linking to document tabs not yet supported. Drop link on document content."); + "Linking to document tabs not yet supported. Drop link on document content."); return; } const linkdrag = de.complete.annoDragData ?? de.complete.linkDragData; @@ -656,95 +662,110 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const customScripts = Cast(this.props.Document.contextMenuScripts, listSpec(ScriptField), []); Cast(this.props.Document.contextMenuLabels, listSpec("string"), []).forEach((label, i) => - cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ this: this.layoutDoc, scriptContext: this.props.scriptContext, self: this.rootDoc }), icon: "sticky-note" })); + 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" })); - 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" }); - DocListCast(this.Document.links).length && appearanceItems.splice(0, 0, { description: `${this.layoutDoc.hideLinkButton ? "Show" : "Hide"} Link Button`, event: action(() => this.layoutDoc.hideLinkButton = !this.layoutDoc.hideLinkButton), icon: "eye" }); - !appearance && cm.addItem({ description: "UI Controls...", subitems: appearanceItems, icon: "compass" }); - - if (!Doc.IsSystem(this.rootDoc) && this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Tree) { - !Doc.UserDoc().noviceMode && appearanceItems.splice(0, 0, { description: `${!this.layoutDoc._showAudio ? "Show" : "Hide"} Audio Button`, event: action(() => this.layoutDoc._showAudio = !this.layoutDoc._showAudio), icon: "microphone" }); - const existingOnClick = cm.findByDescription("OnClick..."); - const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; - - 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" }); - !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" }); - 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) { - const options = cm.findByDescription("Options..."); - const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : []; - !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "compass" }); - - onClicks.push({ description: this.Document.ignoreClick ? "Select" : "Do Nothing", event: () => this.Document.ignoreClick = !this.Document.ignoreClick, icon: this.Document.ignoreClick ? "unlock" : "lock" }); - onClicks.push({ description: this.Document.isLinkButton ? "Remove Follow Behavior" : "Follow Link in Place", event: () => this.toggleFollowLink("inPlace", true, false), icon: "link" }); - !this.Document.isLinkButton && onClicks.push({ description: "Follow Link on Right", event: () => this.toggleFollowLink("add:right", false, false), icon: "link" }); - onClicks.push({ description: this.Document.isLinkButton || this.onClickHandler ? "Remove Click Behavior" : "Follow Link", event: () => this.toggleFollowLink(undefined, false, false), icon: "link" }); - onClicks.push({ description: (this.Document.isPushpin ? "Remove" : "Make") + " Pushpin", event: () => this.toggleFollowLink(undefined, false, true), icon: "map-pin" }); - onClicks.push({ description: "Edit onClick Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"), icon: "terminal" }); - !existingOnClick && cm.addItem({ description: "OnClick...", addDivider: true, noexpand: true, subitems: onClicks, icon: "mouse-pointer" }); - } else if (DocListCast(this.Document.links).length) { - onClicks.push({ description: "Select on Click", event: () => this.selectOnClick(), icon: "link" }); - onClicks.push({ description: "Follow Link on Click", event: () => this.followLinkOnClick(undefined, false), icon: "link" }); - onClicks.push({ description: "Toggle Link Target on Click", event: () => this.toggleTargetOnClick(), icon: "map-pin" }); - !existingOnClick && cm.addItem({ description: "OnClick...", addDivider: true, subitems: onClicks, icon: "mouse-pointer" }); + 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({ + description: "Add a Field", event: () => { + const alias = Doc.MakeAlias(this.rootDoc); + alias.layout = FormattedTextBox.LayoutString("newfield"); + alias.title = "newfield"; + alias._yMargin = 10; + alias._height = 35; + alias._width = 100; + alias.syncLayoutFieldWithTitle = true; + alias.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc.width); + alias.y = NumCast(this.rootDoc.y); + this.props.addDocument?.(alias); + }, icon: "eye" + }); + DocListCast(this.Document.links).length && appearanceItems.splice(0, 0, { description: `${this.layoutDoc.hideLinkButton ? "Show" : "Hide"} Link Button`, event: action(() => this.layoutDoc.hideLinkButton = !this.layoutDoc.hideLinkButton), icon: "eye" }); + !appearance && cm.addItem({ description: "UI Controls...", subitems: appearanceItems, icon: "compass" }); + + if (!Doc.IsSystem(this.rootDoc) && this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Tree) { + !Doc.UserDoc().noviceMode && appearanceItems.splice(0, 0, { description: `${!this.layoutDoc._showAudio ? "Show" : "Hide"} Audio Button`, event: action(() => this.layoutDoc._showAudio = !this.layoutDoc._showAudio), icon: "microphone" }); + const existingOnClick = cm.findByDescription("OnClick..."); + const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; + + 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" }); + !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" }); + 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) { + const options = cm.findByDescription("Options..."); + const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : []; + !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "compass" }); + + onClicks.push({ description: this.Document.ignoreClick ? "Select" : "Do Nothing", event: () => this.Document.ignoreClick = !this.Document.ignoreClick, icon: this.Document.ignoreClick ? "unlock" : "lock" }); + onClicks.push({ description: this.Document.isLinkButton ? "Remove Follow Behavior" : "Follow Link in Place", event: () => this.toggleFollowLink("inPlace", true, false), icon: "link" }); + !this.Document.isLinkButton && onClicks.push({ description: "Follow Link on Right", event: () => this.toggleFollowLink("add:right", false, false), icon: "link" }); + onClicks.push({ description: this.Document.isLinkButton || this.onClickHandler ? "Remove Click Behavior" : "Follow Link", event: () => this.toggleFollowLink(undefined, false, false), icon: "link" }); + onClicks.push({ description: (this.Document.isPushpin ? "Remove" : "Make") + " Pushpin", event: () => this.toggleFollowLink(undefined, false, true), icon: "map-pin" }); + onClicks.push({ description: "Edit onClick Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"), icon: "terminal" }); + !existingOnClick && cm.addItem({ description: "OnClick...", addDivider: true, noexpand: true, subitems: onClicks, icon: "mouse-pointer" }); + } else if (DocListCast(this.Document.links).length) { + onClicks.push({ description: "Select on Click", event: () => this.selectOnClick(), icon: "link" }); + onClicks.push({ description: "Follow Link on Click", event: () => this.followLinkOnClick(undefined, false), icon: "link" }); + onClicks.push({ description: "Toggle Link Target on Click", event: () => this.toggleTargetOnClick(), icon: "map-pin" }); + !existingOnClick && cm.addItem({ description: "OnClick...", addDivider: true, subitems: onClicks, icon: "mouse-pointer" }); + } } - } - const funcs: ContextMenuProps[] = []; - if (!Doc.UserDoc().noviceMode && this.layoutDoc.onDragStart) { - funcs.push({ description: "Drag an Alias", icon: "edit", event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getAlias(this.dragFactory)')) }); - funcs.push({ description: "Drag a Copy", icon: "edit", event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')) }); - funcs.push({ description: "Drag Document", icon: "edit", event: () => this.layoutDoc.onDragStart = undefined }); - cm.addItem({ description: "OnDrag...", noexpand: true, subitems: funcs, icon: "asterisk" }); - } + const funcs: ContextMenuProps[] = []; + if (!Doc.UserDoc().noviceMode && this.layoutDoc.onDragStart) { + funcs.push({ description: "Drag an Alias", icon: "edit", event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getAlias(this.dragFactory)')) }); + funcs.push({ description: "Drag a Copy", icon: "edit", event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')) }); + funcs.push({ description: "Drag Document", icon: "edit", event: () => this.layoutDoc.onDragStart = undefined }); + cm.addItem({ description: "OnDrag...", noexpand: true, subitems: funcs, icon: "asterisk" }); + } - const more = cm.findByDescription("More..."); - const moreItems = more && "subitems" in more ? more.subitems : []; - if (!Doc.IsSystem(this.rootDoc)) { - (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.UserDoc().noviceMode) && moreItems.push({ description: "Share", event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: "users" }); - if (!Doc.UserDoc().noviceMode) { - moreItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: "concierge-bell" }); - moreItems.push({ description: `${this.Document._chromeHidden ? "Show" : "Hide"} Chrome`, event: () => this.Document._chromeHidden = !this.Document._chromeHidden, icon: "project-diagram" }); - - if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) { - moreItems.push({ description: "Export to Google Photos Album", event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.props.Document }).then(console.log), icon: "caret-square-right" }); - moreItems.push({ description: "Tag Child Images via Google Photos", event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: "caret-square-right" }); - moreItems.push({ description: "Write Back Link to Album", event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: "caret-square-right" }); + const more = cm.findByDescription("More..."); + const moreItems = more && "subitems" in more ? more.subitems : []; + if (!Doc.IsSystem(this.rootDoc)) { + (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.UserDoc().noviceMode) && moreItems.push({ description: "Share", event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: "users" }); + if (!Doc.UserDoc().noviceMode) { + moreItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: "concierge-bell" }); + moreItems.push({ description: `${this.Document._chromeHidden ? "Show" : "Hide"} Chrome`, event: () => this.Document._chromeHidden = !this.Document._chromeHidden, icon: "project-diagram" }); + + if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) { + moreItems.push({ description: "Export to Google Photos Album", event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.props.Document }).then(console.log), icon: "caret-square-right" }); + moreItems.push({ description: "Tag Child Images via Google Photos", event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: "caret-square-right" }); + moreItems.push({ description: "Write Back Link to Album", event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: "caret-square-right" }); + } + moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "fingerprint" }); } - moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "fingerprint" }); } - } - if (this.props.removeDocument && !Doc.IsSystem(this.rootDoc) && CurrentUserUtils.ActiveDashboard !== this.props.Document) { // need option to gray out menu items ... preferably with a '?' that explains why they're grayed out (eg., no permissions) - moreItems.push({ description: "Close", event: this.deleteClicked, icon: "times" }); - } - - !more && cm.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" }); - cm.moveAfter(cm.findByDescription("More...")!, cm.findByDescription("OnClick...")!); + if (this.props.removeDocument && !Doc.IsSystem(this.rootDoc) && CurrentUserUtils.ActiveDashboard !== this.props.Document) { // need option to gray out menu items ... preferably with a '?' that explains why they're grayed out (eg., no permissions) + moreItems.push({ description: "Close", event: this.deleteClicked, icon: "times" }); + } - const help = cm.findByDescription("Help..."); - const helpItems: ContextMenuProps[] = help && "subitems" in help ? help.subitems : []; - !Doc.UserDoc().novice && helpItems.push({ description: "Show Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "add:right"), icon: "layer-group" }); - helpItems.push({ description: "Text Shortcuts Ctrl+/", event: () => this.props.addDocTab(Docs.Create.PdfDocument(Utils.prepend("/assets/cheat-sheet.pdf"), { _width: 300, _height: 300 }), "add:right"), icon: "keyboard" }); - !Doc.UserDoc().novice && helpItems.push({ description: "Print Document in Console", event: () => console.log(this.props.Document), icon: "hand-point-right" }); - cm.addItem({ description: "Help...", noexpand: true, subitems: helpItems, icon: "question" }); + !more && cm.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" }); + cm.moveAfter(cm.findByDescription("More...")!, cm.findByDescription("OnClick...")!); + const help = cm.findByDescription("Help..."); + const helpItems: ContextMenuProps[] = help && "subitems" in help ? help.subitems : []; + !Doc.UserDoc().novice && helpItems.push({ description: "Show Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "add:right"), icon: "layer-group" }); + helpItems.push({ description: "Text Shortcuts Ctrl+/", event: () => this.props.addDocTab(Docs.Create.PdfDocument(Utils.prepend("/assets/cheat-sheet.pdf"), { _width: 300, _height: 300 }), "add:right"), icon: "keyboard" }); + !Doc.UserDoc().novice && helpItems.push({ description: "Print Document in Console", event: () => console.log(this.props.Document), icon: "hand-point-right" }); + cm.addItem({ description: "Help...", noexpand: true, subitems: helpItems, icon: "question" }); + } if (!this.topMost) e?.stopPropagation(); // DocumentViews should stop propagation of this event cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15); - !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. + 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. } rootSelected = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || (this.props.Document.rootDocument && this.props.rootSelected?.(outsideReaction)) || false; @@ -792,6 +813,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps </div>; } + get indicatorIcon() { + if (this.props.Document["acl-Public"] !== SharingPermissions.None) return "globe-americas"; + else if (this.props.Document.numGroupsShared || NumCast(this.props.Document.numUsersShared, 0) > 1) return "users"; + else return "user"; + } + @undoBatch hideLinkAnchor = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && (doc.hidden = true), true) anchorPanelWidth = () => this.props.PanelWidth() || 1; @@ -881,7 +908,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps TraceMobx(); const showTitle = this.ShowTitle?.split(":")[0]; const showTitleHover = this.ShowTitle?.includes(":hover"); - const showCaption = StrCast(this.layoutDoc._showCaption); + const showCaption = StrCast(this.layoutDoc._showCaption) && this.Document._viewType !== CollectionViewType.Carousel; const captionView = !showCaption ? (null) : <div className="documentView-captionWrapper" style={{ @@ -889,8 +916,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps color: StrCast(this.layoutDoc["caption-color"]) }}> <DocumentContentsView {...OmitKeys(this.props, ['children']).omit} - yMargin={10} - xMargin={10} + yPadding={10} + xPadding={10} hideOnLeave={true} styleProvider={this.captionStyleProvider} dontRegisterView={true} @@ -951,6 +978,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps let highlighting = !this.props.disableDocBrushing && highlightIndex && !excludeTypes.includes(this.layoutDoc.type as any) && this.layoutDoc._viewType !== CollectionViewType.Linear; highlighting = highlighting && this.props.focus !== emptyFunction && this.layoutDoc.title !== "[pres element template]"; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way + const borderPath = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.BorderPath) || { path: undefined }; + const internal = PresBox.EffectsProvider(this.layoutDoc, this.renderDoc) || this.renderDoc; const boxShadow = highlighting && this.borderRounding && highlightStyle !== "dashed" ? `0 0 0 ${highlightIndex}px ${highlightColor}` : this.boxShadow || (this.props.Document.isTemplateForField ? "black 0.2vw 0.2vw 0.8vw" : undefined); return <div className={DocumentView.ROOT_DIV} ref={this._mainCont} @@ -966,8 +995,21 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps outline: highlighting && !this.borderRounding ? `${highlightColor} ${highlightStyle} ${highlightIndex}px` : "solid 0px", border: highlighting && this.borderRounding && highlightStyle === "dashed" ? `${highlightStyle} ${highlightColor} ${highlightIndex}px` : undefined, boxShadow, + clipPath: borderPath.path ? `path('${borderPath.path}')` : undefined }}> - {PresBox.EffectsProvider(this.layoutDoc, this.renderDoc) || this.renderDoc} + {!borderPath.path ? internal : + <> + {/* <div style={{ clipPath: `path('${borderPath.fill}')` }}> + {internal} + </div> */} + {internal} + <div key="border2" className="documentView-customBorder" style={{ pointerEvents: "none" }} > + <svg style={{ overflow: "visible" }} viewBox={`0 0 ${this.props.PanelWidth()} ${this.props.PanelHeight()}`}> + <path d={borderPath.path} style={{ stroke: "black", fill: "transparent", strokeWidth: borderPath.width }} /> + </svg> + </div> + </> + } </div>; } } diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 0fc7a752f..86250c9d1 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -20,6 +20,7 @@ export interface FieldViewProps extends DocumentViewSharedProps { select: (isCtrlPressed: boolean) => void; isContentActive: (outsideReaction?: boolean) => boolean; + isDocumentActive?: () => boolean; isSelected: (outsideReaction?: boolean) => boolean; scaling?: () => number; setHeight: (height: number) => void; @@ -31,8 +32,8 @@ export interface FieldViewProps extends DocumentViewSharedProps { width?: number; background?: string; color?: string; - xMargin?: number; - yMargin?: number; + xPadding?: number; + yPadding?: number; } @observer diff --git a/src/client/views/nodes/FilterBox.scss b/src/client/views/nodes/FilterBox.scss index d32cc0d2b..107ad2e36 100644 --- a/src/client/views/nodes/FilterBox.scss +++ b/src/client/views/nodes/FilterBox.scss @@ -1,17 +1,155 @@ - - .filterBox-flyout { - width: 400px; display: block; text-align: left; + font-weight: 100; + .filterBox-flyout-facet { - background-color: lightgray; - text-align: left; - display: inline-block; - position: relative; - width: 100%; + background-color: white; + text-align: left; + display: inline-block; + position: relative; + width: 100%; + + .filterBox-flyout-facet-check { + margin-right: 6px; + } + } +} + + +.filter-bookmark { + //display: flex; + + .filter-bookmark-icon { + float: right; + margin-right: 10px; + margin-top: 7px; + } +} + +// .filterBox-bottom { +// // position: fixed; +// // bottom: 0; +// // width: 100%; +// } + +.filterBox-select { + // width: 90%; + margin-top: 5px; + // margin-bottom: 15px; +} + + +.filterBox-saveBookmark { + background-color: #e9e9e9; + border-radius: 11px; + padding-left: 8px; + padding-right: 8px; + padding-top: 5px; + padding-bottom: 5px; + margin: 8px; + display: flex; + font-size: 11px; + cursor: pointer; + + &:hover { + background-color: white; + } + + .filterBox-saveBookmark-icon { + margin-right: 6px; + margin-top: 4px; + margin-left: 2px; + } + +} + +.filterBox-select-scope, +.filterBox-select-bool, +.filterBox-addWrapper, +.filterBox-select-matched, +.filterBox-saveWrapper { + font-size: 10px; + justify-content: center; + justify-items: center; + padding-bottom: 10px; + display: flex; +} + +.filterBox-addWrapper { + font-size: 11px; + width: 100%; +} + +.filterBox-saveWrapper { + width: 100%; +} + +// .filterBox-top { +// padding-bottom: 20px; +// border-bottom: 2px solid black; +// position: fixed; +// top: 0; +// width: 100%; +// } + +.filterBox-select-scope { + padding-bottom: 20px; + border-bottom: 2px solid black; +} + +.filterBox-title { + font-size: 15; + // border: 2px solid black; + width: 100%; + align-self: center; + text-align: center; + background-color: #d3d3d3; +} + +.filterBox-select-bool { + margin-top: 6px; +} + +.filterBox-select-text { + margin-right: 8px; + margin-left: 8px; + margin-top: 3px; +} + +.filterBox-select-box { + margin-right: 2px; + font-size: 30px; + border: 0; + background: transparent; +} + +.filterBox-selection { + border-radius: 6px; + border: none; + background-color: #e9e9e9; + padding: 2px; + + &:hover { + background-color: white; + } +} + +.filterBox-addFilter { + width: 120px; + background-color: #e9e9e9; + border-radius: 12px; + padding: 5px; + margin: 5px; + display: flex; + text-align: center; + justify-content: center; + + &:hover { + background-color: white; } } + .filterBox-treeView { display: flex; flex-direction: column; @@ -20,24 +158,23 @@ position: absolute; right: 0; top: 0; - border-left: solid 1px; z-index: 1; + background-color: #9F9F9F; + + .filterBox-tree { + z-index: 0; + } .filterBox-addfacet { display: inline-block; width: 200px; height: 30px; - background: darkGray; text-align: left; .filterBox-addFacetButton { display: flex; margin: auto; cursor: pointer; - - .filterBox-span { - margin-right: 15px; - } } >div, @@ -50,6 +187,7 @@ .filterBox-tree { display: inline-block; width: 100%; - height: calc(100% - 30px); + margin-bottom: 10px; + //height: calc(100% - 30px); } }
\ No newline at end of file diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx index bf8d2b99e..78449636a 100644 --- a/src/client/views/nodes/FilterBox.tsx +++ b/src/client/views/nodes/FilterBox.tsx @@ -2,55 +2,114 @@ import React = require("react"); import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { computed, observable, action, trace, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { DataSym, Doc, DocListCast, Field, Opt, DocListCastAsync } from "../../../fields/Doc"; +import { Doc, DocListCast, Field, Opt, DocListCastAsync, HeightSym } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; import { List } from "../../../fields/List"; import { RichTextField } from "../../../fields/RichTextField"; import { listSpec, makeInterface } from "../../../fields/Schema"; import { ComputedField, ScriptField } from "../../../fields/ScriptField"; -import { Cast } from "../../../fields/Types"; -import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, returnZero, returnTrue } from "../../../Utils"; +import { Cast, StrCast } from "../../../fields/Types"; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from "../../../Utils"; import { Docs } from "../../documents/Documents"; import { DocumentType } from "../../documents/DocumentTypes"; -import { CollectionDockingView } from "../collections/CollectionDockingView"; import { CollectionTreeView } from "../collections/CollectionTreeView"; import { ViewBoxBaseComponent } from "../DocComponent"; import { SearchBox } from "../search/SearchBox"; import { FieldView, FieldViewProps } from './FieldView'; import './FilterBox.scss'; import { Scripting } from "../../util/Scripting"; +import { SelectionManager } from "../../util/SelectionManager"; import { CollectionView } from "../collections/CollectionView"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; +import Select from "react-select"; +import { UserOptions } from "../../util/GroupManager"; +import { DocumentViewProps } from "./DocumentView"; +import { DefaultStyleProvider, StyleProp } from "../StyleProvider"; +import { CurrentUserUtils } from "../../util/CurrentUserUtils"; +import { EditableView } from "../EditableView"; +import { undoBatch } from "../../util/UndoManager"; type FilterBoxDocument = makeInterface<[typeof documentSchema]>; const FilterBoxDocument = makeInterface(documentSchema); @observer export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDocument>(FilterBoxDocument) { + + constructor(props: Readonly<FieldViewProps>) { + super(props); + const targetDoc = FilterBox.targetDoc; + if (targetDoc && !targetDoc.currentFilter) CurrentUserUtils.setupFilterDocs(targetDoc); + } public static LayoutString(fieldKey: string) { return FieldView.LayoutString(FilterBox, fieldKey); } + public _filterSelected = false; + public _filterMatch = "matched"; + + @computed static get currentContainingCollectionDoc() { + let docView: any = SelectionManager.Views()[0]; + let doc = docView.Document; + + while (docView && doc && doc.type !== "collection") { + doc = docView.props.ContainingCollectionDoc; + docView = docView.props.ContainingCollectionView; + } + + return doc; + } + + // /** + // * @returns the relevant doc according to the value of FilterBox._filterScope i.e. either the Current Dashboard or the Current Collection + // */ + + + // trying to get this to work rather than the version below this + + // @computed static get targetDoc() { + // console.log(FilterBox.currentContainingCollectionDoc.type); + // if (FilterBox._filterScope === "Current Collection") { + // return FilterBox.currentContainingCollectionDoc; + // } + // else return CollectionDockingView.Instance.props.Document; + // // return FilterBox._filterScope === "Current Collection" ? SelectionManager.Views()[0].Document || CollectionDockingView.Instance.props.Document : CollectionDockingView.Instance.props.Document; + // } + + + /** + * @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() { + if (SelectionManager.Views().length) { + return SelectionManager.Views()[0]?.Document.type === DocumentType.COL ? SelectionManager.Views()[0].Document : SelectionManager.Views()[0]?.props.ContainingCollectionDoc!; + } + return CurrentUserUtils.ActiveDashboard; + } + @observable _loaded = false; componentDidMount() { - reaction(() => DocListCastAsync(CollectionDockingView.Instance.props.Document.data), + reaction(() => DocListCastAsync(this.layoutDoc.data), async (activeTabsAsync) => { const activeTabs = await activeTabsAsync; activeTabs && (await SearchBox.foreachRecursiveDocAsync(activeTabs, emptyFunction)); runInAction(() => this._loaded = true); }, { fireImmediately: true }); } + @observable _allDocs = [] as Doc[]; @computed get allDocs() { - const allDocs = new Set<Doc>(); - if (this._loaded && CollectionDockingView.Instance) { - const activeTabs = DocListCast(CollectionDockingView.Instance.props.Document.data); + // trace(); + const targetDoc = FilterBox.targetDoc; + if (this._loaded && targetDoc) { + const allDocs = new Set<Doc>(); + const activeTabs = DocListCast(targetDoc.data); SearchBox.foreachRecursiveDoc(activeTabs, (doc: Doc) => allDocs.add(doc)); - setTimeout(() => CollectionDockingView.Instance.props.Document.allDocuments = new List<Doc>(Array.from(allDocs))); + setTimeout(action(() => this._allDocs = Array.from(allDocs))); } - return allDocs; + return this._allDocs; } @computed get _allFacets() { + // trace(); const noviceReqFields = ["author", "tags", "text", "type"]; const noviceLayoutFields: string[] = [];//["_curPage"]; const noviceFields = [...noviceReqFields, ...noviceLayoutFields]; @@ -60,6 +119,21 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc return Array.from(keys.keys()).filter(key => key[0]).filter(key => key[0] === "#" || key.indexOf("lastModified") !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith("_")) || noviceFields.includes(key) || !Doc.UserDoc().noviceMode).sort(); } + + /** + * The current attributes selected to filter based on + */ + @computed get activeAttributes() { + return DocListCast(this.dataDoc[this.props.fieldKey]); + } + + /** + * @returns a string array of the current attributes + */ + @computed get currentFacets() { + return this.activeAttributes.map(attribute => StrCast(attribute.title)); + } + gatherFieldValues(dashboard: Doc, facetKey: string) { const childDocs = DocListCast((dashboard.data as any)[0].data); const valueSet = new Set<string>(); @@ -80,7 +154,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc subDocs.forEach((t) => { const facetVal = t[facetKey]; if (facetVal instanceof RichTextField) rtFields++; - facetVal && valueSet.add(Field.toString(facetVal as Field)); + facetVal !== undefined && valueSet.add(Field.toString(facetVal as Field)); const fieldKey = Doc.LayoutFieldKey(t); const annos = !Field.toString(Doc.LayoutField(t) as Field).includes("CollectionView"); DocListCast(t[annos ? fieldKey + "-annotations" : fieldKey]).forEach((newdoc) => newarray.push(newdoc)); @@ -92,28 +166,39 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc }); return { strings: Array.from(valueSet.keys()), rtFields }; } - /** - * Responds to clicking the check box in the flyout menu - */ - facetClick = (facetHeader: string) => { - const targetDoc = CollectionDockingView.Instance.props.Document; - const found = DocListCast(this.dataDoc[this.props.fieldKey]).findIndex(doc => doc.title === facetHeader); + removeFilterDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).map(doc => this.removeFilter(StrCast(doc.title))).length ? true : false; + public removeFilter = (filterName: string) => { + const targetDoc = FilterBox.targetDoc; + const filterDoc = targetDoc.currentFilter as Doc; + const attributes = DocListCast(filterDoc.data); + const found = attributes.findIndex(doc => doc.title === filterName); if (found !== -1) { - (this.dataDoc[this.props.fieldKey] as List<Doc>).splice(found, 1); + (filterDoc.data as List<Doc>).splice(found, 1); const docFilter = Cast(targetDoc._docFilters, listSpec("string")); if (docFilter) { let index: number; - while ((index = docFilter.findIndex(item => item.split(":")[0] === facetHeader)) !== -1) { + while ((index = docFilter.findIndex(item => item.split(":")[0] === filterName)) !== -1) { docFilter.splice(index, 1); } } const docRangeFilters = Cast(targetDoc._docRangeFilters, listSpec("string")); if (docRangeFilters) { let index: number; - while ((index = docRangeFilters.findIndex(item => item.split(":")[0] === facetHeader)) !== -1) { + while ((index = docRangeFilters.findIndex(item => item.split(":")[0] === filterName)) !== -1) { docRangeFilters.splice(index, 3); } } + } + return true; + } + /** + * Responds to clicking the check box in the flyout menu + */ + facetClick = (facetHeader: string) => { + const targetDoc = FilterBox.targetDoc; + const found = this.activeAttributes.findIndex(doc => doc.title === facetHeader); + if (found !== -1) { + this.removeFilter(facetHeader); } else { const allCollectionDocs = DocListCast((targetDoc.data as any)[0].data); const facetValues = this.gatherFieldValues(targetDoc, facetHeader); @@ -131,13 +216,18 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc }); let newFacet: Opt<Doc>; if (facetHeader === "text" || facetValues.rtFields / allCollectionDocs.length > 0.1) { - newFacet = Docs.Create.TextDocument("", { _width: 100, _height: 25, system: true, _stayInCollection: true, _hideContextMenu: true, treeViewExpandedView: "layout", title: facetHeader, treeViewOpen: true, _forceActive: true, ignoreClick: true }); + newFacet = Docs.Create.TextDocument("", { + _width: 100, _height: 25, system: true, _stayInCollection: true, target: targetDoc, + treeViewExpandedView: "layout", title: facetHeader, _treeViewOpen: true, _forceActive: true, ignoreClick: true + }); Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox newFacet._textBoxPadding = 4; const scriptText = `setDocFilter(this?.target, "${facetHeader}", text, "match")`; newFacet.onTextChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, text: "string" }); } else if (facetHeader !== "tags" && nonNumbers / facetValues.strings.length < .1) { - newFacet = Docs.Create.SliderDocument({ title: facetHeader, _overflow: "visible", _fitWidth: true, _height: 40, _stayInCollection: true, _hideContextMenu: true, treeViewExpandedView: "layout", treeViewOpen: true }); + newFacet = Docs.Create.SliderDocument({ + title: facetHeader, _overflow: "visible", _fitWidth: true, target: targetDoc, _height: 40, _stayInCollection: true, treeViewExpandedView: "layout", _treeViewOpen: true + }); const newFacetField = Doc.LayoutFieldKey(newFacet); const ranged = Doc.readDocRangeFilter(targetDoc, facetHeader); Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox @@ -147,7 +237,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc newFacet[newFacetField + "-max"] = ranged === undefined ? extendedMaxVal : ranged[1]; Doc.GetProto(newFacet)[newFacetField + "-minThumb"] = extendedMinVal; Doc.GetProto(newFacet)[newFacetField + "-maxThumb"] = extendedMaxVal; - const scriptText = `setDocFilterRange(this?.target, "${facetHeader}", range)`; + const scriptText = `setDocRangeFilter(this?.target, "${facetHeader}", range)`; newFacet.onThumbChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, range: "number" }); } else { newFacet = new Doc(); @@ -160,7 +250,8 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc newFacet.target = targetDoc; newFacet.data = ComputedField.MakeFunction(`readFacetData(self.target, "${facetHeader}")`); } - newFacet && Doc.AddDocToList(this.dataDoc, this.props.fieldKey, newFacet); + newFacet.hideContextMenu = true; + Doc.AddDocToList(this.dataDoc, this.props.fieldKey, newFacet); } } @@ -169,28 +260,145 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc const script = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name }); return script ? () => script : undefined; } + + /** + * Sets whether filters are ANDed or ORed together + */ + @action + changeBool = (e: any) => { + (FilterBox.targetDoc.currentFilter as Doc).filterBoolean = e.currentTarget.value; + } + + /** + * Changes whether to select matched or unmatched documents + */ + @action + changeMatch = (e: any) => { + this._filterMatch = e.currentTarget.value; + } + + @action + changeSelected = () => { + if (this._filterSelected) { + this._filterSelected = false; + SelectionManager.DeselectAll(); + } else { + this._filterSelected = true; + // helper method to select specified docs + } + } + + FilteringStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps | DocumentViewProps>, property: string) { + switch (property.split(":")[0]) { + case StyleProp.Decorations: + if (doc && !doc.treeViewHideHeaderFields) { + return <> + <div style={{ marginRight: "5px", fontSize: "10px" }}> + <select className="filterBox-selection"> + <option value="Is" key="Is">Is</option> + <option value="Is Not" key="Is Not">Is Not</option> + </select> + </div> + <div className="filterBox-treeView-close" onClick={e => this.removeFilter(StrCast(doc.title))}> + <FontAwesomeIcon icon={"times"} size="sm" /> + </div> + </>; + } + } + return DefaultStyleProvider(doc, props, property); + } + suppressChildClick = () => ScriptField.MakeScript("")!; - @computed get flyoutpanel() { - return <div className="filterBox-flyout" style={{ width: `100%`, height: this.props.PanelHeight() - 30 }} onWheel={e => e.stopPropagation()}> - {this._allFacets.map(facet => <label className="filterBox-flyout-facet" key={`${facet}`} onClick={e => this.facetClick(facet)}> - <input type="checkbox" onChange={emptyFunction} checked={DocListCast(this.props.Document[this.props.fieldKey]).some(d => d.title === facet)} /> - <span className="checkmark" /> - {facet} - </label>)} - </div>; + + /** + * Adds a filterDoc to the list of saved filters + */ + saveFilter = () => { + Doc.AddDocToList(Doc.UserDoc(), "savedFilters", this.props.Document); + } + + /** + * Changes the title of the filterDoc + */ + onTitleValueChange = (val: string) => { + this.props.Document.title = val || `FilterDoc for ${FilterBox.targetDoc.title}`; + return true; + } + + /** + * The flyout from which you can select a saved filter to apply + */ + @computed get flyoutPanel() { + return DocListCast(Doc.UserDoc().savedFilters).map(doc => { + return <> + <div className="filterBox-tempFlyout" onWheel={e => e.stopPropagation()} style={{ height: 50, border: "2px" }} onPointerDown={() => this.props.updateFilterDoc?.(doc)}> + {StrCast(doc.title)} + </div> + </>; + } + ); + } + setTreeHeight = (hgt: number) => { + this.layoutDoc._height = hgt + 140; // 50? need to add all the border sizes together. } + + /** + * add lock and hide button decorations for the "Dashboards" flyout TreeView + */ + FilterStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps | DocumentViewProps>, property: string) => { + switch (property.split(":")[0]) { + case StyleProp.Decorations: + return !doc || doc.treeViewHideHeaderFields ? (null) : + <> + <div className={`styleProvider-treeView-hide${doc.hidden ? "-active" : ""}`} onClick={undoBatch(e => + this.removeFilter(StrCast(doc.title)) + )}> + <FontAwesomeIcon icon={"trash"} size="sm" /> + </div> + </>; + } + return this.props.styleProvider?.(doc, props, property); + } + + layoutHeight = () => this.layoutDoc[HeightSym](); render() { const facetCollection = this.props.Document; + // TODO uncomment the line below when the treeview x works + // const options = this._allFacets.filter(facet => this.currentFacets.indexOf(facet) === -1).map(facet => ({ value: facet, label: facet })); + const options = this._allFacets.map(facet => ({ value: facet, label: facet })); return this.props.dontRegisterView ? (null) : <div className="filterBox-treeView" style={{ width: "100%" }}> - <div className="filterBox-addFacet" style={{ width: "100%" }} onPointerDown={e => e.stopPropagation()}> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={this.flyoutpanel}> - <div className="filterBox-addFacetButton"> - <FontAwesomeIcon icon={"edit"} size={"lg"} /> - <span className="filterBox-span">Choose Facets</span> - </div> - </Flyout> + + <div className="filterBox-title"> + <EditableView + key="editableView" + contents={this.props.Document.title} + height={24} + fontSize={15} + GetValue={() => StrCast(this.props.Document.title)} + SetValue={this.onTitleValueChange} + /> </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> + <div className="filterBox-select-text">filters together</div> + </div> + + <div className="filterBox-select"> + <Select + placeholder="Add a filter..." + options={options} + isMulti={false} + onChange={val => this.facetClick((val as UserOptions).value)} + value={null} + closeMenuOnSelect={true} + /> + </div> + <div className="filterBox-tree" key="tree"> <CollectionTreeView Document={facetCollection} @@ -198,15 +406,14 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc fieldKey={this.props.fieldKey} CollectionView={undefined} disableDocBrushing={true} - setHeight={returnFalse} // if the tree view can trigger the height of the filter box to change, then this needs to be filled in. - onChildClick={this.suppressChildClick} + setHeight={this.setTreeHeight} // if the tree view can trigger the height of the filter box to change, then this needs to be filled in. docFilters={returnEmptyFilter} docRangeFilters={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} ContainingCollectionDoc={this.props.ContainingCollectionDoc} ContainingCollectionView={this.props.ContainingCollectionView} PanelWidth={this.props.PanelWidth} - PanelHeight={this.props.PanelHeight} + PanelHeight={this.layoutHeight} rootSelected={this.props.rootSelected} renderDepth={1} dropAction={this.props.dropAction} @@ -220,17 +427,53 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc whenChildContentsActiveChanged={returnFalse} treeViewHideTitle={true} focus={returnFalse} - treeViewHideHeaderFields={true} + treeViewHideHeaderFields={false} onCheckedClick={this.scriptField} dontRegisterView={true} - styleProvider={this.props.styleProvider} + styleProvider={this.FilterStyleProvider} layerProvider={this.props.layerProvider} docViewPath={this.props.docViewPath} scriptContext={this.props.scriptContext} moveDocument={returnFalse} - removeDocument={returnFalse} + removeDocument={this.removeFilterDoc} addDocument={returnFalse} /> </div> + <div className="filterBox-bottom"> + {/* <div className="filterBox-select-matched"> + <input className="filterBox-select-box" type="checkbox" + onChange={this.changeSelected} /> + <div className="filterBox-select-text">select</div> + <select className="filterBox-selection" onChange={e => this.changeMatch(e)}> + <option value="matched" key="matched">matched</option> + <option value="unmatched" key="unmatched">unmatched</option> + </select> + <div className="filterBox-select-text">documents</div> + </div> */} + + <div style={{ display: "flex" }}> + <div className="filterBox-saveWrapper"> + <div className="filterBox-saveBookmark" + onPointerDown={this.saveFilter} + > + <div>SAVE</div> + </div> + </div> + <div className="filterBox-saveWrapper"> + <div className="filterBox-saveBookmark"> + <Flyout className="myFilters-flyout" anchorPoint={anchorPoints.TOP} content={this.flyoutPanel}> + <div>FILTERS</div> + </Flyout> + </div> + </div> + <div className="filterBox-saveWrapper"> + <div className="filterBox-saveBookmark" + onPointerDown={this.props.createNewFilterDoc} + > + <div>NEW</div> + </div> + </div> + </div> + </div> </div>; } } @@ -245,22 +488,26 @@ Scripting.addGlobal(function determineCheckedState(layoutDoc: Doc, facetHeader: } return undefined; }); -Scripting.addGlobal(function readFacetData(targetDoc: Doc, facetHeader: string) { - const allCollectionDocs = DocListCast(CollectionDockingView.Instance?.props.Document.allDocuments); +Scripting.addGlobal(function readFacetData(layoutDoc: Doc, facetHeader: string) { + const allCollectionDocs = new Set<Doc>(); + const activeTabs = DocListCast(layoutDoc.data); + SearchBox.foreachRecursiveDoc(activeTabs, (doc: Doc) => allCollectionDocs.add(doc)); const set = new Set<string>(); if (facetHeader === "tags") allCollectionDocs.forEach(child => Field.toString(child[facetHeader] as Field).split(":").forEach(key => set.add(key))); else allCollectionDocs.forEach(child => set.add(Field.toString(child[facetHeader] as Field))); const facetValues = Array.from(set).filter(v => v); let nonNumbers = 0; + facetValues.map(val => Number.isNaN(Number(val)) && nonNumbers++); const facetValueDocSet = (nonNumbers / facetValues.length > .1 ? facetValues.sort() : facetValues.sort((n1: string, n2: string) => Number(n1) - Number(n2))).map(facetValue => { const doc = new Doc(); doc.system = true; doc.title = facetValue.toString(); - doc.target = targetDoc; + doc.target = layoutDoc; doc.facetHeader = facetHeader; doc.facetValue = facetValue; + doc.treeViewHideHeaderFields = true; doc.treeViewChecked = ComputedField.MakeFunction("determineCheckedState(self.target, self.facetHeader, self.facetValue)"); return doc; }); diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 8a6946b78..c7067f313 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,14 +1,13 @@ -import { action, computed, IReactionDisposer, observable, reaction, runInAction, ObservableMap, untracked } from 'mobx'; +import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx'; import { observer } from "mobx-react"; -import { Dictionary } from 'typescript-collections'; import { DataSym, Doc, DocListCast, WidthSym } from '../../../fields/Doc'; import { documentSchema } from '../../../fields/documentSchemas'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; -import { createSchema, listSpec, makeInterface } from '../../../fields/Schema'; +import { createSchema, makeInterface } from '../../../fields/Schema'; import { ComputedField } from '../../../fields/ScriptField'; -import { Cast, NumCast, StrCast } from '../../../fields/Types'; +import { Cast, NumCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { emptyFunction, OmitKeys, returnOne, Utils } from '../../../Utils'; @@ -59,8 +58,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this._dropDisposer?.(); ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.props.Document)); } + setViewSpec = (anchor: Doc, preview: boolean) => { + + } // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document componentDidMount() { + this.props.setContentView?.(this); // bcz: do not remove this. without it, stepping into an image in the lightbox causes an infinite loop.... this._disposers.sizer = reaction(() => ( { forceFull: this.props.renderDepth < 1 || this.layoutDoc._showFullRes, @@ -102,8 +105,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp const targetDoc = layoutDoc[DataSym]; if (targetDoc[targetField] instanceof ImageField) { this.dataDoc[this.fieldKey] = ObjectField.MakeCopy(targetDoc[targetField] as ImageField); - Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(targetDoc)); - Doc.SetNativeWidth(this.dataDoc, Doc.NativeHeight(targetDoc)); + Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(targetDoc), this.fieldKey); + Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(targetDoc), this.fieldKey); e.stopPropagation(); } } @@ -315,6 +318,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @observable _marqueeing: number[] | undefined; @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); @computed get annotationLayer() { + TraceMobx(); return <div className="imageBox-annotationLayer" style={{ height: this.props.PanelHeight() }} ref={this._annotationLayer} />; } @action @@ -331,7 +335,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp TraceMobx(); const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); const borderRadius = borderRad?.includes("px") ? `${Number(borderRad.split("px")[0]) / (this.props.scaling?.() || 1)}px` : borderRad; - return (<div className={`imageBox`} onContextMenu={this.specificContextMenu} ref={this._mainCont} + return (<div className="imageBox" onContextMenu={this.specificContextMenu} ref={this._mainCont} style={{ width: this.props.PanelWidth() ? undefined : `100%`, height: this.props.PanelWidth() ? undefined : `100%`, @@ -340,32 +344,28 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp }} > <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} renderDepth={this.props.renderDepth + 1} - ContainingCollectionDoc={this.props.ContainingCollectionDoc} - CollectionView={undefined} - PanelHeight={this.props.PanelHeight} - scaling={returnOne} - ScreenToLocalTransform={this.screenToLocalTransform} - PanelWidth={this.props.PanelWidth} fieldKey={this.annotationKey} + CollectionView={undefined} isAnnotationOverlay={true} - docFilters={this.props.docFilters} - docRangeFilters={this.props.docRangeFilters} - searchFilterDocs={this.props.searchFilterDocs} - removeDocument={this.removeDocument} - moveDocument={this.moveDocument} - addDocument={this.addDocument} annotationLayerHostsContent={true} - focus={this.props.focus} - isSelected={this.props.isSelected} + PanelWidth={this.props.PanelWidth} + PanelHeight={this.props.PanelHeight} + ScreenToLocalTransform={this.screenToLocalTransform} select={emptyFunction} isContentActive={this.isContentActive} - whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}> + scaling={returnOne} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + removeDocument={this.removeDocument} + moveDocument={this.moveDocument} + addDocument={this.addDocument}> {this.contentFunc} </CollectionFreeFormView> {this.annotationLayer} {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) : - <MarqueeAnnotator rootDoc={this.rootDoc} - scrollTop={0} down={this._marqueeing} + <MarqueeAnnotator + rootDoc={this.rootDoc} + scrollTop={0} + down={this._marqueeing} scaling={this.props.scaling} docView={this.props.docViewPath().lastElement()} addDocument={this.addDocument} diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index 45e3c8382..b73fb10df 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -26,7 +26,7 @@ interface LinkDocPreviewProps { @observer export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { @action public static Clear() { LinkDocPreview.LinkInfo = undefined; } - @action public static SetLinkInfo(info?: LinkDocPreviewProps) { LinkDocPreview.LinkInfo != info && (LinkDocPreview.LinkInfo = info); } + @action public static SetLinkInfo(info?: LinkDocPreviewProps) { LinkDocPreview.LinkInfo !== info && (LinkDocPreview.LinkInfo = info); } _infoRef = React.createRef<HTMLDivElement>(); @observable public static LinkInfo: Opt<LinkDocPreviewProps>; diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index d55156057..d5056d934 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -262,6 +262,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps loaded={!Doc.NativeAspect(this.dataDoc) ? this.loaded : undefined} setPdfViewer={this.setPdfViewer} addDocument={this.addDocument} + moveDocument={this.moveDocument} + removeDocument={this.removeDocument} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} startupLive={true} ContentScaling={this.props.scaling} diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx index 8c5f77550..f3fb6ff17 100644 --- a/src/client/views/nodes/PresBox.tsx +++ b/src/client/views/nodes/PresBox.tsx @@ -5,7 +5,7 @@ import { action, computed, IReactionDisposer, observable, ObservableMap, reactio import { observer } from "mobx-react"; import { ColorState, SketchPicker } from "react-color"; import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal'; -import { Doc, DocListCast, DocListCastAsync } from "../../../fields/Doc"; +import { Doc, DocListCast, DocListCastAsync, FieldResult } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; import { InkTool } from "../../../fields/InkField"; import { List } from "../../../fields/List"; @@ -29,6 +29,7 @@ import { CollectionFreeFormDocumentView } from "./CollectionFreeFormDocumentView import { FieldView, FieldViewProps } from './FieldView'; import "./PresBox.scss"; import Color = require("color"); +import { LightboxView } from "../LightboxView"; export enum PresMovement { Zoom = "zoom", @@ -246,11 +247,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> // } } - stopTempMedia = (targetDoc: Doc) => { - if (targetDoc.type === DocumentType.AUDIO) { + stopTempMedia = (targetDocField: FieldResult) => { + const targetDoc = Cast(targetDocField, Doc, null); + if (targetDoc?.type === DocumentType.AUDIO) { if (this._mediaTimer && this._mediaTimer[1] === targetDoc) clearTimeout(this._mediaTimer[0]); targetDoc._audioStop = true; - } else if (targetDoc.type === DocumentType.VID) { + } else if (targetDoc?.type === DocumentType.VID) { if (this._mediaTimer && this._mediaTimer[1] === targetDoc) clearTimeout(this._mediaTimer[0]); targetDoc._triggerVideoStop = true; } @@ -330,14 +332,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> this.rootDoc._itemIndex = index; const activeItem: Doc = this.activeItem; const targetDoc: Doc = this.targetDoc; - if (from && from.mediaStopTriggerList && this.layoutDoc.presStatus !== PresStatus.Edit) { - const mediaDocList = DocListCast(from.mediaStopTriggerList); - mediaDocList.forEach((doc) => { - this.stopTempMedia(Cast(doc.presentationTargetDoc, Doc, null)); - }); + if (from?.mediaStopTriggerList && this.layoutDoc.presStatus !== PresStatus.Edit) { + DocListCast(from.mediaStopTriggerList).forEach(this.stopTempMedia); } - if (from && from.mediaStop === "auto" && this.layoutDoc.presStatus !== PresStatus.Edit) { - this.stopTempMedia(Cast(from.presentationTargetDoc, Doc, null)); + if (from?.mediaStop === "auto" && this.layoutDoc.presStatus !== PresStatus.Edit) { + this.stopTempMedia(from.presentationTargetDoc); } // If next slide is audio / video 'Play automatically' then the next slide should be played if (this.layoutDoc.presStatus !== PresStatus.Edit && (targetDoc.type === DocumentType.AUDIO || targetDoc.type === DocumentType.VID) && (activeItem.mediaStart === "auto")) { @@ -433,10 +432,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> }; // If openDocument is selected then it should open the document for the user if (activeItem.openDocument) { - openInTab(targetDoc); + 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 } 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 } @@ -574,6 +575,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> if (this._presTimer) clearTimeout(this._presTimer); this.layoutDoc.presStatus = PresStatus.Manual; this.layoutDoc.presLoop = false; + this.childDocs.forEach(this.stopTempMedia); } } @@ -633,10 +635,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> updateMinimize = () => { const docView = DocumentManager.Instance.getDocumentView(this.layoutDoc); if (CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) { + console.log("case 1"); this.layoutDoc.presStatus = PresStatus.Edit; Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocs as Doc), undefined, this.rootDoc); CollectionDockingView.AddSplit(this.rootDoc, "right"); } else if (this.layoutDoc.context && docView) { + console.log("case 2"); this.layoutDoc.presStatus = PresStatus.Edit; clearTimeout(this._presTimer); const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); @@ -647,15 +651,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> docView.props.removeDocument?.(this.layoutDoc); Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), undefined, this.rootDoc); } else { - this.layoutDoc.presStatus = PresStatus.Edit; - clearTimeout(this._presTimer); - const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); - this.rootDoc.x = pt[0] + (this.props.PanelWidth() - 250); - this.rootDoc.y = pt[1] + 10; - this.rootDoc._height = 35; - this.rootDoc._width = 250; - this.props.addDocTab?.(this.rootDoc, "close"); - Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), undefined, this.rootDoc); + console.log("case 3"); + // TODO glr: fix this case } } @@ -1193,7 +1190,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> const effect = targetDoc.presEffect ? targetDoc.presEffect : 'None'; activeItem.presMovement = activeItem.presMovement ? activeItem.presMovement : 'Zoom'; return ( - <div className={`presBox-ribbon ${this.transitionTools && this.layoutDoc.presStatus === "edit" ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onClick={action(e => { e.stopPropagation(); this.openMovementDropdown = false; this.openEffectDropdown = false; })}> + <div className={`presBox-ribbon ${this.transitionTools && this.layoutDoc.presStatus === PresStatus.Edit ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onClick={action(e => { e.stopPropagation(); this.openMovementDropdown = false; this.openEffectDropdown = false; })}> <div className="ribbon-box"> Movement {isPresCollection || (isPresCollection && isPinWithView) ? @@ -1248,7 +1245,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <div className="ribbon-doubleButton"> {isPresCollection ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Hide before presented"}</div></>}><div className={`ribbon-toggle ${activeItem.presHideBefore ? "active" : ""}`} onClick={() => this.updateHideBefore(activeItem)}>Hide before</div></Tooltip>} {isPresCollection ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Hide after presented"}</div></>}><div className={`ribbon-toggle ${activeItem.presHideAfter ? "active" : ""}`} onClick={() => this.updateHideAfter(activeItem)}>Hide after</div></Tooltip>} - <Tooltip title={<><div className="dash-tooltip">{"Open document in a new tab"}</div></>}><div className="ribbon-toggle" style={{ backgroundColor: activeItem.openDocument ? PresColor.LightBlue : "" }} onClick={() => this.updateOpenDoc(activeItem)}>Open</div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Open in lightbox view"}</div></>}><div className="ribbon-toggle" style={{ backgroundColor: activeItem.openDocument ? PresColor.LightBlue : "" }} onClick={() => this.updateOpenDoc(activeItem)}>Lightbox</div></Tooltip> </div> {(type === DocumentType.AUDIO || type === DocumentType.VID) ? (null) : <> <div className="ribbon-doubleButton" > diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index 862344a94..3c281086a 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -15,16 +15,17 @@ import { emptyFunction, OmitKeys, returnFalse, returnOne, Utils, numberRange } f import { DocUtils } from "../../documents/Documents"; import { DocumentType } from "../../documents/DocumentTypes"; import { Networking } from "../../Network"; +import { CaptureManager } from "../../util/CaptureManager"; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; import { CollectionStackedTimeline } from "../collections/CollectionStackedTimeline"; import { ContextMenu } from "../ContextMenu"; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from "../DocComponent"; import { FieldView, FieldViewProps } from './FieldView'; +import { FormattedTextBox } from "./formattedText/FormattedTextBox"; import "./ScreenshotBox.scss"; import { VideoBox } from "./VideoBox"; import { TraceMobx } from "../../../fields/util"; -import { FormattedTextBox } from "./formattedText/FormattedTextBox"; import { Canvas } from 'react-three-fiber'; import * as THREE from 'three'; import { Vector3, Vector2, Camera } from "three" @@ -116,7 +117,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ScreenshotBox, fieldKey); } private _audioRec: any; private _videoRec: any; - @observable private _videoRef: HTMLVideoElement | undefined; + @observable private _videoRef: HTMLVideoElement | null = null; @observable _screenCapture = false; @computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); } @@ -174,7 +175,15 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl @computed get content() { const interactive = CurrentUserUtils.SelectedTool !== InkTool.None || !this.props.isSelected() ? "" : "-interactive"; - return <video className={"videoBox-content" + interactive} key="video" ref={action((r: any) => this._videoRef = r)} + return <video className={"videoBox-content" + interactive} key="video" + ref={r => { + this._videoRef = r; + setTimeout(() => { + if (this.rootDoc.mediaState === "pendingRecording" && this._videoRef) { // TODO glr: use mediaState + this.toggleRecording(); + } + }, 1000); + }} autoPlay={this._screenCapture} style={{ width: this._screenCapture ? "100%" : undefined, height: this._screenCapture ? "100%" : undefined }} onCanPlay={this.videoLoad} @@ -245,6 +254,8 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl this.dataDoc.mediaState = "paused"; const ind = DocUtils.ActiveRecordings.indexOf(this); ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1)); + + CaptureManager.Instance.open(this.rootDoc); } }); @@ -287,31 +298,27 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl ContainingCollectionDoc={this.props.ContainingCollectionDoc}> {this.contentFunc} </CollectionFreeFormView></div> - {!(this.dataDoc[this.fieldKey + "-dictation"] instanceof Doc) ? (null) : - <div className="videoBox-dictation" style={{ position: "relative", height: this.formattedPanelHeight() }}> + <div style={{ position: "relative", height: this.formattedPanelHeight() }}> + {!(this.dataDoc[this.fieldKey + "-dictation"] instanceof Doc) ? (null) : <FormattedTextBox {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} Document={this.dataDoc[this.fieldKey + "-dictation"]} fieldKey={"text"} PanelHeight={this.formattedPanelHeight} - PanelWidth={this.props.PanelWidth} - focus={this.props.focus} - isSelected={this.props.isSelected} isAnnotationOverlay={true} select={emptyFunction} isContentActive={returnFalse} scaling={returnOne} - xMargin={25} - yMargin={10} + xPadding={25} + yPadding={10} whenChildContentsActiveChanged={emptyFunction} removeDocument={returnFalse} moveDocument={returnFalse} addDocument={returnFalse} CollectionView={undefined} - ScreenToLocalTransform={this.props.ScreenToLocalTransform} renderDepth={this.props.renderDepth + 1} ContainingCollectionDoc={this.props.ContainingCollectionDoc}> - </FormattedTextBox> - </div>} + </FormattedTextBox>} + </div> </div> {!this.props.isSelected() ? (null) : <div className="screenshotBox-uiButtons"> <div className="screenshotBox-recorder" key="snap" onPointerDown={this.toggleRecording} > diff --git a/src/client/views/nodes/SliderBox.tsx b/src/client/views/nodes/SliderBox.tsx index 67f8aed3c..92d1f7446 100644 --- a/src/client/views/nodes/SliderBox.tsx +++ b/src/client/views/nodes/SliderBox.tsx @@ -4,8 +4,7 @@ import * as React from 'react'; import { Handles, Rail, Slider, Ticks, Tracks } from 'react-compound-slider'; import { documentSchema } from '../../../fields/documentSchemas'; import { createSchema, makeInterface } from '../../../fields/Schema'; -import { ScriptField } from '../../../fields/ScriptField'; -import { Cast, NumCast, StrCast } from '../../../fields/Types'; +import { NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxBaseComponent } from '../DocComponent'; @@ -41,7 +40,7 @@ export class SliderBox extends ViewBoxBaseComponent<FieldViewProps, SliderDocume onChange = (values: readonly number[]) => runInAction(() => { this.dataDoc[this.minThumbKey] = values[0]; this.dataDoc[this.maxThumbKey] = values[1]; - Cast(this.layoutDoc.onThumbChanged, ScriptField, null)?.script.run({ + ScriptCast(this.layoutDoc.onThumbChanged, null)?.script.run({ self: this.rootDoc, scriptContext: this.props.scriptContext, range: values, this: this.layoutDoc }); diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss index dd8d77603..30f0c4393 100644 --- a/src/client/views/nodes/VideoBox.scss +++ b/src/client/views/nodes/VideoBox.scss @@ -1,3 +1,13 @@ +.mini-viewer{ + cursor: grab; + position: absolute; + right: 10; + top: 10; + opacity: 0.1; + transition: all 0.4s; + color: white; +} + .videoBox { transform-origin: top left; width: 100%; diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index fb58ddefc..8b75f45b0 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -567,22 +567,22 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp <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}%` }}> <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} + renderDepth={this.props.renderDepth + 1} fieldKey={this.annotationKey} + CollectionView={undefined} isAnnotationOverlay={true} annotationLayerHostsContent={true} - select={emptyFunction} - isContentActive={this.isContentActive} - scaling={returnOne} - docFilters={this.timelineDocFilter} PanelWidth={this.panelWidth} PanelHeight={this.panelHeight} ScreenToLocalTransform={this.screenToLocalTransform} + docFilters={this.timelineDocFilter} + select={emptyFunction} + isContentActive={this.isContentActive} + scaling={returnOne} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} removeDocument={this.removeDocument} moveDocument={this.moveDocument} - addDocument={this.addDocWithTimecode} - CollectionView={undefined} - renderDepth={this.props.renderDepth + 1}> + addDocument={this.addDocWithTimecode}> {this.contentFunc} </CollectionFreeFormView> </div> @@ -591,16 +591,17 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp {this.renderTimeline} {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) : <MarqueeAnnotator - scrollTop={0} rootDoc={this.rootDoc} + scrollTop={0} down={this._marqueeing} - docView={this.props.docViewPath().lastElement()} scaling={this.marqueeFitScaling} + docView={this.props.docViewPath().lastElement()} containerOffset={this.marqueeOffset} addDocument={this.addDocWithTimecode} finishMarquee={this.finishMarquee} savedAnnotations={this._savedAnnotations} - annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} + annotationLayer={this._annotationLayer.current} + mainCont={this._mainCont.current} />} </div> </div >); diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index ab178c60b..fc6f9ceab 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -2,20 +2,22 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import * as WebRequest from 'web-request'; -import { Doc, DocListCast, HeightSym, Opt, StrListCast, WidthSym } from "../../../fields/Doc"; +import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; import { Id } from "../../../fields/FieldSymbols"; import { HtmlField } from "../../../fields/HtmlField"; import { InkTool } from "../../../fields/InkField"; import { List } from "../../../fields/List"; -import { makeInterface, listSpec } from "../../../fields/Schema"; +import { listSpec, makeInterface } from "../../../fields/Schema"; +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, returnOne, smoothScroll, Utils, setupMoveUpEvents } 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 { Scripting } from "../../util/Scripting"; import { SnappingManager } from "../../util/SnappingManager"; import { undoBatch } from "../../util/UndoManager"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; @@ -79,7 +81,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps runInAction(() => { this._url = this.webField?.toString() || ""; - this._annotationKey = "annotations-" + this.urlHash(this._url); + this._annotationKey = "annotations-" + WebBox.urlHash(this._url); + // bcz: need to make sure that doc.data-annotations points to the currently active web page's annotations (this could/should be when the doc is created) + this.dataDoc[this.fieldKey + "-annotations"] = ComputedField.MakeFunction(`copyField(this["${this.fieldKey}-annotations-"+urlHash(this["${this.fieldKey}"]?.url?.toString()))`); }); this._disposers.selection = reaction(() => this.props.isSelected(), @@ -289,7 +293,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (future.length) { history.push(this._url); this.dataDoc[this.fieldKey] = new WebField(new URL(this._url = future.pop()!)); - this._annotationKey = "annotations-" + this.urlHash(this._url); + this._annotationKey = "annotations-" + WebBox.urlHash(this._url); return true; } return false; @@ -303,13 +307,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (future === undefined) this.dataDoc[this.fieldKey + "-future"] = new List<string>([this._url]); else future.push(this._url); this.dataDoc[this.fieldKey] = new WebField(new URL(this._url = history.pop()!)); - this._annotationKey = "annotations-" + this.urlHash(this._url); + this._annotationKey = "annotations-" + WebBox.urlHash(this._url); return true; } return false; } - urlHash = (s: string) => { + static urlHash = (s: string) => { return s.split('').reduce((a: any, b: any) => { a = ((a << 5) - a) + b.charCodeAt(0); return a & a; }, 0); } @@ -331,7 +335,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps future && (future.length = 0); } this._url = newUrl; - this._annotationKey = "annotations-" + this.urlHash(this._url); + this._annotationKey = "annotations-" + WebBox.urlHash(this._url); this.dataDoc[this.fieldKey] = new WebField(new URL(newUrl)); } catch (e) { console.log("WebBox URL error:" + this._url); @@ -507,7 +511,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps PanelHeight={this.panelHeight} dropAction={"alias"} select={emptyFunction} - isContentActive={this.isContentActive} + isContentActive={returnFalse} ContentScaling={returnOne} bringToFront={emptyFunction} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} @@ -555,4 +559,5 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps </button> </div>); } -}
\ No newline at end of file +} +Scripting.addGlobal(function urlHash(url: string) { return WebBox.urlHash(url); });
\ No newline at end of file diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 208accfc7..b98ec870a 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 } from "mobx"; +import { action, computed, IReactionDisposer, reaction, runInAction, observable } from "mobx"; import { observer } from "mobx-react"; import { baseKeymap, selectAll } from "prosemirror-commands"; import { history } from "prosemirror-history"; @@ -68,8 +68,8 @@ 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) - xMargin?: number; // used to override document's settings for xMargin --- see CollectionCarouselView - yMargin?: number; + xPadding?: number; // used to override document's settings for xMargin --- see CollectionCarouselView + yPadding?: number; noSidebar?: boolean; dontSelectOnLoad?: boolean; // suppress selecting the text box when loaded } @@ -297,7 +297,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this._editorView.updateState(EditorState.fromJSON(this.config, json)); } } - if (window.getSelection()?.isCollapsed) AnchorMenu.Instance.fadeOut(true); + if (window.getSelection()?.isCollapsed && this.props.isSelected()) { + AnchorMenu.Instance.fadeOut(true); + } } } @@ -669,7 +671,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if (this._break) { const textanchor = Docs.Create.TextanchorDocument({ title: "dictation anchor" }); this.addDocument(textanchor); - const link = DocUtils.MakeLinkToActiveAudio(textanchor, false).lastElement(); + const link = DocUtils.MakeLinkToActiveAudio(() => textanchor, false).lastElement(); link && (Doc.GetProto(link).isDictation = true); if (!link) return; const audioanchor = Cast(link.anchor2, Doc, null); @@ -1062,8 +1064,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp }); } setupEditor(config: any, fieldKey: string) { - const curText = Cast(this.dataDoc[this.props.fieldKey], RichTextField, null); - const rtfField = Cast((!curText?.Text && this.layoutDoc[this.props.fieldKey]) || this.dataDoc[fieldKey], RichTextField); + const curText = Cast(this.dataDoc[this.props.fieldKey], RichTextField, null) || StrCast(this.dataDoc[this.props.fieldKey]); + const rtfField = Cast((!curText && this.layoutDoc[this.props.fieldKey]) || this.dataDoc[fieldKey], RichTextField); if (this.ProseRef) { const self = this; this._editorView?.destroy(); @@ -1116,7 +1118,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const tr = this._editorView.state.tr.setStoredMarks(storedMarks).insertText(FormattedTextBox.SelectOnLoadChar, this._editorView.state.doc.content.size - 1, this._editorView.state.doc.content.size).setStoredMarks(storedMarks); this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size)))); FormattedTextBox.SelectOnLoadChar = ""; - } else if (curText?.Text && !FormattedTextBox.DontSelectInitialText) { + } else if (curText && !FormattedTextBox.DontSelectInitialText) { selectAll(this._editorView!.state, this._editorView?.dispatch); this.startUndoTypingBatch(); } @@ -1130,6 +1132,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } componentWillUnmount() { + FormattedTextBox.Focused === this && (FormattedTextBox.Focused = undefined); Object.values(this._disposers).forEach(disposer => disposer?.()); this.endUndoTypingBatch(); this.unhighlightSearchTerms(); @@ -1234,8 +1237,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp @action onFocused = (e: React.FocusEvent): void => { //applyDevTools.applyDevTools(this._editorView); + FormattedTextBox.Focused = this; this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props); } + @observable public static Focused: FormattedTextBox | undefined; onClick = (e: React.MouseEvent): void => { if (Math.abs(e.clientX - this._downX) > 4 || Math.abs(e.clientY - this._downY) > 4) { @@ -1337,7 +1342,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this._undoTyping = undefined; return wasUndoing; } + @action onBlur = (e: any) => { + FormattedTextBox.Focused === this && (FormattedTextBox.Focused = undefined); if (RichTextMenu.Instance?.view === this._editorView && !this.props.isSelected(true)) { RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined); } @@ -1470,8 +1477,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp NativeHeight={returnZero} PanelHeight={this.props.PanelHeight} PanelWidth={this.sidebarWidth} - xMargin={0} - yMargin={0} + xPadding={0} + yPadding={0} scaleField={this.SidebarKey + "-scale"} isAnnotationOverlay={false} select={emptyFunction} @@ -1503,7 +1510,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp 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); const minimal = this.props.ignoreAutoHeight; - const margins = NumCast(this.layoutDoc._yMargin, this.props.yMargin || 0); + const margins = NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0); 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" : ""; @@ -1522,8 +1529,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp style={{ overflow: this.autoHeight ? "hidden" : undefined, height: this.props.height || (this.autoHeight && this.props.renderDepth ? "max-content" : undefined), - background: this.props.background ? this.props.background : StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor)), - color: this.props.color ? this.props.color : StrCast(this.layoutDoc[this.props.fieldKey + "-color"], this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color)), + 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), pointerEvents: interactive ? undefined : "none", fontSize: this.props.fontSize || Cast(this.layoutDoc._fontSize, "string", null), fontWeight: Cast(this.layoutDoc._fontWeight, "number", null), @@ -1549,7 +1556,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp onScroll={this.onScroll} onDrop={this.ondrop} > <div className={minimal ? "formattedTextBox-minimal" : `formattedTextBox-inner${rounded}${selPaddingClass}`} ref={this.createDropTarget} style={{ - padding: this.layoutDoc._textBoxPadding ? StrCast(this.layoutDoc._textBoxPadding) : `${padding}px`, + padding: StrCast(this.layoutDoc._textBoxPadding, `${padding}px`), pointerEvents: !active && !SnappingManager.GetIsDragging() ? (this.layoutDoc.isLinkButton ? "none" : undefined) : undefined }} /> @@ -1558,7 +1565,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp {(this.props.noSidebar || this.Document._noSidebar) || this.Document._singleLine ? (null) : this.sidebarHandle} {!this.layoutDoc._showAudio ? (null) : this.audioHandle} </div> - </div> + </div > ); } } diff --git a/src/client/views/nodes/formattedText/RichTextMenu.scss b/src/client/views/nodes/formattedText/RichTextMenu.scss index fbc468292..1d24d6833 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.scss +++ b/src/client/views/nodes/formattedText/RichTextMenu.scss @@ -95,9 +95,10 @@ .color-preview-button { .color-preview { - width: 100%; - height: 3px; - margin-top: 3px; + width: 60%; + top: 80%; + position: absolute; + height: 4px; } } diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 5da868281..071491463 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -976,7 +976,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const row2 = <div className="antimodeMenu-row row-2" key="row2"> {this.collapsed ? this.getDragger() : (null)} <div key="row 2" style={{ display: this.collapsed ? "none" : undefined }}> - <div className="richTextMenu-divider" key="divider 3" />, + <div className="richTextMenu-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); diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 4c181e49a..a2e7b0600 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -9,7 +9,7 @@ import { createSchema } from "../../../fields/Schema"; import { Cast, NumCast, ScriptCast, StrCast } from "../../../fields/Types"; import { PdfField } from "../../../fields/URLField"; import { TraceMobx } from "../../../fields/util"; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, OmitKeys, smoothScroll, Utils } from "../../../Utils"; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, OmitKeys, smoothScroll, Utils, returnFalse } from "../../../Utils"; import { DocUtils } from "../../documents/Documents"; import { Networking } from "../../Network"; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; @@ -516,6 +516,7 @@ export class PDFViewer extends React.Component<IViewerProps> { }}> <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} isAnnotationOverlay={true} + isContentActive={returnFalse} fieldKey={this.props.fieldKey + "-annotations"} setPreviewCursor={this.setPreviewCursor} PanelHeight={this.panelHeight} diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx index a1fc77a92..f15d51764 100644 --- a/src/client/views/presentationview/PresElementBox.tsx +++ b/src/client/views/presentationview/PresElementBox.tsx @@ -57,6 +57,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc @computed get targetDoc() { return Cast(this.rootDoc.presentationTargetDoc, Doc, null) || this.rootDoc; } componentDidMount() { + this.layoutDoc.hideLinkButton = true; this._heightDisposer = reaction(() => [this.rootDoc.presExpandInlineButton, this.collapsedHeight], params => this.layoutDoc._height = NumCast(params[1]) + (Number(params[0]) ? 100 : 0), { fireImmediately: true }); } @@ -114,6 +115,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc searchFilterDocs={this.props.searchFilterDocs} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} + hideLinkButton={true} /> <div className="presItem-embeddedMask" /> </div>; diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 5b3e21e34..bd0ba3ad7 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -9,7 +9,6 @@ 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 { CollectionDockingView } from "../client/views/collections/CollectionDockingView"; import { intersectRect, Utils } from "../Utils"; import { DateField } from "./DateField"; import { Copy, HandleUpdate, Id, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update } from "./FieldSymbols"; @@ -201,7 +200,7 @@ export class Doc extends RefField { public [FieldsSym] = () => this[Self].___fields; // Object.keys(this).reduce((fields, key) => { fields[key] = this[key]; return fields; }, {} as any); public [WidthSym] = () => NumCast(this[SelfProxy]._width); public [HeightSym] = () => NumCast(this[SelfProxy]._height); - public [ToScriptString] = () => `DOC-"${this[Self][Id]}"-`; + public [ToScriptString] = () => `idToDoc("${this[Self][Id]}")`; public [ToString] = () => `Doc(${GetEffectiveAcl(this[SelfProxy]) === AclPrivate ? "-inaccessible-" : this[SelfProxy].title})`; public get [LayoutSym]() { return this[SelfProxy].__LAYOUT__; } public get [DataSym]() { @@ -911,8 +910,8 @@ export namespace Doc { } export function NativeWidth(doc?: Doc, dataDoc?: Doc, useWidth?: boolean) { return !doc ? 0 : NumCast(doc._nativeWidth, NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + "-nativeWidth"], useWidth ? doc[WidthSym]() : 0)); } export function NativeHeight(doc?: Doc, dataDoc?: Doc, useHeight?: boolean) { return !doc ? 0 : NumCast(doc._nativeHeight, NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + "-nativeHeight"], useHeight ? doc[HeightSym]() : 0)); } - export function SetNativeWidth(doc: Doc, width: number | undefined) { doc[Doc.LayoutFieldKey(doc) + "-nativeWidth"] = width; } - export function SetNativeHeight(doc: Doc, height: number | undefined) { doc[Doc.LayoutFieldKey(doc) + "-nativeHeight"] = height; } + export function SetNativeWidth(doc: Doc, width: number | undefined, fieldKey?: string) { doc[(fieldKey ?? Doc.LayoutFieldKey(doc)) + "-nativeWidth"] = width; } + export function SetNativeHeight(doc: Doc, height: number | undefined, fieldKey?: string) { doc[(fieldKey ?? Doc.LayoutFieldKey(doc)) + "-nativeHeight"] = height; } const manager = new DocData(); @@ -1059,8 +1058,8 @@ export namespace Doc { prevLayout === "icon" && (doc.deiconifyLayout = undefined); doc.layoutKey = deiconify || "layout"; } - export function setDocFilterRange(target: Doc, key: string, range?: number[]) { - const container = target ?? CollectionDockingView.Instance.props.Document; + export function setDocRangeFilter(container: Opt<Doc>, key: string, range?: number[]) { + if (!container) return; const docRangeFilters = Cast(container._docRangeFilters, listSpec("string"), []); for (let i = 0; i < docRangeFilters.length; i += 3) { if (docRangeFilters[i] === key) { @@ -1079,9 +1078,9 @@ 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(target: Opt<Doc>, key: string, value: any, modifiers: "remove" | "match" | "check" | "x", toggle?: boolean, fieldPrefix?: string, append: boolean = true) { - const container = target ?? CollectionDockingView.Instance.props.Document; - const filterField = "_" + (fieldPrefix ? fieldPrefix + "-" : "") + "docFilters"; + export function setDocFilter(container: Opt<Doc>, key: string, value: any, modifiers: "remove" | "match" | "check" | "x", toggle?: boolean, fieldSuffix?: string, append: boolean = true) { + if (!container) return; + const filterField = "_" + (fieldSuffix ? fieldSuffix + "-" : "") + "docFilters"; const docFilters = Cast(container[filterField], listSpec("string"), []); runInAction(() => { for (let i = 0; i < docFilters.length; i++) { @@ -1160,12 +1159,12 @@ export namespace Doc { return ndoc; } - export function toIcon(doc: Doc) { - switch (StrCast(doc.type)) { + export function toIcon(doc?: Doc, isOpen?: boolean) { + switch (StrCast(doc?.type)) { case DocumentType.IMG: return "image"; case DocumentType.COMPARISON: return "columns"; case DocumentType.RTF: return "sticky-note"; - case DocumentType.COL: return !doc.isFolder ? "folder" : "chevron-right"; + case DocumentType.COL: return !doc?.isFolder ? "folder" + (isOpen ? "-open" : "") : "chevron-" + (isOpen ? "down" : "right"); case DocumentType.WEB: return "globe-asia"; case DocumentType.SCREENSHOT: return "photo-video"; case DocumentType.WEBCAM: return "video"; @@ -1325,6 +1324,7 @@ export namespace Doc { } +Scripting.addGlobal(function idToDoc(id: string): any { return DocServer.GetCachedRefField(id); }); Scripting.addGlobal(function renameAlias(doc: any, n: any) { return StrCast(Doc.GetProto(doc).title).replace(/\([0-9]*\)/, "") + `(${n})`; }); Scripting.addGlobal(function getProto(doc: any) { return Doc.GetProto(doc); }); Scripting.addGlobal(function getDocTemplate(doc?: any) { return Doc.getDocTemplate(doc); }); @@ -1353,4 +1353,4 @@ Scripting.addGlobal(function selectedDocs(container: Doc, excludeCollections: bo return docs.length ? new List(docs) : prevValue; }); Scripting.addGlobal(function setDocFilter(container: Doc, key: string, value: any, modifiers: "match" | "check" | "x" | "remove") { Doc.setDocFilter(container, key, value, modifiers); }); -Scripting.addGlobal(function setDocFilterRange(container: Doc, key: string, range: number[]) { Doc.setDocFilterRange(container, key, range); }); +Scripting.addGlobal(function setDocRangeFilter(container: Doc, key: string, range: number[]) { Doc.setDocRangeFilter(container, key, range); }); |