diff options
Diffstat (limited to 'src/client')
66 files changed, 3862 insertions, 892 deletions
| diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index dec8724c6..6fa8cf909 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -156,23 +156,23 @@ export namespace DocServer {          let _isReadOnly = false;          export function makeReadOnly() { -            if (_isReadOnly) return; -            _isReadOnly = true; -            _CreateField = field => { -                _cache[field[Id]] = field; -            }; -            _UpdateField = emptyFunction; -            _RespondToUpdate = emptyFunction; +            if (!_isReadOnly) { +                _isReadOnly = true; +                _CreateField = field => _cache[field[Id]] = field; +                _UpdateField = emptyFunction; +                _RespondToUpdate = emptyFunction; +            }          }          export function makeEditable() { -            if (!_isReadOnly) return; -            location.reload(); -            // _isReadOnly = false; -            // _CreateField = _CreateFieldImpl; -            // _UpdateField = _UpdateFieldImpl; -            // _respondToUpdate = _respondToUpdateImpl; -            // _cache = {}; +            if (_isReadOnly) { +                location.reload(); +                // _isReadOnly = false; +                // _CreateField = _CreateFieldImpl; +                // _UpdateField = _UpdateFieldImpl; +                // _respondToUpdate = _respondToUpdateImpl; +                // _cache = {}; +            }          }          export function isReadOnly() { return _isReadOnly; } @@ -451,7 +451,7 @@ export namespace DocServer {      }      function _UpdateFieldImpl(id: string, diff: any) { -        Utils.Emit(_socket, MessageStore.UpdateField, { id, diff }); +        (!DocServer.Control.isReadOnly()) && Utils.Emit(_socket, MessageStore.UpdateField, { id, diff });      }      let _UpdateField: (id: string, diff: any) => void = errorFunc; diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 045f7da76..3ee7ed87a 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -92,6 +92,7 @@ export interface DocumentOptions {      type?: string;      title?: string;      label?: string; +    hidden?: boolean;      toolTip?: string; // tooltip to display on hover      style?: string;      page?: number; @@ -151,6 +152,7 @@ export interface DocumentOptions {      annotationOn?: Doc;      removeDropProperties?: List<string>; // list of properties that should be removed from a document when it is dropped.  e.g., a creator button may be forceActive to allow it be dragged, but the forceActive property can be removed from the dropped document      dbDoc?: Doc; +    iconShape?: string; // shapes of the fonticon border      linkRelationship?: string; // type of relatinoship a link represents      ischecked?: ScriptField; // returns whether a font icon box is checked      activeInkPen?: Doc; // which pen document is currently active (used as the radio button state for the 'unhecked' pen tool scripts) @@ -166,6 +168,7 @@ export interface DocumentOptions {      clipboard?: Doc;      UseCors?: boolean;      icon?: string; +    target?: Doc; // available for use in scripts as the primary target document      sourcePanel?: Doc; // panel to display in 'targetContainer' as the result of a button onClick script      targetContainer?: Doc; // document whose proto will be set to 'panel' as the result of a onClick click script      searchFileTypes?: List<string>; // file types allowed in a search query @@ -300,6 +303,10 @@ export namespace Docs {                  layout: { view: FontIconBox, dataField: defaultDataKey },                  options: { _width: 40, _height: 40, borderRounding: "100%" },              }], +            // [DocumentType.RECOMMENDATION, { +            //     layout: { view: RecommendationsBox, dataField: defaultDataKey }, +            //     options: { _width: 200, _height: 200 }, +            // }],              [DocumentType.WEBCAM, {                  layout: { view: DashWebRTCVideo, dataField: defaultDataKey }              }], @@ -1024,6 +1031,7 @@ export namespace DocUtils {                  event: (args: { x: number, y: number }) => {                      const newDoc = Doc.ApplyTemplate(dragDoc);                      if (newDoc) { +                        newDoc.author = Doc.CurrentUserEmail;                          newDoc.x = x;                          newDoc.y = y;                          docAdder(newDoc); diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 24ffa8b1b..674980b32 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -12,7 +12,7 @@ import { Cast, PromiseValue, StrCast, NumCast } from "../../fields/Types";  import { nullAudio } from "../../fields/URLField";  import { DragManager } from "./DragManager";  import { Scripting } from "./Scripting"; -import { CollectionViewType } from "../views/collections/CollectionView"; +import { CollectionViewType, CollectionView } from "../views/collections/CollectionView";  import { makeTemplate } from "./DropConverter";  import { RichTextField } from "../../fields/RichTextField";  import { PrefetchProxy } from "../../fields/Proxy"; @@ -38,6 +38,8 @@ export class CurrentUserUtils {      @observable public static GuestWorkspace: Doc | undefined;      @observable public static GuestMobile: Doc | undefined; +    @observable public static propertiesWidth: number = 0; +      // sets up the default User Templates - slideView, queryView, descriptionView      static setupUserTemplateButtons(doc: Doc) {          if (doc["template-button-query"] === undefined) { @@ -246,6 +248,8 @@ export class CurrentUserUtils {          if (doc["template-buttons"] === undefined) {              doc["template-buttons"] = new PrefetchProxy(Docs.Create.MasonryDocument(requiredTypes, {                  title: "Advanced Item Prototypes", _xMargin: 0, _showTitle: "title", +                hidden: ComputedField.MakeFunction("self.target.noviceMode") as any, +                target: doc,                  _autoHeight: true, _width: 500, _columnWidth: 35, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled",                  dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }),              })); @@ -429,7 +433,7 @@ export class CurrentUserUtils {              { toolTip: "Drag a collection", title: "Col", icon: "folder", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyCollection as Doc },              { toolTip: "Drag a web page", title: "Web", icon: "globe-asia", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyWebpage as Doc },              { toolTip: "Drag a cat image", title: "Image", icon: "cat", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyImage as Doc }, -            { toolTip: "Drag a comparison box", title: "Comp", icon: "columns", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyComparison as Doc }, +            { toolTip: "Drag a comparison box", title: "Compare", icon: "columns", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyComparison as Doc },              { toolTip: "Drag a screengrabber", title: "Grab", icon: "photo-video", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyScreenshot as Doc },              //  { title: "Drag a webcam", title: "Cam", icon: "video", ignoreClick: true, drag: 'Docs.Create.WebCamDocument("", { _width: 400, _height: 400, title: "a test cam" })' },              { toolTip: "Drag a audio recorder", title: "Audio", icon: "microphone", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyAudio as Doc }, @@ -466,7 +470,7 @@ export class CurrentUserUtils {          }          const buttons = CurrentUserUtils.creatorBtnDescriptors(doc).filter(d => !alreadyCreatedButtons?.includes(d.title));          const creatorBtns = buttons.map(({ title, toolTip, icon, ignoreClick, drag, click, ischecked, activeInkPen, backgroundColor, dragFactory }) => Docs.Create.FontIconDocument({ -            _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, +            _nativeWidth: 50, _nativeHeight: 50, _width: 50, _height: 50,              icon,              title,              toolTip, @@ -493,6 +497,62 @@ export class CurrentUserUtils {          return doc.myItemCreators as Doc;      } +    static menuBtnDescriptions(): { +        title: string, icon: string, click: string, +    }[] { +        return [ +            { title: "Sharing", icon: "users", click: 'scriptContext.selectMenu(self, "Sharing")' }, +            { title: "Workspace", icon: "desktop", click: 'scriptContext.selectMenu(self, "Workspace")' }, +            { title: "Catalog", icon: "file", click: 'scriptContext.selectMenu(self, "Catalog")' }, +            { title: "Archive", icon: "archive", click: 'scriptContext.selectMenu(self, "Archive")' }, +            { title: "Import", icon: "upload", click: 'scriptContext.selectMenu(self, "Import")' }, +            { title: "Tools", icon: "wrench", click: 'scriptContext.selectMenu(self, "Tools")' }, +            { title: "Help", icon: "question-circle", click: 'scriptContext.selectMenu(self, "Help")' }, +            { title: "Settings", icon: "cog", click: 'scriptContext.selectMenu(self, "Settings")' }, +            { title: "User Doc", icon: "address-card", click: 'scriptContext.selectMenu(self, "UserDoc")' }, +        ]; +    } + +    static setupMenuPanel(doc: Doc) { +        if (doc.menuStack === undefined) { +            const menuBtns = CurrentUserUtils.menuBtnDescriptions().map(({ title, icon, click }) => +                Docs.Create.FontIconDocument({ +                    icon, +                    iconShape: "square", +                    title, +                    _backgroundColor: "black", +                    stayInCollection: true, +                    childDropAction: "same", +                    _width: 60, +                    _height: 60, +                    onClick: ScriptField.MakeScript(click, { scriptContext: "any" }), +                })); +            const userDoc = menuBtns[menuBtns.length - 1]; +            userDoc.target = doc; +            userDoc.hidden = ComputedField.MakeFunction("self.target.noviceMode"); + +            doc.menuStack = new PrefetchProxy(Docs.Create.StackingDocument(menuBtns, { +                title: "menuItemPanel", +                dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), +                _backgroundColor: "black", +                _gridGap: 0, +                _yMargin: 0, +                _yPadding: 0, _xMargin: 0, _autoHeight: false, _width: 60, _columnWidth: 60, lockedPosition: true, _chromeStatus: "disabled", +            })); +        } +        // this resets all sidebar buttons to being deactivated +        PromiseValue(Cast(doc.menuStack, Doc)).then(stack => { +            stack && PromiseValue(stack.data).then(btns => { +                DocListCastAsync(btns).then(bts => bts?.forEach(btn => { +                    btn.color = "white"; +                    btn._backgroundColor = ""; +                })); +            }) +        }); +        return doc.menuStack as Doc; +    } + +      // Sets up mobile menu if it is undefined creates a new one, otherwise returns existing menu      static setupActiveMobileMenu(doc: Doc) {          if (doc.activeMobileMenu === undefined) { @@ -591,10 +651,6 @@ export class CurrentUserUtils {          return Cast(userDoc.thumbDoc, Doc);      } -    static setupLibrary(userDoc: Doc) { -        return CurrentUserUtils.setupWorkspaces(userDoc); -    } -      // setup the Creator button which will display the creator panel.  This panel will include the drag creators and the color picker.      // when clicked, this panel will be displayed in the target container (ie, sidebarContainer)      static async setupToolsBtnPanel(doc: Doc, sidebarContainer: Doc) { @@ -602,6 +658,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, @@ -616,131 +674,113 @@ export class CurrentUserUtils {              doc.myColorPicker = new PrefetchProxy(color);          } -        if (doc["tabs-button-tools"] === undefined) { +        if (doc["sidebar-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 +                title: "sidebar-tools", _width: 500, _yMargin: 20, lockedPosition: true, _chromeStatus: "disabled", hideFilterView: true, forceActive: true              })) as any as Doc; -            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)", -                sourcePanel: toolsStack, -                onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), -                dragFactory: toolsStack, -                removeDropProperties: new List<string>(["lockedPosition"]), -                stayInCollection: true, -                targetContainer: new PrefetchProxy(sidebarContainer) as any as Doc, -                onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel"), -            })); + +            doc["sidebar-tools"] = toolsStack;          } -        (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"]); +        if (doc["sidebar-workspaces"] === undefined) { +            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; -        return doc.myWorkspaces as Doc; +            doc["sidebar-workspaces"] = new PrefetchProxy(Docs.Create.TreeDocument([workspaces], { +                treeViewHideTitle: true, _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; +        }      } +      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,              }));          } -        return doc.myCatalog as Doc; + +        if (doc["sidebar-catalog"] === undefined) { +            const catalog = doc.myCatalog as Doc; + +            doc["sidebar-catalog"] = new PrefetchProxy(Docs.Create.TreeDocument([catalog], { +                title: "sidebar-catalog", +                treeViewHideTitle: true, _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; +        }      }      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          PromiseValue(Cast(doc.myRecentlyClosed, Doc)).then(recent => recent && PromiseValue(recent.data).then(DocListCast)); -        const clearAll = ScriptField.MakeScript(`self.data = new List([])`); -        (doc.myRecentlyClosed as Doc).contextMenuScripts = new List<ScriptField>([clearAll!]); -        (doc.myRecentlyClosed as Doc).contextMenuLabels = new List<string>(["Clear All"]); +        if (doc["sidebar-recentlyClosed"] === undefined) { +            const clearAll = ScriptField.MakeScript(`self.data = new List([])`); +            (doc.myRecentlyClosed as Doc).contextMenuScripts = new List<ScriptField>([clearAll!]); +            (doc.myRecentlyClosed as Doc).contextMenuLabels = new List<string>(["Clear All"]); -        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 -    static setupLibraryPanel(doc: Doc, sidebarContainer: Doc) { -        const workspaces = CurrentUserUtils.setupWorkspaces(doc); -        const documents = CurrentUserUtils.setupCatalog(doc); -        const recentlyClosed = CurrentUserUtils.setupRecentlyClosed(doc); - -        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, +            const recentlyClosed = doc.myRecentlyClosed as Doc; + +            doc["sidebar-recentlyClosed"] = new PrefetchProxy(Docs.Create.TreeDocument([recentlyClosed], { +                title: "sidebar-recentlyClosed", +                treeViewHideTitle: true, _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; -            doc["tabs-button-library"] = new PrefetchProxy(Docs.Create.ButtonDocument({ -                _width: 50, _height: 25, title: "Library", _fontSize: "10pt", targetDropAction: "same", -                letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)", -                sourcePanel: libraryStack, -                onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), -                dragFactory: libraryStack, -                removeDropProperties: new List<string>(["lockedPosition"]), -                stayInCollection: true, -                targetContainer: new PrefetchProxy(sidebarContainer) as any as Doc, -                onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel") -            }));          } -        return doc["tabs-button-library"] as Doc;      } - -    // setup the Search button which will display the search panel. -    static setupSearchBtnPanel(doc: Doc, sidebarContainer: Doc) { -        if (doc["tabs-button-search"] === undefined) { -            doc["tabs-button-search"] = new PrefetchProxy(Docs.Create.ButtonDocument({ -                _width: 50, _height: 25, title: "Search", _fontSize: "10pt", -                letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)", -                sourcePanel: new PrefetchProxy(Docs.Create.QueryDocument({ title: "search stack", })) as any as Doc, -                searchFileTypes: new List<string>([DocumentType.RTF, DocumentType.IMG, DocumentType.PDF, DocumentType.VID, DocumentType.WEB, DocumentType.SCRIPTING]), -                targetContainer: new PrefetchProxy(sidebarContainer) as any as Doc, -                lockedPosition: true, -                onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel") -            })); +    static setupUserDoc(doc: Doc) { +        if (doc["sidebar-userDoc"] === undefined) { +            doc.treeViewOpen = true; +            doc.treeViewExpandedView = "fields"; +            doc["sidebar-userDoc"] = new PrefetchProxy(Docs.Create.TreeDocument([doc], { +                treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, title: "sidebar-userDoc", +                treeViewTruncateTitleWidth: 150, hideFilterView: true, treeViewPreventOpen: false, +                lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same" +            })) as any as Doc;          } -        return doc["tabs-button-search"] as Doc;      }      static setupSidebarContainer(doc: Doc) { -        if (doc["tabs-panelContainer"] === undefined) { +        if (doc["sidebar"] === undefined) {              const sidebarContainer = new Doc();              sidebarContainer._chromeStatus = "disabled";              sidebarContainer.onClick = ScriptField.MakeScript("freezeSidebar()"); -            doc["tabs-panelContainer"] = new PrefetchProxy(sidebarContainer); +            doc["sidebar"] = new PrefetchProxy(sidebarContainer);          } -        return doc["tabs-panelContainer"] as Doc; +        return doc["sidebar"] as Doc;      }      // 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); - -        // 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], { -                _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 }); -        } +        await CurrentUserUtils.setupToolsBtnPanel(doc, sidebarContainer); +        CurrentUserUtils.setupWorkspaces(doc); +        CurrentUserUtils.setupCatalog(doc); +        CurrentUserUtils.setupRecentlyClosed(doc); +        CurrentUserUtils.setupUserDoc(doc);      }      static blist = (opts: DocumentOptions, docs: Doc[]) => new PrefetchProxy(Docs.Create.LinearDocument(docs, { @@ -750,7 +790,7 @@ export class CurrentUserUtils {      })) as any as Doc      static ficon = (opts: DocumentOptions) => new PrefetchProxy(Docs.Create.FontIconDocument({ -        ...opts, dropAction: "alias", removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100 +        ...opts, dropAction: "alias", removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 40, _nativeHeight: 40, _width: 40, _height: 40      })) as any as Doc      /// sets up the default list of buttons to be shown in the expanding button menu at the bottom of the Dash window @@ -784,8 +824,8 @@ export class CurrentUserUtils {      // Right sidebar is where mobile uploads are contained      static setupRightSidebar(doc: Doc) { -        if (doc.rightSidebarCollection === undefined) { -            doc.rightSidebarCollection = new PrefetchProxy(Docs.Create.StackingDocument([], { title: "Mobile Uploads" })); +        if (doc["sidebar-sharing"] === undefined) { +            doc["sidebar-sharing"] = new PrefetchProxy(Docs.Create.StackingDocument([], { title: "Mobile Uploads" }));          }      } @@ -849,6 +889,8 @@ export class CurrentUserUtils {          doc.activeDash = StrCast(doc.activeDash, "0");          doc.fontSize = StrCast(doc.fontSize, "12pt");          doc.fontFamily = StrCast(doc.fontFamily, "Arial"); +        doc.fontColor = StrCast(doc.fontColor, "black"); +        doc.fontHighlight = StrCast(doc.fontHighlight, "");          doc["constants-snapThreshold"] = NumCast(doc["constants-snapThreshold"], 10); //          doc["constants-dragThreshold"] = NumCast(doc["constants-dragThreshold"], 4); //          Utils.DRAG_THRESHOLD = NumCast(doc["constants-dragThreshold"]); @@ -856,6 +898,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/GroupManager.tsx b/src/client/util/GroupManager.tsx index 72fba5c1b..229a846a9 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -20,6 +20,9 @@ import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox";  library.add(fa.faPlus, fa.faTimes, fa.faInfoCircle); +/** + * Interface for options for the react-select component + */  export interface UserOptions {      label: string;      value: string; @@ -30,15 +33,13 @@ export default class GroupManager extends React.Component<{}> {      static Instance: GroupManager;      @observable isOpen: boolean = false; // whether the GroupManager is to be displayed or not. -    @observable private dialogueBoxOpacity: number = 1; // opacity of the dialogue box div of the MainViewModal. -    @observable private overlayOpacity: number = 0.4; // opacity of the overlay div of the MainViewModal.      @observable private users: string[] = []; // list of users populated from the database.      @observable private selectedUsers: UserOptions[] | null = null; // list of users selected in the "Select users" dropdown.      @observable currentGroup: Opt<Doc>; // the currently selected group.      @observable private createGroupModalOpen: boolean = false;      private inputRef: React.RefObject<HTMLInputElement> = React.createRef(); // the ref for the input box. -    private createGroupButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); -    private currentUserGroups: string[] = []; +    private createGroupButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // the ref for the group creation button +    private currentUserGroups: string[] = []; // the list of groups the current user is a member of      @observable private buttonColour: "#979797" | "black" = "#979797";      @observable private groupSort: "ascending" | "descending" | "none" = "none"; @@ -49,6 +50,9 @@ export default class GroupManager extends React.Component<{}> {          GroupManager.Instance = this;      } +    /** +     * Populates the list of users and groups. +     */      componentDidMount() {          this.populateUsers();          this.populateGroups(); @@ -62,22 +66,22 @@ export default class GroupManager extends React.Component<{}> {          const userList = await RequestPromise.get(Utils.prepend("/getUsers"));          const raw = JSON.parse(userList) as User[];          const evaluating = raw.map(async user => { -            // const isCandidate = user.email !== Doc.CurrentUserEmail; -            // if (isCandidate) {              const userDocument = await DocServer.GetRefField(user.userDocumentId);              if (userDocument instanceof Doc) { -                const notificationDoc = await Cast(userDocument.rightSidebarCollection, Doc); +                const notificationDoc = await Cast(userDocument["sidebar-sharing"], Doc);                  runInAction(() => {                      if (notificationDoc instanceof Doc) {                          this.users.push(user.email);                      }                  });              } -            // }          });          return Promise.all(evaluating);      } +    /** +     * Populates the list of groups the current user is a member of and sets this list to be used in the GetEffectiveAcl in util.ts +     */      populateGroups = () => {          DocListCastAsync(this.GroupManagerDoc?.data).then(groups => {              groups?.forEach(group => { @@ -101,7 +105,7 @@ export default class GroupManager extends React.Component<{}> {       */      @action      open = () => { -        SelectionManager.DeselectAll(); +        // SelectionManager.DeselectAll();          this.isOpen = true;          this.populateUsers();          this.populateGroups(); @@ -145,25 +149,8 @@ export default class GroupManager extends React.Component<{}> {      }      /** -     * @returns a readonly copy of a single group document +     * Returns an array of the list of members of a given group.       */ -    getGroupCopy(groupName: string): Doc | undefined { -        const groupDoc = this.getGroup(groupName); -        if (groupDoc) { -            const { members, owners } = groupDoc; -            return Doc.assign(new Doc, { groupName, members: StrCast(members), owners: StrCast(owners) }); -        } -        return undefined; -    } -    /** -     * @returns a readonly copy of the list of group documents -     */ -    getAllGroupsCopy(): Doc[] { -        return this.getAllGroups().map(({ groupName, owners, members }) => -            Doc.assign(new Doc, { groupName: (StrCast(groupName)), owners: (StrCast(owners)), members: (StrCast(members)) }) -        ); -    } -      getGroupMembers(group: string | Doc): string[] {          if (group instanceof Doc) return JSON.parse(StrCast(group.members)) as string[];          else return JSON.parse(StrCast(this.getGroup(group)!.members)) as string[]; @@ -316,6 +303,9 @@ export default class GroupManager extends React.Component<{}> {      } +    /** +     * @returns the MainViewModal which allows the user to create groups. +     */      private get groupCreationModal() {          const contents = (              <div className="group-create"> @@ -415,7 +405,7 @@ export default class GroupManager extends React.Component<{}> {                      <div                          className="sort-groups"                          onClick={action(() => this.groupSort = this.groupSort === "ascending" ? "descending" : this.groupSort === "descending" ? "none" : "ascending")}> -                        Name {this.groupSort === "ascending" ? "↑" : this.groupSort === "descending" ? "↓" : ""} {/* → */} +                        Name {this.groupSort === "ascending" ? "↑" : this.groupSort === "descending" ? "↓" : ""}                      </div>                      <div className="group-body">                          {groups.map(group => diff --git a/src/client/util/GroupMemberView.scss b/src/client/util/GroupMemberView.scss index c609c5c7b..2eb164988 100644 --- a/src/client/util/GroupMemberView.scss +++ b/src/client/util/GroupMemberView.scss @@ -41,9 +41,10 @@              margin-top: -5;              height: 20;              text-overflow: ellipsis; +            background: none;              &:hover { -                text-overflow: visible; +                text-overflow: unset;                  overflow-x: auto;              }          } @@ -72,7 +73,7 @@      .editing-contents {          overflow-y: auto; -        height: 65%; +        height: 62%;          width: 100%;          color: black;          margin-top: -15px; diff --git a/src/client/util/GroupMemberView.tsx b/src/client/util/GroupMemberView.tsx index f20670c4e..531ef988a 100644 --- a/src/client/util/GroupMemberView.tsx +++ b/src/client/util/GroupMemberView.tsx @@ -29,13 +29,17 @@ export default class GroupMemberView extends React.Component<GroupMemberViewProp          const options: UserOptions[] = this.props.group ? GroupManager.Instance.options.filter(option => !(JSON.parse(StrCast(this.props.group.members)) as string[]).includes(option.value)) : []; +        const hasEditAccess = GroupManager.Instance.hasEditAccess(this.props.group); +          return (!this.props.group ? null :              <div className="editing-interface">                  <div className="editing-header">                      <input                          className="group-title" +                        style={{ marginLeft: !hasEditAccess ? "-14%" : 0 }}                          value={StrCast(this.props.group.groupName)}                          onChange={e => this.props.group.groupName = e.currentTarget.value} +                        disabled={!hasEditAccess}                      >                      </input>                      <div className={"memberView-closeButton"} onClick={action(this.props.onCloseButtonClick)}> @@ -65,12 +69,15 @@ export default class GroupMemberView extends React.Component<GroupMemberViewProp                          null}                      <div                          className="sort-emails" +                        style={{ paddingTop: hasEditAccess ? 0 : 35 }}                          onClick={action(() => this.memberSort = this.memberSort === "ascending" ? "descending" : this.memberSort === "descending" ? "none" : "ascending")}>                          Emails {this.memberSort === "ascending" ? "↑" : this.memberSort === "descending" ? "↓" : ""} {/* → */}                      </div>                  </div>                  <hr /> -                <div className="editing-contents"> +                <div className="editing-contents" +                    style={{ height: hasEditAccess ? "62%" : "85%" }} +                >                      {members.map(member => (                          <div                              className="editing-row" @@ -79,7 +86,7 @@ export default class GroupMemberView extends React.Component<GroupMemberViewProp                              <div className="user-email">                                  {member}                              </div> -                            {GroupManager.Instance.hasEditAccess(this.props.group) ? +                            {hasEditAccess ?                                  <div className={"remove-button"} onClick={() => GroupManager.Instance.removeMemberFromGroup(this.props.group, member)}>                                      <FontAwesomeIcon icon={fa.faTrashAlt} size={"sm"} />                                  </div> 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/util/SettingsManager.scss b/src/client/util/SettingsManager.scss index c1627e69f..6923fe879 100644 --- a/src/client/util/SettingsManager.scss +++ b/src/client/util/SettingsManager.scss @@ -7,7 +7,7 @@      height: 300px;      button { -        background: $lighter-alt-accent; +        background: #315a96;          outline: none;          border-radius: 5px;          border: 0px; @@ -29,8 +29,12 @@      button {          width: 100%;          align-self: center; -        background: $darker-alt-accent; +        background: #252b33;          margin-top: 4px; + +        &:hover { +            background: $main-accent; +        }      }      .delete-button { @@ -102,11 +106,12 @@      }      h1 { -        color: $dark-color; +        color: #121721;          text-transform: uppercase;          letter-spacing: 2px; -        font-size: 120%; +        font-size: 19;          margin-top: 0; +        font-weight: bold;      }      .container { @@ -151,7 +156,7 @@      .settings-interface button {          width: 100%;          font-size: 30px; -        background: #b2cef8; +        background: #315a96;      }      .settings-interface .settings-heading { diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index a9c2d5e15..d438ec971 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -9,17 +9,18 @@ import "./SettingsManager.scss";  import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";  import { Networking } from "../Network";  import { CurrentUserUtils } from "./CurrentUserUtils"; -import { Utils } from "../../Utils"; +import { Utils, addStyleSheet, addStyleSheetRule, removeStyleSheetRule } from "../../Utils";  import { Doc } from "../../fields/Doc";  import GroupManager from "./GroupManager";  import GoogleAuthenticationManager from "../apis/GoogleAuthenticationManager"; -import { togglePlaygroundMode } from "../../fields/util"; +import { DocServer } from "../DocServer";  library.add(fa.faTimes);  @observer  export default class SettingsManager extends React.Component<{}> {      public static Instance: SettingsManager; +    static _settingsStyle = addStyleSheet();      @observable private isOpen = false;      @observable private dialogueBoxOpacity = 1;      @observable private overlayOpacity = 0.4; @@ -94,8 +95,11 @@ export default class SettingsManager extends React.Component<{}> {      @action      togglePlaygroundMode = () => { -        togglePlaygroundMode();          this.playgroundMode = !this.playgroundMode; +        if (this.playgroundMode) DocServer.Control.makeReadOnly(); +        else DocServer.Control.makeEditable(); + +        addStyleSheetRule(SettingsManager._settingsStyle, "lm_header", { background: "pink !important" });      }      private get settingsInterface() { @@ -126,7 +130,7 @@ export default class SettingsManager extends React.Component<{}> {                              {this.errorText ? <div className="error-text">{this.errorText}</div> : undefined}                              {this.successText ? <div className="success-text">{this.successText}</div> : undefined}                              <button onClick={this.dispatchRequest}>submit</button> -                            <a href="/forgotPassword">forgot password?</a> +                            <a style={{ marginLeft: 65, marginTop: -20 }} href="/forgotPassword">forgot password?</a>                          </div>                          : undefined} diff --git a/src/client/util/SharingManager.scss b/src/client/util/SharingManager.scss index 130785672..8da80ef52 100644 --- a/src/client/util/SharingManager.scss +++ b/src/client/util/SharingManager.scss @@ -1,6 +1,6 @@  .sharing-interface {      width: 600px; -    height: 360px; +    // height: 360px;      .overlay {          transform: translate(-20px, -20px); @@ -23,33 +23,51 @@              z-index: 999;          } -        .share-setup { -            display: flex; -            margin-bottom: 20px; -            align-items: center; -            height: 36; +        .share-container { +            .share-setup { +                display: flex; +                margin-bottom: 20px; +                align-items: center; +                height: 36; -            .user-search { -                width: 90%; +                .user-search { +                    width: 90%; -                input { -                    height: 30; +                    input { +                        height: 30; +                    } +                } + +                .permissions-select { +                    z-index: 1; +                    margin-left: -100; +                    border: none; +                    outline: none; +                    text-align: justify; // for Edge +                    text-align-last: end;                  } -            } -            .permissions-select { -                z-index: 1; -                margin-left: -100; -                border: none; -                outline: none; -                text-align: justify; // for Edge -                text-align-last: end; +                .share-button { +                    height: 105%; +                    margin-left: 2%; +                    background-color: black; +                }              } -            .share-button { -                height: 105%; -                margin-left: 2%; -                background-color: #979797; +            .sort-checkboxes { +                float: left; +                margin-top: -17px; +                margin-bottom: 10px; +                font-size: 10px; + +                input { +                    height: 10px; +                } + +                label { +                    font-weight: normal; +                    font-style: italic; +                }              }          } @@ -92,10 +110,8 @@                      height: 250px;                      margin: 0 2; -                      .none {                          font-style: italic; -                      }                  }              } diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 0d8b33fbe..9d91ce1ba 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -1,7 +1,7 @@  import { observable, runInAction, action } from "mobx";  import * as React from "react";  import MainViewModal from "../views/MainViewModal"; -import { Doc, Opt, DocListCastAsync } from "../../fields/Doc"; +import { Doc, Opt, DocListCastAsync, AclAdmin, DataSym, AclPrivate } from "../../fields/Doc";  import { DocServer } from "../DocServer";  import { Cast, StrCast } from "../../fields/Types";  import * as RequestPromise from "request-promise"; @@ -19,7 +19,7 @@ import GroupMemberView from "./GroupMemberView";  import Select from "react-select";  import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";  import { List } from "../../fields/List"; -import { distributeAcls, SharingPermissions } from "../../fields/util"; +import { distributeAcls, SharingPermissions, GetEffectiveAcl } from "../../fields/util";  import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox";  export interface User { @@ -27,7 +27,10 @@ export interface User {      userDocumentId: string;  } -interface GroupOptions { +/** + * Interface for grouped options for the react-select component. + */ +interface GroupedOptions {      label: string;      options: UserOptions[];  } @@ -36,9 +39,13 @@ interface GroupOptions {  // const PublicKey = "publicLinkPermissions";  // const DefaultColor = "black"; -const groupType = "!groupType/"; +// used to differentiate between individuals and groups when sharing  const indType = "!indType/"; +const groupType = "!groupType/"; +/** + * A user who also has a notificationDoc. + */  interface ValidatedUser {      user: User;      notificationDoc: Doc; @@ -49,41 +56,43 @@ const storage = "data";  @observer  export default class SharingManager extends React.Component<{}> {      public static Instance: SharingManager; -    @observable private isOpen = false; -    @observable private users: ValidatedUser[] = []; -    @observable private targetDoc: Doc | undefined; -    @observable private targetDocView: DocumentView | undefined; +    @observable private isOpen = false; // whether the SharingManager modal is open or not +    @observable private users: ValidatedUser[] = []; // the list of users with notificationDocs +    @observable private targetDoc: Doc | undefined; // the document being shared +    @observable private targetDocView: DocumentView | undefined; // the DocumentView of the document being shared      // @observable private copied = false; -    @observable private dialogueBoxOpacity = 1; -    @observable private overlayOpacity = 0.4; -    @observable private selectedUsers: UserOptions[] | null = null; -    @observable private permissions: SharingPermissions = SharingPermissions.Edit; -    @observable private individualSort: "ascending" | "descending" | "none" = "none"; -    @observable private groupSort: "ascending" | "descending" | "none" = "none"; -    private shareDocumentButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); - - +    @observable private dialogueBoxOpacity = 1; // for the modal +    @observable private overlayOpacity = 0.4; // for the modal +    @observable private selectedUsers: UserOptions[] | null = null; // users (individuals/groups) selected to share with +    @observable private permissions: SharingPermissions = SharingPermissions.Edit; // the permission with which to share with other users +    @observable private individualSort: "ascending" | "descending" | "none" = "none"; // sorting options for the list of individuals +    @observable private groupSort: "ascending" | "descending" | "none" = "none"; // sorting options for the list of groups +    private shareDocumentButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // ref for the share button, used for the position of the popup +    // if both showUserOptions and showGroupOptions are false then both are displayed +    @observable private showUserOptions: boolean = false; // whether to show individuals as options when sharing (in the react-select component) +    @observable private showGroupOptions: boolean = false; // // whether to show groups as options when sharing (in the react-select component)      // private get linkVisible() {      //     return this.sharingDoc ? this.sharingDoc[PublicKey] !== SharingPermissions.None : false;      // }      public open = (target: DocumentView) => { -        SelectionManager.DeselectAll(); -        this.populateUsers().then(action(() => { +        runInAction(() => this.users = []); +        // SelectionManager.DeselectAll(); +        this.populateUsers(); +        runInAction(() => {              this.targetDocView = target;              this.targetDoc = target.props.Document;              DictationOverlay.Instance.hasActiveModal = true;              this.isOpen = true;              this.permissions = SharingPermissions.Edit; -        })); +        });      }      public close = action(() => {          this.isOpen = false; -        this.users = []; -        this.selectedUsers = null; +        this.selectedUsers = null; // resets the list of users and seleected users (in the react-select component)          setTimeout(action(() => {              // this.copied = false; @@ -97,7 +106,18 @@ export default class SharingManager extends React.Component<{}> {          SharingManager.Instance = this;      } +    /** +     * Populates the list of users. +     */ +    componentDidMount() { +        this.populateUsers(); +    } + +    /** +     * Populates the list of validated users (this.users) by adding registered users which have a sidebar-sharing. +     */      populateUsers = async () => { +        runInAction(() => this.users = []);          const userList = await RequestPromise.get(Utils.prepend("/getUsers"));          const raw = JSON.parse(userList) as User[];          const evaluating = raw.map(async user => { @@ -105,7 +125,7 @@ export default class SharingManager extends React.Component<{}> {              if (isCandidate) {                  const userDocument = await DocServer.GetRefField(user.userDocumentId);                  if (userDocument instanceof Doc) { -                    const notificationDoc = await Cast(userDocument.rightSidebarCollection, Doc); +                    const notificationDoc = await Cast(userDocument["sidebar-sharing"], Doc);                      runInAction(() => {                          if (notificationDoc instanceof Doc) {                              this.users.push({ user, notificationDoc }); @@ -117,58 +137,80 @@ export default class SharingManager extends React.Component<{}> {          return Promise.all(evaluating);      } -    setInternalGroupSharing = (group: Doc, permission: string) => { +    /** +     * Sets the permission on the target for the group. +     * @param group  +     * @param permission  +     */ +    setInternalGroupSharing = (group: Doc, permission: string, targetDoc?: Doc) => {          const members: string[] = JSON.parse(StrCast(group.members));          const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email)); -        const target = this.targetDoc!; +        const target = targetDoc || this.targetDoc!;          const ACL = `ACL-${StrCast(group.groupName)}`; -        // fix this - not needed (here and setinternalsharing and removegroup) -        // target[ACL] = permission; -        // Doc.GetProto(target)[ACL] = permission; -        distributeAcls(ACL, permission as SharingPermissions, this.targetDoc!); +        target.author === Doc.CurrentUserEmail && distributeAcls(ACL, permission as SharingPermissions, target); +        // if documents have been shared, add the target to that list if it doesn't already exist, otherwise create a new list with the target          group.docsShared ? DocListCastAsync(group.docsShared).then(resolved => Doc.IndexOf(target, resolved!) === -1 && (group.docsShared as List<Doc>).push(target)) : group.docsShared = new List<Doc>([target]);          users.forEach(({ notificationDoc }) => {              DocListCastAsync(notificationDoc[storage]).then(resolved => { -                if (permission !== SharingPermissions.None) Doc.IndexOf(target, resolved!) === -1 && Doc.AddDocToList(notificationDoc, storage, target); -                else Doc.IndexOf(target, resolved!) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, target); +                if (permission !== SharingPermissions.None) Doc.IndexOf(target, resolved!) === -1 && Doc.AddDocToList(notificationDoc, storage, target); // add the target to the notificationDoc if it hasn't already been added +                else Doc.IndexOf(target, resolved!) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, target); // remove the target from the list if it already exists              });          });      } +    /** +     * Shares the documents shared with a group with a new user who has been added to that group. +     * @param group  +     * @param emailId  +     */      shareWithAddedMember = (group: Doc, emailId: string) => {          const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!;          if (group.docsShared) {              DocListCastAsync(group.docsShared).then(docsShared => {                  docsShared?.forEach(doc => { -                    DocListCastAsync(user.notificationDoc[storage]).then(resolved => Doc.IndexOf(doc, resolved!) === -1 && Doc.AddDocToList(user.notificationDoc, storage, doc)); +                    DocListCastAsync(user.notificationDoc[storage]).then(resolved => Doc.IndexOf(doc, resolved!) === -1 && Doc.AddDocToList(user.notificationDoc, storage, doc)); // add the doc if it isn't already in the list                  });              });          }      } +    shareFromPropertiesSidebar = (shareWith: string, permission: SharingPermissions, target: Doc) => { +        const user = this.users.find(({ user: { email } }) => email === (shareWith === "Me" ? Doc.CurrentUserEmail : shareWith)); +        if (user) this.setInternalSharing(user, permission, target); +        else this.setInternalGroupSharing(GroupManager.Instance.getGroup(shareWith)!, permission, target); +    } + +    /** +     * Removes the documents shared with a user through a group when the user is removed from the group. +     * @param group  +     * @param emailId  +     */      removeMember = (group: Doc, emailId: string) => {          const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!;          if (group.docsShared) {              DocListCastAsync(group.docsShared).then(docsShared => {                  docsShared?.forEach(doc => { -                    DocListCastAsync(user.notificationDoc[storage]).then(resolved => Doc.IndexOf(doc, resolved!) !== -1 && Doc.RemoveDocFromList(user.notificationDoc, storage, doc)); +                    DocListCastAsync(user.notificationDoc[storage]).then(resolved => Doc.IndexOf(doc, resolved!) !== -1 && Doc.RemoveDocFromList(user.notificationDoc, storage, doc)); // remove the doc only if it is in the list                  });              });          }      } +    /** +     * Removes a group's permissions from documents that have been shared with it. +     * @param group  +     */      removeGroup = (group: Doc) => {          if (group.docsShared) {              DocListCastAsync(group.docsShared).then(resolved => {                  resolved?.forEach(doc => {                      const ACL = `ACL-${StrCast(group.groupName)}`; -                    // doc[ACL] = doc[DataSym][ACL] = "Not Shared";                      distributeAcls(ACL, SharingPermissions.None, doc); @@ -182,14 +224,13 @@ export default class SharingManager extends React.Component<{}> {          }      } -    // @action -    setInternalSharing = (recipient: ValidatedUser, permission: string) => { +    setInternalSharing = (recipient: ValidatedUser, permission: string, targetDoc?: Doc) => {          const { user, notificationDoc } = recipient; -        const target = this.targetDoc!; +        const target = targetDoc || this.targetDoc!;          const key = user.email.replace('.', '_');          const ACL = `ACL-${key}`; -        distributeAcls(ACL, permission as SharingPermissions, this.targetDoc!); +        target.author === Doc.CurrentUserEmail && distributeAcls(ACL, permission as SharingPermissions, target);          if (permission !== SharingPermissions.None) {              DocListCastAsync(notificationDoc[storage]).then(resolved => { @@ -291,7 +332,7 @@ export default class SharingManager extends React.Component<{}> {              const { left, width, top, height } = this.shareDocumentButtonRef.current!.getBoundingClientRect();              TaskCompletionBox.popupX = left - 1.5 * width; -            TaskCompletionBox.popupY = top - height; +            TaskCompletionBox.popupY = top - 1.5 * height;              TaskCompletionBox.textDisplayed = "Document shared!";              TaskCompletionBox.taskCompleted = true;              setTimeout(action(() => TaskCompletionBox.taskCompleted = false), 2000); @@ -315,45 +356,67 @@ export default class SharingManager extends React.Component<{}> {      private get sharingInterface() {          const groupList = GroupManager.Instance?.getAllGroups() || []; -        const sortedUsers = this.users.sort(this.sortUsers) +        const sortedUsers = this.users.slice().sort(this.sortUsers)              .map(({ user: { email } }) => ({ label: email, value: indType + email })); -        const sortedGroups = groupList.sort(this.sortGroups) +        const sortedGroups = groupList.slice().sort(this.sortGroups)              .map(({ groupName }) => ({ label: StrCast(groupName), value: groupType + StrCast(groupName) })); -        const options: GroupOptions[] = GroupManager.Instance ? -            [ -                { +        const options: GroupedOptions[] = []; + +        if (GroupManager.Instance) { +            if ((this.showUserOptions && this.showGroupOptions) || (!this.showUserOptions && !this.showGroupOptions)) { +                options.push({                      label: 'Individuals',                      options: sortedUsers                  }, -                { +                    { +                        label: 'Groups', +                        options: sortedGroups +                    }); +            } +            else if (this.showUserOptions) { +                options.push({ +                    label: 'Individuals', +                    options: sortedUsers +                }); +            } +            else { +                options.push({                      label: 'Groups',                      options: sortedGroups -                } -            ] -            : []; +                }); +            } +        }          const users = this.individualSort === "ascending" ? this.users.sort(this.sortUsers) : this.individualSort === "descending" ? this.users.sort(this.sortUsers).reverse() : this.users;          const groups = this.groupSort === "ascending" ? groupList.sort(this.sortGroups) : this.groupSort === "descending" ? groupList.sort(this.sortGroups).reverse() : groupList; +        const effectiveAcl = this.targetDoc ? GetEffectiveAcl(this.targetDoc) : AclPrivate; +          const userListContents: (JSX.Element | null)[] = users.map(({ user, notificationDoc }) => {              const userKey = user.email.replace('.', '_'); -            const permissions = StrCast(this.targetDoc?.[`ACL-${userKey}`], SharingPermissions.None); +            const permissions = StrCast(this.targetDoc?.[`ACL-${userKey}`]); -            return permissions === SharingPermissions.None || user.email === this.targetDoc?.author ? null : ( +            return !permissions || user.email === this.targetDoc?.author ? null : (                  <div                      key={userKey}                      className={"container"}                  >                      <span className={"padding"}>{user.email}</span>                      <div className="edit-actions"> -                        <select -                            className={"permissions-dropdown"} -                            value={permissions} -                            onChange={e => this.setInternalSharing({ user, notificationDoc }, e.currentTarget.value)} -                        > -                            {this.sharingOptions} -                        </select> +                        {effectiveAcl === AclAdmin ? ( +                            <select +                                className={"permissions-dropdown"} +                                value={permissions} +                                onChange={e => this.setInternalSharing({ user, notificationDoc }, e.currentTarget.value)} +                            > +                                {this.sharingOptions} +                            </select> +                        ) : ( +                                <div className={"permissions-dropdown"}> +                                    {permissions} +                                </div> +                            )}                      </div>                  </div>              ); @@ -365,20 +428,34 @@ export default class SharingManager extends React.Component<{}> {                      key={"owner"}                      className={"container"}                  > -                    <span className={"padding"}>{this.targetDoc?.author}</span> +                    <span className={"padding"}>{this.targetDoc?.author === Doc.CurrentUserEmail ? "Me" : this.targetDoc?.author}</span>                      <div className="edit-actions">                          <div className={"permissions-dropdown"}>                              Owner                          </div>                      </div>                  </div> -            ) +            ), +            this.targetDoc?.author !== Doc.CurrentUserEmail ? +                ( +                    <div +                        key={"me"} +                        className={"container"} +                    > +                        <span className={"padding"}>Me</span> +                        <div className="edit-actions"> +                            <div className={"permissions-dropdown"}> +                                {this.targetDoc?.[`ACL-${Doc.CurrentUserEmail.replace(".", "_")}`]} +                            </div> +                        </div> +                    </div> +                ) : null          );          const groupListContents = groups.map(group => { -            const permissions = StrCast(this.targetDoc?.[`ACL-${StrCast(group.groupName)}`], SharingPermissions.None); +            const permissions = StrCast(this.targetDoc?.[`ACL-${StrCast(group.groupName)}`]); -            return permissions === SharingPermissions.None ? null : ( +            return !permissions ? null : (                  <div                      key={StrCast(group.groupName)}                      className={"container"} @@ -400,7 +477,6 @@ export default class SharingManager extends React.Component<{}> {              );          }); -        const displayUserList = !userListContents?.every(user => user === null);          const displayGroupList = !groupListContents?.every(group => group === null);          return ( @@ -446,8 +522,7 @@ export default class SharingManager extends React.Component<{}> {                      <div className={"close-button"} onClick={this.close}>                          <FontAwesomeIcon icon={"times"} color={"black"} size={"lg"} />                      </div> -                    {this.targetDoc?.author !== Doc.CurrentUserEmail ? null -                        : +                    {<div className="share-container">                          <div className="share-setup">                              <Select                                  className={"user-search"} @@ -457,6 +532,11 @@ export default class SharingManager extends React.Component<{}> {                                  options={options}                                  onChange={this.handleUsersChange}                                  value={this.selectedUsers} +                                styles={{ +                                    indicatorSeparator: () => ({ +                                        visibility: "hidden" +                                    }) +                                }}                              />                              <select className="permissions-select" onChange={this.handlePermissionsChange}>                                  {this.sharingOptions} @@ -465,6 +545,11 @@ export default class SharingManager extends React.Component<{}> {                                  Share                              </button>                          </div> +                        <div className="sort-checkboxes"> +                            <input type="checkbox" onChange={action(() => this.showUserOptions = !this.showUserOptions)} /> <label style={{ marginRight: 10 }}>Individuals</label> +                            <input type="checkbox" onChange={action(() => this.showGroupOptions = !this.showGroupOptions)} /> <label>Groups</label> +                        </div> +                    </div>                      }                      <div className="main-container">                          <div className={"individual-container"}> @@ -473,17 +558,8 @@ export default class SharingManager extends React.Component<{}> {                                  onClick={action(() => this.individualSort = this.individualSort === "ascending" ? "descending" : this.individualSort === "descending" ? "none" : "ascending")}>                                  Individuals {this.individualSort === "ascending" ? "↑" : this.individualSort === "descending" ? "↓" : ""} {/* → */}                              </div> -                            <div className={"users-list"} style={{ display: !displayUserList ? "flex" : "block" }}>{/*200*/} -                                { -                                    !displayUserList ? -                                        <div -                                            className={"none"} -                                        > -                                            There are no users this document has been shared with. -                                        </div> -                                        : -                                        userListContents -                                } +                            <div className={"users-list"} style={{ display: "block" }}>{/*200*/} +                                {userListContents}                              </div>                          </div>                          <div className={"group-container"}> 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/DocComponent.tsx b/src/client/views/DocComponent.tsx index 4c82149e2..804c7a8d4 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -7,7 +7,7 @@ import { InteractionUtils } from '../util/InteractionUtils';  import { List } from '../../fields/List';  import { DateField } from '../../fields/DateField';  import { ScriptField } from '../../fields/ScriptField'; -import { GetEffectiveAcl, getPlaygroundMode, SharingPermissions } from '../../fields/util'; +import { GetEffectiveAcl, SharingPermissions } from '../../fields/util';  ///  DocComponent returns a generic React base class used by views that don't have 'fieldKey' props (e.g.,CollectionFreeFormDocumentView, DocumentView) @@ -150,25 +150,25 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T              const effectiveAcl = GetEffectiveAcl(this.dataDoc);              if (added.length) { -                if (effectiveAcl === AclPrivate || (effectiveAcl === AclReadonly && !getPlaygroundMode())) { +                if (effectiveAcl === AclPrivate || effectiveAcl === AclReadonly) {                      return false;                  }                  else { -                    if (this.props.Document[AclSym]) { -                        added.forEach(d => { -                            const dataDoc = d[DataSym]; -                            dataDoc[AclSym] = d[AclSym] = this.props.Document[AclSym]; -                            for (const [key, value] of Object.entries(this.props.Document[AclSym])) { -                                dataDoc[key] = d[key] = this.AclMap.get(value); -                            } -                        }); -                    } +                    // if (this.props.Document[AclSym]) { +                    //     added.forEach(d => { +                    //         const dataDoc = d[DataSym]; +                    //         dataDoc[AclSym] = d[AclSym] = this.props.Document[AclSym]; +                    //         for (const [key, value] of Object.entries(this.props.Document[AclSym])) { +                    //             dataDoc[key] = d[key] = this.AclMap.get(value); +                    //         } +                    //     }); +                    // }                      if (effectiveAcl === AclAddonly) {                          added.map(doc => Doc.AddDocToList(targetDataDoc, this.annotationKey, doc));                      }                      else {                          added.map(doc => doc.context = this.props.Document); -                        targetDataDoc[this.annotationKey] = new List<Doc>([...docList, ...added]); +                        (targetDataDoc[this.annotationKey] as List<Doc>).push(...added);                          targetDataDoc[this.annotationKey + "-lastModified"] = new DateField(new Date(Date.now()));                      }                  } diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 6b85616c2..c9f380737 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -278,10 +278,10 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV              <div className="documentButtonBar-button">                  <DocumentLinksButton links={this.view0.allLinks} View={this.view0} AlwaysOn={true} InMenu={true} StartLink={true} />              </div> -            {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"> +                <DocumentLinksButton links={this.view0.allLinks} View={this.view0} AlwaysOn={true} InMenu={true} StartLink={false} /> +            </div> +            {/* <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 51325ae1b..f16cb273b 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -1,9 +1,9 @@  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 { action, computed, observable, reaction, runInAction, get } from "mobx";  import { observer } from "mobx-react"; -import { Doc, DataSym, Field, WidthSym, HeightSym } from "../../fields/Doc"; +import { Doc, DataSym, Field, WidthSym, HeightSym, AclEdit, AclAdmin } from "../../fields/Doc";  import { Document } from '../../fields/documentSchemas';  import { ScriptField } from '../../fields/ScriptField';  import { Cast, StrCast, NumCast } from "../../fields/Types"; @@ -23,6 +23,10 @@ import { SnappingManager } from '../util/SnappingManager';  import { HtmlField } from '../../fields/HtmlField';  import { InkField } from "../../fields/InkField";  import { Tooltip } from '@material-ui/core'; +import { GetEffectiveAcl } from '../../fields/util'; +import { DocumentIcon } from './nodes/DocumentIcon'; +import { render } from 'react-dom'; +import { createLessThan } from 'typescript';  library.add(faCaretUp);  library.add(faObjectGroup); @@ -44,6 +48,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 }> { @@ -152,8 +157,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>          if (e.button === 0 && !e.altKey && !e.ctrlKey) {              let child = SelectionManager.SelectedDocuments()[0].ContentDiv!.children[0];              while (child.children.length) { -                const next = Array.from(child.children).find(c => typeof (c.className) !== "string"); -                if (typeof (next?.className) === "string" && next?.className.includes("documentView-node")) break; +                const next = Array.from(child.children).find(c => typeof (c.className) === "string"); +                if (next?.className.includes("documentView-node")) break;                  if (next) child = next;                  else break;              } @@ -194,8 +199,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>              SelectionManager.DeselectAll();              selected.map(dv => { -                recent && Doc.AddDocToList(recent, "data", dv.props.Document, undefined, true, true); -                dv.props.removeDocument?.(dv.props.Document); +                const effectiveAcl = GetEffectiveAcl(dv.props.Document); +                if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) { // deletes whatever you have the right to delete +                    recent && Doc.AddDocToList(recent, "data", dv.props.Document, undefined, true, true); +                    dv.props.removeDocument?.(dv.props.Document); +                }              });          }      } @@ -527,12 +535,12 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>                  const ink = Cast(doc.data, InkField)?.inkData;                  if (ink) {                      const newPoints: { X: number, Y: number }[] = []; -                    for (var i = 0; i < ink.length; i++) { +                    ink.forEach(i => {                          // (new x — oldx) + (oldxpoint * newWidt)/oldWidth  -                        const newX = (doc.x - this._inkDocs[index].x) + (ink[i].X * doc._width) / this._inkDocs[index].width; -                        const newY = (doc.y - this._inkDocs[index].y) + (ink[i].Y * doc._height) / this._inkDocs[index].height; +                        const newX = ((doc.x || 0) - this._inkDocs[index].x) + (i.X * (doc._width || 0)) / this._inkDocs[index].width; +                        const newY = ((doc.y || 0) - this._inkDocs[index].y) + (i.Y * (doc._height || 0)) / this._inkDocs[index].height;                          newPoints.push({ X: newX, Y: newY }); -                    } +                    });                      doc.data = new InkField(newPoints);                  } @@ -580,17 +588,18 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>          if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 2 || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) {              return (null);          } +        const canDelete = SelectionManager.SelectedDocuments().map(docView => GetEffectiveAcl(docView.props.ContainingCollectionDoc)).some(permission => permission === AclAdmin || permission === AclEdit);          const minimal = bounds.r - bounds.x < 100 ? true : false;          const maximizeIcon = minimal ? (              <Tooltip title={<><div className="dash-tooltip">Show context menu</div></>} placement="top">                  <div className="documentDecorations-contextMenu" onPointerDown={this.onSettingsDown}>                      <FontAwesomeIcon size="lg" icon="cog" /> -                </div></Tooltip>) : ( -                <Tooltip title={<><div className="dash-tooltip">Delete</div></>} placement="top"> -                    <div className="documentDecorations-closeButton" onClick={this.onCloseClick}> -                        {/* Currently, this is set to be enabled if there is no ink selected. It might be interesting to think about minimizing ink if it's useful? -syip2*/} -                        <FontAwesomeIcon className="documentdecorations-times" icon={faTimes} size="lg" /> -                    </div></Tooltip>); +                </div></Tooltip>) : canDelete ? ( +                    <Tooltip title={<><div className="dash-tooltip">Delete</div></>} placement="top"> +                        <div className="documentDecorations-closeButton" onClick={this.onCloseClick}> +                            {/* Currently, this is set to be enabled if there is no ink selected. It might be interesting to think about minimizing ink if it's useful? -syip2*/} +                            <FontAwesomeIcon className="documentdecorations-times" icon={faTimes} size="lg" /> +                        </div></Tooltip>) : (null);          const titleArea = this._edtingTitle ?              <> @@ -659,7 +668,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..f9d060681 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -187,11 +187,11 @@ export class EditableView extends React.Component<EditableProps> {                      {this.renderEditor()}                  </div> : this.renderEditor();          } else { -            this.props.autosuggestProps?.resetValue(); +            setTimeout(() => this.props.autosuggestProps?.resetValue(), 0);              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/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 5892e8346..8e3f72cee 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -178,7 +178,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume                      if (cm) {                          !Doc.UserDoc().noviceMode && cm.addItem({ description: "Recognize Writing", event: this.analyzeStrokes, icon: "paint-brush" });                          cm.addItem({ description: "Make Mask", event: this.makeMask, icon: "paint-brush" }); -                        cm.addItem({ description: "Format Shape...", event: this.formatShape, icon: "paint-brush" }); +                        //cm.addItem({ description: "Format Shape...", event: this.formatShape, icon: "paint-brush" });                      }                  }}              ><defs> diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss index e1ddbc533..a57d22afd 100644 --- a/src/client/views/MainView.scss +++ b/src/client/views/MainView.scss @@ -102,6 +102,44 @@      user-select: none;  } +.mainView-propertiesDragger { +    background-color: rgb(140, 139, 139); +    height: 55px; +    width: 17px; +    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: 5.5px; +        padding-top: 19px; +    } + +    &:hover { +        cursor: grab; +    } +} + +.mainiView-propertiesView { +    display: flex; +    flex-direction: column; +    height: 100%; +    position: absolute; +    right: 0; +    top: 0; +    border-left: solid 1px; +    z-index: 100000; +    cursor: auto; +} +  .mainView-flyoutContainer {      display: flex;      flex-direction: column; @@ -114,6 +152,75 @@      }  } +.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-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 +269,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 5c34233b4..fccfe325a 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -1,7 +1,6 @@  import { library } from '@fortawesome/fontawesome-svg-core';  import { faHireAHelper, faBuffer } from '@fortawesome/free-brands-svg-icons';  import * as fa from '@fortawesome/free-solid-svg-icons'; -import { ANTIMODEMENU_HEIGHT } from './globalCssVariables.scss';  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';  import { action, computed, configure, observable, reaction, runInAction } from 'mobx';  import { observer } from 'mobx-react'; @@ -12,51 +11,55 @@ import { Doc, DocListCast, Field, Opt } from '../../fields/Doc';  import { Id } from '../../fields/FieldSymbols';  import { List } from '../../fields/List';  import { listSpec } from '../../fields/Schema'; +import { ScriptField } from '../../fields/ScriptField';  import { BoolCast, Cast, FieldValue, StrCast } 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, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents, Utils } from '../../Utils';  import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager';  import { DocServer } from '../DocServer';  import { Docs, DocumentOptions } from '../documents/Documents';  import { DocumentType } from '../documents/DocumentTypes'; +import { CurrentUserUtils } from '../util/CurrentUserUtils'; +import { DocumentManager } from '../util/DocumentManager'; +import GroupManager from '../util/GroupManager';  import { HistoryUtil } from '../util/History'; -import RichTextMenu from './nodes/formattedText/RichTextMenu';  import { Scripting } from '../util/Scripting'; +import { SelectionManager } from '../util/SelectionManager';  import SettingsManager from '../util/SettingsManager'; -import GroupManager from '../util/GroupManager';  import SharingManager from '../util/SharingManager'; +import { SnappingManager } from '../util/SnappingManager';  import { Transform } from '../util/Transform'; +import { TimelineMenu } from './animationtimeline/TimelineMenu';  import { CollectionDockingView } from './collections/CollectionDockingView'; +import FormatShapePane from "./collections/collectionFreeForm/FormatShapePane";  import MarqueeOptionsMenu from './collections/collectionFreeForm/MarqueeOptionsMenu'; +import { PropertiesView } from './collections/collectionFreeForm/PropertiesView';  import { CollectionLinearView } from './collections/CollectionLinearView'; +import CollectionMenu from './collections/CollectionMenu';  import { CollectionView, CollectionViewType } from './collections/CollectionView';  import { ContextMenu } from './ContextMenu';  import { DictationOverlay } from './DictationOverlay';  import { DocumentDecorations } from './DocumentDecorations';  import GestureOverlay from './GestureOverlay'; +import { ANTIMODEMENU_HEIGHT } from './globalCssVariables.scss';  import KeyManager from './GlobalKeyHandler'; +import { LinkMenu } from './linking/LinkMenu';  import "./MainView.scss";  import { MainViewNotifs } from './MainViewNotifs';  import { AudioBox } from './nodes/AudioBox'; +import { DocumentLinksButton } from './nodes/DocumentLinksButton';  import { DocumentView } from './nodes/DocumentView'; +import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; +import RichTextMenu from './nodes/formattedText/RichTextMenu'; +import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup'; +import { LinkDocPreview } from './nodes/LinkDocPreview';  import { RadialMenu } from './nodes/RadialMenu'; +import { TaskCompletionBox } from './nodes/TaskCompletedBox';  import { OverlayView } from './OverlayView';  import PDFMenu from './pdf/PDFMenu';  import { PreviewCursor } from './PreviewCursor'; -import { ScriptField } from '../../fields/ScriptField'; -import { TimelineMenu } from './animationtimeline/TimelineMenu'; -import { SnappingManager } from '../util/SnappingManager'; -import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; -import { DocumentManager } from '../util/DocumentManager'; -import { DocumentLinksButton } from './nodes/DocumentLinksButton'; -import { LinkMenu } from './linking/LinkMenu'; -import { LinkDocPreview } from './nodes/LinkDocPreview'; -import { TaskCompletionBox } from './nodes/TaskCompletedBox'; -import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup'; -import FormatShapePane from "./collections/collectionFreeForm/FormatShapePane"; -import CollectionMenu from './collections/CollectionMenu';  import { Hypothesis } from '../apis/hypothesis/HypothesisUtils'; +import { undoBatch } from '../util/UndoManager';  @observer  export class MainView extends React.Component { @@ -69,16 +72,34 @@ 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(); }      @computed private get mainContainer() { return this.userDoc ? FieldValue(Cast(this.userDoc.activeWorkspace, Doc)) : CurrentUserUtils.GuestWorkspace; }      @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?.["sidebar"]; +    @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; } +    } + +    propertiesWidth = () => Math.max(0, Math.min(this._panelWidth - 50, CurrentUserUtils.propertiesWidth)); + +    @computed get propertiesIcon() { +        if (this.propertiesWidth() < 10) { +            return "chevron-left"; +        } else { +            return "chevron-right"; +        } +    } +    @observable propertiesDownX: number | undefined;      componentDidMount() {          DocServer.setPlaygroundFields(["dataTransition", "_viewTransition", "_panX", "_panY", "_viewScale", "_viewType", "_chromeStatus"]); // can play with these fields on someone else's @@ -120,6 +141,9 @@ export class MainView extends React.Component {          MainView.Instance = this;          this._urlState = HistoryUtil.parseUrl(window.location) || {} as any;          // causes errors to be generated when modifying an observable outside of an action + +        CurrentUserUtils.propertiesWidth = 0; +          configure({ enforceActions: "observed" });          if (window.location.pathname !== "/home") {              const pathname = window.location.pathname.substr(1).split("/"); @@ -128,7 +152,7 @@ export class MainView extends React.Component {                  if (type === "doc") {                      CurrentUserUtils.MainDocId = pathname[1];                      if (!this.userDoc) { -                        runInAction(() => this.flyoutWidth = 0); +                        runInAction(() => this.closeFlyout());                          DocServer.GetRefField(CurrentUserUtils.MainDocId).then(action((field: Opt<Field>) =>                              field instanceof Doc && (CurrentUserUtils.GuestTarget = field)));                      } @@ -150,7 +174,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, faBuffer); +            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, faBuffer, fa.faExpand, fa.faUndo, fa.faSlidersH, fa.faAngleDoubleLeft);          this.initEventListeners();          this.initAuthenticationRouters();      } @@ -212,7 +237,7 @@ export class MainView extends React.Component {          const freeformOptions: DocumentOptions = {              x: 0,              y: 400, -            _width: this._panelWidth * .7, +            _width: this._panelWidth * .7 - this.propertiesWidth() * 0.7,              _height: this._panelHeight,              title: "Collection " + workspaceCount,          }; @@ -265,7 +290,7 @@ export class MainView extends React.Component {          }          // if there is a pending doc, and it has new data, show it (syip: we use a timeout to prevent collection docking view from being uninitialized)          setTimeout(async () => { -            const col = this.userDoc && await Cast(this.userDoc.rightSidebarCollection, Doc); +            const col = this.userDoc && await Cast(this.userDoc["sidebar-sharing"], Doc);              col && Cast(col.data, listSpec(Doc)) && runInAction(() => MainViewNotifs.NotifsCol = col);          }, 100);          return true; @@ -278,16 +303,21 @@ 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; -    defaultBackgroundColors = (doc: Doc) => { +    defaultBackgroundColors = (doc: Opt<Doc>) => { +        if (this.panelContent === doc?.title) return "lightgrey";          if (this.darkScheme) {              switch (doc?.type) { +                case DocumentType.FONTICON: return "white";                  case DocumentType.RTF || DocumentType.LABEL || DocumentType.BUTTON: return "#2d2d2d";                  case DocumentType.LINK:                  case DocumentType.COL: { @@ -297,6 +327,7 @@ export class MainView extends React.Component {              }          } else {              switch (doc?.type) { +                case DocumentType.FONTICON: return "black";                  case DocumentType.RTF: return "#f1efeb";                  case DocumentType.BUTTON:                  case DocumentType.LABEL: return "lightgray"; @@ -309,7 +340,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} @@ -325,7 +357,6 @@ export class MainView extends React.Component {              NativeWidth={returnZero}              PanelWidth={this.getPWidth}              PanelHeight={this.getPHeight} -            renderDepth={0}              focus={emptyFunction}              parentActive={returnTrue}              whenActiveChanged={emptyFunction} @@ -333,103 +364,66 @@ export class MainView extends React.Component {              docFilters={returnEmptyFilter}              ContainingCollectionView={undefined}              ContainingCollectionDoc={undefined} +            renderDepth={-1}          />;      }      @computed get dockingContent() {          TraceMobx();          const mainContainer = this.mainContainer; -        const width = this.flyoutWidth; -        return <Measure offset onResize={this.onResize}> -            {({ measureRef }) => -                <div ref={measureRef} className="mainContent-div" onDrop={this.onDrop} style={{ width: `calc(100% - ${width}px)` }}> -                    {!mainContainer ? (null) : this.mainDocView} -                </div> -            } -        </Measure>; -    } - -    _canClick = false; -    onPointerDown = (e: React.PointerEvent) => { -        if (this._flyoutTranslate) { -            this._canClick = true; -            this._flyoutSizeOnDown = e.clientX; -            document.removeEventListener("pointermove", this.onPointerMove); -            document.removeEventListener("pointerup", this.onPointerUp); -            document.addEventListener("pointermove", this.onPointerMove); -            document.addEventListener("pointerup", this.onPointerUp); -            e.stopPropagation(); -            e.preventDefault(); -        } +        const width = this.flyoutWidth + this.propertiesWidth(); +        return <div className="mainContent-div" onDrop={this.onDrop} style={{ width: `calc(100% - ${width}px)` }}> +            {!mainContainer ? (null) : this.mainDocView} +        </div>;      }      @action -    pointerLeaveDragger = () => { -        if (!this._flyoutTranslate) { -            this.flyoutWidth = 0; -            this._flyoutTranslate = true; -        } +    onPropertiesPointerDown = (e: React.PointerEvent) => { +        setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { +            CurrentUserUtils.propertiesWidth = this._panelWidth - e.clientX; +            return false; +        }), returnFalse, action(() => CurrentUserUtils.propertiesWidth = this.propertiesWidth() < 15 ? Math.min(this._panelWidth - 50, 250) : 0), false);      }      @action -    onPointerMove = (e: PointerEvent) => { -        this.flyoutWidth = Math.max(e.clientX, 0); -        Math.abs(this.flyoutWidth - this._flyoutSizeOnDown) > 6 && (this._canClick = false); -        this.sidebarButtonsDoc._columnWidth = this.flyoutWidth / 3 - 30; -    } -    @action -    onPointerUp = (e: PointerEvent) => { -        if (Math.abs(e.clientX - this._flyoutSizeOnDown) < 4 && this._canClick) { -            this.flyoutWidth = this.flyoutWidth < 15 ? 250 : 0; -            this.flyoutWidth && (this.sidebarButtonsDoc._columnWidth = this.flyoutWidth / 3 - 30); +    onFlyoutPointerDown = (e: React.PointerEvent) => { +        if (this._flyoutTranslate) { +            setupMoveUpEvents(this, e, action((e: PointerEvent) => { +                this.flyoutWidth = Math.max(e.clientX, 0); +                if (this.flyoutWidth < 5) { +                    this.panelContent = "none"; +                    this._lastButton && (this._lastButton.color = "white"); +                    this._lastButton && (this._lastButton._backgroundColor = ""); +                } +                return false; +            }), emptyFunction, action(() => { +                if (this.flyoutWidth < 15) MainView.expandFlyout(); +                else this.closeFlyout(); +            }));          } -        document.removeEventListener("pointermove", this.onPointerMove); -        document.removeEventListener("pointerup", this.onPointerUp);      } +      flyoutWidthFunc = () => this.flyoutWidth;      addDocTabFunc = (doc: Doc, where: string, libraryPath?: Doc[]): boolean => {          return where === "close" ? CollectionDockingView.CloseRightSplit(doc) :              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)) { -            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" }}> +        if (!this.sidebarContent) return null; +        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="lg" /> +                </div> : null} +                  <DocumentView -                    Document={sidebarContent} +                    Document={this.sidebarContent}                      DataDoc={undefined}                      LibraryPath={emptyPath}                      addDocument={undefined} @@ -452,62 +446,180 @@ 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> -                </div> +                    ContainingCollectionDoc={undefined} +                    relative={true} +                />              </div> -            {this.docButtons} +            {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.sidebarScreenToLocal} +                ContentScaling={returnOne} +                PanelWidth={() => 60} +                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 mainContent() { -        const sidebar = this.userDoc?.["tabs-panelContainer"]; -        const n = (RichTextMenu.Instance?.Pinned ? 1 : 0) + (CollectionMenu.Instance?.Pinned ? 1 : 0); -        const height = `calc(100% - ${n * Number(ANTIMODEMENU_HEIGHT.replace("px", ""))}px)`; -        return !this.userDoc || !(sidebar 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%" -            }} > -                <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) }}> -                            <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-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" -                        }}> -                            {this.flyout} -                            {this.expandButton} + +    @action @undoBatch +    closeFlyout = () => { +        this._lastButton && (this._lastButton.color = "white"); +        this._lastButton && (this._lastButton._backgroundColor = ""); +        this.panelContent = "none"; +        this.flyoutWidth = 0; +    } + +    get groupManager() { return GroupManager.Instance; } + +    _lastButton: Doc | undefined; +    @action @undoBatch +    selectMenu = (button: Doc, str: string) => { +        this._lastButton && (this._lastButton.color = "white"); +        this._lastButton && (this._lastButton._backgroundColor = ""); +        if (this.panelContent === str && this.flyoutWidth !== 0) { +            this.panelContent = "none"; +            this.flyoutWidth = 0; +        } else { +            let panelDoc: Doc | undefined; +            switch (this.panelContent = str) { +                case "Tools": panelDoc = Doc.UserDoc()["sidebar-tools"] as Doc ?? undefined; break; +                case "Workspace": panelDoc = Doc.UserDoc()["sidebar-workspaces"] as Doc ?? undefined; break; +                case "Catalog": panelDoc = Doc.UserDoc()["sidebar-catalog"] as Doc ?? undefined; break; +                case "Archive": panelDoc = Doc.UserDoc()["sidebar-recentlyClosed"] as Doc ?? undefined; break; +                case "Settings": SettingsManager.Instance.open(); break; +                case "Sharing": panelDoc = Doc.UserDoc()["sidebar-sharing"] as Doc ?? undefined; break; +                case "UserDoc": panelDoc = Doc.UserDoc()["sidebar-userDoc"] as Doc ?? undefined; break; +            } +            this.sidebarContent.proto = panelDoc; +            if (panelDoc) { +                MainView.expandFlyout(); +                button._backgroundColor = "lightgrey"; +                button.color = "black"; +                this._lastButton = button; +            } else this.flyoutWidth = 0; +        } +        return true; +    } + +    @action @undoBatch +    closeProperties = () => { +        CurrentUserUtils.propertiesWidth = 0; +    } + +    @computed get propertiesView() { +        TraceMobx(); +        return <div className="mainView-propertiesView" style={{ +            overflow: this.propertiesWidth() < 15 ? "hidden" : undefined +        }}> +            <PropertiesView +                width={this.propertiesWidth()} +                height={this._panelHeight} +                renderDepth={1} +                ScreenToLocalTransform={Transform.Identity} +                onDown={this.closeProperties} +            /> +        </div>; +    } + +    @computed get mainInnerContent() { +        const rightFlyout = this.propertiesWidth() - 1; +        return <> +            {this.menuPanel} +            <div style={{ display: "contents", flexDirection: "row", position: "relative" }}> +                <div className="mainView-flyoutContainer" style={{ width: this.flyoutWidth }}> +                    {this.flyoutWidth !== 0 ? <div className="mainView-libraryHandle" +                        onPointerDown={this.onFlyoutPointerDown} +                        style={{ backgroundColor: 'lightgrey' }}> +                        <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 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.2vw" +                    }}> +                        {this.flyout} +                        {this.expandButton}                      </div> -                    {this.dockingContent}                  </div> -            </div>); +                {this.dockingContent} +                <MainViewNotifs /> +                {this.showProperties ? (null) : +                    <div className="mainView-propertiesDragger" title="Properties View Dragger" onPointerDown={this.onPropertiesPointerDown} +                        style={{ right: rightFlyout, top: "50%" }}> +                        <div className="mainView-propertiesDragger-icon"> +                            <FontAwesomeIcon icon={this.propertiesIcon} color="white" size="sm" /> </div> +                    </div> +                } +                {this.propertiesWidth() < 10 ? (null) : +                    <div style={{ width: this.propertiesWidth() }}> {this.propertiesView} </div>} +            </div> +        </>; +    } + +    @computed get mainContent() { +        //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)`; +        const pinned = FormatShapePane.Instance?.Pinned; +        const innerContent = this.mainInnerContent; +        return !this.userDoc ? (null) : ( +            <Measure offset onResize={this.onResize}> +                {({ measureRef }) => +                    <div className="mainView-mainContent" ref={measureRef} style={{ +                        color: this.darkScheme ? "rgb(205,205,205)" : "black", +                        //change to times 2 for both pinned +                        height, +                        width: pinned ? `calc(100% - 200px)` : "100%" +                    }} > +                        {innerContent} +                    </div> +                } +            </Measure>);      }      public static expandFlyout = action(() => {          MainView.Instance._flyoutTranslate = true;          MainView.Instance.flyoutWidth = (MainView.Instance.flyoutWidth || 250); -        MainView.Instance.sidebarButtonsDoc._columnWidth = MainView.Instance.flyoutWidth / 3 - 30; +      });      @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); @@ -524,7 +636,6 @@ export class MainView extends React.Component {          if (dockedBtns instanceof Doc) {              return <div className="mainView-docButtons" ref={this._docBtnRef}                  style={{ height: !dockedBtns.linearViewIsExpanded ? "42px" : undefined }} > -                <MainViewNotifs />                  <CollectionLinearView                      Document={dockedBtns}                      DataDoc={undefined} @@ -532,6 +643,7 @@ export class MainView extends React.Component {                      fieldKey={"data"}                      dropAction={"alias"}                      annotationsKey={""} +                    backgroundColor={this.defaultBackgroundColors}                      rootSelected={returnTrue}                      bringToFront={emptyFunction}                      select={emptyFunction} @@ -601,8 +713,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 /> @@ -610,9 +730,10 @@ export class MainView extends React.Component {              <GroupManager />              <GoogleAuthenticationManager />              <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/MainViewModal.tsx b/src/client/views/MainViewModal.tsx index 249715511..66ea2dbf8 100644 --- a/src/client/views/MainViewModal.tsx +++ b/src/client/views/MainViewModal.tsx @@ -10,7 +10,7 @@ export interface MainViewOverlayProps {      overlayStyle?: React.CSSProperties;      dialogueBoxDisplayedOpacity?: number;      overlayDisplayedOpacity?: number; -    closeOnExternalClick?: () => void; +    closeOnExternalClick?: () => void; // the close method of a MainViewModal, triggered if there is a click on the overlay (closing the modal)  }  @observer diff --git a/src/client/views/MainViewNotifs.scss b/src/client/views/MainViewNotifs.scss index 25ec95643..92d7d6ee3 100644 --- a/src/client/views/MainViewNotifs.scss +++ b/src/client/views/MainViewNotifs.scss @@ -1,5 +1,7 @@  .mainNotifs-container {      position:absolute; +    z-index: 1000; +    top: 12px;      .mainNotifs-badge {          position: absolute; diff --git a/src/client/views/MainViewNotifs.tsx b/src/client/views/MainViewNotifs.tsx index 05f890485..ce47e1cf1 100644 --- a/src/client/views/MainViewNotifs.tsx +++ b/src/client/views/MainViewNotifs.tsx @@ -3,28 +3,33 @@ import { observer } from 'mobx-react';  import "normalize.css";  import * as React from 'react';  import { Doc, DocListCast, Opt } from '../../fields/Doc'; -import { emptyFunction } from '../../Utils'; -import { SetupDrag } from '../util/DragManager'; +import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../Utils'; +import { SetupDrag, DragManager } from '../util/DragManager';  import "./MainViewNotifs.scss"; -import { CollectionDockingView } from './collections/CollectionDockingView'; +import { MainView } from './MainView';  @observer  export class MainViewNotifs extends React.Component { -      @observable static NotifsCol: Opt<Doc>; -    openNotifsCol = () => { -        if (MainViewNotifs.NotifsCol) { -            CollectionDockingView.AddRightSplit(MainViewNotifs.NotifsCol); -        } +    _notifsRef = React.createRef<HTMLDivElement>(); + +    onPointerDown = (e: React.PointerEvent) => { +        setupMoveUpEvents(this, e, +            (e: PointerEvent) => { +                const dragData = new DragManager.DocumentDragData([MainViewNotifs.NotifsCol!]); +                DragManager.StartDocumentDrag([this._notifsRef.current!], dragData, e.x, e.y); +                return true; +            }, +            returnFalse, +            () => MainViewNotifs.NotifsCol && MainView.Instance.selectMenu(MainViewNotifs.NotifsCol, "Sharing"));      } +      render() {          const length = MainViewNotifs.NotifsCol ? DocListCast(MainViewNotifs.NotifsCol.data).length : 0; -        const notifsRef = React.createRef<HTMLDivElement>(); -        const dragNotifs = action(() => MainViewNotifs.NotifsCol!); -        return <div className="mainNotifs-container" ref={notifsRef}> +        return <div className="mainNotifs-container" style={{ width: 15, height: 15 }} ref={this._notifsRef}>              <button className="mainNotifs-badge" style={length > 0 ? { "display": "initial" } : { "display": "none" }} -                onClick={this.openNotifsCol} onPointerDown={MainViewNotifs.NotifsCol ? SetupDrag(notifsRef, dragNotifs) : emptyFunction}> +                onPointerDown={this.onPointerDown} >                  {length}              </button>          </div>; diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx index b4116e980..d7034fcfb 100644 --- a/src/client/views/PreviewCursor.tsx +++ b/src/client/views/PreviewCursor.tsx @@ -112,10 +112,10 @@ export class PreviewCursor extends React.Component<{}> {                  } else if (e.clipboardData.items.length) {                      const batch = UndoManager.StartBatch("collection view drop");                      const files: File[] = []; -                    for (let i = 0; i < e.clipboardData.items.length; i++) { -                        const file = e.clipboardData.items[i].getAsFile(); +                    Array.from(e.clipboardData.items).forEach(item => { +                        const file = item.getAsFile();                          file && files.push(file); -                    } +                    });                      const generatedDocuments = await DocUtils.uploadFilesToDocs(files, { x: newPoint[0], y: newPoint[1] });                      generatedDocuments.forEach(PreviewCursor._addDocument);                      batch.end(); diff --git a/src/client/views/PropertiesButtons.scss b/src/client/views/PropertiesButtons.scss new file mode 100644 index 000000000..1cba252de --- /dev/null +++ b/src/client/views/PropertiesButtons.scss @@ -0,0 +1,129 @@ +@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: 30px; +    width: 30px; +    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: 10px; + +    &:hover { +        background: $main-accent; +        transform: scale(1.05); +        cursor: pointer; +    } +} + +.propertiesButtons { +    margin-top: 3px; +    grid-column: 1/4; +    width: 100%; +    height: auto; +    display: flex; +    flex-direction: row; +    flex-wrap: wrap; +} + +.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: 18px; +    margin-bottom: 8px; +} + +.propertiesButtons-linker { +    height: 30px; +    width: 30px; +    text-align: center; +    border-radius: 5px; +    pointer-events: auto; +    // color: $dark-color; +    // border: $dark-color 1px solid; +    background-color: #252b33; +    color: #fcfbf7; +    transition: 0.2s ease all; +    margin-right: 5px; +    padding-top: 5px; + +    &:hover { +        background: $main-accent; +        transform: scale(1.05); +        cursor: pointer; +    } +} + +.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..d46c03470 --- /dev/null +++ b/src/client/views/PropertiesButtons.tsx @@ -0,0 +1,658 @@ +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'; +import { InkField } from '../../fields/InkField'; +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" : "", 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); +    } +    @undoBatch +    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>; +    } + +    @undoBatch +    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); +            const alias = Doc.MakeAlias(this.selectedDoc); +            alias.x = NumCast(this.selectedDoc.x) + NumCast(this.selectedDoc._width); +            alias.y = NumCast(this.selectedDoc.y) + 30; +            this.selectedDocumentView.props.addDocument?.(alias); +        } +    } + +    @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 @undoBatch +    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.deleteDocument}> +                {<FontAwesomeIcon className="propertiesButtons-icon" +                    icon="trash-alt" size="sm" />} +            </div> +        </Tooltip>; +    } + +    @undoBatch +    @action +    deleteDocument = () => { +        this.selectedDocumentView?.props.ContainingCollectionView?.removeDocument(this.selectedDocumentView?.props.Document); +    } + +    @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; +        } +    } + +    @undoBatch +    @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 clustersButton() { +        const targetDoc = this.selectedDoc; +        return !targetDoc ? (null) : <Tooltip +            title={<><div className="dash-tooltip">{this.selectedDoc?.useClusters ? "Stop Showing Clusters" : "Show Clusters"}</div></>}> +            <div className={"propertiesButtons-linkButton-empty"} +                style={{ backgroundColor: this.selectedDoc?.useClusters ? "#a0a0a0" : "" }} +                onPointerDown={this.changeClusters}> +                {<FontAwesomeIcon className="documentdecorations-icon" +                    color={this.selectedDoc?.useClusters ? "black" : "white"} +                    icon="braille" size="sm" />} +            </div> +        </Tooltip>; +    } + +    @action @undoBatch +    changeFitToBox = () => { +        this.selectedDoc && (this.selectedDoc._fitToBox = !this.selectedDoc._fitToBox); +    } + +    @action @undoBatch +    changeClusters = () => { +        this.selectedDoc && (this.selectedDoc.useClusters = !this.selectedDoc.useClusters); +    } + +    @computed +    get fitContentButton() { +        const targetDoc = this.selectedDoc; +        return !targetDoc ? (null) : <Tooltip +            title={<><div className="dash-tooltip">{this.selectedDoc?._fitToBox ? "Stop Fitting Content" : "Fit Content"}</div></>}> +            <div className={"propertiesButtons-linkButton-empty"} +                style={{ backgroundColor: this.selectedDoc?._fitToBox ? "#a0a0a0" : "" }} +                onPointerDown={this.changeFitToBox}> +                {<FontAwesomeIcon className="documentdecorations-icon" +                    color={this.selectedDoc?._fitToBox ? "black" : "white"} +                    icon="expand" size="sm" />} +            </div> +        </Tooltip>; +    } + +    @undoBatch +    @action +    private makeMask = () => { +        if (this.selectedDoc) { +            this.selectedDoc._backgroundColor = "rgba(0,0,0,0.7)"; +            this.selectedDoc.mixBlendMode = "hard-light"; +            this.selectedDoc.color = "#9b9b9bff"; +            this.selectedDoc.stayInCollection = true; +            this.selectedDoc.isInkMask = true; +        } +    } + +    @computed +    get maskButton() { +        const targetDoc = this.selectedDoc; +        return !targetDoc ? (null) : <Tooltip +            title={<><div className="dash-tooltip">Make Mask</div></>}> +            <div className={"propertiesButtons-linkButton-empty"} +                onPointerDown={this.makeMask}> +                {<FontAwesomeIcon className="documentdecorations-icon" +                    color="white" icon="paint-brush" size="sm" />} +            </div> +        </Tooltip>; +    } + +    @computed +    get contextButton() { +        if (this.selectedDoc) { +            return <Tooltip title={<><div className="dash-tooltip">Show Context</div></>}> +                <div className={"propertiesButtons-linkButton-empty"}> +                    <ParentDocSelector Document={this.selectedDoc} addDocTab={(doc, where) => { +                        where === "onRight" ? CollectionDockingView.AddRightSplit(doc) : +                            this.selectedDocumentView?.props.addDocTab(doc, "onRight"); +                        return true; +                    }} /> +                </div> +            </Tooltip>; +        } else { +            return false; +        } + +    } + +    // @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 isInk = this.selectedDoc[Doc.LayoutFieldKey(this.selectedDoc)] instanceof InkField; +        const isCollection = this.selectedDoc.type === DocumentType.COL ? true : false; +        const isFreeForm = this.selectedDoc._viewType === "freeform" ? 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 className="propertiesButtons-button"> +                {this.deleteButton} +            </div> +            <div className="propertiesButtons-button"> +                {this.onClickButton} +            </div> +            {/* <div className="propertiesButtons-button"> +                {this.contextButton} +            </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 className="propertiesButtons-button" style={{ display: !isFreeForm ? "none" : "" }}> +                {this.clustersButton} +            </div> + +            <div className="propertiesButtons-button" style={{ display: !isFreeForm ? "none" : "" }}> +                {this.fitContentButton} +            </div> + +            <div className="propertiesButtons-button" style={{ display: !isInk ? "none" : "" }}> +                {this.maskButton} +            </div> +        </div> +        </div>; +    } +} diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss index 1895c06a1..6ebd5103b 100644 --- a/src/client/views/collections/CollectionDockingView.scss +++ b/src/client/views/collections/CollectionDockingView.scss @@ -12,12 +12,14 @@          width: 100%;          height: 100%;          position: absolute; +          .miniThumb {              background: #25252525;              position: absolute;          }      }  } +  .lm_title {      margin-top: 3px;      border-radius: 5px; @@ -27,6 +29,7 @@      transform: translate(0px, -3px);      cursor: grab;  } +  .lm_title.focus-visible {      cursor: text;  } @@ -34,23 +37,39 @@  .lm_title_wrap {      overflow: hidden;      height: 19px; -    margin-top: -3px; -    display:inline-block; +    margin-top: -2px; +    display: inline-block;  } +  .lm_active .lm_title {      border: solid 1px lightgray;  } +  .lm_header .lm_tab .lm_close_tab {      position: absolute;      text-align: center;  }  .lm_header .lm_tab { -    padding-right : 20px; +    padding-right: 20px; +    margin-top: -1px; +    border-bottom: 1px black; +    .collectionDockingView-gear { +        display: none; +    } +} + +.lm_header .lm_tab.lm_active { +    padding-right: 20px; +    margin-top: 1px; +    border-bottom: unset; +    .collectionDockingView-gear { +        display: inline-block; +    }  }  .lm_popout { -    display:none; +    display: none;  }  .messageCounter { @@ -73,14 +92,15 @@      position: absolute;      top: 0;      left: 0; +      // overflow: hidden;  // bcz: menus don't show up when this is on (e.g., the parentSelectorMenu)      .collectionDockingView-gear {          padding-left: 5px;          height: 15px;          width: 18px; -        display: inline-block;          margin: auto;      } +      .collectionDockingView-dragAsDocument {          touch-action: none;          position: absolute; diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 53b2d5254..533c8bffe 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -1,9 +1,8 @@  import 'golden-layout/src/css/goldenlayout-base.css';  import 'golden-layout/src/css/goldenlayout-dark-theme.css'; -import { action, computed, Lambda, observable, reaction, runInAction, trace } from "mobx"; +import { action, computed, Lambda, observable, reaction, runInAction, trace, IReactionDisposer } from "mobx";  import { observer } from "mobx-react";  import * as ReactDOM from 'react-dom'; -import Measure from "react-measure";  import * as GoldenLayout from "../../../client/goldenLayout";  import { DateField } from '../../../fields/DateField';  import { Doc, DocListCast, Field, Opt, DataSym } from "../../../fields/Doc"; @@ -31,6 +30,8 @@ import { SnappingManager } from '../../util/SnappingManager';  import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';  import { listSpec } from '../../../fields/Schema';  import { clamp } from 'lodash'; +import { InteractionUtils } from '../../util/InteractionUtils'; +import { InkTool } from '../../../fields/InkField';  const _global = (window /* browser */ || global /* node */) as any;  @observer @@ -464,6 +465,11 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp          if (className === "lm_drag_handle" || className === "lm_close" || className === "lm_maximise" || className === "lm_minimise" || className === "lm_close_tab") {              this._flush = true;          } +        if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.GetSelectedTool() === InkTool.Highlighter || Doc.GetSelectedTool() === InkTool.Pen)) { +            return; +        } else { +            e.stopPropagation(); +        }      }      updateDataField = async (json: string) => { @@ -505,7 +511,6 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp              const doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId) as Doc;              if (doc instanceof Doc) { -                //tab.titleElement[0].outerHTML = `<input class='lm_title' style="background:black" value='${doc.title}' />`;                  tab.titleElement[0].onclick = (e: any) => tab.titleElement[0].focus();                  tab.titleElement[0].onchange = (e: any) => {                      tab.titleElement[0].size = e.currentTarget.value.length + 1; @@ -520,6 +525,10 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp                  gearSpan.style.paddingLeft = "0px";                  gearSpan.style.paddingRight = "12px";                  const stack = tab.contentItem.parent; +                tab.element[0].onpointerdown = (e: any) => { +                    const view = DocumentManager.Instance.getDocumentView(doc); +                    view && SelectionManager.SelectDoc(view, false); +                };                  // shifts the focus to this tab when another tab is dragged over it                  tab.element[0].onmouseenter = (e: any) => {                      if (!this._isPointerDown || !SnappingManager.GetIsDragging()) return; @@ -595,7 +604,6 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp      stackCreated = (stack: any) => {          //stack.header.controlsContainer.find('.lm_popout').hide(); -        stack.header.element[0].style.backgroundColor = DocServer.Control.isReadOnly() ? "#228540" : undefined;          stack.header.element.on('mousedown', (e: any) => {              if (e.target === stack.header.element[0] && e.button === 1) {                  this.AddTab(stack, Docs.Create.FreeformDocument([], { _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), title: "Untitled Collection" })); @@ -675,10 +683,15 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {      @observable private _panelHeight = 0;      @observable private _document: Opt<Doc>;      @observable private _isActive: boolean = false; +    _tabReaction: IReactionDisposer | undefined;      get _stack(): any {          return (this.props as any).glContainer.parent.parent;      } +    get _tab(): any { +        const tab = (this.props as any).glContainer.tab.element[0] as HTMLElement; +        return tab.getElementsByClassName("lm_title")?.[0]; +    }      constructor(props: any) {          super(props);          DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => this._document = f as Doc)); @@ -739,9 +752,16 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {          this.props.glContainer.layoutManager.on("activeContentItemChanged", this.onActiveContentItemChanged);          this.props.glContainer.on("tab", this.onActiveContentItemChanged);          this.onActiveContentItemChanged(); +        this._tabReaction = reaction(() => ({ views: SelectionManager.SelectedDocuments(), color: StrCast(this._document?._backgroundColor, "white") }), +            (data) => { +                const selected = data.views.some(v => Doc.AreProtosEqual(v.props.Document, this._document)); +                this._tab.style.backgroundColor = selected ? data.color : ""; +            } +        );      }      componentWillUnmount() { +        this._tabReaction?.();          this.props.glContainer.layoutManager.off("activeContentItemChanged", this.onActiveContentItemChanged);          this.props.glContainer.off("tab", this.onActiveContentItemChanged);      } @@ -750,6 +770,10 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {      private onActiveContentItemChanged() {          if (this.props.glContainer.tab) {              this._isActive = this.props.glContainer.tab.isActive; +            setTimeout(() => { +                const dv = this._document && DocumentManager.Instance.getFirstDocumentView(this._document); +                dv && SelectionManager.SelectDoc(dv, false); +            });              !this._isActive && this._document && Doc.UnBrushDoc(this._document); // bcz: bad -- trying to simulate a pointer leave event when a new tab is opened up on top of an existing one.          }      } diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx index cd6e60de6..19d1ffa7b 100644 --- a/src/client/views/collections/CollectionLinearView.tsx +++ b/src/client/views/collections/CollectionLinearView.tsx @@ -161,7 +161,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {                                  PanelHeight={nested ? pair.layout[HeightSym] : () => this.dimension()}                                  renderDepth={this.props.renderDepth + 1}                                  focus={emptyFunction} -                                backgroundColor={returnEmptyString} +                                backgroundColor={this.props.backgroundColor}                                  parentActive={returnTrue}                                  whenActiveChanged={emptyFunction}                                  bringToFront={emptyFunction} diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index 9a7ea2c93..c772dcfe7 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -238,7 +238,6 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr              contents: "+ NEW",              HeadingObject: this.props.headingObject,              toggle: this.toggleVisibility, -            color: this.color          };          const showChrome = (chromeStatus !== 'view-mode' && chromeStatus !== 'disabled');          const stackPad = showChrome ? `0px ${this.props.parent.xMargin}px` : `${this.props.parent.yMargin}px ${this.props.parent.xMargin}px 0px ${this.props.parent.xMargin}px `; @@ -278,7 +277,6 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr              oneLine: true,              HeadingObject: this.props.headingObject,              toggle: this.toggleVisibility, -            color: this.color          };          return this.props.parent.props.Document.miniHeaders ?              <div className="collectionStackingView-miniHeader"> diff --git a/src/client/views/collections/CollectionMenu.scss b/src/client/views/collections/CollectionMenu.scss index 9da204787..0a316317f 100644 --- a/src/client/views/collections/CollectionMenu.scss +++ b/src/client/views/collections/CollectionMenu.scss @@ -2,8 +2,8 @@  .collectionMenu-cont { -    position:relative; -    display:inline-flex; +    position: relative; +    display: inline-flex;      width: 100%;      opacity: 0.9;      z-index: 9001; @@ -12,14 +12,15 @@      color: white;      transform-origin: top left;      top: 0; -    width:100%; +    width: 100%;      .antimodeMenu-button {          padding: 0;          width: 30px;          display: flex; +          svg { -            margin:auto; +            margin: auto;          }      } @@ -108,6 +109,7 @@                  margin-top: auto;                  margin-bottom: auto;              } +              .collectionViewBaseChrome-viewSpecs {                  margin-left: 5px;                  display: grid; @@ -318,12 +320,14 @@          text-align: center;          display: block;      } +      .color-previewI {          width: 80%;          height: 20%;          bottom: 0;          position: absolute;      } +      .color-previewII {          width: 80%;          height: 80%; @@ -336,7 +340,7 @@          margin: auto;          /* Make the buttons appear below each other */      } -     +      .btn-draw {          display: inline-flex;          margin: auto; @@ -374,6 +378,7 @@              display: block;              margin: auto;          } +          border-right: solid gray 1px;      }  } @@ -398,14 +403,14 @@      .collectionSchemaViewChrome-toggler {          width: 100px; -        height: 41px; +        height: 35px;          background-color: black;          position: relative;      }      .collectionSchemaViewChrome-togglerButton {          width: 47px; -        height: 35px; +        height: 30px;          background-color: $light-color-secondary;          // position: absolute;          transition: all 0.5s ease; diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index 0ca86172f..a7d2c07fa 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -25,9 +25,13 @@ 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';  import { DocUtils } from "../../documents/Documents"; +import { Tooltip } from "@material-ui/core"; +import { CurrentUserUtils } from "../../util/CurrentUserUtils";  @observer  export default class CollectionMenu extends AntimodeMenu { @@ -47,7 +51,7 @@ export default class CollectionMenu extends AntimodeMenu {      componentDidMount() {          reaction(() => SelectionManager.SelectedDocuments().length && SelectionManager.SelectedDocuments()[0], -            (doc) => doc && this.SetSelection(doc)) +            (doc) => doc && this.SetSelection(doc));      }      @action @@ -63,16 +67,37 @@ export default class CollectionMenu extends AntimodeMenu {          }      } +    @action +    toggleProperties = () => { +        if (CurrentUserUtils.propertiesWidth > 0) { +            CurrentUserUtils.propertiesWidth = 0; +        } else { +            CurrentUserUtils.propertiesWidth = 250; +        } +    } +      render() { -        const button = <button className="antimodeMenu-button" key="pin menu" title="Pin menu" onClick={this.toggleMenuPin} style={{ backgroundColor: "#121721" }}> -            <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.Pinned ? 45 : 0}deg)` }} /> -        </button>; +        const button = <Tooltip title={<div className="dash-tooltip">Pin Menu</div>} key="pin menu" placement="bottom"> +            <button className="antimodeMenu-button" onClick={this.toggleMenuPin} style={{ backgroundColor: "#121721" }}> +                <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.Pinned ? 45 : 0}deg)` }} /> +            </button> +        </Tooltip>; + +        const propIcon = CurrentUserUtils.propertiesWidth > 0 ? "angle-double-right" : "angle-double-left"; +        const propTitle = CurrentUserUtils.propertiesWidth > 0 ? "Close Properties Panel" : "Open Properties Panel"; + +        const prop = <Tooltip title={<div className="dash-tooltip">{propTitle}</div>} key="properties" placement="bottom"> +            <button className="antimodeMenu-button" key="properties" onPointerDown={this.toggleProperties}> +                <FontAwesomeIcon icon={propIcon} size="lg" /> +            </button> +        </Tooltip>;          return this.getElement(!this.SelectedCollection ? [button] :              [<CollectionViewBaseChrome key="chrome"                  docView={this.SelectedCollection}                  fieldKey={Doc.LayoutFieldKey(this.SelectedCollection?.props.Document)}                  type={StrCast(this.SelectedCollection?.props.Document._viewType, CollectionViewType.Invalid) as CollectionViewType} />, +                prop,                  button]);      }  } @@ -160,12 +185,12 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp          initialize: (button: Doc) => { button['target-docFilters'] = this.target._docFilters instanceof ObjectField ? ObjectField.MakeCopy(this.target._docFilters as any as ObjectField) : ""; },      }; -    _freeform_commands = [this._viewCommand, this._saveFilterCommand, this._fitContentCommand, this._clusterCommand, this._contentCommand, this._templateCommand, this._narrativeCommand]; -    _stacking_commands = [this._contentCommand, this._templateCommand]; -    _masonry_commands = [this._contentCommand, this._templateCommand]; -    _schema_commands = [this._templateCommand, this._narrativeCommand]; -    _doc_commands = [this._openLinkInCommand, this._onClickCommand]; -    _tree_commands = []; +    @computed get _freeform_commands() { return Doc.UserDoc().noviceMode ? [this._viewCommand, this._saveFilterCommand] : [this._viewCommand, this._saveFilterCommand, this._contentCommand, this._templateCommand, this._narrativeCommand]; } +    @computed get _stacking_commands() { return Doc.UserDoc().noviceMode ? undefined : [this._contentCommand, this._templateCommand]; } +    @computed get _masonry_commands() { return Doc.UserDoc().noviceMode ? undefined : [this._contentCommand, this._templateCommand]; } +    @computed get _schema_commands() { return Doc.UserDoc().noviceMode ? undefined : [this._templateCommand, this._narrativeCommand]; } +    @computed get _doc_commands() { return Doc.UserDoc().noviceMode ? undefined : [this._openLinkInCommand, this._onClickCommand]; } +    @computed get _tree_commands() { return undefined; }      private get _buttonizableCommands() {          switch (this.props.type) {              default: return this._doc_commands; @@ -185,7 +210,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp      @observable private _currentKey: string = "";      componentDidMount = action(() => { -        this._currentKey = this._currentKey || (this._buttonizableCommands.length ? this._buttonizableCommands[0]?.title : ""); +        this._currentKey = this._currentKey || (this._buttonizableCommands?.length ? this._buttonizableCommands[0]?.title : "");      });      @undoBatch @@ -235,7 +260,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp      protected drop(e: Event, de: DragManager.DropEvent): boolean {          const docDragData = de.complete.docDragData;          if (docDragData?.draggedDocuments.length) { -            this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => c.immediate(docDragData.draggedDocuments || [])); +            this._buttonizableCommands?.filter(c => c.title === this._currentKey).map(c => c.immediate(docDragData.draggedDocuments || []));              e.stopPropagation();          }          return true; @@ -257,76 +282,100 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp      }      dragCommandDown = (e: React.PointerEvent) => {          setupMoveUpEvents(this, e, (e, down, delta) => { -            this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => +            this._buttonizableCommands?.filter(c => c.title === this._currentKey).map(c =>                  DragManager.StartButtonDrag([this._commandRef.current!], c.script, c.title,                      { target: this.document }, c.params, c.initialize, e.clientX, e.clientY));              return true;          }, emptyFunction, () => { -            this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => c.immediate([])); +            this._buttonizableCommands?.filter(c => c.title === this._currentKey).map(c => c.immediate([]));          });      }      @computed get templateChrome() {          return <div className="collectionViewBaseChrome-template" ref={this.createDropTarget} > -            <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._commandRef} onPointerDown={this.dragCommandDown}> -                <button className={"antimodeMenu-button"} > -                    <FontAwesomeIcon icon="bullseye" size="lg" /> -                </button> -                <select -                    className="collectionViewBaseChrome-cmdPicker" onPointerDown={stopPropagation} onChange={this.commandChanged} value={this._currentKey}> -                    <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={"empty"} value={""} /> -                    {this._buttonizableCommands.map(cmd => -                        <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={cmd.title} value={cmd.title}>{cmd.title}</option> -                    )} -                </select> -            </div> +            <Tooltip title={<div className="dash-tooltip">drop document to apply or drag to create button</div>} placement="bottom"> +                <div className="commandEntry-outerDiv" ref={this._commandRef} onPointerDown={this.dragCommandDown}> +                    <button className={"antimodeMenu-button"} > +                        <FontAwesomeIcon icon="bullseye" size="lg" /> +                    </button> +                    <select +                        className="collectionViewBaseChrome-cmdPicker" onPointerDown={stopPropagation} onChange={this.commandChanged} value={this._currentKey}> +                        <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={"empty"} value={""} /> +                        {this._buttonizableCommands?.map(cmd => +                            <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={cmd.title} value={cmd.title}>{cmd.title}</option> +                        )} +                    </select> +                </div> +            </Tooltip>          </div>;      }      @computed get viewModes() {          return <div className="collectionViewBaseChrome-viewModes" > -            <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._viewRef} onPointerDown={this.dragViewDown}> -                <button className={"antimodeMenu-button"}> -                    <FontAwesomeIcon icon="bullseye" size="lg" /> -                </button> -                <select -                    className="collectionViewBaseChrome-viewPicker" -                    onPointerDown={stopPropagation} -                    onChange={this.viewChanged} -                    value={StrCast(this.props.type)}> -                    {Object.values(CollectionViewType).map(type => [CollectionViewType.Invalid, CollectionViewType.Docking].includes(type) ? (null) : ( -                        <option -                            key={Utils.GenerateGuid()} -                            className="collectionViewBaseChrome-viewOption" -                            onPointerDown={stopPropagation} -                            value={type}> -                            {type[0].toUpperCase() + type.substring(1)} -                        </option> -                    ))} -                </select> -            </div> +            <Tooltip title={<div className="dash-tooltip">drop document to apply or drag to create button</div>} placement="bottom"> +                <div className="commandEntry-outerDiv" ref={this._viewRef} onPointerDown={this.dragViewDown}> +                    <button className={"antimodeMenu-button"}> +                        <FontAwesomeIcon icon="bullseye" size="lg" /> +                    </button> +                    <select +                        className="collectionViewBaseChrome-viewPicker" +                        onPointerDown={stopPropagation} +                        onChange={this.viewChanged} +                        value={StrCast(this.props.type)}> +                        {Object.values(CollectionViewType).map(type => [CollectionViewType.Invalid, CollectionViewType.Docking].includes(type) ? (null) : ( +                            <option +                                key={Utils.GenerateGuid()} +                                className="collectionViewBaseChrome-viewOption" +                                onPointerDown={stopPropagation} +                                value={type}> +                                {type[0].toUpperCase() + type.substring(1)} +                            </option> +                        ))} +                    </select> +                </div> +            </Tooltip>          </div>;      } +    @computed get selectedDocumentView() { +        if (SelectionManager.SelectedDocuments().length) { +            return SelectionManager.SelectedDocuments()[0]; +        } else { return undefined; } +    } +    @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } +    @computed get notACollection() { +        if (this.selectedDoc) { +            const layoutField = Doc.LayoutField(this.selectedDoc); +            return this.props.type === CollectionViewType.Docking || +                typeof (layoutField) === "string" && !layoutField?.includes("CollectionView"); +        } +        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.Docking ? (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" /> -                            </button> -                        </div> - -                        {this.props.docView.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform ? (null) : <button className={"antimodeMenu-button"} key="float" -                            style={{ backgroundColor: !this.props.docView.layoutDoc.isAnnotating ? "121212" : undefined, borderRight: "1px solid gray" }} -                            title="Toggle Overlay Layer" -                            onClick={() => DocumentView.FloatDoc(this.props.docView)}> -                            <FontAwesomeIcon icon={["fab", "buffer"]} size={"lg"} /> -                        </button>} +                        {this.notACollection || this.props.type === CollectionViewType.Invalid ? (null) : this.viewModes} +                        {!this._buttonizableCommands ? (null) : this.templateChrome} +                        {Doc.UserDoc().noviceMode ? (null) : +                            <Tooltip title={<div className="dash-tooltip">filter documents to show</div>} placement="bottom"> +                                <div className="collectionViewBaseChrome-viewSpecs" style={{ display: "grid" }}> +                                    <button className={"antimodeMenu-button"} onClick={this.toggleViewSpecs} > +                                        <FontAwesomeIcon icon="filter" size="lg" /> +                                    </button> +                                </div> +                            </Tooltip>} + +                        {this.props.docView.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform ? (null) : +                            <Tooltip title={<div className="dash-tooltip">Toggle Overlay Layer</div>} placement="bottom"> +                                <button className={"antimodeMenu-button"} key="float" +                                    style={{ backgroundColor: this.props.docView.layoutDoc.z ? "121212" : undefined, borderRight: "1px solid gray" }} +                                    onClick={() => DocumentView.FloatDoc(this.props.docView)}> +                                    <FontAwesomeIcon icon={["fab", "buffer"]} size={"lg"} /> +                                </button> +                            </Tooltip>}                      </div>                      {this.subChrome}                  </div> @@ -356,6 +405,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 => { @@ -384,12 +447,9 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu      miniMap = (): void => {          this.document.hideMinimap = !this.document.hideMinimap;      } +      private _palette = ["#D0021B", "#F5A623", "#F8E71C", "#8B572A", "#7ED321", "#417505", "#9013FE", "#4A90E2", "#50E3C2", "#B8E986", "#000000", "#4A4A4A", "#9B9B9B", "#FFFFFF", ""];      private _width = ["1", "5", "10", "100"]; -    // private _draw = ["⎯", "→", "↔︎", "∿", "↝", "↭", "ロ", "O", "∆"]; -    // private _head = ["", "", "arrow", "", "", "arrow", "", "", ""]; -    // private _end = ["", "arrow", "arrow", "", "arrow", "arrow", "", "", ""]; -    // private _shape = ["line", "line", "line", "", "", "", "rectangle", "circle", "triangle"];      private _dotsize = [10, 20, 30, 40];      private _draw = ["∿", "⎯", "→", "↔︎", "ロ", "O"];      private _head = ["", "", "", "arrow", "", ""]; @@ -459,22 +519,24 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu          });          return <div className="btn-draw" key="draw">              {this._draw.map((icon, i) => -                <button className="antimodeMenu-button" title={this._title[i]} key={icon} onPointerDown={() => func(i, false)} onDoubleClick={() => func(i, true)} -                    style={{ backgroundColor: i === this._selected ? "121212" : "", fontSize: "20" }}> -                    {/* {this._draw[i]} */} -                    <FontAwesomeIcon icon={this._faName[i] as IconProp} size="sm" /> - -                </button>)} +                <Tooltip key={icon} title={<div className="dash-tooltip">{this._title[i]}</div>} placement="bottom"> +                    <button className="antimodeMenu-button" onPointerDown={() => func(i, false)} onDoubleClick={() => func(i, true)} +                        style={{ backgroundColor: i === this._selected ? "121212" : "", fontSize: "20" }}> +                        <FontAwesomeIcon icon={this._faName[i] as IconProp} size="sm" /> +                    </button> +                </Tooltip>)}          </div>;      }      toggleButton = (key: string, value: boolean, setter: () => {}, icon: FontAwesomeIconProps["icon"], ele: JSX.Element | null) => { -        return <button className="antimodeMenu-button" key={key} title={key} -            onPointerDown={action(e => setter())} -            style={{ backgroundColor: value ? "121212" : "" }}> -            <FontAwesomeIcon icon={icon} size="lg" /> -            {ele} -        </button>; +        return <Tooltip title={<div className="dash-tooltip">{key}</div>} placement="bottom"> +            <button className="antimodeMenu-button" key={key} +                onPointerDown={action(e => setter())} +                style={{ backgroundColor: value ? "121212" : "" }}> +                <FontAwesomeIcon icon={icon} size="lg" /> +                {ele} +            </button> +        </Tooltip>;      }      @computed get widthPicker() { @@ -483,11 +545,13 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu              <div className="btn2-group" key="width">                  {widthPicker}                  {this._width.map((wid, i) => -                    <button className="antimodeMenu-button" key={wid} title="change width" -                        onPointerDown={action(() => { SetActiveInkWidth(wid); this._widthBtn = false; this.editProperties(wid, "width"); })} -                        style={{ backgroundColor: this._widthBtn ? "121212" : "", zIndex: 1001, fontSize: this._dotsize[i], padding: 0, textAlign: "center" }}> -                        • -                    </button>)} +                    <Tooltip title={<div className="dash-tooltip">change width</div>} placement="bottom"> +                        <button className="antimodeMenu-button" key={wid} +                            onPointerDown={action(() => { SetActiveInkWidth(wid); this._widthBtn = false; this.editProperties(wid, "width"); })} +                            style={{ backgroundColor: this._widthBtn ? "121212" : "", zIndex: 1001, fontSize: this._dotsize[i], padding: 0, textAlign: "center" }}> +                            • +                    </button> +                    </Tooltip>)}              </div>;      } @@ -522,51 +586,56 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu              </div>;      } -    @computed get formatPane() { -        return <button className="antimodeMenu-button" key="format" title="toggle foramatting pane" -            onPointerDown={action(e => FormatShapePane.Instance.Pinned = !FormatShapePane.Instance.Pinned)} -            style={{ backgroundColor: this._fillBtn ? "121212" : "" }}> -            <FontAwesomeIcon icon="angle-double-right" size="lg" /> -        </button>; -    } -      render() { -        return !this.props.docView.layoutDoc ? (null) : <div className="collectionFreeFormMenu-cont"> -            {this.props.docView.props.renderDepth !== 0 ? (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}> -                <FontAwesomeIcon icon={"caret-left"} size={"lg"} /> -            </div> -            <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}> -                <FontAwesomeIcon icon={"caret-right"} size={"lg"} /> -            </div> - -            {!this.props.isOverlay || this.document.type !== DocumentType.WEB ? (null) : -                <button className={"antimodeMenu-button"} key="hypothesis" -                    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.drawButtons} -                    {this.widthPicker} -                    {this.colorPicker} -                    {this.fillPicker} -                    {this.formatPane} -                </> : -                (null) -            } -        </div>; +        return !this.props.docView.layoutDoc ? (null) : +            <div className="collectionFreeFormMenu-cont"> +                {this.props.docView.props.renderDepth !== 0 || this.isText ? (null) : +                    <Tooltip key="map" title={<div className="dash-tooltip">Toggle Mini Map</div>} placement="bottom"> +                        <div className="backKeyframe" onClick={this.miniMap}> +                            <FontAwesomeIcon icon={"map"} size={"lg"} /> +                        </div> +                    </Tooltip> +                } +                {!this.isText ? <Tooltip key="back" title={<div className="dash-tooltip">Back Frame</div>} placement="bottom"> +                    <div className="backKeyframe" onClick={this.prevKeyframe}> +                        <FontAwesomeIcon icon={"caret-left"} size={"lg"} /> +                    </div> +                </Tooltip> : null} +                {!this.isText ? <Tooltip key="num" title={<div className="dash-tooltip">Toggle View All</div>} placement="bottom"> +                    <div className="numKeyframe" style={{ backgroundColor: this.document.editing ? "#759c75" : "#c56565" }} +                        onClick={action(() => this.document.editing = !this.document.editing)} > +                        {NumCast(this.document.currentFrame)} +                    </div> +                </Tooltip> : null} +                {!this.isText ? <Tooltip key="fwd" title={<div className="dash-tooltip">Forward Frame</div>} placement="bottom"> +                    <div className="fwdKeyframe" onClick={this.nextKeyframe}> +                        <FontAwesomeIcon icon={"caret-right"} size={"lg"} /> +                    </div> +                </Tooltip> : null} + +                {!this.props.isOverlay || this.document.type !== DocumentType.WEB || this.isText ? (null) : +                    <Tooltip key="hypothesis" title={<div className="dash-tooltip">Use Hypothesis</div>} placement="bottom"> +                        <button className={"antimodeMenu-button"} key="hypothesis" +                            style={{ +                                backgroundColor: !this.props.docView.layoutDoc.isAnnotating ? "121212" : undefined, +                                borderRight: "1px solid gray" +                            }} +                            onClick={() => this.props.docView.layoutDoc.isAnnotating = !this.props.docView.layoutDoc.isAnnotating}> +                            <FontAwesomeIcon icon={["fab", "hire-a-helper"]} size={"lg"} /> +                        </button> +                    </Tooltip> +                } +                {(!this.props.isOverlay || this.props.docView.layoutDoc.isAnnotating) && !this.isText ? +                    <> +                        {this.drawButtons} +                        {this.widthPicker} +                        {this.colorPicker} +                        {this.fillPicker} +                    </> : +                    (null) +                } +                {this.isText ? <RichTextMenu key="rich" /> : null} +            </div>;      }  }  @observer 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/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 0332b4bf2..cca78cf9f 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -45,7 +45,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument)      @observable _scroll = 0; // used to force the document decoration to update when scrolling      @computed get columnHeaders() { return Cast(this.layoutDoc._columnHeaders, listSpec(SchemaHeaderField)); }      @computed get pivotField() { return StrCast(this.layoutDoc._pivotField); } -    @computed get filteredChildren() { return this.childLayoutPairs.filter(pair => pair.layout instanceof Doc).map(pair => pair.layout); } +    @computed get filteredChildren() { return this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.hidden).map(pair => pair.layout); }      @computed get xMargin() { return NumCast(this.layoutDoc._xMargin, 2 * Math.min(this.gridGap, .05 * this.props.PanelWidth())); }      @computed get yMargin() { return Math.max(this.layoutDoc._showTitle && !this.layoutDoc._showTitleHover ? 30 : 0, NumCast(this.layoutDoc._yMargin, 0)); } // 2 * this.gridGap)); }      @computed get gridGap() { return NumCast(this.layoutDoc._gridGap, 10); } @@ -227,6 +227,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument)              addDocTab={this.addDocTab}              bringToFront={returnFalse}              ContentScaling={returnOne} +            scriptContext={this.props.scriptContext}              pinToPres={this.props.pinToPres}          />;      } @@ -481,7 +482,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument)                      })}                      onDrop={this.onExternalDrop.bind(this)}                      onContextMenu={this.onContextMenu} -                    onWheel={e => this.props.active() && e.stopPropagation()} > +                    onWheel={e => this.props.active(true) && e.stopPropagation()} >                      {this.renderedSections}                      {!this.showAddAGroup ? (null) :                          <div key={`${this.props.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton" diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index 76af70cd1..4042a070d 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -298,7 +298,6 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC              oneLine: true,              HeadingObject: this.props.headingObject,              toggle: this.toggleVisibility, -            color: this._color          };          const newEditableViewProps = {              GetValue: () => "", @@ -306,7 +305,6 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC              contents: "+ NEW",              HeadingObject: this.props.headingObject,              toggle: this.toggleVisibility, -            color: this._color          };          const headingView = this.props.headingObject ?              <div key={heading} className="collectionStackingView-sectionHeader" ref={this._headerRef} @@ -332,11 +330,9 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC                              {this._paletteOn ? this.renderColorPicker() : (null)}                          </div>                      } -                    {evContents === `NO ${key.toUpperCase()} VALUE` ? -                        (null) : -                        <button className="collectionStackingView-sectionDelete" onClick={this.deleteColumn}> -                            <FontAwesomeIcon icon="trash" size="lg" /> -                        </button>} +                    {<button className="collectionStackingView-sectionDelete" onClick={this.deleteColumn}> +                        <FontAwesomeIcon icon="trash" size="lg" /> +                    </button>}                      {evContents === `NO  ${key.toUpperCase()} VALUE` ? (null) :                          <div className="collectionStackingView-sectionOptions">                              <Flyout anchorPoint={anchorPoints.TOP_RIGHT} content={this.renderMenu()}> @@ -355,7 +351,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC              {this.props.parent.Document._columnsHideIfEmpty ? (null) : headingView}              {                  this.collapsed ? (null) : -                    <div style={{ marginTop: 5 }}> +                    <div>                          <div key={`${heading}-stack`} className={`collectionStackingView-masonry${singleColumn ? "Single" : "Grid"}`}                              style={{                                  padding: singleColumn ? `${columnYMargin}px ${0}px ${style.yMargin}px ${0}px` : `${columnYMargin}px ${0}px`, diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 2957f004b..5906282f1 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -185,13 +185,10 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:          @action          protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean {              const docDragData = de.complete.docDragData; -            ScriptCast(this.props.Document.dropConverter)?.script.run({ dragData: docDragData });              if (docDragData) {                  let added = false; -                const dropaction = docDragData.dropAction || docDragData.userDropAction; -                if (dropaction && dropaction !== "move") { -                    added = this.addDocument(docDragData.droppedDocuments); -                } else if (docDragData.moveDocument) { +                const dropAction = docDragData.dropAction || docDragData.userDropAction; +                if ((!dropAction || dropAction === "move") && docDragData.moveDocument) {                      const movedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] === d);                      const addedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] !== d);                      const res = addedDocs.length ? this.addDocument(addedDocs) : true; @@ -201,6 +198,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:                          added = docDragData.moveDocument(movedDocs, this.props.Document, canAdd ? this.addDocument : returnFalse);                      } else added = res;                  } else { +                    ScriptCast(this.props.Document.dropConverter)?.script.run({ dragData: docDragData });                      added = this.addDocument(docDragData.droppedDocuments);                  }                  added && e.stopPropagation(); @@ -296,7 +294,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:                              const reg = new RegExp(Utils.prepend(""), "g");                              const modHtml = srcUrl ? html.replace(reg, srcUrl) : html;                              const htmlDoc = Docs.Create.HtmlDocument(modHtml, { ...options, title: "-web page-", _width: 300, _height: 300 }); -                            Doc.GetProto(htmlDoc)["data-text"] = Doc.GetProto(htmlDoc)["text"] = text; +                            Doc.GetProto(htmlDoc)["data-text"] = Doc.GetProto(htmlDoc).text = text;                              this.props.addDocument(htmlDoc);                              if (srcWeb) {                                  const focusNode = (SelectionManager.SelectedDocuments()[0].ContentDiv?.getElementsByTagName("iframe")[0].contentDocument?.getSelection()?.focusNode as any); diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 705871a6f..3c7471d7c 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); } @@ -101,7 +104,7 @@ class TreeView extends React.Component<TreeViewProps> {          const layout = Doc.LayoutField(this.doc) instanceof Doc ? Doc.LayoutField(this.doc) as Doc : undefined;          return ((this.props.dataDoc ? DocListCast(this.props.dataDoc[field]) : undefined) || // if there's a data doc for an expanded template, use it's data field              (layout ? DocListCast(layout[field]) : undefined) || // else if there's a layout doc, display it's fields -            DocListCast(this.doc[field])) as Doc[]; // otherwise use the document's data field +            DocListCast(this.doc[field])); // otherwise use the document's data field      }      @computed get childDocs() { return this.childDocList(this.fieldKey); }      @computed get childLinks() { return this.childDocList("links"); } @@ -328,7 +331,7 @@ class TreeView extends React.Component<TreeViewProps> {                          [...this.props.renderedIds, this.doc[Id]], this.props.libraryPath, this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields)}              </ul >;          } else if (this.treeViewExpandedView === "fields") { -            return <ul><div ref={this._dref} style={{ display: "inline-block" }} key={this.doc[Id] + this.doc.title}> +            return <ul key={this.doc[Id] + this.doc.title}><div ref={this._dref} style={{ display: "inline-block" }} >                  {this.expandedField}              </div></ul>;          } else { @@ -782,7 +785,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll          onClicks.push({              description: "Edit onChecked Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.doc, undefined, "onCheckedClick"), "edit onCheckedClick"), icon: "edit"          }); -        !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", noexpand: true, subitems: onClicks, icon: "hand-point-right" }); +        !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", noexpand: true, subitems: onClicks, icon: "mouse-pointer" });      }      outerXf = () => Utils.GetScreenTransform(this._mainEle!);      onTreeDrop = (e: React.DragEvent) => this.onExternalDrop(e, {}); 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..89034a0c0 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -17,7 +17,7 @@ import { listSpec } from '../../../fields/Schema';  import { ComputedField, ScriptField } from '../../../fields/ScriptField';  import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';  import { ImageField } from '../../../fields/URLField'; -import { TraceMobx, GetEffectiveAcl, getPlaygroundMode, distributeAcls, SharingPermissions } from '../../../fields/util'; +import { TraceMobx, GetEffectiveAcl, SharingPermissions } from '../../../fields/util';  import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils';  import { Docs, DocUtils } from '../../documents/Documents';  import { DocumentType } from '../../documents/DocumentTypes'; @@ -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 { @@ -142,20 +143,20 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus          const effectiveAcl = GetEffectiveAcl(this.props.Document);          if (added.length) { -            if (effectiveAcl === AclPrivate || (effectiveAcl === AclReadonly && !getPlaygroundMode())) { +            if (effectiveAcl === AclPrivate || effectiveAcl === AclReadonly) {                  return false;              }              else { -                if (this.props.Document[AclSym]) { -                    // change so it only adds if more restrictive -                    added.forEach(d => { -                        // const dataDoc = d[DataSym]; -                        for (const [key, value] of Object.entries(this.props.Document[AclSym])) { -                            distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true); -                        } -                        // dataDoc[AclSym] = d[AclSym] = this.props.Document[AclSym]; -                    }); -                } +                // if (this.props.Document[AclSym]) { +                //     // change so it only adds if more restrictive +                //     added.forEach(d => { +                //         // const dataDoc = d[DataSym]; +                //         for (const [key, value] of Object.entries(this.props.Document[AclSym])) { +                //             // key.substring(4).replace("_", ".") !== Doc.CurrentUserEmail && distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true); +                //             distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true); +                //         } +                //     }); +                // }                  if (effectiveAcl === AclAddonly) {                      added.map(doc => Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc)); @@ -179,7 +180,8 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus                          doc.context = this.props.Document;                      });                      added.map(add => Doc.AddDocToList(Cast(Doc.UserDoc().myCatalog, Doc, null), "data", add)); -                    targetDataDoc[this.props.fieldKey] = new List<Doc>([...docList, ...added]); +                    // targetDataDoc[this.props.fieldKey] = new List<Doc>([...docList, ...added]); +                    (targetDataDoc[this.props.fieldKey] as List<Doc>).push(...added);                      targetDataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now()));                  }              } @@ -189,14 +191,16 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus      @action.bound      removeDocument = (doc: any): boolean => { -        const effectiveAcl = GetEffectiveAcl(this.props.Document); -        if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin || getPlaygroundMode()) { +        const collectionEffectiveAcl = GetEffectiveAcl(this.props.Document); +        const docEffectiveAcl = GetEffectiveAcl(doc); +        // you can remove the document if you either have Admin/Edit access to the collection or to the specific document +        if (collectionEffectiveAcl === AclEdit || collectionEffectiveAcl === AclAdmin || docEffectiveAcl === AclAdmin || docEffectiveAcl === AclEdit) {              const docs = doc instanceof Doc ? [doc] : doc as Doc[];              const targetDataDoc = this.props.Document[DataSym];              const value = DocListCast(targetDataDoc[this.props.fieldKey]); -            const result = value.filter(v => !docs.includes(v)); -            if (result.length !== value.length) { -                targetDataDoc[this.props.fieldKey] = new List<Doc>(result); +            const toRemove = value.filter(v => docs.includes(v)); +            if (toRemove.length !== 0) { +                toRemove.forEach(doc => Doc.RemoveDocFromList(targetDataDoc, this.props.fieldKey, doc));                  return true;              }          } @@ -306,7 +310,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" });              } @@ -335,7 +339,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus                      icon: "edit",                      event: () => Doc.GetProto(this.props.Document)[StrCast(childClick.targetScriptKey)] = ObjectField.MakeCopy(ScriptCast(childClick.data)),                  })); -            !existingOnClick && cm.addItem({ description: "OnClick...", noexpand: true, subitems: onClicks, icon: "hand-point-right" }); +            !existingOnClick && cm.addItem({ description: "OnClick...", noexpand: true, subitems: onClicks, icon: "mouse-pointer" });              if (!Doc.UserDoc().noviceMode) {                  const more = cm.findByDescription("More..."); @@ -365,7 +369,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 +491,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 +561,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 +591,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) : +            {(Doc.UserDoc()?.noviceMode || !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} +            {Doc.UserDoc()?.noviceMode || this.facetWidth() < 10 ? (null) : this.filterView}          </div>);      }  } diff --git a/src/client/views/collections/ParentDocumentSelector.scss b/src/client/views/collections/ParentDocumentSelector.scss index 4e704b58f..bc9cf4848 100644 --- a/src/client/views/collections/ParentDocumentSelector.scss +++ b/src/client/views/collections/ParentDocumentSelector.scss @@ -2,11 +2,13 @@      div {          overflow: visible !important;      } +      .metadataEntry-outerDiv {          overflow: hidden !important;          pointer-events: all;      }  } +  .parentDocumentSelector-flyout {      position: relative;      z-index: 9999; @@ -31,26 +33,31 @@          border-left: 0px;      }  } +  .parentDocumentSelector-button { -    pointer-events: all;  +    pointer-events: all;      position: relative;      display: inline-block; +      svg { -        width:20px !important; -        height:20px; +        // width:20px !important; +        //height:20px;      }  } +  .parentDocumentSelector-metadata {      pointer-events: auto;      padding-right: 5px;      width: 25px;      display: inline-block;  } +  .buttonSelector {      div {          overflow: visible !important;      } -    display: inline-block;  + +    display: inline-block;      width:100%;      height:100%;  }
\ No newline at end of file diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx index 8c0b8de9d..4c8cac3ed 100644 --- a/src/client/views/collections/ParentDocumentSelector.tsx +++ b/src/client/views/collections/ParentDocumentSelector.tsx @@ -42,14 +42,14 @@ export class SelectorContextMenu extends React.Component<SelectorProps> {      async fetchDocuments() {          const aliases = (await SearchUtil.GetAliasesOfDocument(this.props.Document));          const containerProtoSets = await Promise.all(aliases.map(async alias => -            await Promise.all((await SearchUtil.Search("", true, { fq: `data_l:"${alias[Id]}"` })).docs))); +            ((await SearchUtil.Search("", true, { fq: `data_l:"${alias[Id]}"` })).docs)));          const containerProtos = containerProtoSets.reduce((p, set) => { set.map(s => p.add(s)); return p; }, new Set<Doc>());          const containerSets = await Promise.all(Array.from(containerProtos.keys()).map(async container => { -            return (await SearchUtil.GetAliasesOfDocument(container)); +            return (SearchUtil.GetAliasesOfDocument(container));          }));          const containers = containerSets.reduce((p, set) => { set.map(s => p.add(s)); return p; }, new Set<Doc>());          const doclayoutSets = await Promise.all(Array.from(containers.keys()).map(async (dp) => { -            return (await SearchUtil.GetAliasesOfDocument(dp)); +            return (SearchUtil.GetAliasesOfDocument(dp));          }));          const doclayouts = Array.from(doclayoutSets.reduce((p, set) => { set.map(s => p.add(s)); return p; }, new Set<Doc>()).keys());          runInAction(() => { @@ -129,7 +129,7 @@ export class DockingViewButtonSelector extends React.Component<{ views: () => Do                  this.props.views()[0]?.select(false);              }} className="buttonSelector">              <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={this.flyout} stylesheet={this.customStylesheet}> -                <FontAwesomeIcon icon={"cog"} size={"sm"} /> +                <FontAwesomeIcon icon={"arrows-alt"} size={"sm"} />              </Flyout>          </span>;      } diff --git a/src/client/views/collections/SchemaTable.tsx b/src/client/views/collections/SchemaTable.tsx index cde795098..7e2840c2c 100644 --- a/src/client/views/collections/SchemaTable.tsx +++ b/src/client/views/collections/SchemaTable.tsx @@ -148,7 +148,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> {      }      @action -    changeTitleMode = () => this._showTitleDropdown = !this._showTitleDropdown; +    changeTitleMode = () => this._showTitleDropdown = !this._showTitleDropdown      @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); }      @computed get tableColumns(): Column<Doc>[] { @@ -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..badbc48a1 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1144,7 +1144,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P      @action      componentDidMount() {          super.componentDidMount?.(); -        this._layoutComputeReaction = reaction(() => { TraceMobx(); return this.doLayoutComputation }, +        this._layoutComputeReaction = reaction(() => this.doLayoutComputation,              (elements) => this._layoutElements = elements || [],              { fireImmediately: true, name: "doLayout" }); @@ -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..."); @@ -1290,7 +1292,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P                          setTimeout(() => {                              SearchUtil.Search(`{!join from=id to=proto_i}id:link*`, true, {}).then(docs => {                                  docs.docs.forEach(d => LinkManager.Instance.addLink(d)); -                            }) +                            });                          }, 2000); // need to give solr some time to update so that this query will find any link docs we've added.                      }                  } diff --git a/src/client/views/collections/collectionFreeForm/FormatShapePane.scss b/src/client/views/collections/collectionFreeForm/FormatShapePane.scss index 010beb836..d49ab27fb 100644 --- a/src/client/views/collections/collectionFreeForm/FormatShapePane.scss +++ b/src/client/views/collections/collectionFreeForm/FormatShapePane.scss @@ -27,13 +27,15 @@      position: absolute;  } -.sketch-picker { -    background: #323232; -    width: 160px !important; -    height: 80% !important; - -    .flexbox-fit { +.btn-group-palette { +    .sketch-picker {          background: #323232; +        width: 160px !important; +        height: 80% !important; + +        .flexbox-fit { +            background: #323232; +        }      }  } diff --git a/src/client/views/collections/collectionFreeForm/FormatShapePane.tsx b/src/client/views/collections/collectionFreeForm/FormatShapePane.tsx index ddc282e57..6263be261 100644 --- a/src/client/views/collections/collectionFreeForm/FormatShapePane.tsx +++ b/src/client/views/collections/collectionFreeForm/FormatShapePane.tsx @@ -13,7 +13,6 @@ import AntimodeMenu from "../../AntimodeMenu";  import "./FormatShapePane.scss";  import { undoBatch } from "../../../util/UndoManager";  import { ColorState, SketchPicker } from 'react-color'; -import { DocumentView } from "../../../views/nodes/DocumentView"  @observer  export default class FormatShapePane extends AntimodeMenu { @@ -124,12 +123,12 @@ export default class FormatShapePane extends AntimodeMenu {                      console.log(ink);                      if (ink) {                          const newPoints: { X: number, Y: number }[] = []; -                        for (var j = 0; j < ink.length; j++) { +                        ink.forEach(i => {                              // (new x — oldx) + (oldxpoint * newWidt)/oldWidth  -                            const newX = (doc.x - oldX) + (ink[j].X * doc._width) / oldWidth; -                            const newY = (doc.y - oldY) + (ink[j].Y * doc._height) / oldHeight; +                            const newX = ((doc.x || 0) - oldX) + (i.X * (doc._width || 0)) / oldWidth; +                            const newY = ((doc.y || 0) - oldY) + (i.Y * (doc._height || 0)) / oldHeight;                              newPoints.push({ X: newX, Y: newY }); -                        } +                        });                          doc.data = new InkField(newPoints);                      }                  } @@ -148,12 +147,12 @@ export default class FormatShapePane extends AntimodeMenu {                      console.log(ink);                      if (ink) {                          const newPoints: { X: number, Y: number }[] = []; -                        for (var j = 0; j < ink.length; j++) { +                        ink.forEach(i => {                              // (new x — oldx) + (oldxpoint * newWidt)/oldWidth  -                            const newX = (doc.x - oldX) + (ink[j].X * doc._width) / oldWidth; -                            const newY = (doc.y - oldY) + (ink[j].Y * doc._height) / oldHeight; +                            const newX = ((doc.x || 0) - oldX) + (i.X * (doc._width || 0)) / oldWidth; +                            const newY = ((doc.y || 0) - oldY) + (i.Y * (doc._height || 0)) / oldHeight;                              newPoints.push({ X: newX, Y: newY }); -                        } +                        });                          doc.data = new InkField(newPoints);                      }                  } @@ -191,11 +190,11 @@ export default class FormatShapePane extends AntimodeMenu {                  if (ink) {                      const newPoints: { X: number, Y: number }[] = []; -                    for (var i = 0; i < ink.length; i++) { -                        const newX = Math.cos(angle) * (ink[i].X - _centerPoints[index].X) - Math.sin(angle) * (ink[i].Y - _centerPoints[index].Y) + _centerPoints[index].X; -                        const newY = Math.sin(angle) * (ink[i].X - _centerPoints[index].X) + Math.cos(angle) * (ink[i].Y - _centerPoints[index].Y) + _centerPoints[index].Y; +                    ink.forEach(i => { +                        const newX = Math.cos(angle) * (i.X - _centerPoints[index].X) - Math.sin(angle) * (i.Y - _centerPoints[index].Y) + _centerPoints[index].X; +                        const newY = Math.sin(angle) * (i.X - _centerPoints[index].X) + Math.cos(angle) * (i.Y - _centerPoints[index].Y) + _centerPoints[index].Y;                          newPoints.push({ X: newX, Y: newY }); -                    } +                    });                      doc.data = new InkField(newPoints);                      const xs = newPoints.map(p => p.X);                      const ys = newPoints.map(p => p.Y); @@ -395,12 +394,12 @@ export default class FormatShapePane extends AntimodeMenu {      @computed get widInput() { return this.inputBox("wid", this.shapeWid, (val: string) => this.shapeWid = val); }      @computed get rotInput() { return this.inputBoxDuo("rot", this.shapeRot, (val: string) => { this.rotate(Number(val) - Number(this.shapeRot)); this.shapeRot = val; return true; }, "∠:", "rot", this.shapeRot, (val: string) => this.shapeRot = val, ""); } -    @computed get XpsInput() { return this.inputBoxDuo("Xps", this.shapeXps, (val: string) => this.shapeXps = val, "X:", "Yps", this.shapeYps, (val: string) => this.shapeYps = val, "Y:"); }      @computed get YpsInput() { return this.inputBox("Yps", this.shapeYps, (val: string) => this.shapeYps = val); }      @computed get controlPoints() { return this.controlPointsButton(); }      @computed get lockRatio() { return this.lockRatioButton(); }      @computed get rotate90() { return this.rotate90Button(); } +    @computed get XpsInput() { return this.inputBoxDuo("Xps", this.shapeXps, (val: string) => this.shapeXps = val, "X:", "Yps", this.shapeYps, (val: string) => this.shapeYps = val, "Y:"); }      @computed get propertyGroupItems() { diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index c3d81bda4..4334a339a 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,7 +1,7 @@  import { action, computed, observable } from "mobx";  import { observer } from "mobx-react";  import { Doc, Opt, DocListCast, DataSym, AclEdit, AclAddonly, AclAdmin } from "../../../../fields/Doc"; -import { GetEffectiveAcl, getPlaygroundMode } from "../../../../fields/util"; +import { GetEffectiveAcl } from "../../../../fields/util";  import { InkData, InkField, InkTool } from "../../../../fields/InkField";  import { List } from "../../../../fields/List";  import { RichTextField } from "../../../../fields/RichTextField"; @@ -281,7 +281,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque              this._downX = x;              this._downY = y;              const effectiveAcl = GetEffectiveAcl(this.props.Document); -            if ([AclAdmin, AclEdit, AclAddonly].includes(effectiveAcl) || getPlaygroundMode()) PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge); +            if ([AclAdmin, AclEdit, AclAddonly].includes(effectiveAcl)) PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge);              this.clearSelection();          }      }); diff --git a/src/client/views/collections/collectionFreeForm/PropertiesView.scss b/src/client/views/collections/collectionFreeForm/PropertiesView.scss new file mode 100644 index 000000000..7df56115f --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/PropertiesView.scss @@ -0,0 +1,644 @@ +.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; +        font-size: 18px; +        font-weight: bold; +        justify-content: center; + +        .propertiesView-title-icon { +            width: 20px; +            height: 20px; +            padding-left: 38px; +            margin-top: -5px; +            right: 19; +            position: absolute; + +            &: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; +            color: white; +            padding-left: 8px; +            background-color: rgb(51, 51, 51); + +            &:hover { +                cursor: pointer; +            } + +            .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; +            color: white; +            padding-left: 8px; +            background-color: rgb(51, 51, 51); + +            &:hover { +                cursor: pointer; +            } + +            .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; +        } +    } + +    .propertiesView-appearance { +        border-bottom: 1px solid black; +        //padding: 8.5px; + +        .propertiesView-appearance-title { +            font-weight: bold; +            font-size: 12.5px; +            padding: 4px; +            display: flex; +            color: white; +            padding-left: 8px; +            background-color: rgb(51, 51, 51); + +            &:hover { +                cursor: pointer; +            } + +            .propertiesView-appearance-title-icon { +                float: right; +                right: 0; +                position: absolute; +                margin-left: 2px; +                margin-right: 9px; + +                &:hover { +                    cursor: pointer; +                } +            } +        } + +        .propertiesView-appearance-content { +            font-size: 10px; +            padding: 10px; +            margin-left: 5px; +        } +    } + +    .propertiesView-transform { +        border-bottom: 1px solid black; +        //padding: 8.5px; + +        .propertiesView-transform-title { +            font-weight: bold; +            font-size: 12.5px; +            padding: 4px; +            display: flex; +            color: white; +            padding-left: 8px; +            background-color: rgb(51, 51, 51); + +            &:hover { +                cursor: pointer; +            } + +            .propertiesView-transform-title-icon { +                float: right; +                right: 0; +                position: absolute; +                margin-left: 2px; +                margin-right: 9px; + +                &:hover { +                    cursor: pointer; +                } +            } +        } + +        .propertiesView-transform-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-left: 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; */ +        margin-right: 10px; +        background-color: #ececec; +        max-height: 130px; +        overflow-y: scroll; + +        .propertiesView-sharingTable-item { + +            display: flex; +            padding: 3px; +            align-items: center; +            border-bottom: 0.5px solid grey; + +            &:hover .propertiesView-sharingTable-item-name { +                overflow-x: unset; +                white-space: unset; +                overflow-wrap: break-word; +            } + +            .propertiesView-sharingTable-item-name { +                font-weight: bold; +                width: 95px; +                overflow-x: hidden; +                display: inline-block; +                text-overflow: ellipsis; +                white-space: nowrap; +            } + +            .propertiesView-sharingTable-item-permission { +                display: flex; +                right: 34; +                float: right; +                position: absolute; + +                .permissions-select { +                    z-index: 1; +                    border: none; +                    background-color: inherit; +                    width: 75px; +                    //text-align: justify; // for Edge +                    //text-align-last: end; + +                    &: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; +            color: white; +            padding-left: 8px; +            background-color: rgb(51, 51, 51); + +            &:hover { +                cursor: pointer; +            } + +            .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-checkbox { +            float: right; +            height: 20px; +            margin-top: -9px; + +            .propertiesView-fields-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; +            color: white; +            padding-left: 8px; +            background-color: rgb(51, 51, 51); + +            &:hover { +                cursor: pointer; +            } + +            .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; +        } + +    } +} + +.inking-button { + +    display: flex; + +    .inking-button-points { +        background-color: #333333; +        padding: 7px; +        border-radius: 7px; +        margin-right: 32px; +        width: 32; +        height: 32; +        padding-top: 9px; +        margin-left: 18px; + +        &:hover { +            background: rgb(131, 131, 131); +            transform: scale(1.05); +            cursor: pointer; +        } +    } + +    .inking-button-lock { +        background-color: #333333; +        padding: 7px; +        border-radius: 7px; +        margin-right: 32px; +        width: 32; +        height: 32; +        padding-top: 9px; +        padding-left: 10px; + +        &:hover { +            background: rgb(131, 131, 131); +            transform: scale(1.05); +            cursor: pointer; +        } +    } + +    .inking-button-rotate { +        background-color: #333333; +        padding: 7px; +        border-radius: 7px; +        width: 32; +        height: 32; +        padding-top: 9px; +        padding-left: 10px; + +        &:hover { +            background: rgb(131, 131, 131); +            transform: scale(1.05); +            cursor: pointer; +        } +    } +} + +.inputBox-duo { +    display: flex; +} + +.inputBox { + +    margin-top: 10px; +    display: flex; +    height: 19px; +    margin-right: 15px; + +    .inputBox-title { +        font-size: 12px; +        padding-right: 5px; +    } + +    .inputBox-input { +        font-size: 10px; +        width: 50px; +        margin-right: 1px; +        border-radius: 3px; + +        &:hover { +            cursor: pointer; +        } +    } + +    .inputBox-button { + +        .inputBox-button-up { +            background-color: #333333; +            height: 9px; +            padding-left: 3px; +            padding-right: 3px; +            padding-top: 1px; +            padding-bottom: 1px; +            border-radius: 1.5px; + +            &:hover { +                background: rgb(131, 131, 131); +                transform: scale(1.05); +                cursor: pointer; +            } +        } + +        .inputBox-button-down { +            background-color: #333333; +            height: 9px; +            padding-left: 3px; +            padding-right: 3px; +            padding-top: 1px; +            padding-bottom: 1px; +            border-radius: 1.5px; + +            &:hover { +                background: rgb(131, 131, 131); +                transform: scale(1.05); +                cursor: pointer; +            } +        } + +    } +} + +.color-palette { +    width: 160px; +    height: 360; +} + +.strokeAndFill { +    display: flex; +    margin-top: 10px; + +    .fill { +        margin-right: 40px; +        display: flex; +        padding-bottom: 7px; +        margin-left: 35px; + +        .fill-title { +            font-size: 12px; +            margin-right: 2px; +        } + +        .fill-button { +            padding-top: 2px; +            margin-top: -1px; +        } +    } + +    .stroke { +        display: flex; + +        .stroke-title { +            font-size: 12px; +        } + +        .stroke-button { +            padding-top: 2px; +            margin-left: 2px; +            margin-top: -1px; +        } +    } +} + +.widthAndDash { + +    .width { +        .width-top { +            display: flex; + +            .width-title { +                font-size: 12; +                margin-right: 20px; +                margin-left: 35px; +                text-align: center; +            } + +            .width-input { +                margin-right: 30px; +                margin-top: -10px; +            } +        } + +        .width-range { +            margin-right: 1px; +            margin-bottom: 6; +        } +    } + +    .arrows { + +        display: flex; +        margin-bottom: 3px; + +        .arrows-head { + +            display: flex; +            margin-right: 35px; + +            .arrows-head-title { +                font-size: 10; +            } + +            .arrows-head-input { +                margin-left: 6px; +                margin-top: 2px; +            } +        } + +        .arrows-tail { +            display: flex; + +            .arrows-tail-title { +                font-size: 10; +            } + +            .arrows-tail-input { +                margin-left: 6px; +                margin-top: 2px; +            } +        } +    } + +    .dashed { + +        display: flex; +        margin-left: 74px; +        margin-bottom: 6px; + +        .dashed-title { +            font-size: 10; +        } + +        .dashed-input { +            margin-left: 6px; +            margin-top: 2px; +        } +    } +}
\ 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..f5e0cd077 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/PropertiesView.tsx @@ -0,0 +1,850 @@ +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, AclSym, AclPrivate, AclReadonly, AclAddonly, AclEdit, AclAdmin, Opt } 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, Divider } from "@material-ui/core"; +import SharingManager from "../../../util/SharingManager"; +import { DocumentType } from "../../../documents/DocumentTypes"; +import FormatShapePane from "./FormatShapePane"; +import { SharingPermissions, GetEffectiveAcl } from "../../../../fields/util"; +import { InkField } from "../../../../fields/InkField"; +import { undoBatch } from "../../../util/UndoManager"; +import { ColorState, SketchPicker } from "react-color"; +import AntimodeMenu from "../../AntimodeMenu"; +import "./FormatShapePane.scss"; +import { discovery_v1 } from "googleapis"; + + +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; +    @observable openAppearance: boolean = true; +    @observable openTransform: boolean = true; + +    @computed get isInk() { return this.selectedDoc?.type === DocumentType.INK; } + +    @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 (key[0] === "#") { +                    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.substring(0, 3) !== "ACL") +                    || key[0] === "#" || key === "author" || +                    key === "creationDate" || key.indexOf("lastModified") !== -1) { + +                    const contents = doc[key]; +                    if (key[0] === "#") { +                        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; +        } +    } + +    @undoBatch +    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 + `:'${value}'`; +                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} +                    dontRegisterView={true} +                /> +            </div>; +        } else { +            return null; +        } +    } + +    @undoBatch +    changePermissions = (e: any, user: string) => { +        SharingManager.Instance.shareFromPropertiesSidebar(user, e.currentTarget.value as SharingPermissions, this.selectedDoc!); +    } + +    getPermissionsSelect(user: string) { +        return <select className="permissions-select" +            onChange={e => this.changePermissions(e, user)}> +            {Object.values(SharingPermissions).map(permission => { +                return ( +                    <option key={permission} value={permission} selected={this.selectedDoc![`ACL-${user.replace(".", "_")}`] === permission}> +                        {permission} +                    </option>); +            })} +        </select>; +    } + +    @computed get notifyIcon() { +        return <Tooltip title={<><div className="dash-tooltip">Notify with message</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, effectiveAcl: symbol, permission?: string) { +        return <div className="propertiesView-sharingTable-item"> +            <div className="propertiesView-sharingTable-item-name" style={{ width: name !== "Me" ? "85px" : "80px" }}> {name} </div> +            {/* {name !== "Me" ? this.notifyIcon : null} */} +            <div className="propertiesView-sharingTable-item-permission"> +                {effectiveAcl === AclAdmin && permission !== "Owner" ? this.getPermissionsSelect(name) : permission} +                {permission === "Owner" ? this.expansionIcon : null} +            </div> +        </div>; +    } + +    @computed get sharingTable() { +        const AclMap = new Map<symbol, string>([ +            [AclPrivate, SharingPermissions.None], +            [AclReadonly, SharingPermissions.View], +            [AclAddonly, SharingPermissions.Add], +            [AclEdit, SharingPermissions.Edit], +            [AclAdmin, SharingPermissions.Admin] +        ]); + +        const effectiveAcl = GetEffectiveAcl(this.selectedDoc!); +        const tableEntries = []; + +        if (this.selectedDoc![AclSym]) { +            for (const [key, value] of Object.entries(this.selectedDoc![AclSym])) { +                const name = key.substring(4).replace("_", "."); +                if (name !== Doc.CurrentUserEmail && name !== this.selectedDoc!.author) tableEntries.push(this.sharingItem(name, effectiveAcl, AclMap.get(value)!)); +            } +        } + +        tableEntries.unshift(this.sharingItem("Me", effectiveAcl, Doc.CurrentUserEmail === this.selectedDoc!.author ? "Owner" : StrCast(this.selectedDoc![`ACL-${Doc.CurrentUserEmail.replace(".", "_")}`]))); +        if (Doc.CurrentUserEmail !== this.selectedDoc!.author) tableEntries.unshift(this.sharingItem(StrCast(this.selectedDoc!.author), effectiveAcl, "Owner")); + +        return <div className="propertiesView-sharingTable"> +            {tableEntries} +        </div>; +    } + +    @computed get fieldsCheckbox() { +        return <Checkbox +            color="primary" +            onChange={this.toggleCheckbox} +            checked={this.layoutFields} +        />; +    } + +    @undoBatch +    @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} />; +    } + +    @undoBatch +    @action +    setTitle = (value: string) => { +        if (this.dataDoc) { +            this.selectedDoc && (this.selectedDoc.title = value); +            KeyValueBox.SetField(this.dataDoc, "title", value, true); +            return true; +        } +        return false; +    } + + +    @undoBatch +    @action +    rotate = (angle: number) => { +        const _centerPoints: { X: number, Y: number }[] = []; +        if (this.selectedDoc) { +            const doc = this.selectedDoc; +            if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) { +                const ink = Cast(doc.data, InkField)?.inkData; +                if (ink) { +                    const xs = ink.map(p => p.X); +                    const ys = ink.map(p => p.Y); +                    const left = Math.min(...xs); +                    const top = Math.min(...ys); +                    const right = Math.max(...xs); +                    const bottom = Math.max(...ys); +                    _centerPoints.push({ X: left, Y: top }); +                } +            } + +            var index = 0; +            if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) { +                doc.rotation = Number(doc.rotation) + Number(angle); +                const ink = Cast(doc.data, InkField)?.inkData; +                if (ink) { + +                    const newPoints: { X: number, Y: number }[] = []; +                    for (var i = 0; i < ink.length; i++) { +                        const newX = Math.cos(angle) * (ink[i].X - _centerPoints[index].X) - Math.sin(angle) * (ink[i].Y - _centerPoints[index].Y) + _centerPoints[index].X; +                        const newY = Math.sin(angle) * (ink[i].X - _centerPoints[index].X) + Math.cos(angle) * (ink[i].Y - _centerPoints[index].Y) + _centerPoints[index].Y; +                        newPoints.push({ X: newX, Y: newY }); +                    } +                    doc.data = new InkField(newPoints); +                    const xs = newPoints.map(p => p.X); +                    const ys = newPoints.map(p => p.Y); +                    const left = Math.min(...xs); +                    const top = Math.min(...ys); +                    const right = Math.max(...xs); +                    const bottom = Math.max(...ys); + +                    doc._height = (bottom - top); +                    doc._width = (right - left); +                } +                index++; +            } +        } +    } + +    @observable _controlBtn: boolean = false; +    @observable _lock: boolean = false; + +    @computed +    get controlPointsButton() { +        return <div className="inking-button"> +            <Tooltip title={<><div className="dash-tooltip">{"Edit points"}</div></>}> +                <div className="inking-button-points" onPointerDown={action(() => this._controlBtn = !this._controlBtn)} style={{ backgroundColor: this._controlBtn ? "black" : "" }}> +                    <FontAwesomeIcon icon="bezier-curve" color="white" size="lg" /> +                </div> +            </Tooltip> +            <Tooltip title={<><div className="dash-tooltip">{this._lock ? "Unlock points" : "Lock points"}</div></>}> +                <div className="inking-button-lock" onPointerDown={action(() => this._lock = !this._lock)} > +                    <FontAwesomeIcon icon={this._lock ? "unlock" : "lock"} color="white" size="lg" /> +                </div> +            </Tooltip> +            <Tooltip title={<><div className="dash-tooltip">{"Rotate 90˚"}</div></>}> +                <div className="inking-button-rotate" onPointerDown={action(() => this.rotate(Math.PI / 2))}> +                    <FontAwesomeIcon icon="undo" color="white" size="lg" /> +                </div> +            </Tooltip> +        </div>; +    } + +    inputBox = (key: string, value: any, setter: (val: string) => {}, title: string) => { +        return <div className="inputBox" +            style={{ +                marginRight: title === "X:" ? "19px" : "", +                marginLeft: title === "∠:" ? "39px" : "" +            }}> +            <div className="inputBox-title"> {title} </div> +            <input className="inputBox-input" +                type="text" value={value} +                onChange={e => setter(e.target.value)} /> +            <div className="inputBox-button"> +                <div className="inputBox-button-up" key="up2" +                    onPointerDown={undoBatch(action(() => this.upDownButtons("up", key)))} > +                    <FontAwesomeIcon icon="caret-up" color="white" size="sm" /> +                </div> +                <div className="inputbox-Button-down" key="down2" +                    onPointerDown={undoBatch(action(() => this.upDownButtons("down", key)))} > +                    <FontAwesomeIcon icon="caret-down" color="white" size="sm" /> +                </div> +            </div> +        </div>; +    } + +    inputBoxDuo = (key: string, value: any, setter: (val: string) => {}, title1: string, key2: string, value2: any, setter2: (val: string) => {}, title2: string) => { +        return <div className="inputBox-duo"> +            {this.inputBox(key, value, setter, title1)} +            {title2 === "" ? (null) : this.inputBox(key2, value2, setter2, title2)} +        </div>; +    } + +    @action +    upDownButtons = (dirs: string, field: string) => { +        switch (field) { +            case "rot": this.rotate((dirs === "up" ? .1 : -.1)); break; +            // case "rot": this.selectedInk?.forEach(i => i.rootDoc.rotation = NumCast(i.rootDoc.rotation) + (dirs === "up" ? 0.1 : -0.1)); break; +            case "Xps": this.selectedDoc && (this.selectedDoc.x = NumCast(this.selectedDoc?.x) + (dirs === "up" ? 10 : -10)); break; +            case "Yps": this.selectedDoc && (this.selectedDoc.y = NumCast(this.selectedDoc?.y) + (dirs === "up" ? 10 : -10)); break; +            case "stk": this.selectedDoc && (this.selectedDoc.strokeWidth = NumCast(this.selectedDoc?.strokeWidth) + (dirs === "up" ? .1 : -.1)); break; +            case "wid": +                const oldWidth = NumCast(this.selectedDoc?._width); +                const oldHeight = NumCast(this.selectedDoc?._height); +                const oldX = NumCast(this.selectedDoc?.x); +                const oldY = NumCast(this.selectedDoc?.y); +                this.selectedDoc && (this.selectedDoc._width = oldWidth + (dirs === "up" ? 10 : - 10)); +                this._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) / oldWidth * NumCast(this.selectedDoc?._height))); +                const doc = this.selectedDoc; +                if (doc?.type === DocumentType.INK && doc.x && doc.y && doc._height && doc._width) { +                    console.log(doc.x, doc.y, doc._height, doc._width); +                    const ink = Cast(doc.data, InkField)?.inkData; +                    console.log(ink); +                    if (ink) { +                        const newPoints: { X: number, Y: number }[] = []; +                        for (var j = 0; j < ink.length; j++) { +                            // (new x — oldx) + (oldxpoint * newWidt)/oldWidth  +                            const newX = (NumCast(doc.x) - oldX) + (ink[j].X * NumCast(doc._width)) / oldWidth; +                            const newY = (NumCast(doc.y) - oldY) + (ink[j].Y * NumCast(doc._height)) / oldHeight; +                            newPoints.push({ X: newX, Y: newY }); +                        } +                        doc.data = new InkField(newPoints); +                    } +                } +                break; +            case "hgt": +                const oWidth = NumCast(this.selectedDoc?._width); +                const oHeight = NumCast(this.selectedDoc?._height); +                const oX = NumCast(this.selectedDoc?.x); +                const oY = NumCast(this.selectedDoc?.y); +                this.selectedDoc && (this.selectedDoc._height = oHeight + (dirs === "up" ? 10 : - 10)); +                this._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) / oHeight * NumCast(this.selectedDoc?._width))); +                const docu = this.selectedDoc; +                if (docu?.type === DocumentType.INK && docu.x && docu.y && docu._height && docu._width) { +                    console.log(docu.x, docu.y, docu._height, docu._width); +                    const ink = Cast(docu.data, InkField)?.inkData; +                    console.log(ink); +                    if (ink) { +                        const newPoints: { X: number, Y: number }[] = []; +                        for (var j = 0; j < ink.length; j++) { +                            // (new x — oldx) + (oldxpoint * newWidt)/oldWidth  +                            const newX = (NumCast(docu.x) - oX) + (ink[j].X * NumCast(docu._width)) / oWidth; +                            const newY = (NumCast(docu.y) - oY) + (ink[j].Y * NumCast(docu._height)) / oHeight; +                            newPoints.push({ X: newX, Y: newY }); +                        } +                        docu.data = new InkField(newPoints); +                    } +                } +                break; +        } +    } + +    getField(key: string) { +        //if (this.selectedDoc) { +        return Field.toString(this.selectedDoc[key] as Field); +        // } else { +        //     return undefined as Opt<string>; +        // } +    } + +    @computed get shapeXps() { return this.getField("x"); } +    @computed get shapeYps() { return this.getField("y"); } +    @computed get shapeRot() { return this.getField("rotation"); } +    @computed get shapeHgt() { return this.getField("_height"); } +    @computed get shapeWid() { return this.getField("_width"); } +    set shapeXps(value) { this.selectedDoc && (this.selectedDoc.x = Number(value)); } +    set shapeYps(value) { this.selectedDoc && (this.selectedDoc.y = Number(value)); } +    set shapeRot(value) { this.selectedDoc && (this.selectedDoc.rotation = Number(value)); } +    set shapeWid(value) { +        const oldWidth = NumCast(this.selectedDoc?._width); +        this.selectedDoc && (this.selectedDoc._width = Number(value)); +        this._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) * NumCast(this.selectedDoc?._height)) / oldWidth); +    } +    set shapeHgt(value) { +        const oldHeight = NumCast(this.selectedDoc?._height); +        this.selectedDoc && (this.selectedDoc._height = Number(value)); +        this._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) * NumCast(this.selectedDoc?._width)) / oldHeight); +    } + +    @computed get hgtInput() { return this.inputBoxDuo("hgt", this.shapeHgt, (val: string) => this.shapeHgt = val, "H:", "wid", this.shapeWid, (val: string) => this.shapeWid = val, "W:"); } +    @computed get XpsInput() { return this.inputBoxDuo("Xps", this.shapeXps, (val: string) => this.shapeXps = val, "X:", "Yps", this.shapeYps, (val: string) => this.shapeYps = val, "Y:"); } +    @computed get rotInput() { return this.inputBoxDuo("rot", this.shapeRot, (val: string) => { this.rotate(Number(val) - Number(this.shapeRot)); this.shapeRot = val; return true; }, "∠:", "rot", this.shapeRot, (val: string) => this.shapeRot = val, ""); } + +    @observable private _fillBtn = false; +    @observable private _lineBtn = false; + +    private _lastFill = "#D0021B"; +    private _lastLine = "#D0021B"; +    private _lastDash: any = "2"; + +    @computed get colorFil() { const ccol = this.getField("fillColor") || ""; ccol && (this._lastFill = ccol); return ccol; } +    @computed get colorStk() { const ccol = this.getField("color") || ""; ccol && (this._lastLine = ccol); return ccol; } +    set colorFil(value) { value && (this._lastFill = value); this.selectedDoc && (this.selectedDoc.fillColor = value ? value : undefined); } +    set colorStk(value) { value && (this._lastLine = value); this.selectedDoc && (this.selectedDoc.color = value ? value : undefined); } + +    colorButton(value: string, setter: () => {}) { +        return <div className="color-button" key="color" onPointerDown={undoBatch(action(e => setter()))}> +            <div className="color-button-preview" style={{ +                backgroundColor: value ?? "121212", width: 15, height: 15, +                display: value === "" || value === "transparent" ? "none" : "" +            }} /> +            {value === "" || value === "transparent" ? <p style={{ fontSize: 25, color: "red", marginTop: -14, position: "fixed" }}>☒</p> : ""} +        </div>; +    } + +    @undoBatch +    @action +    switchStk = (color: ColorState) => { +        const val = String(color.hex); +        this.colorStk = val; +        return true; +    } +    @undoBatch +    @action +    switchFil = (color: ColorState) => { +        const val = String(color.hex); +        this.colorFil = val; +        return true; +    } + +    colorPicker(setter: (color: string) => {}, type: string) { +        return <SketchPicker onChange={type === "stk" ? this.switchStk : this.switchFil} +            presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', +                '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', +                '#FFFFFF', '#f1efeb', 'transparent']} +            color={type === "stk" ? this.colorStk : this.colorFil} />; +    } + +    @computed get fillButton() { return this.colorButton(this.colorFil, () => { this._fillBtn = !this._fillBtn; this._lineBtn = false; return true; }); } +    @computed get lineButton() { return this.colorButton(this.colorStk, () => { this._lineBtn = !this._lineBtn; this._fillBtn = false; return true; }); } + +    @computed get fillPicker() { return this.colorPicker((color: string) => this.colorFil = color, "fil"); } +    @computed get linePicker() { return this.colorPicker((color: string) => this.colorStk = color, "stk"); } + +    @computed get strokeAndFill() { +        return <div> +            <div key="fill" className="strokeAndFill"> +                <div className="fill"> +                    <div className="fill-title">Fill:</div> +                    <div className="fill-button">{this.fillButton}</div> +                </div> +                <div className="stroke"> +                    <div className="stroke-title"> Stroke: </div> +                    <div className="stroke-button">{this.lineButton}</div> +                </div> +            </div> +            {this._fillBtn ? this.fillPicker : ""} +            {this._lineBtn ? this.linePicker : ""} +        </div>; +    } + +    @computed get solidStk() { return this.selectedDoc?.color && (!this.selectedDoc?.strokeDash || this.selectedDoc?.strokeDash === "0") ? true : false; } +    @computed get dashdStk() { return this.selectedDoc?.strokeDash || ""; } +    @computed get unStrokd() { return this.selectedDoc?.color ? true : false; } +    @computed get widthStk() { return this.getField("strokeWidth") || "1"; } +    @computed get markHead() { return this.getField("strokeStartMarker") || ""; } +    @computed get markTail() { return this.getField("strokeEndMarker") || ""; } +    set solidStk(value) { this.dashdStk = ""; this.unStrokd = !value; } +    set dashdStk(value) { +        value && (this._lastDash = value) && (this.unStrokd = false); +        this.selectedDoc && (this.selectedDoc.strokeDash = value ? this._lastDash : undefined); +    } +    set widthStk(value) { this.selectedDoc && (this.selectedDoc.strokeWidth = Number(value)); } +    set unStrokd(value) { this.colorStk = value ? "" : this._lastLine; } +    set markHead(value) { this.selectedDoc && (this.selectedDoc.strokeStartMarker = value); } +    set markTail(value) { this.selectedDoc && (this.selectedDoc.strokeEndMarker = value); } + + +    @computed get stkInput() { return this.regInput("stk", this.widthStk, (val: string) => this.widthStk = val); } + + +    regInput = (key: string, value: any, setter: (val: string) => {}) => { +        return <div className="inputBox"> +            <input className="inputBox-input" +                type="text" value={value} +                onChange={e => setter(e.target.value)} /> +            <div className="inputBox-button"> +                <div className="inputBox-button-up" key="up2" +                    onPointerDown={undoBatch(action(() => this.upDownButtons("up", key)))} > +                    <FontAwesomeIcon icon="caret-up" color="white" size="sm" /> +                </div> +                <div className="inputbox-Button-down" key="down2" +                    onPointerDown={undoBatch(action(() => this.upDownButtons("down", key)))} > +                    <FontAwesomeIcon icon="caret-down" color="white" size="sm" /> +                </div> +            </div> +        </div>; +    } + +    @computed get widthAndDash() { +        return <div className="widthAndDash"> +            <div className="width"> +                <div className="width-top"> +                    <div className="width-title">Width:</div> +                    <div className="width-input">{this.stkInput}</div> +                </div> +                <input className="width-range" type="range" +                    defaultValue={Number(this.widthStk)} min={1} max={100} +                    onChange={undoBatch(action((e) => this.widthStk = e.target.value))} /> +            </div> + +            <div className="arrows"> +                <div className="arrows-head"> +                    <div className="arrows-head-title" >Arrow Head: </div> +                    <input key="markHead" className="arrows-head-input" type="checkbox" +                        checked={this.markHead !== ""} +                        onChange={undoBatch(action(() => this.markHead = this.markHead ? "" : "arrow"))} /> +                </div> +                <div className="arrows-tail"> +                    <div className="arrows-tail-title" >Arrow End: </div> +                    <input key="markTail" className="arrows-tail-input" type="checkbox" +                        checked={this.markTail !== ""} +                        onChange={undoBatch(action(() => this.markTail = this.markTail ? "" : "arrow"))} /> +                </div> +            </div> +            <div className="dashed"> +                <div className="dashed-title">Dash:</div> +                <input key="markHead" className="dashed-input" +                    type="checkbox" checked={this.dashdStk === "2"} +                    onChange={this.changeDash} /> +            </div> +        </div>; +    } + +    @undoBatch @action +    changeDash = () => { +        this.dashdStk = this.dashdStk === "2" ? "0" : "2"; +    } + +    @computed get appearanceEditor() { +        return <div className="appearance-editor"> +            {this.widthAndDash} +            {this.strokeAndFill} +        </div>; +    } + +    @computed get transformEditor() { +        return <div className="transform-editor"> +            {this.controlPointsButton} +            {this.hgtInput} +            {this.XpsInput} +            {this.rotInput} +        </div>; +    } + +    render() { + +        if (!this.selectedDoc) { +            return <div className="propertiesView" style={{ width: this.props.width }}> +                <div className="propertiesView-title" style={{ width: this.props.width }}> +                    No Document Selected +                <div className="propertiesView-title-icon" onPointerDown={this.props.onDown}> +                        <FontAwesomeIcon icon="times" color="black" size="xs" /> +                    </div> +                </div> +            </div>; +        } + +        const novice = Doc.UserDoc().noviceMode; + +        return <div className="propertiesView" style={{ width: this.props.width }} > +            <div className="propertiesView-title" style={{ width: this.props.width }}> +                Properties +                <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" +                    onPointerDown={() => runInAction(() => { this.openActions = !this.openActions; })} +                    style={{ backgroundColor: this.openActions ? "black" : "" }}> +                    Actions +                    <div className="propertiesView-settings-title-icon"> +                        <FontAwesomeIcon icon={this.openActions ? "caret-down" : "caret-right"} size="lg" color="white" /> +                    </div> +                </div> +                {!this.openActions ? (null) : +                    <div className="propertiesView-settings-content"> +                        <PropertiesButtons /> +                    </div>} +            </div> +            <div className="propertiesView-sharing"> +                <div className="propertiesView-sharing-title" +                    onPointerDown={() => runInAction(() => { this.openSharing = !this.openSharing; })} +                    style={{ backgroundColor: this.openSharing ? "black" : "" }}> +                    Sharing {"&"} Permissions +                    <div className="propertiesView-sharing-title-icon"> +                        <FontAwesomeIcon icon={this.openSharing ? "caret-down" : "caret-right"} size="lg" color="white" /> +                    </div> +                </div> +                {!this.openSharing ? (null) : +                    <div className="propertiesView-sharing-content"> +                        {this.sharingTable} +                    </div>} +            </div> + +            {!this.isInk ? (null) : +                <div className="propertiesView-appearance"> +                    <div className="propertiesView-appearance-title" +                        onPointerDown={() => runInAction(() => { this.openAppearance = !this.openAppearance; })} +                        style={{ backgroundColor: this.openAppearance ? "black" : "" }}> +                        Appearance +                        <div className="propertiesView-appearance-title-icon"> +                            <FontAwesomeIcon icon={this.openAppearance ? "caret-down" : "caret-right"} size="lg" color="white" /> +                        </div> +                    </div> +                    {!this.openAppearance ? (null) : +                        <div className="propertiesView-appearance-content"> +                            {this.appearanceEditor} +                        </div>} +                </div>} + +            {this.isInk ? <div className="propertiesView-transform"> +                <div className="propertiesView-transform-title" +                    onPointerDown={() => runInAction(() => { this.openTransform = !this.openTransform; })} +                    style={{ backgroundColor: this.openTransform ? "black" : "" }}> +                    Transform +                    <div className="propertiesView-transform-title-icon"> +                        <FontAwesomeIcon icon={this.openTransform ? "caret-down" : "caret-right"} size="lg" color="white" /> +                    </div> +                </div> +                {this.openTransform ? <div className="propertiesView-transform-content"> +                    {this.transformEditor} +                </div> : null} +            </div> : null} + +            <div className="propertiesView-fields"> +                <div className="propertiesView-fields-title" +                    onPointerDown={() => runInAction(() => { this.openFields = !this.openFields; })} +                    style={{ backgroundColor: this.openFields ? "black" : "" }}> +                    <div className="propertiesView-fields-title-name"> +                        Fields {"&"} Tags +                        <div className="propertiesView-fields-title-icon"> +                            <FontAwesomeIcon icon={this.openFields ? "caret-down" : "caret-right"} size="lg" color="white" /> +                        </div> +                    </div> +                </div> +                {!novice && this.openFields ? <div className="propertiesView-fields-checkbox"> +                    {this.fieldsCheckbox} +                    <div className="propertiesView-fields-checkbox-text">Layout</div> +                </div> : null} +                {!this.openFields ? (null) : +                    <div className="propertiesView-fields-content"> +                        {novice ? this.noviceFields : this.expandedField} +                    </div>} +            </div> +            <div className="propertiesView-layout"> +                <div className="propertiesView-layout-title" +                    onPointerDown={() => runInAction(() => { this.openLayout = !this.openLayout; })} +                    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/globalCssVariables.scss b/src/client/views/globalCssVariables.scss index 3e54d001b..4c79a7c2f 100644 --- a/src/client/views/globalCssVariables.scss +++ b/src/client/views/globalCssVariables.scss @@ -9,10 +9,10 @@ $main-accent: #aaaaa3;  //$alt-accent: #59dff7;  $alt-accent: #c2c2c5;  $lighter-alt-accent: rgb(207, 220, 240); -$darker-alt-accent: rgb(178, 206, 248); +$darker-alt-accent: #b2cef8;  $intermediate-color: #9c9396;  $dark-color: #121721; -$link-color: lightBlue; +$link-color: #add8e6;  $antimodemenu-height: 35px;  // fonts  $sans-serif: "Noto Sans", diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx index 737e0ca28..fb014af51 100644 --- a/src/client/views/linking/LinkEditor.tsx +++ b/src/client/views/linking/LinkEditor.tsx @@ -1,20 +1,16 @@  import { library } from "@fortawesome/fontawesome-svg-core";  import { faArrowLeft, faCog, faEllipsisV, faExchangeAlt, faPlus, faTable, faTimes, faTrash } from '@fortawesome/free-solid-svg-icons';  import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, observable, computed, toJS } from "mobx"; +import { Tooltip } from "@material-ui/core"; +import { action, computed, observable } from "mobx";  import { observer } from "mobx-react"; -import { Doc, Opt } from "../../../fields/Doc"; -import { StrCast, DateCast } from "../../../fields/Types"; +import { Doc } from "../../../fields/Doc"; +import { DateCast, StrCast } from "../../../fields/Types";  import { Utils } from "../../../Utils";  import { LinkManager } from "../../util/LinkManager"; +import { undoBatch } from "../../util/UndoManager";  import './LinkEditor.scss';  import React = require("react"); -import { DocumentView } from "../nodes/DocumentView"; -import { DocumentLinksButton } from "../nodes/DocumentLinksButton"; -import { EditableView } from "../EditableView"; -import { RefObject } from "react"; -import { Tooltip } from "@material-ui/core"; -import { undoBatch } from "../../util/UndoManager";  library.add(faArrowLeft, faEllipsisV, faTable, faTrash, faCog, faExchangeAlt, faTimes, faPlus); @@ -296,21 +292,18 @@ export class LinkEditor extends React.Component<LinkEditorProps> {      //@observable description = this.props.linkDoc.description ? StrCast(this.props.linkDoc.description) : "DESCRIPTION"; -    @undoBatch -    @action +    @undoBatch @action      deleteLink = (): void => {          LinkManager.Instance.deleteLink(this.props.linkDoc);          this.props.showLinks();      } -    @action +    @undoBatch @action      setDescripValue = (value: string) => {          if (LinkManager.currentLink) {              LinkManager.currentLink.description = value;              this.buttonColor = "rgb(62, 133, 55)"; -            setTimeout(action(() => { -                this.buttonColor = "black"; -            }), 750); +            setTimeout(action(() => this.buttonColor = "black"), 750);              return true;          }      } @@ -362,7 +355,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {          this.openDropdown = !this.openDropdown;      } -    @action +    @undoBatch @action      changeFollowBehavior = (follow: string) => {          this.openDropdown = false;          Doc.GetProto(this.props.linkDoc).followLinkLocation = follow; diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index 40a16961a..b29754d45 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -180,6 +180,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {          DocumentLinksButton.EditLink = undefined;      } +    @undoBatch      @action      showLink = () => {          this.props.linkDoc.hidden = !this.props.linkDoc.hidden; @@ -195,7 +196,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; @@ -209,6 +210,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/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index 3736cd3b2..00477874b 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -4,7 +4,7 @@ import { action, computed, observable, runInAction } from "mobx";  import { observer } from "mobx-react";  import { Doc, DocListCast } from "../../../fields/Doc";  import { DocumentType } from "../../documents/DocumentTypes"; -import { emptyFunction, setupMoveUpEvents, returnFalse, Utils } from "../../../Utils"; +import { emptyFunction, setupMoveUpEvents, returnFalse, Utils, emptyPath } from "../../../Utils";  import { TraceMobx } from "../../../fields/util";  import { DocUtils } from "../../documents/Documents";  import { DragManager } from "../../util/DragManager"; @@ -75,7 +75,11 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp          setupMoveUpEvents(this, e, this.onLinkButtonMoved, emptyFunction, action((e, doubleTap) => {              if (doubleTap && this.props.InMenu && this.props.StartLink) {                  //action(() => Doc.BrushDoc(this.props.View.Document)); -                DocumentLinksButton.StartLink = this.props.View.props.Document; +                if (DocumentLinksButton.StartLink === this.props.View.props.Document) { +                    DocumentLinksButton.StartLink = undefined; +                } else { +                    DocumentLinksButton.StartLink = this.props.View.props.Document; +                }              } else if (!this.props.InMenu) {                  DocumentLinksButton.EditLink = this.props.View;                  DocumentLinksButton.EditLinkLoc = [e.clientX + 10, e.clientY]; @@ -86,7 +90,12 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp      @action @undoBatch      onLinkClick = (e: React.MouseEvent): void => {          if (this.props.InMenu && this.props.StartLink) { -            DocumentLinksButton.StartLink = this.props.View.props.Document; +            if (DocumentLinksButton.StartLink === this.props.View.props.Document) { +                DocumentLinksButton.StartLink = undefined; +            } else { +                DocumentLinksButton.StartLink = this.props.View.props.Document; +            } +              //action(() => Doc.BrushDoc(this.props.View.Document));          } else if (!this.props.InMenu) {              DocumentLinksButton.EditLink = this.props.View; @@ -94,97 +103,95 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp          }      } -    @action @undoBatch      completeLink = (e: React.PointerEvent): void => { -        setupMoveUpEvents(this, e, returnFalse, emptyFunction, action((e, doubleTap) => { +        setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action((e, doubleTap) => {              if (doubleTap && !this.props.StartLink) {                  if (DocumentLinksButton.StartLink === this.props.View.props.Document) {                      DocumentLinksButton.StartLink = undefined;                      DocumentLinksButton.AnnotationId = undefined; -                } else { -                    if (DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document) { -                        const sourceDoc = DocumentLinksButton.StartLink; -                        const targetDoc = this.props.View.props.Document; -                        const linkDoc = DocUtils.MakeLink({ doc: sourceDoc }, { doc: targetDoc }, DocumentLinksButton.AnnotationId ? "hypothes.is annotation" : "long drag"); - -                        // TODO: Not currently possible to drag to complete links to annotations -                        if (DocumentLinksButton.AnnotationId && DocumentLinksButton.AnnotationUri) { -                            Doc.GetProto(linkDoc as Doc).linksToAnnotation = true; -                            Doc.GetProto(linkDoc as Doc).annotationId = DocumentLinksButton.AnnotationId; -                            Doc.GetProto(linkDoc as Doc).annotationUri = DocumentLinksButton.AnnotationUri; -                            Hypothesis.makeLink(StrCast(targetDoc.title), Utils.prepend("/doc/" + targetDoc[Id]), DocumentLinksButton.AnnotationId); // update and link placeholder annotation -                        } +                } else if (DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document) { +                    const sourceDoc = DocumentLinksButton.StartLink; +                    const targetDoc = this.props.View.props.Document; +                    const linkDoc = DocUtils.MakeLink({ doc: sourceDoc }, { doc: targetDoc }, DocumentLinksButton.AnnotationId ? "hypothes.is annotation" : "long drag"); -                        LinkManager.currentLink = linkDoc; +                    // TODO: Not currently possible to drag to complete links to annotations +                    if (DocumentLinksButton.AnnotationId && DocumentLinksButton.AnnotationUri) { +                        Doc.GetProto(linkDoc as Doc).linksToAnnotation = true; +                        Doc.GetProto(linkDoc as Doc).annotationId = DocumentLinksButton.AnnotationId; +                        Doc.GetProto(linkDoc as Doc).annotationUri = DocumentLinksButton.AnnotationUri; +                        Hypothesis.makeLink(StrCast(targetDoc.title), Utils.prepend("/doc/" + targetDoc[Id]), DocumentLinksButton.AnnotationId); // update and link placeholder annotation +                    } + +                    LinkManager.currentLink = linkDoc; -                        runInAction(() => { -                            if (linkDoc) { -                                TaskCompletionBox.textDisplayed = "Link Created"; -                                TaskCompletionBox.popupX = e.screenX; -                                TaskCompletionBox.popupY = e.screenY - 133; -                                TaskCompletionBox.taskCompleted = true; +                    runInAction(() => { +                        if (linkDoc) { +                            TaskCompletionBox.textDisplayed = "Link Created"; +                            TaskCompletionBox.popupX = e.screenX; +                            TaskCompletionBox.popupY = e.screenY - 133; +                            TaskCompletionBox.taskCompleted = true; -                                LinkDescriptionPopup.popupX = e.screenX; -                                LinkDescriptionPopup.popupY = e.screenY - 100; -                                LinkDescriptionPopup.descriptionPopup = true; +                            LinkDescriptionPopup.popupX = e.screenX; +                            LinkDescriptionPopup.popupY = e.screenY - 100; +                            LinkDescriptionPopup.descriptionPopup = true; -                                setTimeout(action(() => { TaskCompletionBox.taskCompleted = false; }), 2500); -                            } +                            LinkDescriptionPopup.popupX = e.screenX; +                            LinkDescriptionPopup.popupY = e.screenY - 100; +                            LinkDescriptionPopup.descriptionPopup = true; -                        }); -                    } +                            setTimeout(action(() => TaskCompletionBox.taskCompleted = false), 2500); +                        } +                    });                  }              } -        })); +        })));      } - -    @action @undoBatch -    finishLinkClick = (screenX: number, screenY: number) => { +    finishLinkClick = undoBatch(action((screenX: number, screenY: number) => {          if (DocumentLinksButton.StartLink === this.props.View.props.Document) {              DocumentLinksButton.StartLink = undefined;              DocumentLinksButton.AnnotationId = undefined;          } else { -            if (!this.props.StartLink) { -                if (DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document) { -                    const linkDoc = DocUtils.MakeLink({ doc: DocumentLinksButton.StartLink }, { doc: this.props.View.props.Document }, DocumentLinksButton.AnnotationId ? "hypothes.is annotation" : "long drag"); -                    // this notifies any of the subviews that a document is made so that they can make finer-grained hyperlinks ().  see note above in onLInkButtonMoved -                    runInAction(() => DocumentLinksButton.StartLink!._link = this.props.View._link = linkDoc); -                    setTimeout(action(() => DocumentLinksButton.StartLink!._link = this.props.View._link = undefined), 0); -                    LinkManager.currentLink = linkDoc; - -                    if (DocumentLinksButton.AnnotationId && DocumentLinksButton.AnnotationUri) { // if linking from a Hypothes.is annotation -                        const targetDoc = this.props.View.props.Document; -                        Doc.GetProto(linkDoc as Doc).linksToAnnotation = true; -                        Doc.GetProto(linkDoc as Doc).annotationId = DocumentLinksButton.AnnotationId; -                        Doc.GetProto(linkDoc as Doc).annotationUri = DocumentLinksButton.AnnotationUri; -                        Hypothesis.makeLink(StrCast(targetDoc.title), Utils.prepend("/doc/" + targetDoc[Id]), DocumentLinksButton.AnnotationId); // edit annotation to add a Dash hyperlink to the linked doc -                    } +            if (!this.props.StartLink && DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document) { +                const linkDoc = DocUtils.MakeLink({ doc: DocumentLinksButton.StartLink }, { doc: this.props.View.props.Document }, DocumentLinksButton.AnnotationId ? "hypothes.is annotation" : "long drag"); +                // this notifies any of the subviews that a document is made so that they can make finer-grained hyperlinks ().  see note above in onLInkButtonMoved +                DocumentLinksButton.StartLink._link = this.props.View._link = linkDoc; +                setTimeout(action(() => DocumentLinksButton.StartLink!._link = this.props.View._link = undefined), 0); +                LinkManager.currentLink = linkDoc; -                    runInAction(() => { -                        if (linkDoc) { -                            TaskCompletionBox.textDisplayed = "Link Created"; -                            TaskCompletionBox.popupX = screenX; -                            TaskCompletionBox.popupY = screenY - 133; -                            TaskCompletionBox.taskCompleted = true; +                if (DocumentLinksButton.AnnotationId && DocumentLinksButton.AnnotationUri) { // if linking from a Hypothes.is annotation +                    const targetDoc = this.props.View.props.Document; +                    Doc.GetProto(linkDoc as Doc).linksToAnnotation = true; +                    Doc.GetProto(linkDoc as Doc).annotationId = DocumentLinksButton.AnnotationId; +                    Doc.GetProto(linkDoc as Doc).annotationUri = DocumentLinksButton.AnnotationUri; +                    Hypothesis.makeLink(StrCast(targetDoc.title), Utils.prepend("/doc/" + targetDoc[Id]), DocumentLinksButton.AnnotationId); // edit annotation to add a Dash hyperlink to the linked doc +                } -                            if (LinkDescriptionPopup.showDescriptions === "ON" || !LinkDescriptionPopup.showDescriptions) { -                                LinkDescriptionPopup.popupX = screenX; -                                LinkDescriptionPopup.popupY = screenY - 100; -                                LinkDescriptionPopup.descriptionPopup = true; -                            } +                if (linkDoc) { +                    TaskCompletionBox.textDisplayed = "Link Created"; +                    TaskCompletionBox.popupX = screenX; +                    TaskCompletionBox.popupY = screenY - 133; +                    TaskCompletionBox.taskCompleted = true; -                            setTimeout(action(() => { TaskCompletionBox.taskCompleted = false; }), 2500); -                        } -                    }); +                    if (LinkDescriptionPopup.showDescriptions === "ON" || !LinkDescriptionPopup.showDescriptions) { +                        LinkDescriptionPopup.popupX = screenX; +                        LinkDescriptionPopup.popupY = screenY - 100; +                        LinkDescriptionPopup.descriptionPopup = true; +                    } +                    setTimeout(action(() => { TaskCompletionBox.taskCompleted = false; }), 2500);                  }              }          } -    } +    }));      @observable      public static EditLink: DocumentView | undefined;      public static EditLinkLoc: number[] = [0, 0]; + +    @action clearLinks() { +        DocumentLinksButton.StartLink = undefined; +    } +      @computed      get linkButton() {          TraceMobx(); @@ -226,27 +233,39 @@ 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.props.Document ? <div className={"documentLinksButton-endLink"} -                style={{ width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px" }} -                onPointerDown={this.completeLink} onClick={e => this.finishLinkClick(e.screenX, e.screenY)} /> : (null)} +            {this.props.InMenu && !this.props.StartLink && +                DocumentLinksButton.StartLink !== this.props.View.props.Document ? <div className={"documentLinksButton-endLink"} +                    style={{ +                        width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px", +                        backgroundColor: DocumentLinksButton.StartLink ? "" : "grey", +                        border: DocumentLinksButton.StartLink ? "" : "none" +                    }} +                    onPointerDown={DocumentLinksButton.StartLink ? this.completeLink : emptyFunction} +                    onClick={e => DocumentLinksButton.StartLink ? this.finishLinkClick(e.screenX, e.screenY) : emptyFunction} /> : (null)}              {DocumentLinksButton.StartLink === this.props.View.props.Document && this.props.InMenu && this.props.StartLink ? <div className={"documentLinksButton-startLink"} -                style={{ width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px" }} /> : (null)} -        </div>; +                style={{ width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px" }} +                onPointerDown={this.clearLinks} onClick={this.clearLinks} +            /> : (null)} +        </div >;          return (!links.length) && !this.props.AlwaysOn ? (null) : -            this.props.InMenu ? +            this.props.InMenu && (this.props.StartLink || DocumentLinksButton.StartLink) ?                  <Tooltip title={<><div className="dash-tooltip">{title}</div></>}>                      {linkButton} -                </Tooltip> : !!!DocumentLinksButton.EditLink ? +                </Tooltip> : !!!DocumentLinksButton.EditLink && !this.props.InMenu ?                      <Tooltip title={<><div className="dash-tooltip">{title}</div></>}>                          {linkButton}                      </Tooltip> :                      linkButton;      } +      render() {          return this.linkButton;      } diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index b978f6245..e6b8928d4 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -54,6 +54,15 @@          }      } +    .documentView-anchorCont { +        position: absolute; +        top: 0;  +        left: 0;  +        width: 100%; +        height: 100%;  +        display: inline-block; +    } +      .documentView-lock {          width: 20;           height: 20;  diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 15cf9556b..a195f2813 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 @@ -287,7 +289,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu      }      onClick = action((e: React.MouseEvent | React.PointerEvent) => { -        if (!e.nativeEvent.cancelBubble && !this.Document.ignoreClick && +        if (!e.nativeEvent.cancelBubble && !this.Document.ignoreClick && this.props.renderDepth >= 0 &&              (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) {              let stopPropagate = true;              let preventDefault = true; @@ -319,10 +321,11 @@ 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); -                if (this.props.Document !== Doc.UserDoc()["dockedBtn-undo"] && this.props.Document !== Doc.UserDoc()["dockedBtn-redo"]) { +                if (!Doc.AreProtosEqual(this.props.Document, Doc.UserDoc()["dockedBtn-undo"] as Doc) && !Doc.AreProtosEqual(this.props.Document, Doc.UserDoc()["dockedBtn-redo"] as Doc)) {                      UndoManager.RunInBatch(func, "on click");                  } else func();              } else if (this.Document["onClick-rawScript"] && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) {// bcz: hack? don't edit a script if you're clicking on a scripting box itself @@ -584,6 +587,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) { @@ -661,6 +676,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu      }      @action +    onCopy = () => { +        const alias = Doc.MakeAlias(this.props.Document); +        alias.x = NumCast(this.props.Document.x) + NumCast(this.props.Document._width); +        alias.y = NumCast(this.props.Document.y) + 30; +        this.props.addDocument?.(alias); +    } + +    @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)) { @@ -704,14 +727,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu          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) { @@ -724,6 +747,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: "Download document", icon: "download", event: async () => Doc.Zip(this.props.Document) }); +        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" });          if (!Doc.UserDoc().noviceMode) {              moreItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: "concierge-bell" });              moreItems.push({ description: `${this.Document._chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document._chromeStatus = (this.Document._chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" }); @@ -735,16 +761,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" }); @@ -786,8 +814,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} @@ -823,6 +852,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu                  ChromeHeight={this.chromeHeight}                  isSelected={this.isSelected}                  select={this.select} +                scriptContext={this.props.scriptContext}                  onClick={this.onClickFunc}                  layoutKey={this.finalLayoutKey} />              {this.layoutDoc.hideAllLinks ? (null) : this.allAnchors} @@ -860,7 +890,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu              this.rootDoc.type === DocumentType.LINK ||              this.props.dontRegisterView ? (null) : // view that are not registered              DocUtils.FilterDocs(this.directLinks, this.props.docFilters(), []).filter(d => !d.hidden && this.isNonTemporalLink).map((d, i) => -                <DocumentView {...this.props} key={i + 1} +                <div className="documentView-anchorCont" key={i + 1}> <DocumentView {...this.props}                      Document={d}                      ContainingCollectionView={this.props.ContainingCollectionView}                      ContainingCollectionDoc={this.props.Document} // bcz: hack this.props.Document is not a collection  Need a better prop for passing the containing document to the LinkAnchorBox @@ -873,12 +903,16 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu                      pointerEvents={false}                      LayoutTemplate={undefined}                      LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(d, this.props.Document)}`)} -                />); +                /></div >);      }      @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>; @@ -1004,7 +1038,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/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 48e1f6ce3..23ae48108 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -60,6 +60,7 @@ export interface FieldViewProps {      color?: string;      xMargin?: number;      yMargin?: number; +    scriptContext?: any;  }  @observer diff --git a/src/client/views/nodes/FontIconBox.scss b/src/client/views/nodes/FontIconBox.scss index 5b85d8b0b..5bdafd857 100644 --- a/src/client/views/nodes/FontIconBox.scss +++ b/src/client/views/nodes/FontIconBox.scss @@ -1,25 +1,62 @@ -.fontIconBox-outerDiv { +.fontIconBox-label { +    color: white; +    margin-right: 4px; +    margin-top: 1px; +    position: relative; +    text-align: center; +    font-size: 7px; +    letter-spacing: normal; +    background-color: inherit; +    border-radius: 8px; +    margin-top: -8px; +    padding: 0; +    width: 100%; +} + +.menuButton-round { +    border-radius: 100%; + +    .fontIconBox-label { +        margin-left: -10px; // button padding is 10px; +        bottom: 0; +        position: absolute; +    } +} + +.menuButton-square { +    padding-top: 3px; +    padding-bottom: 3px; +    padding-left: 5px; + +    .fontIconBox-label { +        border-radius: 0px; +        margin-top: 0px; +        border-radius: "inherit"; +    } +} + +.menuButton, +.menuButton-round, +.menuButton-square {      width: 100%;      height: 100%;      pointer-events: all;      touch-action: none; -    border-radius: inherit; -    background: black; -    border-radius: 100%; -    transform-origin: top left; -    .fontIconBox-label { -        background: gray; -        color:white; +    .menuButton-wrap { +        touch-action: none;          border-radius: 8px; -        width:100%; -        position: absolute; -        text-align: center; -        font-size: 8px; -        margin-top:4px; -        letter-spacing: normal; -        left: 0; -        overflow: hidden; + +        // &:hover { +        //     background: rgb(61, 61, 61); +        //     cursor: pointer; +        // } +    } + +    .menuButton-icon-square { +        width: auto; +        height: 32px; +        padding: 4px;      }      svg { diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx index 2611d2ca7..eff5a4160 100644 --- a/src/client/views/nodes/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox.tsx @@ -6,14 +6,14 @@ import { DocComponent } from '../DocComponent';  import './FontIconBox.scss';  import { FieldView, FieldViewProps } from './FieldView';  import { StrCast, Cast, NumCast } from '../../../fields/Types'; -import { Utils } from "../../../Utils"; +import { Utils, emptyFunction } from "../../../Utils";  import { runInAction, observable, reaction, IReactionDisposer } from 'mobx';  import { Doc } from '../../../fields/Doc';  import { ContextMenu } from '../ContextMenu';  import { ScriptField } from '../../../fields/ScriptField';  import { Tooltip } from '@material-ui/core';  const FontIconSchema = createSchema({ -    icon: "string" +    icon: "string",  });  type FontIconDocument = makeInterface<[typeof FontIconSchema]>; @@ -59,17 +59,20 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>(      }      render() { -        const referenceDoc = (this.layoutDoc.dragFactory instanceof Doc ? this.layoutDoc.dragFactory : this.layoutDoc); -        const refLayout = Doc.Layout(referenceDoc); -        const button = <button className="fontIconBox-outerDiv" ref={this._ref} onContextMenu={this.specificContextMenu} -            style={{ -                padding: Cast(this.layoutDoc._xPadding, "number", null), -                background: StrCast(refLayout._backgroundColor, StrCast(refLayout.backgroundColor)), -                boxShadow: this.layoutDoc.ischecked ? `4px 4px 12px black` : undefined -            }}> -            <FontAwesomeIcon className="fontIconBox-icon" icon={StrCast(this.dataDoc.icon, "user") as any} color={StrCast(this.layoutDoc.color, this._foregroundColor)} size="sm" /> -            {!this.rootDoc.title ? (null) : <div className="fontIconBox-label" style={{ width: this.rootDoc.label ? "max-content" : undefined }}> {StrCast(this.rootDoc.label, StrCast(this.rootDoc.title).substring(0, 6))} </div>} -        </button>; +        const label = StrCast(this.rootDoc.label, StrCast(this.rootDoc.title)); +        const color = StrCast(this.layoutDoc.color, this._foregroundColor); +        const backgroundColor = StrCast(this.layoutDoc._backgroundColor, StrCast(this.rootDoc.backgroundColor, this.props.backgroundColor?.(this.rootDoc))); +        const shape = StrCast(this.layoutDoc.iconShape, "round"); +        const button = <> +            <button className={`menuButton-${shape}`} ref={this._ref} onContextMenu={this.specificContextMenu} +                style={{ boxShadow: this.layoutDoc.ischecked ? `4px 4px 12px black` : undefined, backgroundColor }}> +                <div className="menuButton-wrap"> +                    {<FontAwesomeIcon className={`menuButton-icon-${shape}`} icon={StrCast(this.dataDoc.icon, "user") as any} color={color} +                        size={this.layoutDoc.iconShape === "square" ? "sm" : "lg"} />} +                    {!label ? (null) : <div className="fontIconBox-label" style={{ color, backgroundColor }}> {label} </div>} +                </div> +            </button> +        </>;          return !this.layoutDoc.toolTip ? button :              <Tooltip title={<div className="dash-tooltip">{StrCast(this.layoutDoc.toolTip)}</div>}>                  {button} diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index 0dfbdc5cf..05ba6628c 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -41,7 +41,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps, LabelDocument              }, icon: "trash"          }); -        ContextMenu.Instance.addItem({ description: "OnClick...", noexpand: true, subitems: funcs, icon: "asterisk" }); +        ContextMenu.Instance.addItem({ description: "OnClick...", noexpand: true, subitems: funcs, icon: "mouse-pointer" });      }      @undoBatch diff --git a/src/client/views/nodes/LinkDescriptionPopup.tsx b/src/client/views/nodes/LinkDescriptionPopup.tsx index d8fe47f4e..720af6c9d 100644 --- a/src/client/views/nodes/LinkDescriptionPopup.tsx +++ b/src/client/views/nodes/LinkDescriptionPopup.tsx @@ -19,12 +19,15 @@ export class LinkDescriptionPopup extends React.Component<{}> {      @action      descriptionChanged = (e: React.ChangeEvent<HTMLInputElement>) => { -        LinkManager.currentLink && (LinkManager.currentLink.description = e.currentTarget.value); +        this.description = e.currentTarget.value;      }      @action -    onDismiss = () => { +    onDismiss = (add: boolean) => {          LinkDescriptionPopup.descriptionPopup = false; +        if (add) { +            LinkManager.currentLink && (LinkManager.currentLink.description = this.description); +        }      }      @action @@ -50,15 +53,16 @@ export class LinkDescriptionPopup extends React.Component<{}> {                  left: LinkDescriptionPopup.popupX ? LinkDescriptionPopup.popupX : 700,                  top: LinkDescriptionPopup.popupY ? LinkDescriptionPopup.popupY : 350,              }}> -            <input className="linkDescriptionPopup-input" onKeyPress={e => e.key === "Enter" && this.onDismiss()} +            <input className="linkDescriptionPopup-input" +                onKeyPress={e => e.key === "Enter" && this.onDismiss(true)}                  placeholder={"(optional) enter link label..."}                  onChange={(e) => this.descriptionChanged(e)}>              </input>              <div className="linkDescriptionPopup-btn">                  <div className="linkDescriptionPopup-btn-dismiss" -                    onPointerDown={this.onDismiss}> Dismiss </div> +                    onPointerDown={e => this.onDismiss(false)}> Dismiss </div>                  <div className="linkDescriptionPopup-btn-add" -                    onPointerDown={this.onDismiss}> Add </div> +                    onPointerDown={e => this.onDismiss(true)}> Add </div>              </div>          </div>;      } diff --git a/src/client/views/nodes/TaskCompletedBox.tsx b/src/client/views/nodes/TaskCompletedBox.tsx index 89602f219..2a3dd8d2d 100644 --- a/src/client/views/nodes/TaskCompletedBox.tsx +++ b/src/client/views/nodes/TaskCompletedBox.tsx @@ -1,7 +1,5 @@  import React = require("react");  import { observer } from "mobx-react"; -import { documentSchema } from "../../../fields/documentSchemas"; -import { makeInterface } from "../../../fields/Schema";  import "./TaskCompletedBox.scss";  import { observable, action } from "mobx";  import { Fade } from "@material-ui/core"; diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index d30f1499e..646a94aa7 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -4,7 +4,7 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction  import { observer } from "mobx-react";  import { Dictionary } from "typescript-collections";  import * as WebRequest from 'web-request'; -import { Doc, DocListCast, Opt } from "../../../fields/Doc"; +import { Doc, DocListCast, Opt, AclAddonly, AclEdit, AclAdmin } from "../../../fields/Doc";  import { documentSchema } from "../../../fields/documentSchemas";  import { Id } from "../../../fields/FieldSymbols";  import { HtmlField } from "../../../fields/HtmlField"; @@ -13,7 +13,7 @@ import { List } from "../../../fields/List";  import { listSpec, makeInterface } from "../../../fields/Schema";  import { Cast, NumCast, StrCast } from "../../../fields/Types";  import { WebField } from "../../../fields/URLField"; -import { TraceMobx } from "../../../fields/util"; +import { TraceMobx, GetEffectiveAcl } from "../../../fields/util";  import { addStyleSheet, clearStyleSheetRules, emptyFunction, returnOne, returnZero, Utils, returnTrue } from "../../../Utils";  import { Docs, DocUtils } from "../../documents/Documents";  import { DragManager } from "../../util/DragManager"; @@ -535,9 +535,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum      @action      highlight = (color: string) => {          // creates annotation documents for current highlights -        const annotationDoc = this.makeAnnotationDocument(color); -        annotationDoc && Doc.AddDocToList(this.props.Document, this.annotationKey, annotationDoc); -        return annotationDoc; +        const effectiveAcl = GetEffectiveAcl(this.props.Document); +        const annotationDoc = [AclAddonly, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeAnnotationDocument(color); +        annotationDoc && this.addDocument?.(annotationDoc); +        return annotationDoc ?? undefined;      }      /**       * This is temporary for creating annotations from highlights. It will diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index 958a37568..8ae71c035 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -190,7 +190,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna              }              list.map(c => c.heading).indexOf(this._fieldKey) === -1 && list.push(new SchemaHeaderField(this._fieldKey, "#f1efeb"));              list.map(c => c.heading).indexOf("text") === -1 && list.push(new SchemaHeaderField("text", "#f1efeb")); -            alias._pivotField = this._fieldKey; +            alias._pivotField = this._fieldKey.startsWith("#") ? "#" : this._fieldKey;              this.props.tbox.props.addDocTab(alias, "onRight");          }      } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 627c6e363..fc65f34eb 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; @@ -1346,7 +1346,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp      @computed get sidebarColor() { return StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "transparent")); }      render() {          TraceMobx(); -        const scale = this.props.ContentScaling() * NumCast(this.layoutDoc._viewScale, 1); +        const scale = this.props.hideOnLeave ? 1 : this.props.ContentScaling() * NumCast(this.layoutDoc._viewScale, 1);          const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : "";          const interactive = Doc.GetSelectedTool() === InkTool.None && !this.layoutDoc.isBackground;          setTimeout(() => this._editorView && RichTextMenu.Instance.updateFromDash(this._editorView, undefined, this.props), this.props.isSelected() ? 10 : 0); // need to make sure that we update a text box that is selected after updating the one that was deselected diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 47a4911b8..6e268be48 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 }, @@ -184,11 +185,15 @@ export default class RichTextMenu extends AntimodeMenu {          const active = this.getActiveFontStylesOnSelection();          const activeFamilies = active.activeFamilies;          const activeSizes = active.activeSizes; +        const activeColors = active.activeColors; +        const activeHighlights = active.activeHighlights;          this.activeListType = this.getActiveListStyle();          this.activeAlignment = this.getActiveAlignment();          this.activeFontFamily = !activeFamilies.length ? "Arial" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various";          this.activeFontSize = !activeSizes.length ? "13pt" : activeSizes.length === 1 ? String(activeSizes[0]) : "..."; +        this.activeFontColor = !activeColors.length ? "black" : activeColors.length === 1 ? String(activeColors[0]) : "..."; +        this.activeHighlightColor = !activeHighlights.length ? "" : activeHighlights.length === 1 ? String(activeHighlights[0]) : "...";          // update link in current selection          const targetTitle = await this.getTextLinkTargetTitle(); @@ -223,7 +228,7 @@ export default class RichTextMenu extends AntimodeMenu {          if (this.view && this.TextView.props.isSelected(true)) {              const path = (this.view.state.selection.$from as any).path;              for (let i = path.length - 3; i < path.length && i >= 0; i -= 3) { -                if (path[i]?.type === this.view.state.schema.nodes.paragraph) { +                if (path[i]?.type === this.view.state.schema.nodes.paragraph || path[i]?.type === this.view.state.schema.nodes.heading) {                      return path[i].attrs.align || "left";                  }              } @@ -249,10 +254,12 @@ export default class RichTextMenu extends AntimodeMenu {      // finds font sizes and families in selection      getActiveFontStylesOnSelection() { -        if (!this.view) return { activeFamilies: [], activeSizes: [] }; +        if (!this.view) return { activeFamilies: [], activeSizes: [], activeColors: [], activeHighlights: [] };          const activeFamilies: string[] = [];          const activeSizes: string[] = []; +        const activeColors: string[] = []; +        const activeHighlights: string[] = [];          if (this.TextView.props.isSelected(true)) {              const state = this.view.state;              const pos = this.view.state.selection.$from; @@ -260,15 +267,20 @@ export default class RichTextMenu extends AntimodeMenu {              if (ref_node && ref_node !== this.view.state.doc && ref_node.isText) {                  ref_node.marks.forEach(m => {                      m.type === state.schema.marks.pFontFamily && activeFamilies.push(m.attrs.family); +                    m.type === state.schema.marks.pFontColor && activeColors.push(m.attrs.color);                      m.type === state.schema.marks.pFontSize && activeSizes.push(String(m.attrs.fontSize) + "pt"); +                    m.type === state.schema.marks.marker && activeHighlights.push(String(m.attrs.highlight));                  });              }              !activeFamilies.length && (activeFamilies.push(StrCast(this.TextView.layoutDoc._fontFamily, StrCast(Doc.UserDoc().fontFamily))));              !activeSizes.length && (activeSizes.push(StrCast(this.TextView.layoutDoc._fontSize, StrCast(Doc.UserDoc().fontSize)))); +            !activeColors.length && (activeSizes.push(StrCast(this.TextView.layoutDoc.color, StrCast(Doc.UserDoc().fontColor))));          }          !activeFamilies.length && (activeFamilies.push(StrCast(Doc.UserDoc().fontFamily)));          !activeSizes.length && (activeSizes.push(StrCast(Doc.UserDoc().fontSize))); -        return { activeFamilies, activeSizes }; +        !activeColors.length && (activeColors.push(StrCast(Doc.UserDoc().fontColor, "black"))); +        !activeHighlights.length && (activeHighlights.push(StrCast(Doc.UserDoc().fontHighlight, ""))); +        return { activeFamilies, activeSizes, activeColors, activeHighlights };      }      getMarksInSelection(state: EditorState<any>) { @@ -425,10 +437,16 @@ export default class RichTextMenu extends AntimodeMenu {      }      changeFontSize = (mark: Mark, view: EditorView) => { +        if ((this.view?.state.selection.$from.pos || 0) < 2) { +            this.TextView.layoutDoc._fontSize = mark.attrs.fontSize; +        }          this.setMark(view.state.schema.marks.pFontSize.create({ fontSize: mark.attrs.fontSize }), view.state, view.dispatch, true);      }      changeFontFamily = (mark: Mark, view: EditorView) => { +        if ((this.view?.state.selection.$from.pos || 0) < 2) { +            this.TextView.layoutDoc._fontFamily = mark.attrs.family; +        }          this.setMark(view.state.schema.marks.pFontFamily.create({ family: mark.attrs.family }), view.state, view.dispatch, true);      } @@ -490,7 +508,7 @@ export default class RichTextMenu extends AntimodeMenu {      alignParagraphs(state: EditorState<any>, align: "left" | "right" | "center", dispatch: any) {          var tr = state.tr;          state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { -            if (node.type === schema.nodes.paragraph) { +            if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) {                  tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, align }, node.marks);                  return false;              } @@ -503,7 +521,7 @@ export default class RichTextMenu extends AntimodeMenu {      insetParagraph(state: EditorState<any>, dispatch: any) {          var tr = state.tr;          state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { -            if (node.type === schema.nodes.paragraph) { +            if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) {                  const inset = (node.attrs.inset ? Number(node.attrs.inset) : 0) + 10;                  tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, inset }, node.marks);                  return false; @@ -516,7 +534,7 @@ export default class RichTextMenu extends AntimodeMenu {      outsetParagraph(state: EditorState<any>, dispatch: any) {          var tr = state.tr;          state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { -            if (node.type === schema.nodes.paragraph) { +            if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) {                  const inset = Math.max(0, (node.attrs.inset ? Number(node.attrs.inset) : 0) - 10);                  tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, inset }, node.marks);                  return false; @@ -529,8 +547,9 @@ export default class RichTextMenu extends AntimodeMenu {      indentParagraph(state: EditorState<any>, dispatch: any) {          var tr = state.tr; +        const heading = false;          state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { -            if (node.type === schema.nodes.paragraph) { +            if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) {                  const nodeval = node.attrs.indent ? Number(node.attrs.indent) : undefined;                  const indent = !nodeval ? 25 : nodeval < 0 ? 0 : nodeval + 25;                  tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, indent }, node.marks); @@ -538,14 +557,14 @@ export default class RichTextMenu extends AntimodeMenu {              }              return true;          }); -        dispatch?.(tr); +        !heading && dispatch?.(tr);          return true;      }      hangingIndentParagraph(state: EditorState<any>, dispatch: any) {          var tr = state.tr;          state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { -            if (node.type === schema.nodes.paragraph) { +            if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) {                  const nodeval = node.attrs.indent ? Number(node.attrs.indent) : undefined;                  const indent = !nodeval ? -25 : nodeval > 0 ? 0 : nodeval - 10;                  tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, indent }, node.marks); @@ -604,8 +623,11 @@ export default class RichTextMenu extends AntimodeMenu {              label = "No marks are currently stored";          } +        //onPointerDown={onBrushClick} +          const button = <Tooltip title={<div className="dash-tooltip">style brush</div>} placement="bottom"> -            <button className="antimodeMenu-button" onPointerDown={onBrushClick} style={this.brushMarks?.size > 0 ? { backgroundColor: "121212" } : {}}> + +            <button className="antimodeMenu-button" style={this.brushMarks?.size > 0 ? { backgroundColor: "121212" } : {}}>                  <FontAwesomeIcon icon="paint-roller" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.brushMarks?.size > 0 ? 45 : 0}deg)` }} />              </button>          </Tooltip>; @@ -614,11 +636,11 @@ export default class RichTextMenu extends AntimodeMenu {              <div className="dropdown">                  <p>{label}</p>                  <button onPointerDown={this.clearBrush}>Clear brush</button> -                <input placeholder="-brush name-" ref={this._brushNameRef} onKeyPress={this.onBrushNameKeyPress}></input> +                <input placeholder="-brush name-" ref={this._brushNameRef} onKeyPress={this.onBrushNameKeyPress} />              </div>;          return ( -            <ButtonDropdown view={this.view} key={"brush dropdown"} button={button} dropdownContent={dropdownContent} /> +            <ButtonDropdown view={this.view} key={"brush dropdown"} button={button} dropdownContent={dropdownContent} openDropdownOnButton={true} />          );      } @@ -677,8 +699,9 @@ export default class RichTextMenu extends AntimodeMenu {              self.TextView.EditorView!.focus();          } +        // onPointerDown={onColorClick}          const button = <Tooltip title={<div className="dash-tooltip">set font color</div>} placement="bottom"> -            <button className="antimodeMenu-button color-preview-button" onPointerDown={onColorClick}> +            <button className="antimodeMenu-button color-preview-button">                  <FontAwesomeIcon icon="palette" size="lg" />                  <div className="color-preview" style={{ backgroundColor: this.activeFontColor }}></div>              </button> @@ -699,7 +722,7 @@ export default class RichTextMenu extends AntimodeMenu {              </div>;          return ( -            <ButtonDropdown view={this.view} key={"color dropdown"} button={button} dropdownContent={dropdownContent} /> +            <ButtonDropdown view={this.view} key={"color dropdown"} button={button} dropdownContent={dropdownContent} openDropdownOnButton={true} />          );      } @@ -731,8 +754,9 @@ export default class RichTextMenu extends AntimodeMenu {              UndoManager.RunInBatch(() => self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch), "rt highlighter");          } +        //onPointerDown={onHighlightClick}          const button = <Tooltip title={<div className="dash-tooltip">set highlight color</div>} placement="bottom"> -            <button className="antimodeMenu-button color-preview-button" key="highilghter-button" onPointerDown={onHighlightClick}> +            <button className="antimodeMenu-button color-preview-button" key="highilghter-button" >                  <FontAwesomeIcon icon="highlighter" size="lg" />                  <div className="color-preview" style={{ backgroundColor: this.activeHighlightColor }}></div>              </button> @@ -753,7 +777,7 @@ export default class RichTextMenu extends AntimodeMenu {              </div>;          return ( -            <ButtonDropdown view={this.view} key={"highlighter"} button={button} dropdownContent={dropdownContent} /> +            <ButtonDropdown view={this.view} key={"highlighter"} button={button} dropdownContent={dropdownContent} openDropdownOnButton={true} />          );      } @@ -776,7 +800,9 @@ export default class RichTextMenu extends AntimodeMenu {          const link = this.currentLink ? this.currentLink : "";          const button = <Tooltip title={<div className="dash-tooltip">set hyperlink</div>} placement="bottom"> -            <div><FontAwesomeIcon icon="link" size="lg" /> </div> +            <button className="antimodeMenu-button color-preview-button"> +                <FontAwesomeIcon icon="link" size="lg" /> +            </button>          </Tooltip>;          const dropdownContent = @@ -788,7 +814,8 @@ export default class RichTextMenu extends AntimodeMenu {                  <button className="remove-button" onPointerDown={e => this.deleteLink()}>Remove link</button>              </div>; -        return <ButtonDropdown view={this.view} key={"link button"} button={button} dropdownContent={dropdownContent} openDropdownOnButton={true} />; +        return <ButtonDropdown view={this.view} key={"link button"} button={button} dropdownContent={dropdownContent} +            openDropdownOnButton={true} link={true} />;      }      async getTextLinkTargetTitle() { @@ -827,6 +854,7 @@ export default class RichTextMenu extends AntimodeMenu {      }      // TODO: should check for valid URL +    @undoBatch      makeLinkToURL = (target: string, lcoation: string) => {          ((this.view as any)?.TextView as FormattedTextBox).makeLinkToSelection("", target, "onRight", "", target);      } @@ -874,10 +902,11 @@ export default class RichTextMenu extends AntimodeMenu {          if (pos.nodeBefore !== null && pos.nodeBefore !== undefined) {              ref_node = pos.nodeBefore;          } -        else if (pos.nodeAfter !== null && pos.nodeAfter !== undefined) { -            ref_node = pos.nodeAfter; +        if (pos.nodeAfter !== null && pos.nodeAfter !== undefined) { +            if (!pos.nodeBefore || this.view.state.selection.$from.pos !== this.view.state.selection.$to.pos) +                ref_node = pos.nodeAfter;          } -        else if (pos.pos > 0) { +        if (!ref_node && pos.pos > 0) {              let skip = false;              for (let i: number = pos.pos - 1; i > 0; i--) {                  this.view.state.doc.nodesBetween(i, pos.pos, (node: ProsNode) => { @@ -918,16 +947,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 +990,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 ( @@ -980,6 +1015,7 @@ interface ButtonDropdownProps {      button: JSX.Element;      dropdownContent: JSX.Element;      openDropdownOnButton?: boolean; +    link?: boolean;  }  @observer @@ -1022,18 +1058,10 @@ export class ButtonDropdown extends React.Component<ButtonDropdownProps> {      render() {          return (              <div className="button-dropdown-wrapper" ref={node => this.ref = node}> -                {this.props.openDropdownOnButton ? -                    <button className="antimodeMenu-button dropdown-button-combined" onPointerDown={this.onDropdownClick}> -                        {this.props.button} -                        <FontAwesomeIcon icon="caret-down" size="sm" /> -                    </button> : -                    <> -                        {this.props.button} -                        <button className="dropdown-button antimodeMenu-button" key="antimodebutton" onPointerDown={this.onDropdownClick}> -                            <FontAwesomeIcon icon="caret-down" size="sm" /> -                        </button> -                    </>} - +                <div className="antimodeMenu-button dropdown-button-combined" onPointerDown={this.onDropdownClick}> +                    {this.props.button} +                    <div style={{ marginTop: "-8.5" }}><FontAwesomeIcon icon="caret-down" size="sm" /></div> +                </div>                  {this.showDropdown ? this.props.dropdownContent : (null)}              </div>          ); diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index 1af821738..1616500f6 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -66,9 +66,11 @@ export const nodes: { [index: string]: NodeSpec } = {      // should hold the number 1 to 6. Parsed and serialized as `<h1>` to      // `<h6>` elements.      heading: { -        attrs: { level: { default: 1 } }, -        content: "inline*", -        group: "block", +        ...ParagraphNodeSpec, +        attrs: { +            ...ParagraphNodeSpec.attrs, +            level: { default: 1 }, +        },          defining: true,          parseDOM: [{ tag: "h1", attrs: { level: 1 } },          { tag: "h2", attrs: { level: 2 } }, @@ -76,7 +78,18 @@ export const nodes: { [index: string]: NodeSpec } = {          { tag: "h4", attrs: { level: 4 } },          { tag: "h5", attrs: { level: 5 } },          { tag: "h6", attrs: { level: 6 } }], -        toDOM(node: any) { return ["h" + node.attrs.level, 0]; } +        toDOM(node) { +            const dom = toParagraphDOM(node) as any; +            const level = node.attrs.level || 1; +            dom[0] = 'h' + level; +            return dom; +        }, +        getAttrs(dom: any) { +            const attrs = getParagraphNodeAttrs(dom) as any; +            const level = Number(dom.nodeName.substring(1)) || 1; +            attrs.level = level; +            return attrs; +        }      },      // :: NodeSpec A code listing. Disallows marks or non-text inline diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx index c3e1ae22f..7bea8d01b 100644 --- a/src/client/views/pdf/PDFMenu.tsx +++ b/src/client/views/pdf/PDFMenu.tsx @@ -93,7 +93,7 @@ export default class PDFMenu extends AntimodeMenu {      @computed get highlighter() {          const button = -            <button className="antimodeMenu-button color-preview-button" title="" key="highilghter-button" onPointerDown={this.highlightClicked}> +            <button className="antimodeMenu-button color-preview-button" title="" key="highlighter-button" onPointerDown={this.highlightClicked}>                  <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} />                  <div className="color-preview" style={{ backgroundColor: this.highlightColor }}></div>              </button>; diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index c792df882..5a43a076b 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -4,7 +4,7 @@ const pdfjs = require('pdfjs-dist/es5/build/pdf.js');  import * as Pdfjs from "pdfjs-dist";  import "pdfjs-dist/web/pdf_viewer.css";  import { Dictionary } from "typescript-collections"; -import { Doc, DocListCast, FieldResult, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; +import { Doc, DocListCast, FieldResult, HeightSym, Opt, WidthSym, AclAddonly, AclEdit, AclAdmin } from "../../../fields/Doc";  import { documentSchema } from "../../../fields/documentSchemas";  import { Id } from "../../../fields/FieldSymbols";  import { InkTool } from "../../../fields/InkField"; @@ -13,7 +13,7 @@ import { createSchema, makeInterface } from "../../../fields/Schema";  import { ScriptField } from "../../../fields/ScriptField";  import { Cast, NumCast } from "../../../fields/Types";  import { PdfField } from "../../../fields/URLField"; -import { TraceMobx } from "../../../fields/util"; +import { TraceMobx, GetEffectiveAcl } from "../../../fields/util";  import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, emptyPath, intersectRect, returnZero, smoothScroll, Utils } from "../../../Utils";  import { Docs, DocUtils } from "../../documents/Documents";  import { DocumentType } from "../../documents/DocumentTypes"; @@ -565,9 +565,10 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu      @action      highlight = (color: string) => {          // creates annotation documents for current highlights -        const annotationDoc = this.makeAnnotationDocument(color); -        annotationDoc && this.props.addDocument?.(annotationDoc); -        return annotationDoc; +        const effectiveAcl = GetEffectiveAcl(this.props.Document); +        const annotationDoc = [AclAddonly, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeAnnotationDocument(color); +        annotationDoc && this.addDocument?.(annotationDoc); +        return annotationDoc as Doc ?? undefined;      }      /** | 
