diff options
| author | geireann <geireann.lindfield@gmail.com> | 2021-10-14 14:30:47 -0400 |
|---|---|---|
| committer | geireann <geireann.lindfield@gmail.com> | 2021-10-14 14:30:47 -0400 |
| commit | 0f83dfd2aa9738509fdb09cd54005d1ccf5c552c (patch) | |
| tree | a9032715c1ca3b9eb3dadbb17776ed158c30b4a5 /src/client/views/linking | |
| parent | 53019659c2335906ac9e42d755548ea35dfc0365 (diff) | |
| parent | 662176f25e25d3bf31cfb8ec6e3792d18f77f37d (diff) | |
Merge branch 'master' into splash-screen-3-Anh-En-Hua
Diffstat (limited to 'src/client/views/linking')
| -rw-r--r-- | src/client/views/linking/LinkEditor.scss | 18 | ||||
| -rw-r--r-- | src/client/views/linking/LinkEditor.tsx | 116 | ||||
| -rw-r--r-- | src/client/views/linking/LinkMenu.tsx | 2 | ||||
| -rw-r--r-- | src/client/views/linking/LinkMenuGroup.tsx | 20 | ||||
| -rw-r--r-- | src/client/views/linking/LinkMenuItem.tsx | 22 | ||||
| -rw-r--r-- | src/client/views/linking/LinkPopup.scss | 2 | ||||
| -rw-r--r-- | src/client/views/linking/LinkPopup.tsx | 44 | ||||
| -rw-r--r-- | src/client/views/linking/LinkRelationshipSearch.tsx | 63 |
8 files changed, 231 insertions, 56 deletions
diff --git a/src/client/views/linking/LinkEditor.scss b/src/client/views/linking/LinkEditor.scss index e45a91d57..abd413f57 100644 --- a/src/client/views/linking/LinkEditor.scss +++ b/src/client/views/linking/LinkEditor.scss @@ -106,6 +106,24 @@ } } +.linkEditor-relationship-dropdown { + position: absolute; + width: 154px; + max-height: 90px; + overflow: auto; + background: white; + + p { + padding: 3px; + cursor: pointer; + border: 1px solid $medium-gray; + } + + p:hover { + background: $light-blue; + } +} + .linkEditor-followingDropdown { padding-left: 26px; padding-right: 6.5px; diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx index 9d0938a6e..5b5c3cd01 100644 --- a/src/client/views/linking/LinkEditor.tsx +++ b/src/client/views/linking/LinkEditor.tsx @@ -2,12 +2,14 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Tooltip } from "@material-ui/core"; import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { Doc } from "../../../fields/Doc"; -import { DateCast, StrCast } from "../../../fields/Types"; +import { Doc, NumListCast, StrListCast, Field } from "../../../fields/Doc"; +import { DateCast, StrCast, Cast } from "../../../fields/Types"; import { LinkManager } from "../../util/LinkManager"; import { undoBatch } from "../../util/UndoManager"; import './LinkEditor.scss'; +import { LinkRelationshipSearch } from "./LinkRelationshipSearch"; import React = require("react"); +import { ToString } from "../../../fields/FieldSymbols"; interface LinkEditorProps { @@ -19,13 +21,15 @@ interface LinkEditorProps { @observer export class LinkEditor extends React.Component<LinkEditorProps> { - @observable description = StrCast(LinkManager.currentLink?.description); + @observable description = Field.toString(LinkManager.currentLink?.description as any as Field); @observable relationship = StrCast(LinkManager.currentLink?.linkRelationship); @observable openDropdown: boolean = false; @observable showInfo: boolean = false; @computed get infoIcon() { if (this.showInfo) { return "chevron-up"; } return "chevron-down"; } @observable private buttonColor: string = ""; @observable private relationshipButtonColor: string = ""; + @observable private relationshipSearchVisibility: string = "none"; + @observable private searchIsActive: boolean = false; //@observable description = this.props.linkDoc.description ? StrCast(this.props.linkDoc.description) : "DESCRIPTION"; @@ -38,24 +42,74 @@ export class LinkEditor extends React.Component<LinkEditorProps> { @undoBatch setRelationshipValue = 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; } }); + /** + * returns list of strings with possible existing relationships that contain what is currently in the input field + */ + @action + getRelationshipResults = () => { + const query = this.relationship; //current content in input box + const linkRelationshipList = StrListCast(Doc.UserDoc().linkRelationshipList); + if (linkRelationshipList) { + return linkRelationshipList.filter(rel => rel.includes(query)); + } + } + + /** + * toggles visibility of the relationship search results when the input field is focused on + */ + @action + toggleRelationshipResults = () => { + this.relationshipSearchVisibility = this.relationshipSearchVisibility === "none" ? "block" : "none"; + } + @undoBatch setDescripValue = action((value: string) => { if (LinkManager.currentLink) { - LinkManager.currentLink.description = value; + Doc.GetProto(LinkManager.currentLink).description = value; this.buttonColor = "rgb(62, 133, 55)"; setTimeout(action(() => this.buttonColor = ""), 750); return true; } }); - onKey = (e: React.KeyboardEvent<HTMLInputElement>) => { + onDescriptionKey = (e: React.KeyboardEvent<HTMLInputElement>) => { if (e.key === "Enter") { this.setDescripValue(this.description); document.getElementById('input')?.blur(); @@ -69,16 +123,38 @@ export class LinkEditor extends React.Component<LinkEditorProps> { } } - onDown = () => this.setDescripValue(this.description); + onDescriptionDown = () => this.setDescripValue(this.description); onRelationshipDown = () => this.setRelationshipValue(this.relationship); + onBlur = () => { + //only hide the search results if the user clicks out of the input AND not on any of the search results + // i.e. if search is not active + if (!this.searchIsActive) { + this.toggleRelationshipResults(); + } + } + onFocus = () => { + this.toggleRelationshipResults(); + } + toggleSearchIsActive = () => { + this.searchIsActive = !this.searchIsActive; + } + @action - handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.description = e.target.value; } + handleDescriptionChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.description = e.target.value; } @action - handleRelationshipChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.relationship = e.target.value; } - + handleRelationshipChange = (e: React.ChangeEvent<HTMLInputElement>) => { + this.relationship = e.target.value; + } + @action + handleRelationshipSearchChange = (result: string) => { + this.setRelationshipValue(result); + this.toggleRelationshipResults(); + this.relationship = result; + } @computed get editRelationship() { + //NOTE: confusingly, the classnames for the following relationship JSX elements are the same as the for the description elements for shared CSS return <div className="linkEditor-description"> <div className="linkEditor-description-label">Link Relationship:</div> <div className="linkEditor-description-input"> @@ -87,11 +163,19 @@ export class LinkEditor extends React.Component<LinkEditorProps> { style={{ width: "100%" }} id="input" value={this.relationship} - placeholder={"enter link label"} - // color={"rgb(88, 88, 88)"} + autoComplete={"off"} + placeholder={"Enter link relationship"} onKeyDown={this.onRelationshipKey} onChange={this.handleRelationshipChange} + onFocus={this.onFocus} + onBlur={this.onBlur} ></input> + <LinkRelationshipSearch + results={this.getRelationshipResults()} + display={this.relationshipSearchVisibility} + handleRelationshipSearchChange={this.handleRelationshipSearchChange} + toggleSearch={this.toggleSearchIsActive} + /> </div> <div className="linkEditor-description-add-button" style={{ background: this.relationshipButtonColor }} @@ -108,17 +192,17 @@ export class LinkEditor extends React.Component<LinkEditorProps> { <div className="linkEditor-description-editing"> <input style={{ width: "100%" }} + autoComplete={"off"} id="input" value={this.description} - placeholder={"enter link label"} - // color={"rgb(88, 88, 88)"} - onKeyDown={this.onKey} - onChange={this.handleChange} + placeholder={"Enter link description"} + onKeyDown={this.onDescriptionKey} + onChange={this.handleDescriptionChange} ></input> </div> <div className="linkEditor-description-add-button" style={{ background: this.buttonColor }} - onPointerDown={this.onDown}>Set</div> + onPointerDown={this.onDescriptionDown}>Set</div> </div> </div>; } diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx index 6fc860447..53fe3f682 100644 --- a/src/client/views/linking/LinkMenu.tsx +++ b/src/client/views/linking/LinkMenu.tsx @@ -41,7 +41,7 @@ export class LinkMenu extends React.Component<Props> { /** * maps each link to a JSX element to be rendered - * @param groups LinkManager containing info of all of the links + * @param groups containing info of all of the links * @returns list of link JSX elements if there at least one linked element */ renderAllGroups = (groups: Map<string, Array<Doc>>): Array<JSX.Element> => { diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx index c7586a467..03377ad4e 100644 --- a/src/client/views/linking/LinkMenuGroup.tsx +++ b/src/client/views/linking/LinkMenuGroup.tsx @@ -1,5 +1,5 @@ import { observer } from "mobx-react"; -import { Doc } from "../../../fields/Doc"; +import { Doc, StrListCast } from "../../../fields/Doc"; import { Id } from "../../../fields/FieldSymbols"; import { Cast } from "../../../fields/Types"; import { LinkManager } from "../../util/LinkManager"; @@ -20,6 +20,22 @@ interface LinkMenuGroupProps { export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { private _menuRef = React.createRef<HTMLDivElement>(); + getBackgroundColor = (): string => { + const linkRelationshipList = StrListCast(Doc.UserDoc().linkRelationshipList); + const linkColorList = StrListCast(Doc.UserDoc().linkColorList); + let color = "white"; + // if this link's relationship property is not default "link", set its color + if (linkRelationshipList) { + const relationshipIndex = linkRelationshipList.indexOf(this.props.groupType); + const RGBcolor: string = linkColorList[relationshipIndex]; + if (RGBcolor) { + //set opacity to 0.25 by modifiying the rgb string + color = RGBcolor.slice(0, RGBcolor.length - 1) + ", 0.25)"; + } + } + return color; + } + render() { const set = new Set<Doc>(this.props.group); const groupItems = Array.from(set.keys()).map(linkDoc => { @@ -39,7 +55,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { return ( <div className="linkMenu-group" ref={this._menuRef}> - <div className="linkMenu-group-name"> + <div className="linkMenu-group-name" style={{ background: this.getBackgroundColor() }}> <p className={this.props.groupType === "*" || this.props.groupType === "" ? "" : "expand-one"}> {this.props.groupType}:</p> </div> <div className="linkMenu-group-wrapper"> diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index 53a7ae9ab..96cc6d600 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -79,7 +79,9 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { onEdit = (e: React.PointerEvent): void => { LinkManager.currentLink = this.props.linkDoc; setupMoveUpEvents(this, e, e => { - DragManager.StartDocumentDrag([this._editRef.current!], new DragManager.DocumentDragData([this.props.linkDoc]), e.x, e.y); + const dragData = new DragManager.DocumentDragData([this.props.linkDoc], "alias"); + dragData.removeDropProperties = ["hidden"]; + DragManager.StartDocumentDrag([this._editRef.current!], dragData, e.x, e.y); return true; }, emptyFunction, () => this.props.showEditor(this.props.linkDoc)); } @@ -163,19 +165,19 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { <div className="linkMenu-item-buttons" ref={this._buttonRef} > - <Tooltip title={<><div className="dash-tooltip">{this.props.linkDoc.hidden ? "Show Anchor" : "Hide Anchor"}</div></>}> - <div className="button" ref={this._editRef} onPointerDown={this.showAnchor} onClick={e => e.stopPropagation()}> - <FontAwesomeIcon className="fa-icon" icon={this.props.linkDoc.hidden ? "eye-slash" : "eye"} size="sm" /></div> + <Tooltip title={<><div className="dash-tooltip">{this.props.linkDoc.hidden ? "Show Link Anchor" : "Hide Link Anchor"}</div></>}> + <div className="button" ref={this._editRef} style={{ background: this.props.linkDoc.hidden ? "" : "#4476f7" /* $medium-blue */ }} onPointerDown={this.showAnchor} onClick={e => e.stopPropagation()}> + <FontAwesomeIcon className="fa-icon" icon={"eye"} size="sm" /></div> </Tooltip> - <Tooltip title={<><div className="dash-tooltip">{!this.props.linkDoc.linkDisplay ? "Show link" : "Hide link"}</div></>}> - <div className="button" ref={this._editRef} onPointerDown={this.showLink} onClick={e => e.stopPropagation()}> - <FontAwesomeIcon className="fa-icon" icon={!this.props.linkDoc.linkDisplay ? "eye-slash" : "eye"} size="sm" /></div> + <Tooltip title={<><div className="dash-tooltip">{this.props.linkDoc.linkDisplay ? "Hide Link Line" : "Show Link Line"}</div></>}> + <div className="button" ref={this._editRef} style={{ background: this.props.linkDoc.hidden ? "gray" : this.props.linkDoc.linkDisplay ? "#4476f7"/* $medium-blue */ : "" }} onPointerDown={this.showLink} onClick={e => e.stopPropagation()}> + <FontAwesomeIcon className="fa-icon" icon={"project-diagram"} size="sm" /></div> </Tooltip> - <Tooltip title={<><div className="dash-tooltip">{!this.props.linkDoc.linkAutoMove ? "Auto move dot" : "Freeze dot position"}</div></>}> - <div className="button" ref={this._editRef} onPointerDown={this.autoMove} onClick={e => e.stopPropagation()}> - <FontAwesomeIcon className="fa-icon" icon={this.props.linkDoc.linkAutoMove ? "play" : "pause"} size="sm" /></div> + <Tooltip title={<><div className="dash-tooltip">{this.props.linkDoc.linkAutoMove ? "Click to freeze link anchor position" : "Click to auto move link anchor"}</div></>}> + <div className="button" ref={this._editRef} style={{ background: this.props.linkDoc.hidden ? "gray" : !this.props.linkDoc.linkAutoMove ? "" : "#4476f7" /* $medium-blue */ }} onPointerDown={this.autoMove} onClick={e => e.stopPropagation()}> + <FontAwesomeIcon className="fa-icon" icon={"play"} size="sm" /></div> </Tooltip> <Tooltip title={<><div className="dash-tooltip">Edit Link</div></>}> diff --git a/src/client/views/linking/LinkPopup.scss b/src/client/views/linking/LinkPopup.scss index 8ae65158d..60c9ebfcd 100644 --- a/src/client/views/linking/LinkPopup.scss +++ b/src/client/views/linking/LinkPopup.scss @@ -5,7 +5,7 @@ height: 200px; width: 200px; position: absolute; - padding: 15px; + // padding: 15px; border-radius: 3px; input { diff --git a/src/client/views/linking/LinkPopup.tsx b/src/client/views/linking/LinkPopup.tsx index df469c53b..c8be9069c 100644 --- a/src/client/views/linking/LinkPopup.tsx +++ b/src/client/views/linking/LinkPopup.tsx @@ -1,33 +1,21 @@ -import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tooltip } from '@material-ui/core'; -import { action, observable, runInAction } from 'mobx'; +import { action, observable } from 'mobx'; import { observer } from "mobx-react"; -import { Doc, DocListCast } from '../../../fields/Doc'; -import { Cast, StrCast } from '../../../fields/Types'; -import { WebField } from '../../../fields/URLField'; -import { emptyFunction, setupMoveUpEvents, returnFalse, returnTrue, returnEmptyDoclist, returnEmptyFilter } from '../../../Utils'; -import { DocumentType } from '../../documents/DocumentTypes'; -import { DocumentManager } from '../../util/DocumentManager'; -import { DragManager } from '../../util/DragManager'; -import { Hypothesis } from '../../util/HypothesisUtils'; -import { LinkManager } from '../../util/LinkManager'; -import { undoBatch } from '../../util/UndoManager'; -import { DocumentLinksButton } from '../nodes/DocumentLinksButton'; -import { DocumentView, DocumentViewSharedProps } from '../nodes/DocumentView'; -import { LinkDocPreview } from '../nodes/LinkDocPreview'; -import './LinkPopup.scss'; -import React = require("react"); +import { EditorView } from 'prosemirror-view'; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../../Utils'; +import { DocUtils } from '../../documents/Documents'; import { CurrentUserUtils } from '../../util/CurrentUserUtils'; -import { DefaultStyleProvider } from '../StyleProvider'; import { Transform } from '../../util/Transform'; -import { DocUtils } from '../../documents/Documents'; -import { SearchBox } from '../search/SearchBox'; -import { EditorView } from 'prosemirror-view'; +import { undoBatch } from '../../util/UndoManager'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; +import { SearchBox } from '../search/SearchBox'; +import { DefaultStyleProvider } from '../StyleProvider'; +import './LinkPopup.scss'; +import React = require("react"); +import { Doc, Opt } from '../../../fields/Doc'; interface LinkPopupProps { showPopup: boolean; + linkFrom?: () => Doc | undefined; // groupType: string; // linkDoc: Doc; // docView: DocumentView; @@ -62,9 +50,10 @@ export class LinkPopup extends React.Component<LinkPopupProps> { render() { const popupVisibility = this.props.showPopup ? "block" : "none"; + const linkDoc = this.props.linkFrom ? this.props.linkFrom : undefined; return ( <div className="linkPopup-container" style={{ display: popupVisibility }}> - <div className="linkPopup-url-container"> + {/* <div className="linkPopup-url-container"> <input autoComplete="off" type="text" value={this.linkURL} placeholder="Enter URL..." onChange={this.onLinkChange} /> <button onPointerDown={e => this.makeLinkToURL(this.linkURL, "add:right")} style={{ display: "block", margin: "10px auto", }}>Apply hyperlink</button> @@ -72,14 +61,17 @@ export class LinkPopup extends React.Component<LinkPopupProps> { <div className="divider"> <div className="line"></div> <p className="divider-text">or</p> - </div> + </div> */} <div className="linkPopup-document-search-container"> {/* <i></i> <input defaultValue={""} autoComplete="off" type="text" placeholder="Search for Document..." id="search-input" className="linkPopup-searchBox searchBox-input" /> */} - <SearchBox Document={CurrentUserUtils.MySearchPanelDoc} + <SearchBox + Document={CurrentUserUtils.MySearchPanelDoc} DataDoc={CurrentUserUtils.MySearchPanelDoc} + linkFrom={linkDoc} + linkSearch={true} fieldKey="data" dropAction="move" isSelected={returnTrue} diff --git a/src/client/views/linking/LinkRelationshipSearch.tsx b/src/client/views/linking/LinkRelationshipSearch.tsx new file mode 100644 index 000000000..53da880e4 --- /dev/null +++ b/src/client/views/linking/LinkRelationshipSearch.tsx @@ -0,0 +1,63 @@ +import { observer } from "mobx-react"; +import './LinkEditor.scss'; +import React = require("react"); + +interface LinkRelationshipSearchProps { + results: string[] | undefined; + display: string; + //callback fn to set rel + hide dropdown upon setting + handleRelationshipSearchChange: (result: string) => void; + toggleSearch: () => void; +} +@observer +export class LinkRelationshipSearch extends React.Component<LinkRelationshipSearchProps> { + + handleResultClick = (e: React.MouseEvent) => { + const relationship = (e.target as HTMLParagraphElement).textContent; + if (relationship) { + this.props.handleRelationshipSearchChange(relationship); + } + } + + handleMouseEnter = () => { + this.props.toggleSearch(); + } + + handleMouseLeave = () => { + this.props.toggleSearch(); + } + + /** + * Render an empty div to increase the height of LinkEditor to accommodate 2+ results + */ + emptyDiv = () => { + if (this.props.results && this.props.results.length > 2 && this.props.display === "block") { + return <div style={{ height: "50px" }} />; + } + } + + render() { + return ( + <div className="linkEditor-relationship-dropdown-container"> + <div className="linkEditor-relationship-dropdown" + style={{ display: this.props.display }} + onMouseEnter={this.handleMouseEnter} + onMouseLeave={this.handleMouseLeave} + > + { // return a dropdown of relationship results if there exist results + this.props.results + ? this.props.results.map(result => { + return <p key={result} onClick={this.handleResultClick}> + {result} + </p>; + }) + : <p>No matching relationships</p> + } + </div> + + {/*Render an empty div to increase the height of LinkEditor to accommodate 2+ results */} + {this.emptyDiv()} + </div> + ); + } +}
\ No newline at end of file |
