diff options
-rw-r--r-- | src/client/util/SelectionManager.ts | 4 | ||||
-rw-r--r-- | src/client/views/PropertiesView.scss | 42 | ||||
-rw-r--r-- | src/client/views/PropertiesView.tsx | 236 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx | 53 | ||||
-rw-r--r-- | src/fields/documentSchemas.ts | 1 |
5 files changed, 318 insertions, 18 deletions
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 2cba2c1f2..6674f684d 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -4,6 +4,7 @@ import { Doc, Opt } from "../../fields/Doc"; import { DocumentType } from "../documents/DocumentTypes"; import { CollectionViewType } from "../views/collections/CollectionView"; import { DocumentView } from "../views/nodes/DocumentView"; +import { CurrentUserUtils } from "./CurrentUserUtils"; export namespace SelectionManager { @@ -43,6 +44,9 @@ export namespace SelectionManager { } @action DeselectAll(): void { + if (CurrentUserUtils.propertiesWidth > 0) { + CurrentUserUtils.propertiesWidth = 0; + } manager.SelectedSchemaDocument = undefined; Array.from(manager.SelectedViews.keys()).map(dv => dv.props.whenChildContentsActiveChanged(false)); manager.SelectedViews.clear(); diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss index 554f137cb..437df4739 100644 --- a/src/client/views/PropertiesView.scss +++ b/src/client/views/PropertiesView.scss @@ -2,6 +2,7 @@ .propertiesView { height: 100%; + width: 250; font-family: "Roboto"; cursor: auto; @@ -836,3 +837,44 @@ .properties-flyout { grid-column: 2/4; } + +.propertiesView-linking { + padding: 5%; +} + +.propertiesView-section { + padding: 10px 0; +} + +.propertiesView-input { + padding: 4px 8px; + + .text { + width: 100%; + } + + &.first { + padding-top: 6px; + } + + &.inline { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + } + + .propertiesButton { + width: 4rem; + } +} + +.propertiesView-label { + font-weight: bold; + font-size: 12.5px; + padding: 4px; + display: flex; + color: white; + padding-left: 8px; + background-color: rgb(51, 51, 51); +}
\ No newline at end of file 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> diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index 9cc887e3d..c35bb3581 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -11,6 +11,8 @@ import { SnappingManager } from "../../../util/SnappingManager"; import { DocumentView } from "../../nodes/DocumentView"; import "./CollectionFreeFormLinkView.scss"; import React = require("react"); +import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; +import { Colors } from "../../global/globalEnums"; export interface CollectionFreeFormLinkViewProps { @@ -138,6 +140,39 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo return left; //top <= document.documentElement.clientHeight && getComputedStyle(document.documentElement).overflow === "hidden"; } + @action + toggleProperties = () => { + if (CurrentUserUtils.propertiesWidth > 0) { + CurrentUserUtils.propertiesWidth = 0; + } else { + CurrentUserUtils.propertiesWidth = 250; + } + } + + onClickLine = () => { + SelectionManager.SelectSchemaViewDoc(this.props.LinkDocs[0], true) + this.toggleProperties() + } + + // componentToHex = (c: number) => { + // let hex = c.toString(16); + // return hex.length == 1 ? "0" + hex : hex; + // } + + // rgbToHex = (rgbString: string) => { + // if (rgbString != "black") { + // const splitString = rgbString.split(/rgb|\(|\)|,| /) + // let values: number[] = [] + // splitString.forEach(elt => { + // if (elt) { + // values.push(parseInt(elt)) + // } + // }) + // return "#" + this.componentToHex(values[0]) + this.componentToHex(values[1]) + this.componentToHex(values[2]); + // } + // return "#000000" + // } + @computed.struct get renderData() { this._start; SnappingManager.GetIsDragging(); const { A, B, LinkDocs } = this.props; @@ -208,15 +243,27 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo //access stroke color using index of the relationship in the color list (default black) const stroke = currRelationshipIndex === -1 || currRelationshipIndex >= linkColorList.length ? "black" : linkColorList[currRelationshipIndex]; + // const hexStroke = this.rgbToHex(stroke) //calculate stroke width/thickness based on the relative importance of the relationshipship (i.e. how many links the relationship has) //thickness varies linearly from 3px to 12px for increasing link count const strokeWidth = linkSize === -1 ? "3px" : Math.floor(2 + 10 * (linkSize / Math.max(...linkRelationshipSizes))) + "px"; + if (this.props.LinkDocs[0].displayArrow == undefined) { + this.props.LinkDocs[0].displayArrow = false; + } + return !a.width || !b.width || ((!this.props.LinkDocs[0].linkDisplay) && !aActive && !bActive) ? (null) : (<> - <path className="collectionfreeformlinkview-linkLine" style={{ pointerEvents: "all", opacity: this._opacity, strokeDasharray: SelectionManager.SelectedSchemaDoc() === this.props.LinkDocs[0] ? "2 2" : undefined, stroke, strokeWidth }} - onClick={() => SelectionManager.SelectSchemaViewDoc(this.props.LinkDocs[0], true)} - d={`M ${pt1[0]} ${pt1[1]} C ${pt1[0] + pt1norm[0]} ${pt1[1] + pt1norm[1]}, ${pt2[0] + pt2norm[0]} ${pt2[1] + pt2norm[1]}, ${pt2[0]} ${pt2[1]}`} /> + <defs> + <marker id="arrowhead" markerWidth="4" markerHeight="3" + refX="0" refY="1.5" orient="auto"> + <polygon points="0 0, 3 1.5, 0 3" fill={Colors.DARK_GRAY} /> + </marker> + </defs> + <path className="collectionfreeformlinkview-linkLine" style={{ pointerEvents: "all", opacity: this._opacity, stroke: SelectionManager.SelectedSchemaDoc() === this.props.LinkDocs[0] ? Colors.MEDIUM_BLUE : stroke, strokeWidth }} + onClick={this.onClickLine} + d={`M ${pt1[0]} ${pt1[1]} C ${pt1[0] + pt1norm[0]} ${pt1[1] + pt1norm[1]}, ${pt2[0] + pt2norm[0]} ${pt2[1] + pt2norm[1]}, ${pt2[0]} ${pt2[1]}`} + markerEnd={this.props.LinkDocs[0].displayArrow ? "url(#arrowhead)" : ""} /> {textX === undefined ? (null) : <text className="collectionfreeformlinkview-linkText" x={textX} y={textY} onPointerDown={this.pointerDown} > {Field.toString(this.props.LinkDocs[0].description as any as Field)} </text>} diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts index c35c52699..4d5ae1018 100644 --- a/src/fields/documentSchemas.ts +++ b/src/fields/documentSchemas.ts @@ -95,6 +95,7 @@ export const documentSchema = createSchema({ layers: listSpec("string"), // which layers the document is part of _lockedPosition: "boolean", // whether the document can be moved (dragged) _lockedTransform: "boolean",// whether a freeformview can pan/zoom + displayArrow: "boolean", // toggles directed arrows // drag drop properties _stayInCollection: "boolean",// whether document can be dropped into a different collection |