diff options
29 files changed, 2224 insertions, 143 deletions
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index 7578b7df0..e98539498 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -13,6 +13,7 @@ export enum DocumentType { INK = "ink", // ink stroke SCREENSHOT = "screenshot", // view of a desktop application FONTICON = "fonticonbox", // font icon + MENUICON = "menuiconbox", QUERY = "query", // search query LABEL = "label", // simple text label BUTTON = "button", // onClick button diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 7e094089f..f46b38883 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -31,6 +31,7 @@ import { ColorBox } from "../views/nodes/ColorBox"; import { ComparisonBox } from "../views/nodes/ComparisonBox"; import { DocHolderBox } from "../views/nodes/DocHolderBox"; import { FontIconBox } from "../views/nodes/FontIconBox"; +import { MenuIconBox } from "../views/nodes/MenuIconBox"; import { FormattedTextBox } from "../views/nodes/formattedText/FormattedTextBox"; import { ImageBox } from "../views/nodes/ImageBox"; import { KeyValueBox } from "../views/nodes/KeyValueBox"; @@ -301,6 +302,9 @@ export namespace Docs { layout: { view: FontIconBox, dataField: defaultDataKey }, options: { _width: 40, _height: 40, borderRounding: "100%" }, }], + [DocumentType.MENUICON, { + layout: { view: MenuIconBox, dataField: defaultDataKey }, + }], [DocumentType.RECOMMENDATION, { layout: { view: RecommendationsBox, dataField: defaultDataKey }, options: { _width: 200, _height: 200 }, @@ -791,6 +795,10 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.FONTICON), undefined, { hideLinkButton: true, ...(options || {}) }); } + export function MenuIconDocument(options?: DocumentOptions) { + return InstanceFromProto(Prototypes.get(DocumentType.MENUICON), undefined, { hideLinkButton: true, ...(options || {}) }); + } + export function PresElementBoxDocument(options?: DocumentOptions) { return InstanceFromProto(Prototypes.get(DocumentType.PRESELEMENT), undefined, { ...(options || {}) }); } diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 6d752832a..442be98d2 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -38,6 +38,18 @@ export class CurrentUserUtils { @observable public static GuestWorkspace: Doc | undefined; @observable public static GuestMobile: Doc | undefined; + @observable public static toolsBtn: any | undefined; + @observable public static libraryBtn: any | undefined; + @observable public static searchBtn: any | undefined; + + @observable public static toolsStack: any | undefined; + @observable public static workspaceStack: any | undefined; + @observable public static catalogStack: any | undefined; + @observable public static closedStack: any | undefined; + @observable public static searchStack: any | undefined; + + @observable public static panelContent: string = "none"; + // sets up the default User Templates - slideView, queryView, descriptionView static setupUserTemplateButtons(doc: Doc) { if (doc["template-button-query"] === undefined) { @@ -492,6 +504,53 @@ export class CurrentUserUtils { return doc.myItemCreators as Doc; } + static menuBtnDescriptions(): { + title: string, icon: string, click: string, + }[] { + return [ + { title: "Workspace", icon: "desktop", click: 'scriptContext.selectMenu("workspace")' }, + { title: "Catalog", icon: "file", click: 'scriptContext.selectMenu("catalog")' }, + { title: "Archive", icon: "archive", click: 'scriptContext.selectMenu("deleted")' }, + { title: "Import", icon: "upload", click: 'scriptContext.selectMenu("upload")' }, + { title: "Sharing", icon: "users", click: 'GroupManager.Instance.open()' }, + { title: "Tools", icon: "wrench", click: 'scriptContext.selectMenu("tools")' }, + { title: "Help", icon: "question-circle", click: 'scriptContext.selectMenu("help")' }, + { title: "Settings", icon: "cog", click: 'SettingsManager.Instance.open()' }, + ]; + } + + static setupMenuButtons(doc: Doc) { + if (doc.menuStackBtns === undefined) { + const buttons = CurrentUserUtils.menuBtnDescriptions(); + const menuBtns = buttons.map(({ title, icon, click }) => Docs.Create.MenuIconDocument({ + icon, + title, + stayInCollection: true, + _width: 60, + _height: 70, + onClick: ScriptField.MakeScript(click), + })); + + doc.menuStackBtns = new PrefetchProxy(Docs.Create.MasonryDocument(menuBtns, { + _xMargin: 0, _autoHeight: false, _width: 60, _columnWidth: 60, lockedPosition: true, _chromeStatus: "disabled", + })); + } + return doc.menuStackBtns as Doc; + } + + static setupMenuPanel(doc: Doc) { + doc.menuStack = undefined; + if (doc.menuStack === undefined) { + const menuBtns = CurrentUserUtils.setupMenuButtons(doc); + doc.menuStack = new PrefetchProxy(Docs.Create.StackingDocument([menuBtns], { + _yMargin: 0, _autoHeight: true, _xMargin: 0, + _width: 60, lockedPosition: true, _chromeStatus: "disabled", + })) as any as Doc; + } + return doc.menuStack; + } + + // Sets up mobile menu if it is undefined creates a new one, otherwise returns existing menu static setupActiveMobileMenu(doc: Doc) { if (doc.activeMobileMenu === undefined) { @@ -601,6 +660,8 @@ export class CurrentUserUtils { const creatorBtns = await CurrentUserUtils.setupCreatorButtons(doc); const templateBtns = CurrentUserUtils.setupUserTemplateButtons(doc); + doc["tabs-button-tools"] = undefined; + if (doc.myCreators === undefined) { doc.myCreators = new PrefetchProxy(Docs.Create.StackingDocument([creatorBtns, templateBtns], { title: "all Creators", _yMargin: 0, _autoHeight: true, _xMargin: 0, @@ -617,8 +678,11 @@ export class CurrentUserUtils { if (doc["tabs-button-tools"] === undefined) { const toolsStack = new PrefetchProxy(Docs.Create.StackingDocument([doc.myCreators as Doc, doc.myColorPicker as Doc], { - _width: 500, lockedPosition: true, _chromeStatus: "disabled", title: "tools stack", forceActive: true + _width: 500, lockedPosition: true, _chromeStatus: "disabled", hideFilterView: true, title: "tools stack", forceActive: true })) as any as Doc; + + CurrentUserUtils.toolsStack = toolsStack; + doc["tabs-button-tools"] = new PrefetchProxy(Docs.Create.ButtonDocument({ _width: 35, _height: 25, title: "Tools", _fontSize: "10pt", letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)", @@ -627,41 +691,63 @@ export class CurrentUserUtils { dragFactory: toolsStack, removeDropProperties: new List<string>(["lockedPosition"]), stayInCollection: true, + hideFilterView: true, targetContainer: new PrefetchProxy(sidebarContainer) as any as Doc, onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel"), })); } (doc["tabs-button-tools"] as Doc).sourcePanel; // prefetch sourcePanel + return doc["tabs-button-tools"] as Doc; } static setupWorkspaces(doc: Doc) { // setup workspaces library item + doc.myWorkspaces === undefined; if (doc.myWorkspaces === undefined) { doc.myWorkspaces = new PrefetchProxy(Docs.Create.TreeDocument([], { - title: "WORKSPACES", _height: 100, forceActive: true, boxShadow: "0 0", lockedPosition: true, + title: "WORKSPACES", _height: 100, forceActive: true, boxShadow: "0 0", lockedPosition: true, treeViewOpen: true, })); } const newWorkspace = ScriptField.MakeScript(`createNewWorkspace()`); (doc.myWorkspaces as Doc).contextMenuScripts = new List<ScriptField>([newWorkspace!]); (doc.myWorkspaces as Doc).contextMenuLabels = new List<string>(["Create New Workspace"]); + const workspaces = doc.myWorkspaces as Doc; + + CurrentUserUtils.workspaceStack = new PrefetchProxy(Docs.Create.TreeDocument([workspaces], { + title: " ", _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "alias", + treeViewTruncateTitleWidth: 150, hideFilterView: true, treeViewPreventOpen: false, treeViewOpen: true, + lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same" + })) as any as Doc; + return doc.myWorkspaces as Doc; } static setupCatalog(doc: Doc) { + doc.myCatalog === undefined; if (doc.myCatalog === undefined) { doc.myCatalog = new PrefetchProxy(Docs.Create.SchemaDocument([], [], { title: "CATALOG", _height: 1000, _fitWidth: true, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: false, - childDropAction: "alias", targetDropAction: "same", stayInCollection: true, + childDropAction: "alias", targetDropAction: "same", stayInCollection: true, treeViewOpen: true, })); } + + const catalog = doc.myCatalog as Doc; + + CurrentUserUtils.catalogStack = new PrefetchProxy(Docs.Create.TreeDocument([catalog], { + title: " ", _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "alias", + treeViewTruncateTitleWidth: 150, hideFilterView: true, treeViewPreventOpen: false, treeViewOpen: true, + lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same" + })) as any as Doc; + return doc.myCatalog as Doc; } static setupRecentlyClosed(doc: Doc) { // setup Recently Closed library item + doc.myRecentlyClosed === undefined; if (doc.myRecentlyClosed === undefined) { doc.myRecentlyClosed = new PrefetchProxy(Docs.Create.TreeDocument([], { - title: "RECENTLY CLOSED", _height: 75, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: true, stayInCollection: true, + title: "RECENTLY CLOSED", _height: 75, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: false, treeViewOpen: true, stayInCollection: true, })); } // this is equivalent to using PrefetchProxies to make sure the recentlyClosed doc is ready @@ -670,6 +756,14 @@ export class CurrentUserUtils { (doc.myRecentlyClosed as Doc).contextMenuScripts = new List<ScriptField>([clearAll!]); (doc.myRecentlyClosed as Doc).contextMenuLabels = new List<string>(["Clear All"]); + const recentlyClosed = doc.myRecentlyClosed as Doc; + + CurrentUserUtils.closedStack = new PrefetchProxy(Docs.Create.TreeDocument([recentlyClosed], { + title: " ", _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "alias", + treeViewTruncateTitleWidth: 150, hideFilterView: true, treeViewPreventOpen: false, treeViewOpen: true, + lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same" + })) as any as Doc; + return doc.myRecentlyClosed as Doc; } // setup the Library button which will display the library panel. This panel includes a collection of workspaces, documents, and recently closed views @@ -681,7 +775,7 @@ export class CurrentUserUtils { if (doc["tabs-button-library"] === undefined) { const libraryStack = new PrefetchProxy(Docs.Create.TreeDocument([workspaces, documents, recentlyClosed, doc], { title: "Library", _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "alias", - treeViewTruncateTitleWidth: 150, + treeViewTruncateTitleWidth: 150, hideFilterView: true, treeViewPreventOpen: false, lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same" })) as any as Doc; doc["tabs-button-library"] = new PrefetchProxy(Docs.Create.ButtonDocument({ @@ -701,6 +795,7 @@ export class CurrentUserUtils { // setup the Search button which will display the search panel. static setupSearchBtnPanel(doc: Doc, sidebarContainer: Doc) { + doc["tabs-button-search"] = undefined; if (doc["tabs-button-search"] === undefined) { doc["tabs-button-search"] = new PrefetchProxy(Docs.Create.ButtonDocument({ _width: 50, _height: 25, title: "Search", _fontSize: "10pt", @@ -711,6 +806,7 @@ export class CurrentUserUtils { lockedPosition: true, onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel") })); + CurrentUserUtils.searchStack = new PrefetchProxy(Docs.Create.QueryDocument({ title: "search stack", })) as any as Doc; } return doc["tabs-button-search"] as Doc; } @@ -728,17 +824,17 @@ export class CurrentUserUtils { // setup the list of sidebar mode buttons which determine what is displayed in the sidebar static async setupSidebarButtons(doc: Doc) { const sidebarContainer = CurrentUserUtils.setupSidebarContainer(doc); - const toolsBtn = await CurrentUserUtils.setupToolsBtnPanel(doc, sidebarContainer); - const libraryBtn = CurrentUserUtils.setupLibraryPanel(doc, sidebarContainer); - const searchBtn = CurrentUserUtils.setupSearchBtnPanel(doc, sidebarContainer); + CurrentUserUtils.toolsBtn = await CurrentUserUtils.setupToolsBtnPanel(doc, sidebarContainer); + CurrentUserUtils.libraryBtn = CurrentUserUtils.setupLibraryPanel(doc, sidebarContainer); + CurrentUserUtils.searchBtn = CurrentUserUtils.setupSearchBtnPanel(doc, sidebarContainer); // Finally, setup the list of buttons to display in the sidebar if (doc["tabs-buttons"] === undefined) { - doc["tabs-buttons"] = new PrefetchProxy(Docs.Create.StackingDocument([libraryBtn, searchBtn, toolsBtn], { + doc["tabs-buttons"] = new PrefetchProxy(Docs.Create.StackingDocument([CurrentUserUtils.libraryBtn, CurrentUserUtils.searchBtn, CurrentUserUtils.toolsBtn], { _width: 500, _height: 80, boxShadow: "0 0", _pivotField: "title", _columnsHideIfEmpty: true, ignoreClick: true, _chromeStatus: "view-mode", title: "sidebar btn row stack", backgroundColor: "dimGray", })); - (toolsBtn.onClick as ScriptField).script.run({ this: toolsBtn }); + (CurrentUserUtils.toolsBtn.onClick as ScriptField).script.run({ this: CurrentUserUtils.toolsBtn }); } } @@ -855,6 +951,7 @@ export class CurrentUserUtils { this.setupDocTemplates(doc); // sets up the template menu of templates this.setupRightSidebar(doc); // sets up the right sidebar collection for mobile upload documents and sharing this.setupActiveMobileMenu(doc); // sets up the current mobile menu for Dash Mobile + this.setupMenuPanel(doc); this.setupOverlays(doc); // documents in overlay layer this.setupDockedButtons(doc); // the bottom bar of font icons this.setupDefaultPresentation(doc); // presentation that's initially triggered diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 20d881961..05ba00331 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -12,6 +12,7 @@ export namespace SelectionManager { @observable IsDragging: boolean = false; SelectedDocuments: ObservableMap<DocumentView, boolean> = new ObservableMap(); + @action SelectDoc(docView: DocumentView, ctrlPressed: boolean): void { @@ -32,6 +33,7 @@ export namespace SelectionManager { } @action DeselectDoc(docView: DocumentView): void { + if (manager.SelectedDocuments.get(docView)) { manager.SelectedDocuments.delete(docView); docView.props.whenActiveChanged(false); @@ -40,6 +42,7 @@ export namespace SelectionManager { } @action DeselectAll(): void { + Array.from(manager.SelectedDocuments.keys()).map(dv => dv.props.whenActiveChanged(false)); manager.SelectedDocuments.clear(); Doc.UserDoc().activeSelection = new List<Doc>([]); diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx index 81432968d..7e233ec04 100644 --- a/src/client/views/ContextMenuItem.tsx +++ b/src/client/views/ContextMenuItem.tsx @@ -80,7 +80,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select render() { if ("event" in this.props) { return ( - <div className={"contextMenu-item" + (this.props.selected ? " contextMenu-itemSelected" : "")} onClick={this.handleEvent}> + <div className={"contextMenu-item" + (this.props.selected ? " contextMenu-itemSelected" : "")} onPointerDown={this.handleEvent}> {this.props.icon ? ( <span className="icon-background"> <FontAwesomeIcon icon={this.props.icon} size="sm" /> @@ -95,7 +95,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select const where = !this.overItem ? "" : this._overPosY < window.innerHeight / 3 ? "flex-start" : this._overPosY > window.innerHeight * 2 / 3 ? "flex-end" : "center"; const marginTop = !this.overItem ? "" : this._overPosY < window.innerHeight / 3 ? "20px" : this._overPosY > window.innerHeight * 2 / 3 ? "-20px" : ""; const submenu = !this.overItem ? (null) : - <div className="contextMenu-subMenu-cont" style={{ marginLeft: "25%", left: "0px", marginTop }}> + <div className="contextMenu-subMenu-cont" style={{ marginLeft: "90%", left: "0px", marginTop }}> {this._items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} />)} </div>; if (!("noexpand" in this.props)) { diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 6b85616c2..23a3e1684 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -281,7 +281,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV {DocumentLinksButton.StartLink ? <div className="documentButtonBar-button"> <DocumentLinksButton links={this.view0.allLinks} View={this.view0} AlwaysOn={true} InMenu={true} StartLink={false} /> </div> : null} - <div className="documentButtonBar-button"> + {/* <div className="documentButtonBar-button"> {this.templateButton} </div> <div className="documentButtonBar-button"> @@ -289,16 +289,16 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV </div> <div className="documentButtonBar-button"> {this.contextButton} - </div> + </div> */} <div className="documentButtonBar-button"> {this.pinButton} </div> - <div className="documentButtonBar-button" style={{ display: !considerPush ? "none" : "" }}> + {/* <div className="documentButtonBar-button" style={{ display: !considerPush ? "none" : "" }}> {this.considerGoogleDocsPush} </div> <div className="documentButtonBar-button" style={{ display: !considerPull ? "none" : "" }}> {this.considerGoogleDocsPull} - </div> + </div> */} </div>; } } diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 190dbc8c3..f7bd6b358 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -1,5 +1,5 @@ import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; -import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faTextHeight, faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faShare, faStopCircle, faSyncAlt, faTag, faTimes, faAngleLeft, faAngleRight, faAngleDoubleLeft, faAngleDoubleRight, faPause } from '@fortawesome/free-solid-svg-icons'; +import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faTextHeight, faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faShare, faStopCircle, faSyncAlt, faTag, faTimes, faAngleLeft, faAngleRight, faAngleDoubleLeft, faAngleDoubleRight, faPause, faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; @@ -44,6 +44,7 @@ library.add(faAngleDoubleRight); library.add(faAngleLeft); library.add(faAngleRight); library.add(faPause); +library.add(faExternalLinkAlt); @observer export class DocumentDecorations extends React.Component<{}, { value: string }> { @@ -610,9 +611,9 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> </div>} </> : <> - {minimal ? (null) : <Tooltip title={<><div className="dash-tooltip">Show context menu</div></>} placement="top"><div className="documentDecorations-contextMenu" key="menu" onPointerDown={this.onSettingsDown}> - <FontAwesomeIcon size="lg" icon="cog" /> - </div></Tooltip>} + {/* {minimal ? (null) : <Tooltip title={<><div className="dash-tooltip">Show context menu</div></>} placement="top"><div className="documentDecorations-contextMenu" key="menu" onPointerDown={this.onSettingsDown}> + <FontAwesomeIcon size="lg" icon="bars" /> + </div></Tooltip>} */} <div className="documentDecorations-title" key="title" onPointerDown={this.onTitleDown} > <span style={{ width: "100%", display: "inline-block", cursor: "move" }}>{`${this.selectionTitle}`}</span> </div> @@ -659,7 +660,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> {"_"} </div></Tooltip>} <Tooltip title={<><div className="dash-tooltip">Open Document In Tab</div></>} placement="top"><div className="documentDecorations-openInTab" onPointerDown={this.onMaximizeDown}> - {SelectionManager.SelectedDocuments().length === 1 ? DocumentDecorations.DocumentIcon(StrCast(seldoc.props.Document.layout, "...")) : "..."} + {SelectionManager.SelectedDocuments().length === 1 ? <FontAwesomeIcon icon="external-link-alt" className="documentView-minimizedIcon" /> : "..."} </div></Tooltip> <div id="documentDecorations-rotation" className="documentDecorations-rotation" onPointerDown={this.onRotateDown}> ⟲ </div> diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index ad61d3f91..5fc1d6fcb 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -191,7 +191,7 @@ export class EditableView extends React.Component<EditableProps> { return (this.props.contents instanceof ObjectField ? (null) : <div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`} ref={this._ref} - style={{ display: this.props.display, minHeight: "20px", height: `${this.props.height ? this.props.height : "auto"}`, maxHeight: `${this.props.maxHeight}` }} + style={{ display: this.props.display, minHeight: "17px", whiteSpace: "nowrap", height: `${this.props.height ? this.props.height : "auto"}`, maxHeight: `${this.props.maxHeight}` }} onClick={this.onClick} placeholder={this.props.placeholder}> <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize, diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss index e1ddbc533..fe01c27a3 100644 --- a/src/client/views/MainView.scss +++ b/src/client/views/MainView.scss @@ -102,6 +102,45 @@ user-select: none; } +.mainView-propertiesDragger { + background-color: rgb(140, 139, 139); + height: 55px; + width: 15.5px; + position: absolute; + top: 55%; + border: 1px black solid; + border-radius: 0; + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; + border-right: unset; + z-index: 2; + + .mainView-propertiesDragger-icon { + width: 10px; + height: 10px; + float: left; + margin-left: 3px; + padding-top: 19px; + } + + &:hover { + cursor: pointer; + } +} + +.mainiView-propertiesView { + display: flex; + flex-direction: column; + width: 200px; + height: 100%; + position: absolute; + right: 0; + top: 0; + border-left: solid 1px; + z-index: 100000; + cursor: auto; +} + .mainView-flyoutContainer { display: flex; flex-direction: column; @@ -114,6 +153,119 @@ } } +.mainView-menuPanel { + + width: 60px; + background-color: black; + height: 100%; + //overflow-y: scroll; + //overflow-x: hidden; + + + .mainView-menuPanel-button { + padding: 7px; + padding-left: 7px; + width: 100%; + + .mainView-menuPanel-button-wrap { + width: 45px; + /* padding: 5px; */ + touch-action: none; + background: black; + transform-origin: top left; + /* margin-bottom: 5px; */ + margin-top: 5px; + margin-right: 25px; + border-radius: 8px; + + &:hover { + background: rgb(61, 61, 61); + cursor: pointer; + } + } + } + + .mainView-menuPanel-button-label { + color: white; + margin-left: px; + margin-right: 4px; + border-radius: 8px; + width: 42px; + position: relative; + text-align: center; + font-size: 8px; + margin-top: 1px; + letter-spacing: normal; + padding: 3px; + background-color: inherit; + } + + .mainView-menuPanel-button-icon { + width: auto; + height: 35px; + padding: 5px; + } + + svg { + width: 95% !important; + height: 95%; + } +} + +// .mainView-menuPanel-bottomButton { +// width: 45px; +// height: 45px; +// padding: 10px; +// pointer-events: all; +// touch-action: none; +// //border-radius: inherit; +// background: black; +// background-color: black; +// //border-radius: 100%; +// transform-origin: top left; +// margin-bottom: 20px; +// margin-top: 5px; + +// margin-right: 25px; + +// .mainView-menuPanel-bottomButton-label { +// background: black; +// color: white; +// margin-left: -10px; +// border-radius: 8px; +// width: 65px; +// position: absolute; +// text-align: center; +// font-size: 9.5px; +// margin-top: 2px; +// letter-spacing: normal; +// padding: 3px; +// //margin-bottom: 23px; +// } + +// .mainView-menuPanel-bottomButton-icon { +// width: 50px; +// height: 50px; +// color: white; +// } + +// svg { +// width: 95% !important; +// height: 95%; +// } +// } + + +.mainView-searchPanel { + width: 100%; + height: 33px; + background-color: black; + color: white; + text-align: center; + vertical-align: middle; + padding-top: 6px; +} + .mainView-mainDiv { width: 100%; height: 100%; @@ -162,26 +314,44 @@ display: flex; flex-direction: column; z-index: 2; + + .mainView-libraryFlyout-close { + right: 6; + top: 5; + position: absolute; + margin-right: 6px; + z-index: 10; + margin-bottom: 10; + } } .mainView-expandFlyoutButton { position: absolute; - top: 100px; - right: 30px; + top: 120px; + right: 55px; cursor: pointer; } .mainView-libraryHandle { - width: 20px; + width: 28px; left: calc(100% - 10px); - height: 40px; + height: 55px; top: 50%; border: 1px solid black; - border-radius: 5px; + border-radius: 8px; position: absolute; z-index: 2; touch-action: none; - cursor: ew-resize; + cursor: grab; + + .mainView-libraryHandle-icon { + width: 10px; + height: 10px; + float: right; + margin-right: 3px; + padding-top: 19px; + } + } .mainView-workspace { diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 37d99fb16..9bad0867c 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -12,10 +12,10 @@ import { Doc, DocListCast, Field, Opt } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { List } from '../../fields/List'; import { listSpec } from '../../fields/Schema'; -import { BoolCast, Cast, FieldValue, StrCast } from '../../fields/Types'; +import { BoolCast, Cast, FieldValue, StrCast, NumCast } from '../../fields/Types'; import { TraceMobx } from '../../fields/util'; import { CurrentUserUtils } from '../util/CurrentUserUtils'; -import { emptyFunction, emptyPath, returnFalse, returnOne, returnZero, returnTrue, Utils, returnEmptyFilter } from '../../Utils'; +import { emptyFunction, emptyPath, returnFalse, returnOne, returnZero, returnTrue, Utils, returnEmptyFilter, setupMoveUpEvents } from '../../Utils'; import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager'; import { DocServer } from '../DocServer'; import { Docs, DocumentOptions } from '../documents/Documents'; @@ -44,7 +44,7 @@ import { RadialMenu } from './nodes/RadialMenu'; import { OverlayView } from './OverlayView'; import PDFMenu from './pdf/PDFMenu'; import { PreviewCursor } from './PreviewCursor'; -import { ScriptField } from '../../fields/ScriptField'; +import { ScriptField, ComputedField } from '../../fields/ScriptField'; import { TimelineMenu } from './animationtimeline/TimelineMenu'; import { SnappingManager } from '../util/SnappingManager'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; @@ -57,6 +57,14 @@ import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup'; import FormatShapePane from "./collections/collectionFreeForm/FormatShapePane"; import HypothesisAuthenticationManager from '../apis/HypothesisAuthenticationManager'; import CollectionMenu from './collections/CollectionMenu'; +import { Tooltip, AccordionActions } from '@material-ui/core'; +import { PropertiesView } from './collections/collectionFreeForm/PropertiesView'; +import { SelectionManager } from '../util/SelectionManager'; +import { PrefetchProxy } from '../../fields/Proxy'; +import { DragManager } from '../util/DragManager'; +import { discovery_v1, dialogflow_v2beta1 } from 'googleapis'; +import { undo } from 'prosemirror-history'; +import { undoBatch } from '../util/UndoManager'; @observer export class MainView extends React.Component { @@ -69,8 +77,8 @@ export class MainView extends React.Component { @observable private _panelWidth: number = 0; @observable private _panelHeight: number = 0; - @observable private _flyoutTranslate: boolean = true; - @observable public flyoutWidth: number = 250; + @observable private _flyoutTranslate: boolean = false; + @observable public flyoutWidth: number = 0; private get darkScheme() { return BoolCast(Cast(this.userDoc?.activeWorkspace, Doc, null)?.darkScheme); } @computed private get userDoc() { return Doc.UserDoc(); } @@ -78,7 +86,26 @@ export class MainView extends React.Component { @computed public get mainFreeform(): Opt<Doc> { return (docs => (docs && docs.length > 1) ? docs[1] : undefined)(DocListCast(this.mainContainer!.data)); } @computed public get sidebarButtonsDoc() { return Cast(this.userDoc["tabs-buttons"], Doc) as Doc; } + @observable public sidebarContent: any = this.userDoc?.["tabs-panelContainer"]; + @observable public panelContent: string = "none"; + @observable public showProperties: boolean = false; public isPointerDown = false; + @computed get selectedDocumentView() { + if (SelectionManager.SelectedDocuments().length) { + return SelectionManager.SelectedDocuments()[0]; + } else { return undefined; } + } + + @observable _propertiesWidth: number = 0; + propertiesWidth = () => Math.max(0, Math.min(this._panelWidth - 50, this._propertiesWidth)); + + @computed get propertiesIcon() { + if (this.propertiesWidth() < 10) { + return "chevron-left"; + } else { + return "chevron-right"; + } + } componentDidMount() { DocServer.setPlaygroundFields(["dataTransition", "_viewTransition", "_panX", "_panY", "_viewScale", "_viewType", "_chromeStatus"]); // can play with these fields on someone else's @@ -148,7 +175,8 @@ export class MainView extends React.Component { fa.faEye, fa.faArrowsAlt, fa.faQuoteLeft, fa.faSortAmountDown, fa.faAlignLeft, fa.faAlignCenter, fa.faAlignRight, fa.faHeading, fa.faRulerCombined, fa.faFillDrip, fa.faLink, fa.faUnlink, fa.faBold, fa.faItalic, fa.faChevronLeft, fa.faUnderline, fa.faStrikethrough, fa.faSuperscript, fa.faSubscript, fa.faIndent, fa.faEyeDropper, fa.faPaintRoller, fa.faBars, fa.faBrush, fa.faShapes, fa.faEllipsisH, fa.faHandPaper, fa.faMap, fa.faUser, faHireAHelper, - fa.faBezierCurve, fa.faCircle, fa.faLongArrowAltRight, fa.faPenFancy, fa.faAngleDoubleRight); + fa.faDesktop, fa.faTrashRestore, fa.faUsers, fa.faWrench, fa.faCog, fa.faMap, fa.faBellSlash, fa.faExpandAlt, fa.faArchive, fa.faBezierCurve, fa.faCircle, + fa.faLongArrowAltRight, fa.faPenFancy, fa.faAngleDoubleRight); this.initEventListeners(); this.initAuthenticationRouters(); } @@ -210,7 +238,7 @@ export class MainView extends React.Component { const freeformOptions: DocumentOptions = { x: 0, y: 400, - _width: this._panelWidth * .7, + _width: this._panelWidth * .7 - this._propertiesWidth, _height: this._panelHeight, title: "Collection " + workspaceCount, }; @@ -276,10 +304,13 @@ export class MainView extends React.Component { @action onResize = (r: any) => { - this._panelWidth = r.offset.width; + this._panelWidth = r.offset.width - this._propertiesWidth; this._panelHeight = r.offset.height; } - getPWidth = () => this._panelWidth; + + @action + getPWidth = () => this._panelWidth - this._propertiesWidth; + getPHeight = () => this._panelHeight; getContentsHeight = () => this._panelHeight - this._buttonBarHeight; @@ -307,7 +338,8 @@ export class MainView extends React.Component { } } @computed get mainDocView() { - return <DocumentView Document={this.mainContainer!} + return <DocumentView + Document={this.mainContainer!} DataDoc={undefined} LibraryPath={emptyPath} addDocument={undefined} @@ -323,7 +355,6 @@ export class MainView extends React.Component { NativeWidth={returnZero} PanelWidth={this.getPWidth} PanelHeight={this.getPHeight} - renderDepth={0} focus={emptyFunction} parentActive={returnTrue} whenActiveChanged={emptyFunction} @@ -331,6 +362,7 @@ export class MainView extends React.Component { docFilters={returnEmptyFilter} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} + renderDepth={0} />; } @computed get dockingContent() { @@ -347,8 +379,12 @@ export class MainView extends React.Component { } _canClick = false; + + @action onPointerDown = (e: React.PointerEvent) => { if (this._flyoutTranslate) { + this.panelContent = "none"; + CurrentUserUtils.panelContent = "none"; this._canClick = true; this._flyoutSizeOnDown = e.clientX; document.removeEventListener("pointermove", this.onPointerMove); @@ -389,45 +425,24 @@ export class MainView extends React.Component { doc.dockingConfig ? this.openWorkspace(doc) : CollectionDockingView.AddRightSplit(doc, libraryPath); } - sidebarScreenToLocal = () => new Transform(0, (RichTextMenu.Instance.Pinned ? -35 : 0) + (CollectionMenu.Instance.Pinned ? -35 : 0), 1); + sidebarScreenToLocal = () => new Transform(0, (CollectionMenu.Instance.Pinned ? -35 : 0), 1); + //sidebarScreenToLocal = () => new Transform(0, (RichTextMenu.Instance.Pinned ? -35 : 0) + (CollectionMenu.Instance.Pinned ? -35 : 0), 1); mainContainerXf = () => this.sidebarScreenToLocal().translate(0, -this._buttonBarHeight); + @computed get closePosition() { return 55 + this.flyoutWidth } @computed get flyout() { - const sidebarContent = this.userDoc?.["tabs-panelContainer"]; - if (!(sidebarContent instanceof Doc)) { + if (!(this.sidebarContent instanceof Doc)) { return (null); } - return <div className="mainView-flyoutContainer" > - <div className="mainView-tabButtons" style={{ height: `${this._buttonBarHeight - 10/*margin-top*/}px`, backgroundColor: StrCast(this.sidebarButtonsDoc.backgroundColor) }}> - <DocumentView - Document={this.sidebarButtonsDoc} - DataDoc={undefined} - LibraryPath={emptyPath} - addDocument={undefined} - rootSelected={returnTrue} - addDocTab={this.addDocTabFunc} - pinToPres={emptyFunction} - removeDocument={undefined} - onClick={undefined} - ScreenToLocalTransform={this.sidebarScreenToLocal} - ContentScaling={returnOne} - NativeHeight={returnZero} - NativeWidth={returnZero} - PanelWidth={this.flyoutWidthFunc} - PanelHeight={this.getPHeight} - renderDepth={0} - focus={emptyFunction} - backgroundColor={this.defaultBackgroundColors} - parentActive={returnTrue} - whenActiveChanged={emptyFunction} - bringToFront={emptyFunction} - docFilters={returnEmptyFilter} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} /> - </div> - <div className="mainView-contentArea" style={{ position: "relative", height: `calc(100% - ${this._buttonBarHeight}px)`, width: "100%", overflow: "visible" }}> + return <div className="mainView-libraryFlyout"> + <div className="mainView-contentArea" style={{ position: "relative", height: `100%`, width: "100%", overflow: "visible" }}> + {this.flyoutWidth > 0 ? <div className="mainView-libraryFlyout-close" + onPointerDown={this.closeFlyout}> + <FontAwesomeIcon icon="times" color="black" size="sm" /> + </div> : null} + <DocumentView - Document={sidebarContent} + Document={this.sidebarContent} DataDoc={undefined} LibraryPath={emptyPath} addDocument={undefined} @@ -450,50 +465,256 @@ export class MainView extends React.Component { bringToFront={emptyFunction} docFilters={returnEmptyFilter} ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} /> - <div className="buttonContainer" > - <button className="mainView-settings" key="settings" onClick={() => SettingsManager.Instance.open()}> - <FontAwesomeIcon icon="cog" size="lg" /> - </button> + ContainingCollectionDoc={undefined} + relative={true} + /> + </div> + {this.docButtons}</div>; + } + + // @computed get menuPanel() { + + // return <div className="mainView-menuPanel"> + // <DocumentView + // Document={Doc.UserDoc().menuStack as Doc} + // DataDoc={undefined} + // LibraryPath={emptyPath} + // addDocument={undefined} + // addDocTab={this.addDocTabFunc} + // pinToPres={emptyFunction} + // NativeHeight={returnZero} + // NativeWidth={returnZero} + // rootSelected={returnTrue} + // removeDocument={returnFalse} + // onClick={undefined} + // ScreenToLocalTransform={this.mainContainerXf} + // ContentScaling={returnOne} + // PanelWidth={() => 80} + // PanelHeight={this.getContentsHeight} + // renderDepth={0} + // focus={emptyFunction} + // backgroundColor={this.defaultBackgroundColors} + // parentActive={returnTrue} + // whenActiveChanged={emptyFunction} + // bringToFront={emptyFunction} + // docFilters={returnEmptyFilter} + // ContainingCollectionView={undefined} + // ContainingCollectionDoc={undefined} + // relative={true} + // scriptContext={this} + // /> + // </div>; + // } + + @computed get menuPanel() { + return <div className="mainView-menuPanel"> + <div className="mainView-menuPanel-button" style={{ backgroundColor: this.panelContent === "workspace" ? "lightgrey" : "" }}> + <div className="mainView-menuPanel-button-wrap" + style={{ backgroundColor: this.panelContent === "workspace" ? "lightgrey" : "" }} + onPointerDown={e => this.selectPanel("workspace")}> + <FontAwesomeIcon className="mainView-menuPanel-button-icon" icon="desktop" + color={this.panelContent === "workspace" ? "black" : "white"} size="lg" /> + <div className="mainView-menuPanel-button-label" + style={{ color: this.panelContent === "workspace" ? "black" : "white" }}> Workspace </div> + </div> + </div> + + <div className="mainView-menuPanel-button" style={{ backgroundColor: this.panelContent === "catalog" ? "lightgrey" : "" }}> + <div className="mainView-menuPanel-button-wrap" + style={{ backgroundColor: this.panelContent === "catalog" ? "lightgrey" : "" }} + onPointerDown={e => this.selectPanel("catalog")}> + <FontAwesomeIcon className="mainView-menuPanel-button-icon" icon="file" + color={this.panelContent === "catalog" ? "black" : "white"} size="lg" /> + <div className="mainView-menuPanel-button-label" + style={{ color: this.panelContent === "catalog" ? "black" : "white" }}> Catalog </div> + </div> + </div> + + <div className="mainView-menuPanel-button" style={{ backgroundColor: this.panelContent === "deleted" ? "lightgrey" : "" }}> + <div className="mainView-menuPanel-button-wrap" + style={{ backgroundColor: this.panelContent === "deleted" ? "lightgrey" : "" }} + onPointerDown={e => this.selectPanel("deleted")}> + <FontAwesomeIcon className="mainView-menuPanel-button-icon" icon="archive" + color={this.panelContent === "deleted" ? "black" : "white"} size="lg" /> + <div className="mainView-menuPanel-button-label" + style={{ color: this.panelContent === "deleted" ? "black" : "white" }}> Recently Used </div> + </div> + </div> + + <div className="mainView-menuPanel-button"> + <div className="mainView-menuPanel-button-wrap" + onPointerDown={e => this.selectPanel("upload")}> + <FontAwesomeIcon className="mainView-menuPanel-button-icon" icon="upload" color="white" size="lg" /> + <div className="mainView-menuPanel-button-label"> Import </div> + </div> + </div> + + <div className="mainView-menuPanel-button"> + <div className="mainView-menuPanel-button-wrap" + //onPointerDown={e => this.selectPanel("sharing")} + onClick={() => GroupManager.Instance.open()}> + <FontAwesomeIcon className="mainView-menuPanel-button-icon" icon="users" color="white" size="lg" /> + <div className="mainView-menuPanel-button-label"> Sharing </div> + </div> + </div> + + <div className="mainView-menuPanel-button" style={{ marginBottom: "110px", backgroundColor: this.panelContent === "tools" ? "lightgrey" : "", }}> + <div className="mainView-menuPanel-button-wrap" + onPointerDown={e => this.selectPanel("tools")} + style={{ + backgroundColor: this.panelContent === "tools" ? "lightgrey" : "", + }}> + <FontAwesomeIcon className="mainView-menuPanel-button-icon" icon="wrench" + color={this.panelContent === "tools" ? "black" : "white"} size="lg" /> + <div className="mainView-menuPanel-button-label" + style={{ color: this.panelContent === "tools" ? "black" : "white" }}> Tools </div> + </div> + </div> + + <div className="mainView-menuPanel-button"> + <div className="mainView-menuPanel-button-wrap" + // style={{backgroundColor: this.panelContent= "help" ? "lightgrey" : "black"}} + onPointerDown={e => this.selectPanel("help")} > + <FontAwesomeIcon className="mainView-menuPanel-button-icon" icon="question-circle" color="white" size="lg" /> + <div className="mainView-menuPanel-button-label"> Help </div> + </div> + </div> + + <div className="mainView-menuPanel-button"> + <div className="mainView-menuPanel-button-wrap" + // onPointerDown={e => this.selectPanel("settings")} + onClick={() => SettingsManager.Instance.open()}> + <FontAwesomeIcon className="mainView-menuPanel-button-icon" icon="cog" color="white" size="lg" /> + <div className="mainView-menuPanel-button-label"> Settings </div> </div> </div> - {this.docButtons} + </div>; + } + + @action @undoBatch + selectPanel = (str: string) => { + if (this.panelContent === str && this.flyoutWidth !== 0) { + this.closeFlyout(); + } else { + this.panelContent = str; + MainView.expandFlyout(); + if (str === "tools") { + CurrentUserUtils.toolsBtn; + this.sidebarContent.proto = CurrentUserUtils.toolsStack; + } else if (str === "workspace") { + this.sidebarContent.proto = CurrentUserUtils.workspaceStack; + } else if (str === "catalog") { + this.sidebarContent.proto = CurrentUserUtils.catalogStack; + } else if (str === "deleted") { + this.sidebarContent.proto = CurrentUserUtils.closedStack; + } else if (str === "search") { + this.sidebarContent.proto = CurrentUserUtils.searchStack; + } + } + return true; + } + + @action @undoBatch + closeFlyout = () => { + this.panelContent = "none"; + this.flyoutWidth = 0; + } + + @action @undoBatch + selectMenu = (str: string) => { + if (CurrentUserUtils.panelContent === str && this.flyoutWidth !== 0) { + CurrentUserUtils.panelContent = "none"; + this.flyoutWidth = 0; + } else { + CurrentUserUtils.panelContent = str; + MainView.expandFlyout(); + if (str === "tools") { + CurrentUserUtils.toolsBtn; + this.sidebarContent.proto = CurrentUserUtils.toolsStack; + } else if (str === "workspace") { + this.sidebarContent.proto = CurrentUserUtils.workspaceStack; + } else if (str === "catalog") { + this.sidebarContent.proto = CurrentUserUtils.catalogStack; + } else if (str === "deleted") { + this.sidebarContent.proto = CurrentUserUtils.closedStack; + } else if (str === "search") { + this.sidebarContent.proto = CurrentUserUtils.searchStack; + } + } + return true; + } + + @action @undoBatch + onDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { + this._propertiesWidth = this._panelWidth - Math.max(Transform.Identity().transformPoint(e.clientX, 0)[0], 0); + return false; + }), returnFalse, action(() => this._propertiesWidth = this.propertiesWidth() < 15 ? Math.min(this._panelWidth - 50, 200) : 0), false); + } + + @computed get propertiesView() { + TraceMobx(); + return <div className="mainView-propertiesView" style={{ + width: `200px`, + overflow: this.propertiesWidth() < 15 ? "hidden" : undefined + }}> + <PropertiesView + width={200} + height={this._panelHeight} + renderDepth={1} + ScreenToLocalTransform={Transform.Identity} + onDown={this.onDown} + /> </div>; } @computed get mainContent() { - const sidebar = this.userDoc?.["tabs-panelContainer"]; - const n = (RichTextMenu.Instance?.Pinned ? 1 : 0) + (CollectionMenu.Instance?.Pinned ? 1 : 0); + //const n = (RichTextMenu.Instance?.Pinned ? 1 : 0) + (CollectionMenu.Instance?.Pinned ? 1 : 0); + const n = (CollectionMenu.Instance?.Pinned ? 1 : 0); const height = `calc(100% - ${n * Number(ANTIMODEMENU_HEIGHT.replace("px", ""))}px)`; - return !this.userDoc || !(sidebar instanceof Doc) ? (null) : ( + + const rightFlyout = this.selectedDocumentView ? this._propertiesWidth - 1 : this.propertiesWidth() > 10 ? 151.5 : 0; + return !this.userDoc || !(this.sidebarContent instanceof Doc) ? (null) : ( <div className="mainView-mainContent" style={{ color: this.darkScheme ? "rgb(205,205,205)" : "black", //change to times 2 for both pinned height, width: (FormatShapePane.Instance?.Pinned) ? `calc(100% - 200px)` : "100%" }} > + {this.menuPanel} <div style={{ display: "contents", flexDirection: "row", position: "relative" }}> <div className="mainView-flyoutContainer" onPointerLeave={this.pointerLeaveDragger} style={{ width: this.flyoutWidth }}> - <div className="mainView-libraryHandle" onPointerDown={this.onPointerDown} - style={{ backgroundColor: this.defaultBackgroundColors(sidebar) }}> + {this.flyoutWidth !== 0 ? <div className="mainView-libraryHandle" + onPointerDown={this.onPointerDown} + style={{ backgroundColor: this.defaultBackgroundColors(this.sidebarContent) }}> <span title="library View Dragger" style={{ width: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "100%" : "3vw", //height: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "100%" : "100vh", position: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "absolute" : "fixed", top: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "" : "0" }} /> - </div> + <div className="mainview-libraryHandle-icon"> + <FontAwesomeIcon icon="chevron-left" color="black" size="sm" /> </div> + </div> : null} <div className="mainView-libraryFlyout" style={{ //transformOrigin: this._flyoutTranslate ? "" : "left center", transition: this._flyoutTranslate ? "" : "width .5s", //transform: `scale(${this._flyoutTranslate ? 1 : 0.8})`, - boxShadow: this._flyoutTranslate ? "" : "rgb(156, 147, 150) 0.2vw 0.2vw 0.8vw" + boxShadow: this._flyoutTranslate ? "" : "rgb(156, 147, 150) 0.2vw 0.2vw 0.2vw" }}> {this.flyout} {this.expandButton} </div> </div> {this.dockingContent} + {this.showProperties ? (null) : + <div className="mainView-propertiesDragger" title="Properties View Dragger" onPointerDown={this.onDown} + style={{ right: rightFlyout, top: "45%" }}> + <div className="mainView-propertiesDragger-icon"> + <FontAwesomeIcon icon={this.propertiesIcon} color="white" size="sm" /> </div> + </div> + } + {this.propertiesWidth() < 10 ? (null) : this.propertiesView} </div> </div>); } @@ -505,7 +726,7 @@ export class MainView extends React.Component { }); @computed get expandButton() { - return !this._flyoutTranslate ? (<div className="mainView-expandFlyoutButton" title="Re-attach sidebar" onPointerDown={MainView.expandFlyout}><FontAwesomeIcon icon="chevron-right" color="grey" size="lg" /></div>) : (null); + return !this._flyoutTranslate ? (<div className="mainView-expandFlyoutButton" title="Re-attach sidebar" onPointerDown={MainView.expandFlyout}></div>) : (null); } addButtonDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg: boolean, doc) => flg && Doc.AddDocToList(Doc.UserDoc().dockedBtns as Doc, "data", doc), true); @@ -599,8 +820,16 @@ export class MainView extends React.Component { </svg>; } + @computed get search() { + return <div className="mainView-searchPanel"> + <div style={{ float: "left", marginLeft: "10px" }}>{Doc.CurrentUserEmail}</div> + <div>SEARCH GOES HERE</div> + </div>; + } + render() { return (<div className={"mainView-container" + (this.darkScheme ? "-dark" : "")} ref={this._mainViewRef}> + {this.inkResources} <DictationOverlay /> <SharingManager /> @@ -609,9 +838,10 @@ export class MainView extends React.Component { <GoogleAuthenticationManager /> <HypothesisAuthenticationManager /> <DocumentDecorations /> + {/* {this.search} */} <CollectionMenu /> <FormatShapePane /> - <RichTextMenu key="rich" /> + <div style={{ display: "none" }}><RichTextMenu key="rich" /></div> {LinkDescriptionPopup.descriptionPopup ? <LinkDescriptionPopup /> : null} {DocumentLinksButton.EditLink ? <LinkMenu location={DocumentLinksButton.EditLinkLoc} docView={DocumentLinksButton.EditLink} addDocTab={DocumentLinksButton.EditLink.props.addDocTab} changeFlyout={emptyFunction} /> : (null)} {LinkDocPreview.LinkInfo ? <LinkDocPreview location={LinkDocPreview.LinkInfo.Location} backgroundColor={this.defaultBackgroundColors} diff --git a/src/client/views/PropertiesButtons.scss b/src/client/views/PropertiesButtons.scss new file mode 100644 index 000000000..11265b413 --- /dev/null +++ b/src/client/views/PropertiesButtons.scss @@ -0,0 +1,119 @@ +@import "globalCssVariables"; + +$linkGap : 3px; + +.propertiesButtons-linkFlyout { + grid-column: 2/4; +} + +.propertiesButtons-linkButton-empty:hover { + background: $main-accent; + transform: scale(1.05); + cursor: pointer; +} + +.propertiesButtons-linkButton-nonempty:hover { + background: $main-accent; + transform: scale(1.05); + cursor: pointer; +} + +.propertiesButtons-linkButton-empty, +.propertiesButtons-linkButton-nonempty { + height: 25px; + width: 25px; + border-radius: 5px; + opacity: 0.9; + pointer-events: auto; + background-color: #121721; + color: #fcfbf7; + text-transform: uppercase; + letter-spacing: 2px; + font-size: 75%; + transition: transform 0.2s; + text-align: center; + display: flex; + justify-content: center; + align-items: center; + margin-right: 5px; + + &:hover { + background: $main-accent; + transform: scale(1.05); + cursor: pointer; + } +} + +.propertiesButtons { + margin-top: $linkGap; + grid-column: 1/4; + width: max-content; + height: auto; + display: flex; + flex-direction: row; + overflow-x: visible; +} + +.onClickFlyout-editScript { + text-align: center; + border: 0.5px solid grey; + background-color: rgb(230, 230, 230); + border-radius: 9px; + padding: 4px; +} + + +.propertiesButtons-button { + pointer-events: auto; + padding-right: 5px; + width: 25px; + border-radius: 5px; + margin-right: 8px; +} + +.propertiesButtons-linker { + height: 25px; + width: 25px; + text-align: center; + border-radius: 5px; + pointer-events: auto; + color: $dark-color; + border: $dark-color 1px solid; + transition: 0.2s ease all; + margin-right: 5px; +} + +.propertiesButtons-linker:hover { + cursor: pointer; + transform: scale(1.05); +} + + +@-moz-keyframes spin { + 100% { + -moz-transform: rotate(360deg); + } +} + +@-webkit-keyframes spin { + 100% { + -webkit-transform: rotate(360deg); + } +} + +@keyframes spin { + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +@keyframes shadow-pulse { + 0% { + box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.8); + } + + 100% { + box-shadow: 0 0 0 10px rgba(0, 255, 0, 0); + } +}
\ No newline at end of file diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx new file mode 100644 index 000000000..cc15a8195 --- /dev/null +++ b/src/client/views/PropertiesButtons.tsx @@ -0,0 +1,547 @@ +import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; +import { faArrowAltCircleDown, faArrowAltCircleRight, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faPhotoVideo, faShare, faStopCircle, faSyncAlt, faTag, faTimes } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, computed, observable, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import { Doc, DocListCast } from "../../fields/Doc"; +import { RichTextField } from '../../fields/RichTextField'; +import { Cast, NumCast, BoolCast } from "../../fields/Types"; +import { emptyFunction, setupMoveUpEvents, Utils } from "../../Utils"; +import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager'; +import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils'; +import { Docs, DocUtils } from '../documents/Documents'; +import { DragManager } from '../util/DragManager'; +import { CollectionDockingView, DockedFrameRenderer } from './collections/CollectionDockingView'; +import { ParentDocSelector } from './collections/ParentDocumentSelector'; +import './collections/ParentDocumentSelector.scss'; +import './PropertiesButtons.scss'; +import { MetadataEntryMenu } from './MetadataEntryMenu'; +import { DocumentView } from './nodes/DocumentView'; +import { GoogleRef } from "./nodes/formattedText/FormattedTextBox"; +import { TemplateMenu } from "./TemplateMenu"; +import { Template, Templates } from "./Templates"; +import React = require("react"); +import { Tooltip } from '@material-ui/core'; +import { SelectionManager } from '../util/SelectionManager'; +import SharingManager from '../util/SharingManager'; +import { GooglePhotos } from '../apis/google_docs/GooglePhotosClientUtils'; +import { ImageField } from '../../fields/URLField'; +import { undoBatch, UndoManager } from '../util/UndoManager'; +import { DocumentType } from '../documents/DocumentTypes'; +import { CollectionFreeFormView } from './collections/collectionFreeForm/CollectionFreeFormView'; +const higflyout = require("@hig/flyout"); +export const { anchorPoints } = higflyout; +export const Flyout = higflyout.default; + +library.add(faLink); +library.add(faTag); +library.add(faTimes); +library.add(faArrowAltCircleDown); +library.add(faArrowAltCircleUp); +library.add(faArrowAltCircleRight); +library.add(faStopCircle); +library.add(faCheckCircle); +library.add(faCloudUploadAlt); +library.add(faSyncAlt); +library.add(faShare); +library.add(faPhotoVideo); + +const cloud: IconProp = "cloud-upload-alt"; +const fetch: IconProp = "sync-alt"; + +enum UtilityButtonState { + Default, + OpenRight, + OpenExternally +} + +@observer +export class PropertiesButtons extends React.Component<{}, {}> { + private _dragRef = React.createRef<HTMLDivElement>(); + private _pullAnimating = false; + private _pushAnimating = false; + private _pullColorAnimating = false; + + @observable private pushIcon: IconProp = "arrow-alt-circle-up"; + @observable private pullIcon: IconProp = "arrow-alt-circle-down"; + @observable private pullColor: string = "white"; + @observable public isAnimatingFetch = false; + @observable public isAnimatingPulse = false; + + @observable private openHover: UtilityButtonState = UtilityButtonState.Default; + + @observable public static Instance: PropertiesButtons; + public static hasPushedHack = false; + public static hasPulledHack = false; + + + @computed get selectedDocumentView() { + if (SelectionManager.SelectedDocuments().length) { + return SelectionManager.SelectedDocuments()[0]; + } else { return undefined; } + } + @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } + @computed get dataDoc() { return this.selectedDocumentView?.dataDoc; } + + @computed get onClick() { return this.selectedDoc?.onClickBehavior ? this.selectedDoc?.onClickBehavior : "nothing"; } + + public startPullOutcome = action((success: boolean) => { + if (!this._pullAnimating) { + this._pullAnimating = true; + this.pullIcon = success ? "check-circle" : "stop-circle"; + setTimeout(() => runInAction(() => { + this.pullIcon = "arrow-alt-circle-down"; + this._pullAnimating = false; + }), 1000); + } + }); + + public startPushOutcome = action((success: boolean) => { + this.isAnimatingPulse = false; + if (!this._pushAnimating) { + this._pushAnimating = true; + this.pushIcon = success ? "check-circle" : "stop-circle"; + setTimeout(() => runInAction(() => { + this.pushIcon = "arrow-alt-circle-up"; + this._pushAnimating = false; + }), 1000); + } + }); + + public setPullState = action((unchanged: boolean) => { + this.isAnimatingFetch = false; + if (!this._pullColorAnimating) { + this._pullColorAnimating = true; + this.pullColor = unchanged ? "lawngreen" : "red"; + setTimeout(this.clearPullColor, 1000); + } + }); + + private clearPullColor = action(() => { + this.pullColor = "white"; + this._pullColorAnimating = false; + }); + + @computed + get considerGoogleDocsPush() { + const targetDoc = this.selectedDoc; + const published = targetDoc && Doc.GetProto(targetDoc)[GoogleRef] !== undefined; + const animation = this.isAnimatingPulse ? "shadow-pulse 1s linear infinite" : "none"; + return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{`${published ? "Push" : "Publish"} to Google Docs`}</div></>}> + <div + className="propertiesButtons-linker" + style={{ animation }} + onClick={async () => { + await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(); + !published && runInAction(() => this.isAnimatingPulse = true); + PropertiesButtons.hasPushedHack = false; + targetDoc[Pushes] = NumCast(targetDoc[Pushes]) + 1; + }}> + <FontAwesomeIcon className="documentdecorations-icon" icon={published ? (this.pushIcon as any) : cloud} size={published ? "sm" : "xs"} /> + </div></Tooltip>; + } + + @computed + get considerGoogleDocsPull() { + const targetDoc = this.selectedDoc; + const dataDoc = targetDoc && Doc.GetProto(targetDoc); + const animation = this.isAnimatingFetch ? "spin 0.5s linear infinite" : "none"; + + const title = (() => { + switch (this.openHover) { + default: + case UtilityButtonState.Default: return `${!dataDoc?.unchanged ? "Pull from" : "Fetch"} Google Docs`; + case UtilityButtonState.OpenRight: return "Open in Right Split"; + case UtilityButtonState.OpenExternally: return "Open in new Browser Tab"; + } + })(); + + return !targetDoc || !dataDoc || !dataDoc[GoogleRef] ? (null) : <Tooltip + title={<><div className="dash-tooltip">{title}</div></>}> + <div className="propertiesButtons-linker" + style={{ backgroundColor: this.pullColor }} + onPointerEnter={action(e => { + if (e.altKey) { + this.openHover = UtilityButtonState.OpenExternally; + } else if (e.shiftKey) { + this.openHover = UtilityButtonState.OpenRight; + } + })} + onPointerLeave={action(() => this.openHover = UtilityButtonState.Default)} + onClick={async e => { + const googleDocUrl = `https://docs.google.com/document/d/${dataDoc[GoogleRef]}/edit`; + if (e.shiftKey) { + e.preventDefault(); + let googleDoc = await Cast(dataDoc.googleDoc, Doc); + if (!googleDoc) { + const options = { _width: 600, _nativeWidth: 960, _nativeHeight: 800, isAnnotating: false, UseCors: false }; + googleDoc = Docs.Create.WebDocument(googleDocUrl, options); + dataDoc.googleDoc = googleDoc; + } + CollectionDockingView.AddRightSplit(googleDoc); + } else if (e.altKey) { + e.preventDefault(); + window.open(googleDocUrl); + } else { + this.clearPullColor(); + PropertiesButtons.hasPulledHack = false; + targetDoc[Pulls] = NumCast(targetDoc[Pulls]) + 1; + dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true); + } + }}> + <FontAwesomeIcon className="documentdecorations-icon" size="sm" + style={{ WebkitAnimation: animation, MozAnimation: animation }} + icon={(() => { + switch (this.openHover) { + default: + case UtilityButtonState.Default: return dataDoc.unchanged === false ? (this.pullIcon as any) : fetch; + case UtilityButtonState.OpenRight: return "arrow-alt-circle-right"; + case UtilityButtonState.OpenExternally: return "share"; + } + })()} + /> + </div></Tooltip>; + } + @computed + get pinButton() { + const targetDoc = this.selectedDoc; + const isPinned = targetDoc && Doc.isDocPinned(targetDoc); + return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{Doc.isDocPinned(targetDoc) ? "Unpin from presentation" : "Pin to presentation"}</div></>}> + <div className="propertiesButtons-linker" + style={{ backgroundColor: isPinned ? "white" : "rgb(37, 43, 51)", color: isPinned ? "black" : "white" }} + onClick={e => DockedFrameRenderer.PinDoc(targetDoc, isPinned)}> + <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" + /> + </div></Tooltip>; + } + + @computed + get metadataButton() { + //const view0 = this.view0; + if (this.selectedDoc) { + return <Tooltip title={<><div className="dash-tooltip">Show metadata panel</div></>}> + <div className="propertiesButtons-linkFlyout"> + <Flyout anchorPoint={anchorPoints.LEFT_TOP} + content={<MetadataEntryMenu docs={[this.selectedDoc]} suggestWithFunction /> /* tfs: @bcz This might need to be the data document? */}> + <div className={"propertiesButtons-linkButton-" + "empty"} onPointerDown={e => e.stopPropagation()} > + {<FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="sm" />} + </div> + </Flyout> + </div></Tooltip>; + } else { + return null; + } + + } + + @observable _aliasDown = false; + onAliasButtonDown = (e: React.PointerEvent): void => { + setupMoveUpEvents(this, e, this.onAliasButtonMoved, emptyFunction, emptyFunction); + } + onAliasButtonMoved = () => { + if (this._dragRef.current) { + const dragDocView = this.selectedDocumentView!; + const dragData = new DragManager.DocumentDragData([dragDocView.props.Document]); + const [left, top] = dragDocView.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); + dragData.dropAction = "alias"; + DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, left, top, { + offsetX: dragData.offset[0], + offsetY: dragData.offset[1], + hideSource: false + }); + return true; + } + return false; + } + + @computed + get templateButton() { + const docView = this.selectedDocumentView; + const templates: Map<Template, boolean> = new Map(); + const views = [this.selectedDocumentView]; + Array.from(Object.values(Templates.TemplateList)).map(template => + templates.set(template, views.reduce((checked, doc) => checked || doc?.props.Document["_show" + template.Name] ? true : false, false as boolean))); + return !docView ? (null) : + <Tooltip title={<><div className="dash-tooltip">Customize layout</div></>}> + <div className="propertiesButtons-linkFlyout"> + <Flyout anchorPoint={anchorPoints.LEFT_TOP} //onOpen={action(() => this._aliasDown = true)} onClose={action(() => this._aliasDown = false)} + content={<TemplateMenu docViews={views.filter(v => v).map(v => v as DocumentView)} templates={templates} />}> + <div className={"propertiesButtons-linkButton-empty"} > + {<FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" />} + </div> + </Flyout> + </div></Tooltip>; + } + + onCopy = () => { + if (this.selectedDoc && this.selectedDocumentView) { + const copy = Doc.MakeCopy(this.selectedDocumentView.props.Document, true); + copy.x = NumCast(this.selectedDoc.x) + NumCast(this.selectedDoc._width); + copy.y = NumCast(this.selectedDoc.y) + 30; + this.selectedDocumentView.props.addDocument?.(copy); + } + } + + @computed + get copyButton() { + const targetDoc = this.selectedDoc; + return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Tap or Drag to create an alias"}</div></>}> + <div className={"propertiesButtons-linkButton-empty"} + ref={this._dragRef} + onPointerDown={this.onAliasButtonDown} + onClick={this.onCopy}> + {<FontAwesomeIcon className="documentdecorations-icon" icon="copy" size="sm" />} + </div> + </Tooltip>; + } + + @action + onLock = () => { + this.selectedDocumentView?.toggleLockPosition(); + } + + @computed + get lockButton() { + const targetDoc = this.selectedDoc; + return !targetDoc ? (null) : <Tooltip + title={<><div className="dash-tooltip">{this.selectedDoc?.lockedPosition ? + "Unlock Position" : "Lock Position"}</div></>}> + <div className={"propertiesButtons-linkButton-empty"} + onPointerDown={this.onLock} > + {<FontAwesomeIcon className="documentdecorations-icon" + icon={BoolCast(this.selectedDoc?.lockedPosition) ? "unlock" : "lock"} size="sm" />} + </div> + </Tooltip>; + } + + @computed + get downloadButton() { + const targetDoc = this.selectedDoc; + return !targetDoc ? (null) : <Tooltip + title={<><div className="dash-tooltip">{"Download Document"}</div></>}> + <div className={"propertiesButtons-linkButton-empty"} + onPointerDown={async () => { + if (this.selectedDocumentView?.props.Document) { + Doc.Zip(this.selectedDocumentView?.props.Document); + } + }}> + {<FontAwesomeIcon className="propertiesButtons-icon" + icon="download" size="sm" />} + </div> + </Tooltip>; + } + + @computed + get deleteButton() { + const targetDoc = this.selectedDoc; + return !targetDoc ? (null) : <Tooltip + title={<><div className="dash-tooltip">{"Delete Document"}</div></>}> + <div className={"propertiesButtons-linkButton-empty"} + onPointerDown={() => { + this.selectedDocumentView?.props.ContainingCollectionView?.removeDocument(this.selectedDocumentView?.props.Document); + }}> + {<FontAwesomeIcon className="propertiesButtons-icon" + icon="trash-alt" size="sm" />} + </div> + </Tooltip>; + } + + @computed + get sharingButton() { + const targetDoc = this.selectedDoc; + return !targetDoc ? (null) : <Tooltip + title={<><div className="dash-tooltip">{"Share Document"}</div></>}> + <div className={"propertiesButtons-linkButton-empty"} + onPointerDown={() => { + if (this.selectedDocumentView) { + SharingManager.Instance.open(this.selectedDocumentView); + } + }}> + {<FontAwesomeIcon className="propertiesButtons-icon" + icon="users" size="sm" />} + </div> + </Tooltip>; + } + + @computed + get onClickButton() { + if (this.selectedDoc) { + return <Tooltip title={<><div className="dash-tooltip">Choose onClick behavior</div></>}> + <div className="propertiesButtons-linkFlyout"> + <Flyout anchorPoint={anchorPoints.LEFT_TOP} + content={this.onClickFlyout}> + <div className={"propertiesButtons-linkButton-" + "empty"} onPointerDown={e => e.stopPropagation()} > + {<FontAwesomeIcon className="documentdecorations-icon" icon="mouse-pointer" size="sm" />} + </div> + </Flyout> + </div></Tooltip>; + } else { + return null; + } + } + + @action + handleOptionChange = (e: any) => { + const value = e.target.value; + this.selectedDoc && (this.selectedDoc.onClickBehavior = e.target.value); + if (value === "nothing") { + this.selectedDocumentView?.noOnClick(); + } else if (value === "enterPortal") { + this.selectedDocumentView?.noOnClick(); + this.selectedDocumentView?.makeIntoPortal(); + } else if (value === "toggleDetail") { + this.selectedDocumentView?.noOnClick(); + this.selectedDocumentView?.toggleDetail(); + } else if (value === "linkInPlace") { + this.selectedDocumentView?.noOnClick(); + this.selectedDocumentView?.toggleFollowLink("inPlace", true, false); + } else if (value === "linkOnRight") { + this.selectedDocumentView?.noOnClick(); + this.selectedDocumentView?.toggleFollowLink("onRight", false, false); + } + } + + @undoBatch @action + editOnClickScript = () => { + if (this.selectedDoc) { + DocUtils.makeCustomViewClicked(this.selectedDoc, undefined, "onClick"); + } + } + + @computed + get onClickFlyout() { + return <div><form> + <div className="radio"> + <label> + <input type="radio" value="nothing" + checked={this.onClick === 'nothing'} + onChange={this.handleOptionChange} /> + Select Document + </label> + </div> + <div className="radio"> + <label> + <input type="radio" value="enterPortal" + checked={this.onClick === 'enterPortal'} + onChange={this.handleOptionChange} /> + Enter Portal + </label> + </div> + <div className="radio"> + <label> + <input type="radio" value="toggleDetail" + checked={this.onClick === 'toggleDetail'} + onChange={this.handleOptionChange} /> + Toggle Detail + </label> + </div> + <div className="radio"> + <label> + <input type="radio" value="linkInPlace" + checked={this.onClick === 'linkInPlace'} + onChange={this.handleOptionChange} /> + Follow Link + </label> + </div> + <div className="radio"> + <label> + <input type="radio" value="linkOnRight" + checked={this.onClick === 'linkOnRight'} + onChange={this.handleOptionChange} /> + Open Link on Right + </label> + </div> + </form> + <div onPointerDown={this.editOnClickScript} className="onClickFlyout-editScript"> Edit onClick Script</div> + </div>; + } + + @computed + get googlePhotosButton() { + const targetDoc = this.selectedDoc; + return !targetDoc ? (null) : <Tooltip + title={<><div className="dash-tooltip">{"Export to Google Photos"}</div></>}> + <div className={"propertiesButtons-linkButton-empty"} + onPointerDown={() => { + if (this.selectedDocumentView) { + GooglePhotos.Export.CollectionToAlbum({ collection: this.selectedDocumentView.Document }).then(console.log) + } + }}> + {<FontAwesomeIcon className="documentdecorations-icon" + icon="cloud-upload-alt" size="sm" />} + </div> + </Tooltip>; + } + + // @computed + // get importButton() { + // const targetDoc = this.selectedDoc; + // return !targetDoc ? (null) : <Tooltip + // title={<><div className="dash-tooltip">{"Import a Document"}</div></>}> + // <div className={"propertiesButtons-linkButton-empty"} + // onPointerDown={() => { + // if (this.selectedDocumentView) { + // CollectionFreeFormView.importDocument(100, 100); + // } + // }}> + // {<FontAwesomeIcon className="documentdecorations-icon" + // icon="upload" size="sm" />} + // </div> + // </Tooltip>; + // } + + render() { + if (!this.selectedDoc) return (null); + + const isText = this.selectedDoc[Doc.LayoutFieldKey(this.selectedDoc)] instanceof RichTextField; + const considerPull = isText && this.considerGoogleDocsPull; + const considerPush = isText && this.considerGoogleDocsPush; + const isImage = this.selectedDoc[Doc.LayoutFieldKey(this.selectedDoc)] instanceof ImageField; + const isCollection = this.selectedDoc.type === DocumentType.COL ? true : false; + + return <div><div className="propertiesButtons" style={{ paddingBottom: "5.5px" }}> + <div className="propertiesButtons-button"> + {this.templateButton} + </div> + {/* <div className="propertiesButtons-button"> + {this.metadataButton} + </div> */} + <div className="propertiesButtons-button"> + {this.pinButton} + </div> + <div className="propertiesButtons-button"> + {this.copyButton} + </div> + <div className="propertiesButtons-button"> + {this.lockButton} + </div> + <div className="propertiesButtons-button"> + {this.downloadButton} + </div> + </div> + <div className="propertiesButtons"> + <div className="propertiesButtons-button"> + {this.deleteButton} + </div> + <div className="propertiesButtons-button"> + {this.onClickButton} + </div> + <div className="propertiesButtons-button"> + {this.sharingButton} + </div> + <div className="propertiesButtons-button" style={{ display: !considerPush ? "none" : "" }}> + {this.considerGoogleDocsPush} + </div> + <div className="propertiesButtons-button" style={{ display: !considerPull ? "none" : "" }}> + {this.considerGoogleDocsPull} + </div> + <div className="propertiesButtons-button" style={{ display: !isImage ? "none" : "" }}> + {this.googlePhotosButton} + </div> + {/* <div className="propertiesButtons-button" style={{ display: !isCollection ? "none" : "" }}> + {this.importButton} + </div> */} + </div> + </div>; + } +} diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index d6cb79e9c..e7c0ad6e6 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -25,6 +25,8 @@ import { SelectionManager } from "../../util/SelectionManager"; import { DocumentView } from "../nodes/DocumentView"; import { ColorState } from "react-color"; import { ObjectField } from "../../../fields/ObjectField"; +import RichTextMenu from "../nodes/formattedText/RichTextMenu"; +import { RichTextField } from "../../../fields/RichTextField"; import { ScriptField } from "../../../fields/ScriptField"; import { IconProp } from '@fortawesome/fontawesome-svg-core'; @@ -307,13 +309,28 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp </div>; } + @computed get selectedDocumentView() { + if (SelectionManager.SelectedDocuments().length) { + return SelectionManager.SelectedDocuments()[0]; + } else { return undefined; } + } + @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } + @computed get isText() { + if (this.selectedDoc) { + return this.selectedDoc[Doc.LayoutFieldKey(this.selectedDoc)] instanceof RichTextField; + } + else return false; + } + render() { return ( <div className="collectionMenu-cont" > <div className="collectionMenu"> <div className="collectionViewBaseChrome"> - {this.props.type === CollectionViewType.Invalid || this.props.type === CollectionViewType.Docking ? (null) : this.viewModes} - {this.props.type === CollectionViewType.Invalid || this.props.type === CollectionViewType.Docking ? (null) : this.templateChrome} + {this.props.type === CollectionViewType.Invalid || + this.props.type === CollectionViewType.Docking || this.isText ? (null) : this.viewModes} + {this.props.type === CollectionViewType.Invalid || + this.props.type === CollectionViewType.Docking || this.isText ? (null) : this.templateChrome} <div className="collectionViewBaseChrome-viewSpecs" title="filter documents to show" style={{ display: "grid" }}> <button className={"antimodeMenu-button"} onClick={this.toggleViewSpecs} > <FontAwesomeIcon icon="filter" size="lg" /> @@ -348,6 +365,20 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu @computed get childDocs() { return DocListCast(this.dataField); } + + @computed get selectedDocumentView() { + if (SelectionManager.SelectedDocuments().length) { + return SelectionManager.SelectedDocuments()[0]; + } else { return undefined; } + } + @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } + @computed get isText() { + if (this.selectedDoc) { + return this.selectedDoc[Doc.LayoutFieldKey(this.selectedDoc)] instanceof RichTextField; + } + else return false; + } + @undoBatch @action nextKeyframe = (): void => { @@ -524,31 +555,34 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu render() { return !this.props.docView.layoutDoc ? (null) : <div className="collectionFreeFormMenu-cont"> - {this.props.docView.props.renderDepth !== 0 ? (null) : + {this.props.docView.props.renderDepth !== 0 || this.isText ? (null) : <div key="map" title="mini map" className="backKeyframe" onClick={this.miniMap}> <FontAwesomeIcon icon={"map"} size={"lg"} /> </div> } - <div key="back" title="back frame" className="backKeyframe" onClick={this.prevKeyframe}> + {!!!this.isText ? <div key="back" title="back frame" className="backKeyframe" onClick={this.prevKeyframe}> <FontAwesomeIcon icon={"caret-left"} size={"lg"} /> - </div> - <div key="num" title="toggle view all" className="numKeyframe" style={{ backgroundColor: this.document.editing ? "#759c75" : "#c56565" }} + </div> : null} + {!!!this.isText ? <div key="num" title="toggle view all" className="numKeyframe" style={{ backgroundColor: this.document.editing ? "#759c75" : "#c56565" }} onClick={action(() => this.document.editing = !this.document.editing)} > {NumCast(this.document.currentFrame)} - </div> - <div key="fwd" title="forward frame" className="fwdKeyframe" onClick={this.nextKeyframe}> + </div> : null} + {!!!this.isText ? <div key="fwd" title="forward frame" className="fwdKeyframe" onClick={this.nextKeyframe}> <FontAwesomeIcon icon={"caret-right"} size={"lg"} /> - </div> + </div> : null} - {!this.props.isOverlay ? (null) : + {!this.props.isOverlay || this.isText ? (null) : <button className={"antimodeMenu-button"} key="hypothesis" - style={{ backgroundColor: !this.props.docView.layoutDoc.isAnnotating ? "121212" : undefined, borderRight: "1px solid gray" }} + style={{ + backgroundColor: !this.props.docView.layoutDoc.isAnnotating ? "121212" : undefined, + borderRight: "1px solid gray" + }} title="Use Hypothesis" onClick={() => this.props.docView.layoutDoc.isAnnotating = !this.props.docView.layoutDoc.isAnnotating}> <FontAwesomeIcon icon={["fab", "hire-a-helper"]} size={"lg"} /> </button> } - {!this.props.isOverlay || this.props.docView.layoutDoc.isAnnotating ? + {(!this.props.isOverlay || this.props.docView.layoutDoc.isAnnotating) && !this.isText ? <> {this.drawButtons} {this.widthPicker} @@ -558,6 +592,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu </> : (null) } + {this.isText ? <RichTextMenu key="rich" /> : null} </div>; } } diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 5553bbbb7..f67e049fd 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -253,7 +253,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { <div className="collectionSchema-headerMenu-group"> <div onClick={() => this.typesDropdownChange(!this._openTypes)}> <label>Column type:</label> - <FontAwesomeIcon icon={"caret-down"} size="sm" style={{ float: "right" }} /> + <FontAwesomeIcon icon={"caret-down"} size="lg" style={{ float: "right" }} /> </div> {this._openTypes ? allColumnTypes : justColType} </div > diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 651357e5d..44414fa2b 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -90,7 +90,10 @@ class TreeView extends React.Component<TreeViewProps> { get displayName() { return "TreeView(" + this.doc.title + ")"; } // this makes mobx trace() statements more descriptive get defaultExpandedView() { return this.childDocs ? this.fieldKey : StrCast(this.doc.defaultExpandedView, this.noviceMode ? "layout" : "fields"); } @observable _overrideTreeViewOpen = false; // override of the treeViewOpen field allowing the display state to be independent of the document's state - set treeViewOpen(c: boolean) { if (this.props.treeViewPreventOpen) this._overrideTreeViewOpen = c; else this.doc.treeViewOpen = this._overrideTreeViewOpen = c; } + set treeViewOpen(c: boolean) { + if (this.props.treeViewPreventOpen) this._overrideTreeViewOpen = c; + else this.doc.treeViewOpen = this._overrideTreeViewOpen = c; + } @computed get treeViewOpen() { return (!this.props.treeViewPreventOpen && !this.doc.treeViewPreventOpen && BoolCast(this.doc.treeViewOpen)) || this._overrideTreeViewOpen; } @computed get treeViewExpandedView() { return StrCast(this.doc.treeViewExpandedView, this.defaultExpandedView); } @computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.containingCollection.maxEmbedHeight, 200); } diff --git a/src/client/views/collections/CollectionView.scss b/src/client/views/collections/CollectionView.scss index b630f9cf8..a5aef86de 100644 --- a/src/client/views/collections/CollectionView.scss +++ b/src/client/views/collections/CollectionView.scss @@ -24,6 +24,7 @@ border-right: unset; z-index: 2; } + .collectionTimeView-treeView { display: flex; flex-direction: column; diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 42d320308..9a7df8693 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -78,6 +78,7 @@ export interface CollectionViewCustomProps { childLayoutTemplate?: () => Opt<Doc>; // specify a layout Doc template to use for children of the collection childLayoutString?: string; // specify a layout string to use for children of the collection childOpacity?: () => number; + hideFilter?: true; } export interface CollectionRenderProps { @@ -306,7 +307,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus const options = cm.findByDescription("Options..."); const optionItems = options && "subitems" in options ? options.subitems : []; - optionItems.splice(0, 0, { description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" }); + !Doc.UserDoc().noviceMode ? optionItems.splice(0, 0, { description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" }) : null; if (this.props.Document.childLayout instanceof Doc) { optionItems.push({ description: "View Child Layout", event: () => this.props.addDocTab(this.props.Document.childLayout as Doc, "onRight"), icon: "project-diagram" }); } @@ -365,7 +366,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus get _facetWidth() { return NumCast(this.props.Document._facetWidth); } set _facetWidth(value) { this.props.Document._facetWidth = value; } - bodyPanelWidth = () => this.props.PanelWidth() - this.facetWidth(); + bodyPanelWidth = () => this.props.PanelWidth(); facetWidth = () => Math.max(0, Math.min(this.props.PanelWidth() - 25, this._facetWidth)); @computed get dataDoc() { @@ -487,6 +488,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus return false; }), returnFalse, action(() => this._facetWidth = this.facetWidth() < 15 ? Math.min(this.props.PanelWidth() - 25, 200) : 0), false); } + filterBackground = () => "rgba(105, 105, 105, 0.432)"; get ignoreFields() { return ["_docFilters", "_docRangeFilters"]; } // this makes the tree view collection ignore these filters (otherwise, the filters would filter themselves) @computed get scriptField() { @@ -556,6 +558,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus </div> </div>; } + childLayoutTemplate = () => this.props.childLayoutTemplate?.() || Cast(this.props.Document.childLayoutTemplate, Doc, null); childLayoutString = this.props.childLayoutString || StrCast(this.props.Document.childLayoutString); @@ -585,11 +588,11 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus Utils.CorsProxy(Cast(d.data, ImageField)!.url.href) : Cast(d.data, ImageField)!.url.href : ""))} - {(!this.props.isSelected() && !this.props.Document.forceActive) || this.props.Document.hideFilterView ? (null) : + {/* {(!this.props.isSelected() && !this.props.Document.forceActive) || this.props.Document.hideFilterView ? (null) : <div className="collectionView-filterDragger" title="library View Dragger" onPointerDown={this.onPointerDown} style={{ right: this.facetWidth() - 1, top: this.props.Document._viewType === CollectionViewType.Docking ? "25%" : "55%" }} /> } - {this.facetWidth() < 10 ? (null) : this.filterView} + {this.facetWidth() < 10 ? (null) : this.filterView} */} </div>); } } diff --git a/src/client/views/collections/SchemaTable.tsx b/src/client/views/collections/SchemaTable.tsx index cde795098..75e693f96 100644 --- a/src/client/views/collections/SchemaTable.tsx +++ b/src/client/views/collections/SchemaTable.tsx @@ -208,7 +208,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { }}> {col.heading}</div>; - const sortIcon = col.desc === undefined ? "circle" : col.desc === true ? "caret-down" : "caret-up"; + const sortIcon = col.desc === undefined ? "caret-right" : col.desc === true ? "caret-down" : "caret-up"; const header = <div //className="collectionSchemaView-header" @@ -224,12 +224,12 @@ export class SchemaTable extends React.Component<SchemaTableProps> { {keysDropdown} </div> <div onClick={e => this.changeSorting(col)} - style={{ paddingRight: "6px", display: "inline" }}> + style={{ paddingRight: "6px", marginLeft: "4px", display: "inline" }}> <FontAwesomeIcon icon={sortIcon} size="sm" /> </div> <div onClick={e => this.props.openHeader(col, e.clientX, e.clientY)} style={{ float: "right", paddingRight: "6px" }}> - <FontAwesomeIcon icon={"compass"} size="sm" /> + <FontAwesomeIcon icon={"cog"} size="sm" /> </div> </div>; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 57336131a..e0981d797 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1239,13 +1239,15 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const appearanceItems = appearance && "subitems" in appearance ? appearance.subitems : []; appearanceItems.push({ description: "Reset View", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document[this.scaleFieldKey] = 1; }, icon: "compress-arrows-alt" }); appearanceItems.push({ description: `${this.fitToContent ? "Make Zoomable" : "Scale to Window"}`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" }); - appearanceItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" }); + !Doc.UserDoc().noviceMode ? appearanceItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" }) : null; !appearance && ContextMenu.Instance.addItem({ description: "Appearance...", subitems: appearanceItems, icon: "eye" }); const viewctrls = ContextMenu.Instance.findByDescription("UI Controls..."); const viewCtrlItems = viewctrls && "subitems" in viewctrls ? viewctrls.subitems : []; - viewCtrlItems.push({ description: (Doc.UserDoc().showSnapLines ? "Hide" : "Show") + " Snap Lines", event: () => Doc.UserDoc().showSnapLines = !Doc.UserDoc().showSnapLines, icon: "compress-arrows-alt" }); - viewCtrlItems.push({ description: (this.Document.useClusters ? "Hide" : "Show") + " Clusters", event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" }); + + + !Doc.UserDoc().noviceMode ? viewCtrlItems.push({ description: (Doc.UserDoc().showSnapLines ? "Hide" : "Show") + " Snap Lines", event: () => Doc.UserDoc().showSnapLines = !Doc.UserDoc().showSnapLines, icon: "compress-arrows-alt" }) : null; + !Doc.UserDoc().noviceMode ? viewCtrlItems.push({ description: (this.Document.useClusters ? "Hide" : "Show") + " Clusters", event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" }) : null; !viewctrls && ContextMenu.Instance.addItem({ description: "UI Controls...", subitems: viewCtrlItems, icon: "eye" }); const options = ContextMenu.Instance.findByDescription("Options..."); diff --git a/src/client/views/collections/collectionFreeForm/PropertiesView.scss b/src/client/views/collections/collectionFreeForm/PropertiesView.scss new file mode 100644 index 000000000..cb4b7375b --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/PropertiesView.scss @@ -0,0 +1,309 @@ +.propertiesView { + + background-color: rgb(205, 205, 205); + height: 100%; + font-family: "Noto Sans"; + cursor: auto; + + overflow-x: visible; + overflow-y: visible; + + .propertiesView-title { + background-color: rgb(159, 159, 159); + text-align: center; + padding-top: 12px; + padding-bottom: 12px; + display: flex; + + .propertiesView-title-name { + font-size: 18px; + font-weight: bold; + padding-left: 45px; + } + + .propertiesView-title-icon { + width: 20px; + height: 20px; + padding-left: 38px; + margin-top: -5px; + + &:hover { + color: grey; + cursor: pointer; + } + + } + + } + + .propertiesView-name { + border-bottom: 1px solid black; + padding: 8.5px; + font-size: 12.5px; + + &:hover { + cursor: pointer; + } + } + + .propertiesView-settings { + border-bottom: 1px solid black; + //padding: 8.5px; + font-size: 12.5px; + font-weight: bold; + + .propertiesView-settings-title { + font-weight: bold; + font-size: 12.5px; + padding: 4px; + display: flex; + width: 200px; + color: white; + padding-left: 8px; + background-color: rgb(51, 51, 51); + + .propertiesView-settings-title-icon { + float: right; + right: 0; + position: absolute; + margin-left: 2px; + margin-right: 9px; + + &:hover { + cursor: pointer; + } + } + } + + .propertiesView-settings-content { + margin-left: 12px; + padding-bottom: 10px; + padding-top: 8px; + } + + } + + .propertiesView-sharing { + border-bottom: 1px solid black; + //padding: 8.5px; + + .propertiesView-sharing-title { + font-weight: bold; + font-size: 12.5px; + padding: 4px; + display: flex; + width: 200px; + color: white; + padding-left: 8px; + background-color: rgb(51, 51, 51); + + .propertiesView-sharing-title-icon { + float: right; + right: 0; + position: absolute; + margin-left: 2px; + margin-right: 9px; + + &:hover { + cursor: pointer; + } + } + } + + .propertiesView-sharing-content { + font-size: 10px; + padding: 10px; + margin-left: 5px; + } + } + + .notify-button { + padding: 2px; + width: 12px; + height: 12px; + background-color: black; + border-radius: 10px; + padding-left: 2px; + padding-right: 2px; + margin-top: 2px; + margin-right: 3px; + + .notify-button-icon { + width: 6px; + height: 6.5px; + margin-left: .5px; + } + + &:hover { + background-color: rgb(158, 158, 158); + cursor: pointer; + } + } + + .expansion-button-icon { + width: 11px; + height: 11px; + color: black; + margin-left: 27px; + + &:hover { + color: rgb(131, 131, 131); + cursor: pointer; + } + } + + .propertiesView-sharingTable { + + border: 1px solid black; + padding: 5px; + border-radius: 6px; + width: 170px; + background-color: #ececec; + + .propertiesView-sharingTable-item { + + display: flex; + padding: 3px; + + border-bottom: 0.5px solid grey; + + .propertiesView-sharingTable-item-name { + font-weight: bold; + width: 80px; + overflow-x: hidden; + display: inline-block; + } + + .propertiesView-sharingTable-item-permission { + + display: flex; + + .permissions-select { + z-index: 1; + border: none; + background-color: inherit; + width: 75px; + + &:hover { + cursor: pointer; + } + } + } + + &:last-child { + border-bottom: none; + } + } + } + + .propertiesView-fields { + border-bottom: 1px solid black; + //padding: 8.5px; + + .propertiesView-fields-title { + font-weight: bold; + font-size: 12.5px; + padding: 4px; + display: flex; + width: 200px; + color: white; + padding-left: 8px; + background-color: rgb(51, 51, 51); + + .propertiesView-fields-title-name { + font-size: 12.5px; + font-weight: bold; + white-space: nowrap; + width: 35px; + display: flex; + } + + .propertiesView-fields-title-icon { + float: right; + right: 0; + position: absolute; + margin-left: 2px; + margin-right: 9px; + + &:hover { + cursor: pointer; + } + } + + .propertiesView-fields-title-checkbox { + float: right; + height: 20px; + margin-top: -13px; + margin-left: 115px; + + .propertiesView-fields-title-checkbox-text { + font-size: 7px; + margin-top: -10px; + margin-left: 6px; + } + } + } + + .propertiesView-fields-content { + font-size: 10px; + margin-left: 2px; + padding: 10px; + + &:hover { + cursor: pointer; + } + } + } + + .field { + display: flex; + font-size: 7px; + background-color: #e8e8e8; + padding-right: 3px; + border: 0.5px solid grey; + border-radius: 5px; + padding-left: 3px; + } + + .uneditable-field { + display: flex; + overflow-y: visible; + margin-bottom: 2px; + + &:hover { + cursor: auto; + } + } + + .propertiesView-layout { + + .propertiesView-layout-title { + font-weight: bold; + font-size: 12.5px; + padding: 4px; + display: flex; + width: 200px; + color: white; + padding-left: 8px; + background-color: rgb(51, 51, 51); + + .propertiesView-layout-title-icon { + float: right; + right: 0; + position: absolute; + margin-left: 2px; + margin-right: 9px; + + &:hover { + cursor: pointer; + } + } + } + + .propertiesView-layout-content { + overflow: hidden; + padding: 10px; + } + + } +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/PropertiesView.tsx b/src/client/views/collections/collectionFreeForm/PropertiesView.tsx new file mode 100644 index 000000000..d5317efcb --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/PropertiesView.tsx @@ -0,0 +1,417 @@ +import React = require("react"); +import { observer } from "mobx-react"; +import "./PropertiesView.scss"; +import { observable, action, computed, runInAction } from "mobx"; +import { Doc, Field, DocListCast, WidthSym, HeightSym } from "../../../../fields/Doc"; +import { DocumentView } from "../../nodes/DocumentView"; +import { ComputedField } from "../../../../fields/ScriptField"; +import { EditableView } from "../../EditableView"; +import { KeyValueBox } from "../../nodes/KeyValueBox"; +import { Cast, NumCast, StrCast } from "../../../../fields/Types"; +import { listSpec } from "../../../../fields/Schema"; +import { ContentFittingDocumentView } from "../../nodes/ContentFittingDocumentView"; +import { returnFalse, returnOne, emptyFunction, emptyPath, returnTrue, returnZero, returnEmptyFilter, Utils } from "../../../../Utils"; +import { Id } from "../../../../fields/FieldSymbols"; +import { Transform } from "../../../util/Transform"; +import { PropertiesButtons } from "../../PropertiesButtons"; +import { SelectionManager } from "../../../util/SelectionManager"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Tooltip, Checkbox } from "@material-ui/core"; +import SharingManager from "../../../util/SharingManager"; + + +interface PropertiesViewProps { + width: number; + height: number; + renderDepth: number; + ScreenToLocalTransform: () => Transform; + onDown: (event: any) => void; +} + +@observer +export class PropertiesView extends React.Component<PropertiesViewProps> { + + @computed get MAX_EMBED_HEIGHT() { return 200; } + + @computed get selectedDocumentView() { + if (SelectionManager.SelectedDocuments().length) { + return SelectionManager.SelectedDocuments()[0]; + } else { return undefined; } + } + @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } + @computed get dataDoc() { return this.selectedDocumentView?.dataDoc; } + + @observable layoutFields: boolean = false; + + @observable openActions: boolean = true; + @observable openSharing: boolean = true; + @observable openFields: boolean = true; + @observable openLayout: boolean = true; + + @action + rtfWidth = () => { + if (this.selectedDoc) { + return Math.min(this.selectedDoc?.[WidthSym](), this.props.width - 20); + } else { + return 0; + } + } + @action + rtfHeight = () => { + if (this.selectedDoc) { + return this.rtfWidth() <= this.selectedDoc?.[WidthSym]() ? Math.min(this.selectedDoc?.[HeightSym](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT; + } else { + return 0; + } + } + + @action + docWidth = () => { + if (this.selectedDoc) { + const layoutDoc = this.selectedDoc; + const aspect = NumCast(layoutDoc._nativeHeight, layoutDoc._fitWidth ? 0 : layoutDoc[HeightSym]()) / NumCast(layoutDoc._nativeWidth, layoutDoc._fitWidth ? 1 : layoutDoc[WidthSym]()); + if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.width - 20)); + return NumCast(layoutDoc._nativeWidth) ? Math.min(layoutDoc[WidthSym](), this.props.width - 20) : this.props.width - 20; + } else { + return 0; + } + } + + @action + docHeight = () => { + if (this.selectedDoc && this.dataDoc) { + const layoutDoc = this.selectedDoc; + return Math.max(70, Math.min(this.MAX_EMBED_HEIGHT, (() => { + const aspect = NumCast(layoutDoc._nativeHeight, layoutDoc._fitWidth ? 0 : layoutDoc[HeightSym]()) / NumCast(layoutDoc._nativeWidth, layoutDoc._fitWidth ? 1 : layoutDoc[WidthSym]()); + if (aspect) return this.docWidth() * aspect; + return layoutDoc._fitWidth ? (!this.dataDoc._nativeHeight ? NumCast(this.props.height) : + Math.min(this.docWidth() * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc._nativeHeight)) / NumCast(layoutDoc._nativeWidth, + NumCast(this.props.height)))) : + NumCast(layoutDoc._height) ? NumCast(layoutDoc._height) : 50; + })())); + } else { + return 0; + } + } + + @computed get expandedField() { + if (this.dataDoc && this.selectedDoc) { + const ids: { [key: string]: string } = {}; + const doc = this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc; + doc && Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key)); + const rows: JSX.Element[] = []; + for (const key of Object.keys(ids).slice().sort()) { + const contents = doc[key]; + if (contents === "UNDEFINED") { + rows.push(<div style={{ display: "flex", overflowY: "visible", marginBottom: "2px" }} key={key}> + <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key}</span> + + </div>); + } else { + let contentElement: (JSX.Element | null)[] | JSX.Element = []; + contentElement = <EditableView key="editableView" + contents={contents !== undefined ? Field.toString(contents as Field) : "null"} + height={13} + fontSize={10} + GetValue={() => Field.toKeyValueString(doc, key)} + SetValue={(value: string) => KeyValueBox.SetField(doc, key, value, true)} + />; + rows.push(<div style={{ display: "flex", overflowY: "visible", marginBottom: "-1px" }} key={key}> + <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key + ":"}</span> + + {contentElement} + </div>); + } + } + rows.push(<div className="field" key={"newKeyValue"} style={{ marginTop: "3px" }}> + <EditableView + key="editableView" + contents={"add key:value or #tags"} + height={13} + fontSize={10} + GetValue={() => ""} + SetValue={this.setKeyValue} /> + </div>); + return rows; + } + } + + @computed get noviceFields() { + if (this.dataDoc && this.selectedDoc) { + const ids: { [key: string]: string } = {}; + const doc = this.dataDoc; + doc && Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key)); + const rows: JSX.Element[] = []; + for (const key of Object.keys(ids).slice().sort()) { + if (key[0] === key[0].toUpperCase() || key[0] === "#" || key === "author" || key === "creationDate" || key.indexOf("lastModified") !== -1) { + const contents = doc[key]; + if (contents === "UNDEFINED") { + rows.push(<div className="uneditable-field" key={key}> + <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key}</span> + + </div>); + } else { + const value = Field.toString(contents as Field); + if (key === "author" || key === "creationDate" || key.indexOf("lastModified") !== -1) { + rows.push(<div className="uneditable-field" key={key}> + <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key + ": "}</span> + <div style={{ whiteSpace: "nowrap", overflowX: "hidden" }}>{value}</div> + </div>); + } else { + let contentElement: (JSX.Element | null)[] | JSX.Element = []; + contentElement = <EditableView key="editableView" + contents={contents !== undefined ? Field.toString(contents as Field) : "null"} + height={13} + fontSize={10} + GetValue={() => Field.toKeyValueString(doc, key)} + SetValue={(value: string) => KeyValueBox.SetField(doc, key, value, true)} + />; + + rows.push(<div style={{ display: "flex", overflowY: "visible", marginBottom: "-1px" }} key={key}> + <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key + ":"}</span> + + {contentElement} + </div>); + } + } + } + } + rows.push(<div className="field" key={"newKeyValue"} style={{ marginTop: "3px" }}> + <EditableView + key="editableView" + contents={"add key:value or #tags"} + height={13} + fontSize={10} + GetValue={() => ""} + SetValue={this.setKeyValue} /> + </div>); + return rows; + } + } + + + setKeyValue = (value: string) => { + if (this.selectedDoc && this.dataDoc) { + const doc = this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc; + if (value.indexOf(":") !== -1) { + const newVal = value[0].toUpperCase() + value.substring(1, value.length); + KeyValueBox.SetField(doc, newVal.substring(0, newVal.indexOf(":")), newVal.substring(newVal.indexOf(":") + 1, newVal.length), true); + return true; + } else if (value[0] === "#") { + const newVal = value + ":'UNDEFINED'"; + KeyValueBox.SetField(doc, newVal.substring(0, newVal.indexOf(":")), newVal.substring(newVal.indexOf(":") + 1, newVal.length), true); + return true; + } + } + return false; + } + + @computed get layoutPreview() { + if (this.selectedDoc) { + const layoutDoc = Doc.Layout(this.selectedDoc); + const panelHeight = StrCast(Doc.LayoutField(layoutDoc)).includes("FormattedTextBox") ? this.rtfHeight : this.docHeight; + const panelWidth = StrCast(Doc.LayoutField(layoutDoc)).includes("FormattedTextBox") ? this.rtfWidth : this.docWidth; + return <div style={{ display: "inline-block", height: panelHeight() }} key={this.selectedDoc[Id]}> + <ContentFittingDocumentView + Document={layoutDoc} + DataDoc={this.dataDoc} + LibraryPath={emptyPath} + renderDepth={this.props.renderDepth + 1} + rootSelected={returnFalse} + treeViewDoc={undefined} + backgroundColor={() => "lightgrey"} + fitToBox={false} + FreezeDimensions={true} + NativeWidth={layoutDoc.type === + StrCast(Doc.LayoutField(layoutDoc)).includes("FormattedTextBox") ? this.rtfWidth : returnZero} + NativeHeight={layoutDoc.type === + StrCast(Doc.LayoutField(layoutDoc)).includes("FormattedTextBox") ? this.rtfHeight : returnZero} + PanelWidth={panelWidth} + PanelHeight={panelHeight} + focus={returnFalse} + ScreenToLocalTransform={this.props.ScreenToLocalTransform} + docFilters={returnEmptyFilter} + ContainingCollectionDoc={undefined} + ContainingCollectionView={undefined} + addDocument={returnFalse} + moveDocument={undefined} + removeDocument={returnFalse} + parentActive={() => false} + whenActiveChanged={emptyFunction} + addDocTab={returnFalse} + pinToPres={emptyFunction} + bringToFront={returnFalse} + ContentScaling={returnOne} + /> + </div>; + } else { + return null; + } + } + + @computed get permissionsSelect() { + return <select className="permissions-select" onChange={emptyFunction}> + <option key={"Can Edit"} value={"Can Edit"}> + Can Edit + </option> + <option key={"Can Add"} value={"Can Add"}> + Can Add + </option> + <option key={"Can View"} value={"Can View"}> + Can View + </option> + <option key={"Not Shared"} value={"Not Shared"}> + Not Shared + </option> + </select>; + } + + @computed get notifyIcon() { + return <Tooltip title={<><div className="dash-tooltip">{"Notify group of permissions change"}</div></>}> + <div className="notify-button"> + <FontAwesomeIcon className="notify-button-icon" icon="bell" color="white" size="sm" /> + </div> + </Tooltip>; + } + + @computed get expansionIcon() { + return <Tooltip title={<><div className="dash-tooltip">{"Show more permissions"}</div></>}> + <div className="expansion-button" onPointerDown={() => { + if (this.selectedDocumentView) { + SharingManager.Instance.open(this.selectedDocumentView); + } + }}> + <FontAwesomeIcon className="expansion-button-icon" icon="ellipsis-h" color="black" size="sm" /> + </div> + </Tooltip>; + } + + sharingItem(name: string, notify: boolean, editable: boolean, permission?: string) { + return <div className="propertiesView-sharingTable-item"> + <div className="propertiesView-sharingTable-item-name" style={{ width: notify ? "70px" : "80px" }}> {name} </div> + {notify ? this.notifyIcon : null} + <div className="propertiesView-sharingTable-item-permission"> + {editable ? this.permissionsSelect : permission} + {permission === "Owner" ? this.expansionIcon : null} + </div> + </div>; + } + + @computed get sharingTable() { + return <div className="propertiesView-sharingTable"> + {this.sharingItem("Me", false, false, "Owner")} + {this.sharingItem("Public", false, true)} + {this.sharingItem("Group 1", true, true)} + {this.sharingItem("Group 2", true, true)} + </div>; + } + + @computed get fieldsCheckbox() { + return <Checkbox + color="primary" + onChange={this.toggleCheckbox} + checked={this.layoutFields} + />; + } + + @action + toggleCheckbox = () => { + this.layoutFields = !this.layoutFields; + } + + @computed get editableTitle() { + return <EditableView + key="editableView" + contents={StrCast(this.selectedDoc?.title)} + height={25} + fontSize={14} + GetValue={() => StrCast(this.selectedDoc?.title)} + SetValue={this.setTitle} />; + } + + setTitle = (value: string) => { + if (this.dataDoc) { + this.selectedDoc && (this.selectedDoc.title = value); + KeyValueBox.SetField(this.dataDoc, "title", value, true); + return true; + } + return false; + } + + render() { + + if (!this.selectedDoc) { + return <div className="propertiesView" > + <div className="propertiesView-title"> + No Document Selected + </div> </div>; + } + + const novice = Doc.UserDoc().noviceMode; + + return <div className="propertiesView" > + <div className="propertiesView-title"> + <div className="propertiesView-title-name">Properties </div> + <div className="propertiesView-title-icon" onPointerDown={this.props.onDown}> + <FontAwesomeIcon icon="times" color="black" size="sm" /> + </div> + </div> + <div className="propertiesView-name"> + {this.editableTitle} + </div> + <div className="propertiesView-settings"> + <div className="propertiesView-settings-title" style={{ backgroundColor: this.openActions ? "black" : "" }}> + Document Actions + <div className="propertiesView-settings-title-icon" + onPointerDown={() => runInAction(() => { this.openActions = !this.openActions; })}> + <FontAwesomeIcon icon={this.openActions ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + {this.openActions ? <div className="propertiesView-settings-content"> + <PropertiesButtons /> + </div> : null} + </div> + <div className="propertiesView-sharing"> + <div className="propertiesView-sharing-title" style={{ backgroundColor: this.openSharing ? "black" : "" }}> + Sharing {"&"} Permissions + <div className="propertiesView-sharing-title-icon" + onPointerDown={() => runInAction(() => { this.openSharing = !this.openSharing; })}> + <FontAwesomeIcon icon={this.openSharing ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + {this.openSharing ? <div className="propertiesView-sharing-content"> + {this.sharingTable} + </div> : null} + </div> + <div className="propertiesView-fields"> + <div className="propertiesView-fields-title" style={{ backgroundColor: this.openFields ? "black" : "" }}> + <div className="propertiesView-fields-title-name"> + Fields {"&"} Tags + <div className="propertiesView-fields-title-icon" + onPointerDown={() => runInAction(() => { this.openFields = !this.openFields; })}> + <FontAwesomeIcon icon={this.openFields ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + </div> + {!novice && this.openFields ? <div className="propertiesView-fields-title-checkbox"> + {this.fieldsCheckbox} + <div className="propertiesView-fields-title-checkbox-text">Layout</div> + </div> : null} + {this.openFields ? + <div className="propertiesView-fields-content"> + {novice ? this.noviceFields : this.expandedField} + </div> : null} + </div> + <div className="propertiesView-layout"> + <div className="propertiesView-layout-title" style={{ backgroundColor: this.openLayout ? "black" : "" }}> + Layout + <div className="propertiesView-layout-title-icon" + onPointerDown={() => runInAction(() => { this.openLayout = !this.openLayout; })}> + <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> + </div>; + } +}
\ No newline at end of file diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index d1c839c3b..0aabf5319 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -182,7 +182,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { switch (this.props.destinationDoc.type) { case DocumentType.IMG: destinationIcon = "image"; break; case DocumentType.COMPARISON: destinationIcon = "columns"; break; - case DocumentType.RTF: destinationIcon = "font"; break; + case DocumentType.RTF: destinationIcon = "sticky-note"; break; case DocumentType.COL: destinationIcon = "folder"; break; case DocumentType.WEB: destinationIcon = "globe-asia"; break; case DocumentType.SCREENSHOT: destinationIcon = "photo-video"; break; @@ -196,6 +196,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { case DocumentType.DOCHOLDER: destinationIcon = "expand"; break; case DocumentType.VID: destinationIcon = "video"; break; case DocumentType.INK: destinationIcon = "pen-nib"; break; + case DocumentType.PDF: destinationIcon = "file"; break; default: destinationIcon = "question"; break; } diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 47dc0a773..d72133cb9 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -18,6 +18,7 @@ import { DocHolderBox } from "./DocHolderBox"; import { DocumentViewProps } from "./DocumentView"; import "./DocumentView.scss"; import { FontIconBox } from "./FontIconBox"; +import { MenuIconBox } from "./MenuIconBox"; import { FieldView, FieldViewProps } from "./FieldView"; import { FormattedTextBox } from "./formattedText/FormattedTextBox"; import { ImageBox } from "./ImageBox"; @@ -190,7 +191,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { blacklistedAttrs={[]} renderInWrapper={false} components={{ - FormattedTextBox, ImageBox, DirectoryImportBox, FontIconBox, LabelBox, SliderBox, FieldView, + FormattedTextBox, ImageBox, DirectoryImportBox, FontIconBox, MenuIconBox, LabelBox, SliderBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, PresBox, YoutubeBox, PresElementBox, QueryBox, ColorBox, DashWebRTCVideo, LinkAnchorBox, InkingStroke, DocHolderBox, LinkBox, ScriptingBox, diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index 3b91ac6b5..198a90225 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -198,8 +198,11 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp // }))} > + {/* {this.props.InMenu ? this.props.StartLink ? <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" /> : + <FontAwesomeIcon className="documentdecorations-icon" icon="hand-paper" size="sm" /> : links.length} */} + {this.props.InMenu ? this.props.StartLink ? <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" /> : - <FontAwesomeIcon className="documentdecorations-icon" icon="hand-paper" size="sm" /> : links.length} + link : links.length} </div> {DocumentLinksButton.StartLink && this.props.InMenu && !!!this.props.StartLink && DocumentLinksButton.StartLink !== this.props.View ? <div className={"documentLinksButton-endLink"} diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index c47edefd6..f5ae9349a 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -94,6 +94,8 @@ export interface DocumentViewProps { layoutKey?: string; radialMenu?: String[]; display?: string; + relative?: boolean; + scriptContext?: any; } @observer @@ -311,6 +313,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const func = () => this.onClickHandler.script.run({ this: this.layoutDoc, self: this.rootDoc, + scriptContext: this.props.scriptContext, thisContainer: this.props.ContainingCollectionDoc, shiftKey: e.shiftKey }, console.log); @@ -576,6 +579,18 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } } + + @undoBatch + noOnClick = (): void => { + this.Document.ignoreClick = false; + this.Document.isLinkButton = false; + } + + @undoBatch + toggleDetail = (): void => { + this.Document.onClick = ScriptField.MakeScript(`toggleDetail(self, "${this.Document.layoutKey}")`); + } + @undoBatch @action drop = async (e: Event, de: DragManager.DropEvent) => { if (this.props.Document === Doc.UserDoc().activeWorkspace) { @@ -653,6 +668,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } @action + onCopy = () => { + const copy = Doc.MakeCopy(this.props.Document, true); + copy.x = NumCast(this.props.Document.x) + NumCast(this.props.Document._width); + copy.y = NumCast(this.props.Document.y) + 30; + this.props.addDocument?.(copy); + } + + @action onContextMenu = async (e: React.MouseEvent | Touch): Promise<void> => { // the touch onContextMenu is button 0, the pointer onContextMenu is button 2 if (!(e instanceof Touch)) { @@ -689,21 +712,21 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const options = cm.findByDescription("Options..."); const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : []; - optionItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" }); + //optionItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" }); !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "compass" }); const existingOnClick = cm.findByDescription("OnClick..."); const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" }); - onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(self, "${this.Document.layoutKey}")`), icon: "window-restore" }); + onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(self, "${this.Document.layoutKey}")`), icon: "concierge-bell" }); 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: "concierge-bell" }); - !this.Document.isLinkButton && onClicks.push({ description: "Follow Link on Right", event: () => this.toggleFollowLink("onRight", false, false), icon: "concierge-bell" }); - onClicks.push({ description: this.Document.isLinkButton || this.onClickHandler ? "Remove Click Behavior" : "Follow Link", event: () => this.toggleFollowLink(undefined, false, false), icon: "concierge-bell" }); - onClicks.push({ description: (this.Document.isPushpin ? "Remove" : "Make") + " Pushpin", event: () => this.toggleFollowLink(undefined, false, true), icon: "snowflake" }); - onClicks.push({ description: "Edit onClick Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"), icon: "edit" }); - !existingOnClick && cm.addItem({ description: "OnClick...", noexpand: true, addDivider: true, subitems: onClicks, icon: "hand-point-right" }); + 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("onRight", 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...", noexpand: true, addDivider: true, subitems: onClicks, icon: "mouse-pointer" }); const funcs: ContextMenuProps[] = []; if (this.layoutDoc.onDragStart) { @@ -715,6 +738,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const more = cm.findByDescription("More..."); const moreItems = more && "subitems" in more ? more.subitems : []; + moreItems.push({ description: "Share", event: () => SharingManager.Instance.open(this), icon: "users" }); + moreItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" }); + moreItems.push({ description: "Create an Alias", event: () => this.onCopy(), icon: "copy" }); moreItems.push({ description: "Download document", icon: "download", event: async () => Doc.Zip(this.props.Document) }); if (!Doc.UserDoc().noviceMode) { moreItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: "concierge-bell" }); @@ -727,16 +753,18 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "fingerprint" }); } + //GetEffectiveAcl(this.props.Document) === AclEdit && moreItems.push({ description: "Delete", event: this.deleteClicked, icon: "trash" }); + const effectiveAcl = GetEffectiveAcl(this.props.Document); (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) && moreItems.push({ description: "Delete", event: this.deleteClicked, icon: "trash" }); - moreItems.push({ description: "Share", event: () => SharingManager.Instance.open(this), icon: "external-link-alt" }); + !more && cm.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" }); cm.moveAfter(cm.findByDescription("More...")!, cm.findByDescription("OnClick...")!); const help = cm.findByDescription("Help..."); const helpItems: ContextMenuProps[] = help && "subitems" in help ? help.subitems : []; + !Doc.UserDoc().novice && helpItems.push({ description: "Show Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "onRight"), 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 }), "onRight"), icon: "keyboard" }); - helpItems.push({ description: "Show Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "onRight"), icon: "layer-group" }); 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" }); @@ -778,8 +806,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu childScaling = () => (this.layoutDoc._fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling()); @computed.struct get linkOffset() { return [-15, 0]; } @computed get contents() { + const pos = this.props.relative ? "relative " : "absolute"; TraceMobx(); - return (<div style={{ position: "absolute", width: "100%", height: "100%" }}> + return (<div style={{ width: "100%", height: "100%" }}> <DocumentContentsView key={1} docFilters={this.props.docFilters} ContainingCollectionView={this.props.ContainingCollectionView} @@ -868,8 +897,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } @computed get innards() { TraceMobx(); + const pos = this.props.relative ? "relative" : undefined; if (this.props.treeViewDoc && !this.props.LayoutTemplateString?.includes("LinkAnchorBox")) { // this happens when the document is a tree view label (but not an anchor dot) - return <div className="documentView-treeView" style={{ maxWidth: this.props.PanelWidth() || undefined }}> + return <div className="documentView-treeView" style={{ + maxWidth: this.props.PanelWidth() || undefined, + position: pos + }}> {StrCast(this.props.Document.title)} {this.allAnchors} </div>; @@ -995,7 +1028,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu background: finalColor, opacity: finalOpacity, fontFamily: StrCast(this.Document._fontFamily, "inherit"), - fontSize: Cast(this.Document._fontSize, "string", null) + fontSize: Cast(this.Document._fontSize, "string", null), }}> {this.onClickHandler && this.props.ContainingCollectionView?.props.Document._viewType === CollectionViewType.Time ? <> {this.innards} diff --git a/src/client/views/nodes/MenuIconBox.scss b/src/client/views/nodes/MenuIconBox.scss new file mode 100644 index 000000000..1b72f5a8f --- /dev/null +++ b/src/client/views/nodes/MenuIconBox.scss @@ -0,0 +1,49 @@ +.menuButton { + //padding: 7px; + padding-left: 7px; + width: 100%; + width: 60px; + height: 70px; + + .menuButton-wrap { + width: 45px; + /* padding: 5px; */ + touch-action: none; + background: black; + transform-origin: top left; + /* margin-bottom: 5px; */ + margin-top: 5px; + margin-right: 25px; + border-radius: 8px; + + &:hover { + background: rgb(61, 61, 61); + cursor: pointer; + } + } + + .menuButton-label { + color: white; + margin-right: 4px; + border-radius: 8px; + width: 42px; + position: relative; + text-align: center; + font-size: 8px; + margin-top: 1px; + letter-spacing: normal; + padding: 3px; + background-color: inherit; + } + + .menuButton-icon { + width: auto; + height: 35px; + padding: 5px; + } + + svg { + width: 95% !important; + height: 95%; + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/MenuIconBox.tsx b/src/client/views/nodes/MenuIconBox.tsx new file mode 100644 index 000000000..e1656fcba --- /dev/null +++ b/src/client/views/nodes/MenuIconBox.tsx @@ -0,0 +1,41 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { createSchema, makeInterface } from '../../../fields/Schema'; +import { DocComponent } from '../DocComponent'; +import './MenuIconBox.scss'; +import { FieldView, FieldViewProps } from './FieldView'; +import { StrCast, Cast, NumCast } from '../../../fields/Types'; +import { Utils } from "../../../Utils"; +import { runInAction, observable, reaction, IReactionDisposer } from 'mobx'; +import { Doc } from '../../../fields/Doc'; +import { ScriptField } from '../../../fields/ScriptField'; +import { CurrentUserUtils } from '../../util/CurrentUserUtils'; +const MenuIconSchema = createSchema({ + icon: "string" +}); + +type MenuIconDocument = makeInterface<[typeof MenuIconSchema]>; +const MenuIconDocument = makeInterface(MenuIconSchema); +@observer +export class MenuIconBox extends DocComponent<FieldViewProps, MenuIconDocument>(MenuIconDocument) { + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(MenuIconBox, fieldKey); } + _ref: React.RefObject<HTMLButtonElement> = React.createRef(); + + render() { + + const menuBTN = <div className="menuButton" style={{ backgroundColor: CurrentUserUtils.panelContent === this.dataDoc.title ? "lightgrey" : "" }}> + <div className="menuButton-wrap" + style={{ backgroundColor: CurrentUserUtils.panelContent === this.dataDoc.title ? "lightgrey" : "" }} + //onPointerDown={this.dataDoc.click} + > + <FontAwesomeIcon className="menuButton-icon" icon={StrCast(this.dataDoc.icon, "user") as any} + color={CurrentUserUtils.panelContent === this.dataDoc.title ? "black" : "white"} size="lg" /> + <div className="menuButton-label" + style={{ color: CurrentUserUtils.panelContent === this.dataDoc.title ? "black" : "white" }}> {this.dataDoc.title} </div> + </div> + </div>; + + return menuBTN; + } +}
\ 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 627c6e363..3d389cf87 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1267,7 +1267,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this.doLinkOnDeselect(); // move the richtextmenu offscreen - if (!RichTextMenu.Instance.Pinned) RichTextMenu.Instance.delayHide(); + //if (!RichTextMenu.Instance.Pinned) RichTextMenu.Instance.delayHide(); } _lastTimedMark: Mark | undefined = undefined; diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 47a4911b8..9de2fc18b 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -77,7 +77,8 @@ export default class RichTextMenu extends AntimodeMenu { super(props); RichTextMenu.Instance = this; this._canFade = false; - this.Pinned = BoolCast(Doc.UserDoc()["menuRichText-pinned"]); + //this.Pinned = BoolCast(Doc.UserDoc()["menuRichText-pinned"]); + this.Pinned = true; this.fontSizeOptions = [ { mark: schema.marks.pFontSize.create({ fontSize: 7 }), title: "Set font size", label: "7pt", command: this.changeFontSize }, @@ -918,16 +919,22 @@ export default class RichTextMenu extends AntimodeMenu { render() { TraceMobx(); const row1 = <div className="antimodeMenu-row" key="row 1" style={{ display: this.collapsed ? "none" : undefined }}>{[ - !this.collapsed ? this.getDragger() : (null), - !this.Pinned ? (null) : <div key="frag1"> {[ - this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)), - this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)), - this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)), - this.createButton("strikethrough", "Strikethrough", this.strikethroughActive, toggleMark(schema.marks.strikethrough)), - this.createButton("superscript", "Superscript", this.superscriptActive, toggleMark(schema.marks.superscript)), - this.createButton("subscript", "Subscript", this.subscriptActive, toggleMark(schema.marks.subscript)), - <div className="richTextMenu-divider" key="divider" /> - ]}</div>, + //!this.collapsed ? this.getDragger() : (null), + // !this.Pinned ? (null) : <div key="frag1"> {[ + // this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)), + // this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)), + // this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)), + // this.createButton("strikethrough", "Strikethrough", this.strikethroughActive, toggleMark(schema.marks.strikethrough)), + // this.createButton("superscript", "Superscript", this.superscriptActive, toggleMark(schema.marks.superscript)), + // this.createButton("subscript", "Subscript", this.subscriptActive, toggleMark(schema.marks.subscript)), + // <div className="richTextMenu-divider" key="divider" /> + // ]}</div>, + this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)), + this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)), + this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)), + this.createButton("strikethrough", "Strikethrough", this.strikethroughActive, toggleMark(schema.marks.strikethrough)), + this.createButton("superscript", "Superscript", this.superscriptActive, toggleMark(schema.marks.superscript)), + this.createButton("subscript", "Subscript", this.subscriptActive, toggleMark(schema.marks.subscript)), this.createColorButton(), this.createHighlighterButton(), this.createLinkButton(), @@ -955,16 +962,16 @@ export default class RichTextMenu extends AntimodeMenu { this.createButton("minus", "Horizontal Rule", undefined, this.insertHorizontalRule), <div className="richTextMenu-divider" key="divider 5" />,]} </div> - <div key="collapser"> - {/* <div key="collapser"> + {/* <div key="collapser"> + {<div key="collapser"> <button className="antimodeMenu-button" key="collapse menu" title="Collapse menu" onClick={this.toggleCollapse} style={{ backgroundColor: this.collapsed ? "#121212" : "", width: 25 }}> <FontAwesomeIcon icon="chevron-left" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.3s", transform: `rotate(${this.collapsed ? 180 : 0}deg)` }} /> </button> - </div> */} + </div> } <button className="antimodeMenu-button" key="pin menu" title="Pin menu" onClick={this.toggleMenuPin} style={{ backgroundColor: this.Pinned ? "#121212" : "", display: this.collapsed ? "none" : undefined }}> <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.Pinned ? 45 : 0}deg)` }} /> </button> - </div> + </div> */} </div>; return ( |