diff options
Diffstat (limited to 'src/client/views/PropertiesView.tsx')
-rw-r--r-- | src/client/views/PropertiesView.tsx | 236 |
1 files changed, 221 insertions, 15 deletions
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 8e2426006..92e6cbb89 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -1,18 +1,19 @@ import React = require("react"); import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faAnchor, faArrowRight } from '@fortawesome/free-solid-svg-icons' import { Checkbox, Tooltip } from "@material-ui/core"; import { intersection } from "lodash"; import { action, autorun, computed, Lambda, observable } from "mobx"; import { observer } from "mobx-react"; import { ColorState, SketchPicker } from "react-color"; -import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, AclSelfEdit, AclSym, AclUnset, DataSym, Doc, Field, HeightSym, Opt, WidthSym } from "../../fields/Doc"; +import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, AclSelfEdit, AclSym, AclUnset, DataSym, Doc, Field, HeightSym, NumListCast, Opt, StrListCast, WidthSym } from "../../fields/Doc"; import { Id } from "../../fields/FieldSymbols"; import { InkField } from "../../fields/InkField"; import { List } from "../../fields/List"; import { ComputedField } from "../../fields/ScriptField"; import { BoolCast, Cast, NumCast, StrCast } from "../../fields/Types"; import { denormalizeEmail, GetEffectiveAcl, SharingPermissions } from "../../fields/util"; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from "../../Utils"; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents } from "../../Utils"; import { DocumentType } from "../documents/DocumentTypes"; import { CurrentUserUtils } from "../util/CurrentUserUtils"; import { DocumentManager } from "../util/DocumentManager"; @@ -31,6 +32,7 @@ import { PropertiesButtons } from "./PropertiesButtons"; import { PropertiesDocContextSelector } from "./PropertiesDocContextSelector"; import "./PropertiesView.scss"; import { DefaultStyleProvider } from "./StyleProvider"; +import { LinkManager } from "../util/LinkManager"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -57,6 +59,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @computed get isPres(): boolean { return this.selectedDoc?.type === DocumentType.PRES; } + @computed get isLink(): boolean { + return this.selectedDoc?.type === DocumentType.LINK; + } @computed get dataDoc() { return this.selectedDoc?.[DataSym]; } @observable layoutFields: boolean = false; @@ -146,8 +151,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { if (key[0] === "#") { rows.push(<div style={{ display: "flex", overflowY: "visible", marginBottom: "2px" }} key={key}> <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key}</span> - - </div>); + + </div>); } else { const contentElement = <EditableView key="editableView" contents={contents !== undefined ? Field.toString(contents as Field) : "null"} @@ -214,8 +219,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { rows.push(<div style={{ display: "flex", overflowY: "visible", marginBottom: "-1px" }} key={key}> <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key + ":"}</span> - - {contentElement} + + {contentElement} </div>); } } @@ -871,7 +876,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { onPointerDown={action(() => this.openOptions = !this.openOptions)} style={{ backgroundColor: this.openOptions ? "black" : "" }}> Options - <div className="propertiesView-settings-title-icon"> + <div className="propertiesView-settings-title-icon"> <FontAwesomeIcon icon={this.openOptions ? "caret-down" : "caret-right"} size="lg" color="white" /> </div> </div> @@ -888,7 +893,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { onPointerDown={action(() => this.openSharing = !this.openSharing)} style={{ backgroundColor: this.openSharing ? "black" : "" }}> Sharing {"&"} Permissions - <div className="propertiesView-sharing-title-icon"> + <div className="propertiesView-sharing-title-icon"> <FontAwesomeIcon icon={this.openSharing ? "caret-down" : "caret-right"} size="lg" color="white" /> </div> </div> @@ -964,7 +969,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { onPointerDown={action(() => this.openFilters = !this.openFilters)} style={{ backgroundColor: this.openFilters ? "black" : "" }}> Filters - <div className="propertiesView-filters-title-icon"> + <div className="propertiesView-filters-title-icon"> <FontAwesomeIcon icon={this.openFilters ? "caret-down" : "caret-right"} size="lg" color="white" /> </div> </div> @@ -1013,7 +1018,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { onPointerDown={action(() => this.openAppearance = !this.openAppearance)} style={{ backgroundColor: this.openAppearance ? "black" : "" }}> Appearance - <div className="propertiesView-appearance-title-icon"> + <div className="propertiesView-appearance-title-icon"> <FontAwesomeIcon icon={this.openAppearance ? "caret-down" : "caret-right"} size="lg" color="white" /> </div> </div> @@ -1028,7 +1033,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { onPointerDown={action(() => this.openTransform = !this.openTransform)} style={{ backgroundColor: this.openTransform ? "black" : "" }}> Transform - <div className="propertiesView-transform-title-icon"> + <div className="propertiesView-transform-title-icon"> <FontAwesomeIcon icon={this.openTransform ? "caret-down" : "caret-right"} size="lg" color="white" /> </div> </div> @@ -1045,7 +1050,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { onPointerDown={action(() => this.openFields = !this.openFields)} style={{ backgroundColor: this.openFields ? "black" : "" }}> Fields {"&"} Tags - <div className="propertiesView-fields-title-icon"> + <div className="propertiesView-fields-title-icon"> <FontAwesomeIcon icon={this.openFields ? "caret-down" : "caret-right"} size="lg" color="white" /> </div> </div> @@ -1066,7 +1071,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { onPointerDown={action(() => this.openContexts = !this.openContexts)} style={{ backgroundColor: this.openContexts ? "black" : "" }}> Contexts - <div className="propertiesView-contexts-title-icon"> + <div className="propertiesView-contexts-title-icon"> <FontAwesomeIcon icon={this.openContexts ? "caret-down" : "caret-right"} size="lg" color="white" /> </div> </div> @@ -1080,7 +1085,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { onPointerDown={action(() => this.openLayout = !this.openLayout)} style={{ backgroundColor: this.openLayout ? "black" : "" }}> Layout - <div className="propertiesView-layout-title-icon"> + <div className="propertiesView-layout-title-icon"> <FontAwesomeIcon icon={this.openLayout ? "caret-down" : "caret-right"} size="lg" color="white" /> </div> </div> @@ -1088,7 +1093,147 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div>; } + @observable description = Field.toString(LinkManager.currentLink?.description as any as Field); + @observable relationship = StrCast(LinkManager.currentLink?.linkRelationship); + @observable private relationshipButtonColor: string = ""; + + // @action + // handleDescriptionChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.description = e.target.value; } + // handleRelationshipChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.relationship = e.target.value; } + + @undoBatch + handleDescriptionChange = action((value: string) => { + if (LinkManager.currentLink) { + this.selectedDoc.description = value; + this.description = value; + return true; + } + }); + @undoBatch + handleLinkRelationshipChange = action((value: string) => { + if (LinkManager.currentLink) { + this.selectedDoc.linkRelationship = value; + this.relationship = value; + return true; + } + }); + + @undoBatch + setDescripValue = action((value: string) => { + if (LinkManager.currentLink) { + Doc.GetProto(LinkManager.currentLink).description = value; + return true; + } + }); + + @undoBatch + setLinkRelationshipValue = action((value: string) => { + if (LinkManager.currentLink) { + const prevRelationship = LinkManager.currentLink.linkRelationship as string; + LinkManager.currentLink.linkRelationship = value; + Doc.GetProto(LinkManager.currentLink).linkRelationship = value; + const linkRelationshipList = StrListCast(Doc.UserDoc().linkRelationshipList); + const linkRelationshipSizes = NumListCast(Doc.UserDoc().linkRelationshipSizes); + const linkColorList = StrListCast(Doc.UserDoc().linkColorList); + + // if the relationship does not exist in the list, add it and a corresponding unique randomly generated color + if (!linkRelationshipList?.includes(value)) { + linkRelationshipList.push(value); + linkRelationshipSizes.push(1); + const randColor = "rgb(" + Math.floor(Math.random() * 255) + "," + Math.floor(Math.random() * 255) + "," + Math.floor(Math.random() * 255) + ")"; + linkColorList.push(randColor); + // if the relationship is already in the list AND the new rel is different from the prev rel, update the rel sizes + } else if (linkRelationshipList && value !== prevRelationship) { + const index = linkRelationshipList.indexOf(value); + //increment size of new relationship size + if (index !== -1 && index < linkRelationshipSizes.length) { + const pvalue = linkRelationshipSizes[index]; + linkRelationshipSizes[index] = (pvalue === undefined || !Number.isFinite(pvalue) ? 1 : pvalue + 1); + } + //decrement the size of the previous relationship if it already exists (i.e. not default 'link' relationship upon link creation) + if (linkRelationshipList.includes(prevRelationship)) { + const pindex = linkRelationshipList.indexOf(prevRelationship); + if (pindex !== -1 && pindex < linkRelationshipSizes.length) { + const pvalue = linkRelationshipSizes[pindex]; + linkRelationshipSizes[pindex] = Math.max(0, (pvalue === undefined || !Number.isFinite(pvalue) ? 1 : pvalue - 1)); + } + } + + } + this.relationshipButtonColor = "rgb(62, 133, 55)"; + setTimeout(action(() => this.relationshipButtonColor = ""), 750); + return true; + } + }); + + @undoBatch + changeFollowBehavior = action((follow: string) => { + if (LinkManager.currentLink) { + this.selectedDoc.followLinkLocation = follow; + return true; + } + }); + + onSelectOutDesc = () => { + this.setDescripValue(this.description); + document.getElementById('link_description_input')?.blur(); + } + + onDescriptionKey = (e: React.KeyboardEvent<HTMLInputElement>) => { + if (e.key === "Enter") { + this.setDescripValue(this.description); + document.getElementById('link_description_input')?.blur(); + } + } + + onSelectOutRelationship = () => { + this.setLinkRelationshipValue(this.relationship); + document.getElementById('link_relationship_input')?.blur(); + } + + onRelationshipKey = (e: React.KeyboardEvent<HTMLInputElement>) => { + if (e.key === "Enter") { + this.setLinkRelationshipValue(this.relationship); + document.getElementById('link_relationship_input')?.blur(); + } + } + + toggleAnchor = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => this.selectedDoc.linkAutoMove = !this.selectedDoc.linkAutoMove))); + } + + toggleArrow = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => this.selectedDoc.displayArrow = !this.selectedDoc.displayArrow))); + } + + @computed + get editRelationship() { + return <input + autoComplete={"off"} + id="link_relationship_input" + value={StrCast(this.selectedDoc.linkRelationship)} + onKeyDown={this.onRelationshipKey} + onBlur={this.onSelectOutRelationship} + onChange={e => this.handleLinkRelationshipChange(e.currentTarget.value)} + className="text" + type="text" + /> + } + + @computed + get editDescription() { + return <input + autoComplete={"off"} + id="link_description_input" + value={StrCast(this.selectedDoc.description)} + onKeyDown={this.onDescriptionKey} + onBlur={this.onSelectOutDesc} + onChange={e => this.handleDescriptionChange(e.currentTarget.value)} + className="text" + type="text" + /> + } /** * Handles adding and removing members from the sharing panel @@ -1111,6 +1256,67 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div>; } else { + if (this.selectedDoc && this.isLink) { + return <div className="propertiesView"> + <div className="propertiesView-title"> + Linking + </div> + <div className="propertiesView-section"> + <p className="propertiesView-label">Information</p> + <div className="propertiesView-input first" id="propertiesView-category"> + <p>Link Relationship</p> + {this.editRelationship} + </div> + <div className="propertiesView-input" id="propertiesView-description"> + <p>Description</p> + {this.editDescription} + </div> + </div> + <div className="propertiesView-section"> + <p className="propertiesView-label">Behavior</p> + <div className="propertiesView-input inline first" id="propertiesView-follow"> + <p>Follow</p> + <select + name="selectList" + id="selectList" + onChange={e => this.changeFollowBehavior(e.currentTarget.value)} + value={StrCast(this.selectedDoc.followLinkLocation, "default")}> + <option value="default">Default</option> + <option value="add:left">Open in new left pane</option> + <option value="add:right">Open in new right pane</option> + <option value="replace:left">Replace left tab</option> + <option value="replace:right">Replace right tab</option> + <option value="fullScreen">Open full screen</option> + <option value="add">Open in new tab</option> + <option value="replace">Replace current tab</option> + {this.selectedDoc.linksToAnnotation + ? <option value="openExternal">Open in external page</option> + : null} + </select> + </div> + <div className="propertiesView-input inline" id="propertiesView-anchor"> + <p>Auto-move anchor</p> + <button + style={{ background: this.selectedDoc.hidden ? "gray" : !this.selectedDoc.linkAutoMove ? "" : "#4476f7", borderRadius: 3 }} + onPointerDown={this.toggleAnchor} onClick={e => e.stopPropagation()} + className="propertiesButton" + > + <FontAwesomeIcon className="fa-icon" icon={faAnchor} size="lg" /> + </button> + </div> + <div className="propertiesView-input inline" id="propertiesView-displayArrow"> + <p>Display arrow</p> + <button + style={{ background: this.selectedDoc.hidden ? "gray" : !this.selectedDoc.displayArrow ? "" : "#4476f7", borderRadius: 3 }} + onPointerDown={this.toggleArrow} onClick={e => e.stopPropagation()} + className="propertiesButton" + > + <FontAwesomeIcon className="fa-icon" icon={faArrowRight} size="lg" /> + </button> + </div> + </div> + </div >; + } if (this.selectedDoc && !this.isPres) { return <div className="propertiesView" style={{ width: this.props.width, @@ -1164,7 +1370,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { onPointerDown={action(() => { this.openPresTransitions = !this.openPresTransitions; })} style={{ backgroundColor: this.openPresTransitions ? "black" : "" }}> <FontAwesomeIcon style={{ alignSelf: "center" }} icon={"rocket"} /> Transitions - <div className="propertiesView-presTrails-title-icon"> + <div className="propertiesView-presTrails-title-icon"> <FontAwesomeIcon icon={this.openPresTransitions ? "caret-down" : "caret-right"} size="lg" color="white" /> </div> </div> |