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, 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'); interface LinkEditorProps { sourceDoc: Doc; linkDoc: Doc; showLinks: () => void; hideback?: boolean; } @observer export class LinkEditor extends React.Component { @observable description = Field.toString(LinkManager.currentLink?.description as any as Field); @observable relationship = StrCast(LinkManager.currentLink?.linkRelationship); @observable zoomFollow = StrCast(this.props.sourceDoc.followLinkZoom); @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"; @undoBatch deleteLink = (): void => { LinkManager.Instance.deleteLink(this.props.linkDoc); this.props.showLinks(); }; @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) { Doc.GetProto(LinkManager.currentLink).description = value; this.buttonColor = 'rgb(62, 133, 55)'; setTimeout( action(() => (this.buttonColor = '')), 750 ); return true; } }); onDescriptionKey = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { this.setDescripValue(this.description); document.getElementById('input')?.blur(); } e.stopPropagation(); }; onRelationshipKey = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { this.setRelationshipValue(this.relationship); document.getElementById('input')?.blur(); } e.stopPropagation(); }; 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 handleDescriptionChange = (e: React.ChangeEvent) => { this.description = e.target.value; }; @action handleRelationshipChange = (e: React.ChangeEvent) => { this.relationship = e.target.value; }; @action handleZoomFollowChange = (e: React.ChangeEvent) => { this.props.sourceDoc.followLinkZoom = e.target.checked; }; @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 (
Link Relationship:
Set
); } @computed get editZoomFollow() { //NOTE: confusingly, the classnames for the following relationship JSX elements are the same as the for the description elements for shared CSS return (
Zoom To Link Target:
); } @computed get editDescription() { return (
Link Description:
Set
); } @action changeDropdown = () => { this.openDropdown = !this.openDropdown; }; @undoBatch changeFollowBehavior = action((follow: string) => { this.openDropdown = false; Doc.GetProto(this.props.linkDoc).followLinkLocation = follow; }); @computed get followingDropdown() { return (
Follow Behavior:
{StrCast(this.props.linkDoc.followLinkLocation, 'default')}
this.changeFollowBehavior('default')}> Default
this.changeFollowBehavior('add:left')}> Always open in new left pane
this.changeFollowBehavior('add:right')}> Always open in new right pane
this.changeFollowBehavior('replace:right')}> Always replace right tab
this.changeFollowBehavior('replace:left')}> Always replace left tab
this.changeFollowBehavior('fullScreen')}> Always open full screen
this.changeFollowBehavior('add')}> Always open in a new tab
this.changeFollowBehavior('replace')}> Replace Tab
{this.props.linkDoc.linksToAnnotation ? (
this.changeFollowBehavior('openExternal')}> Always open in external page
) : null}
); } @action changeInfo = () => { this.showInfo = !this.showInfo; }; render() { const destination = LinkManager.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc); return !destination ? null : (
e.stopPropagation()}>
Return to link menu
} placement="top">

Editing Link to: {StrCast(destination.proto?.title, StrCast(destination.title, 'untitled'))}

Show more link information
} placement="top">
{this.showInfo ? (
{this.props.linkDoc.author ? (
{' '} Author: {StrCast(this.props.linkDoc.author)}
) : null}
{this.props.linkDoc.creationDate ? (
{' '} Creation Date: {DateCast(this.props.linkDoc.creationDate).toString()}
) : null}
) : null} {this.editDescription} {this.editRelationship} {this.editZoomFollow} {this.followingDropdown}
); } }