aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/PropertiesView.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/PropertiesView.tsx')
-rw-r--r--src/client/views/PropertiesView.tsx236
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>
- &nbsp;
- </div>);
+ &nbsp;
+ </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>
- &nbsp;
- {contentElement}
+ &nbsp;
+ {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" : "" }}>
&nbsp; <FontAwesomeIcon style={{ alignSelf: "center" }} icon={"rocket"} /> &nbsp; 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>