diff options
| author | brynnchernosky <56202540+brynnchernosky@users.noreply.github.com> | 2023-01-19 14:33:22 -0500 |
|---|---|---|
| committer | brynnchernosky <56202540+brynnchernosky@users.noreply.github.com> | 2023-01-19 14:33:22 -0500 |
| commit | 0ef7050b0792ce183c7d5cda637cb79b7a92b704 (patch) | |
| tree | d1dca8f09ddc2954c2ce88439172aeded672c0b6 /src/client/views/linking | |
| parent | ceb338752aacc383c97a0e3a9b608365a1cf39b6 (diff) | |
| parent | d5f796b433d7e72130d4109a3775347ccb10c454 (diff) | |
Merge branch 'master' of github.com:brown-dash/Dash-Web into master
Diffstat (limited to 'src/client/views/linking')
| -rw-r--r-- | src/client/views/linking/LinkEditor.scss | 334 | ||||
| -rw-r--r-- | src/client/views/linking/LinkEditor.tsx | 394 | ||||
| -rw-r--r-- | src/client/views/linking/LinkMenu.scss | 8 | ||||
| -rw-r--r-- | src/client/views/linking/LinkMenu.tsx | 50 | ||||
| -rw-r--r-- | src/client/views/linking/LinkMenuGroup.tsx | 71 | ||||
| -rw-r--r-- | src/client/views/linking/LinkMenuItem.scss | 30 | ||||
| -rw-r--r-- | src/client/views/linking/LinkMenuItem.tsx | 89 | ||||
| -rw-r--r-- | src/client/views/linking/LinkPopup.scss | 5 | ||||
| -rw-r--r-- | src/client/views/linking/LinkPopup.tsx | 13 |
9 files changed, 134 insertions, 860 deletions
diff --git a/src/client/views/linking/LinkEditor.scss b/src/client/views/linking/LinkEditor.scss deleted file mode 100644 index b0ee4e46d..000000000 --- a/src/client/views/linking/LinkEditor.scss +++ /dev/null @@ -1,334 +0,0 @@ -@import '../global/globalCssVariables'; - -.linkEditor { - width: 100%; - height: auto; - font-size: 13px; // TODO - user-select: none; - max-width: 280px; -} - -.linkEditor-button-back { - //margin-bottom: 6px; - border-radius: 10px; - width: 18px; - height: 18px; - padding: 0; - - &:hover { - cursor: pointer; - } -} - -.linkEditor-info { - padding-top: 12px; - padding-left: 5px; - padding-bottom: 3px; - //margin-bottom: 6px; - display: flex; - justify-content: space-between; - color: black; - - .linkEditor-linkedTo { - width: calc(100% - 46px); - overflow: hidden; - position: relative; - text-overflow: ellipsis; - white-space: pre; - - .linkEditor-downArrow { - &:hover { - cursor: pointer; - } - } - } -} - -.linkEditor-moreInfo { - margin-left: 12px; - padding-left: 13px; - padding-right: 6.5px; - padding-bottom: 4px; - font-size: 9px; - //font-style: italic; - text-decoration-color: grey; - - .button { - color: black; - - &:hover { - cursor: pointer; - } - } -} - -.linkEditor-zoomFollow { - padding-left: 26px; - padding-right: 6.5px; - padding-bottom: 3.5px; - display: flex; - - .linkEditor-zoomFollow-label { - text-decoration-color: black; - color: black; - line-height: 1.7; - } - - .linkEditor-zoomFollow-input { - display: block; - width: 20px; - } -} -.linkEditor-deleteBtn { - padding-left: 3px; -} - -.linkEditor-description { - padding-left: 26px; - padding-bottom: 3.5px; - display: flex; - - .linkEditor-description-label { - text-decoration-color: black; - color: black; - } - - .linkEditor-description-input { - display: flex; - - .linkEditor-description-editing { - min-width: 85%; - //border: 1px solid grey; - //border-radius: 4px; - padding-left: 2px; - //margin-right: 4px; - color: black; - text-decoration-color: grey; - } - - .linkEditor-description-add-button { - display: inline; - border-radius: 7px; - font-size: 9px; - background: black; - height: 80%; - color: white; - padding: 3px; - margin-left: 3px; - - &:hover { - cursor: pointer; - background: grey; - } - } - } -} - -.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; - padding-bottom: 15px; - display: flex; - - &:hover { - cursor: pointer; - } - - .linkEditor-followingDropdown-label { - color: black; - padding-right: 3px; - } - - .linkEditor-followingDropdown-dropdown { - .linkEditor-followingDropdown-header { - border: 1px solid grey; - border-radius: 4px; - //background-color: rgb(236, 236, 236); - padding-left: 2px; - padding-right: 2px; - text-decoration-color: black; - color: rgb(94, 94, 94); - - .linkEditor-followingDropdown-icon { - float: right; - color: black; - } - } - - .linkEditor-followingDropdown-optionsList { - padding-left: 3px; - padding-right: 3px; - - &:last-child { - border-bottom: none; - } - - .linkEditor-followingDropdown-option { - border: 0.25px solid grey; - //background-color: rgb(236, 236, 236); - padding-left: 2px; - padding-right: 2px; - color: grey; - text-decoration-color: grey; - font-size: 9px; - border-top: none; - - &:hover { - background-color: rgb(187, 220, 231); - } - } - } - } -} - -.linkEditor-button, -.linkEditor-addbutton { - width: 15%; - border-radius: 7px; - font-size: 9px; - background: black; - padding: 3px; - height: 80%; - color: white; - text-align: center; - margin: auto; - margin-left: 3px; - > svg { - margin: auto; - } - &:disabled { - background-color: gray; - } -} - -.linkEditor-addbutton { - margin-left: 0px; -} - -.linkEditor-groupsLabel { - display: flex; - justify-content: space-between; -} - -.linkEditor-group { - background-color: $light-gray; - padding: 6px; - margin: 3px 0; - border-radius: 3px; - - .linkEditor-group-row { - display: flex; - margin-bottom: 3px; - } - - .linkEditor-group-row-label { - margin-right: 6px; - display: inline-block; - } - - .linkEditor-metadata-row { - display: flex; - justify-content: space-between; - margin-bottom: 6px; - - .linkEditor-error { - border-color: red; - } - - input { - width: calc(50% - 16px); - height: 20px; - } - - button { - width: 20px; - height: 20px; - margin-left: 3px; - padding: 0; - font-size: 10px; - } - } -} - -.linkEditor-dropdown { - width: 100%; - position: relative; - z-index: 999; - - input { - width: 100%; - } - - .linkEditor-options-wrapper { - width: 100%; - position: absolute; - top: 19px; - left: 0; - display: flex; - flex-direction: column; - } - - .linkEditor-option { - background-color: $light-gray; - border: 1px solid $medium-gray; - border-top: 0; - padding: 3px; - cursor: pointer; - - &:hover { - background-color: lightgray; - } - - &.onDown { - background-color: gray; - } - } -} - -.linkEditor-typeButton { - background-color: transparent; - color: $dark-gray; - height: 20px; - padding: 0 3px; - padding-bottom: 2px; - text-align: left; - text-transform: none; - letter-spacing: normal; - font-size: 12px; - font-weight: bold; - display: inline-block; - width: calc(100% - 40px); - - &:hover { - background-color: $white; - } -} - -.linkEditor-group-buttons { - height: 20px; - display: flex; - justify-content: flex-end; - margin-top: 5px; - - .linkEditor-button { - margin-left: 3px; - } -} diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx deleted file mode 100644 index 1697062f4..000000000 --- a/src/client/views/linking/LinkEditor.tsx +++ /dev/null @@ -1,394 +0,0 @@ -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, BoolCast } 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 { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils'; - -interface LinkEditorProps { - sourceDoc: Doc; - linkDoc: Doc; - showLinks?: () => void; - hideback?: boolean; -} -@observer -export class LinkEditor extends React.Component<LinkEditorProps> { - @observable description = Field.toString(LinkManager.currentLink?.description as any as Field); - @observable relationship = StrCast(LinkManager.currentLink?.linkRelationship); - @observable zoomFollow = BoolCast(this.props.sourceDoc.followLinkZoom); - @observable audioFollow = BoolCast(this.props.sourceDoc.followLinkAudio); - @observable openDropdown: boolean = false; - @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 - 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<HTMLInputElement>) => { - if (e.key === 'Enter') { - this.setDescripValue(this.description); - document.getElementById('input')?.blur(); - } - e.stopPropagation(); - }; - - onRelationshipKey = (e: React.KeyboardEvent<HTMLInputElement>) => { - 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<HTMLInputElement>) => { - this.description = e.target.value; - }; - @action - handleRelationshipChange = (e: React.ChangeEvent<HTMLInputElement>) => { - this.relationship = e.target.value; - }; - @action - handleZoomFollowChange = () => { - this.props.sourceDoc.followLinkZoom = !this.props.sourceDoc.followLinkZoom; - }; - @action - handleAudioFollowChange = () => { - this.props.sourceDoc.followLinkAudio = !this.props.sourceDoc.followLinkAudio; - }; - @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">Relationship:</div> - <div className="linkEditor-description-input"> - <div className="linkEditor-description-editing"> - <input - style={{ width: '100%' }} - id="input" - value={this.relationship} - 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 }} onPointerDown={this.onRelationshipDown}> - Set - </div> - </div> - </div> - ); - } - @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 ( - <div className="linkEditor-zoomFollow"> - <div className="linkEditor-zoomFollow-label">Zoom To Link Target:</div> - <div className="linkEditor-zoomFollow-input"> - <div className="linkEditor-zoomFollow-editing"> - <input type="checkbox" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, this.handleZoomFollowChange)} defaultChecked={this.zoomFollow} /> - </div> - </div> - </div> - ); - } - - @computed - get editAudioFollow() { - //NOTE: confusingly, the classnames for the following relationship JSX elements are the same as the for the description elements for shared CSS - console.log('AudioFollow:' + this.audioFollow); - return ( - <div className="linkEditor-zoomFollow"> - <div className="linkEditor-zoomFollow-label">Play Target Audio:</div> - <div className="linkEditor-zoomFollow-input"> - <div className="linkEditor-zoomFollow-editing"> - <input type="checkbox" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, this.handleAudioFollowChange)} defaultChecked={this.audioFollow} /> - </div> - </div> - </div> - ); - } - - @computed - get editDescription() { - return ( - <div className="linkEditor-description"> - <div className="linkEditor-description-label">Description:</div> - <div className="linkEditor-description-input"> - <div className="linkEditor-description-editing"> - <input style={{ width: '100%' }} autoComplete={'off'} id="input" value={this.description} placeholder={'Enter link description'} onKeyDown={this.onDescriptionKey} onChange={this.handleDescriptionChange}></input> - </div> - <div className="linkEditor-description-add-button" style={{ background: this.buttonColor }} onPointerDown={this.onDescriptionDown}> - Set - </div> - </div> - </div> - ); - } - - @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 ( - <div className="linkEditor-followingDropdown"> - <div className="linkEditor-followingDropdown-label">Follow Behavior:</div> - <div className="linkEditor-followingDropdown-dropdown"> - <div className="linkEditor-followingDropdown-header" onPointerDown={this.changeDropdown}> - {StrCast(this.props.linkDoc.followLinkLocation, 'default')} - <FontAwesomeIcon className="linkEditor-followingDropdown-icon" icon={this.openDropdown ? 'chevron-up' : 'chevron-down'} size={'lg'} /> - </div> - <div className="linkEditor-followingDropdown-optionsList" style={{ display: this.openDropdown ? '' : 'none' }}> - <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('default')}> - Default - </div> - <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('add:left')}> - Always open in new left pane - </div> - <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('add:right')}> - Always open in new right pane - </div> - <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('replace:right')}> - Always replace right tab - </div> - <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('replace:left')}> - Always replace left tab - </div> - <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('fullScreen')}> - Always open full screen - </div> - <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('add')}> - Always open in a new tab - </div> - <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('replace')}> - Replace Tab - </div> - {this.props.linkDoc.linksToAnnotation ? ( - <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('openExternal')}> - Always open in external page - </div> - ) : null} - </div> - </div> - </div> - ); - } - - autoMove = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.linkAutoMove = !this.props.linkDoc.linkAutoMove)))); - }; - - showAnchor = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.hidden = !this.props.linkDoc.hidden)))); - }; - - showLink = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.linkDisplay = !this.props.linkDoc.linkDisplay)))); - }; - - deleteLink = (e: React.PointerEvent): void => { - setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => LinkManager.Instance.deleteLink(this.props.linkDoc)))); - }; - - render() { - const destination = LinkManager.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc); - - return !destination ? null : ( - <div className="linkEditor" tabIndex={0} onKeyDown={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> - <div className="linkEditor-info"> - {!this.props.showLinks ? null : ( - <Tooltip title={<div className="dash-tooltip">Return to link menu</div>} placement="top"> - <button className="linkEditor-button-back" style={{ display: this.props.hideback ? 'none' : '' }} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => this.props.showLinks?.())}> - <FontAwesomeIcon icon="arrow-left" size="sm" />{' '} - </button> - </Tooltip> - )} - <p className="linkEditor-linkedTo"> - Editing Link to: <b>{StrCast(destination.proto?.title, StrCast(destination.title, 'untitled'))}</b> - </p> - <Tooltip title={<div className="dash-tooltip">Delete Link</div>}> - <div className="linkEditor-deleteBtn" onPointerDown={this.deleteLink} onClick={e => e.stopPropagation()}> - <FontAwesomeIcon className="fa-icon" icon="trash" size="sm" /> - </div> - </Tooltip> - </div> - <div className="linkEditor-moreInfo"> - {this.props.linkDoc.author ? ( - <> - {' '} - <b>Author:</b> {StrCast(this.props.linkDoc.author)} - </> - ) : null} - {this.props.linkDoc.creationDate ? ( - <> - {' '} - <b>Creation Date:</b> - {DateCast(this.props.linkDoc.creationDate).toString()} - </> - ) : null} - </div> - - {this.editDescription} - {this.editRelationship} - {this.editZoomFollow} - {this.editAudioFollow} - <div className="linkEditor-description"> - Show Anchor: - <Tooltip title={<div className="dash-tooltip">{this.props.linkDoc.hidden ? 'Show Link Anchor' : 'Hide Link Anchor'}</div>}> - <div - className="linkEditor-button" - style={{ background: this.props.linkDoc.hidden ? 'gray' : '#4476f7' /* $medium-blue */ }} - onPointerDown={this.showAnchor} - onClick={e => e.stopPropagation()}> - <FontAwesomeIcon className="fa-icon" icon={'eye'} size="sm" /> - </div> - </Tooltip> - </div> - <div className="linkEditor-description"> - Show Link Line: - <Tooltip title={<div className="dash-tooltip">{this.props.linkDoc.linkDisplay ? 'Hide Link Line' : 'Show Link Line'}</div>}> - <div - className="linkEditor-button" - 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> - </div> - <div className="linkEditor-description"> - Freeze Anchor: - <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="linkEditor-button" - 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> - </div> - {this.followingDropdown} - </div> - ); - } -} diff --git a/src/client/views/linking/LinkMenu.scss b/src/client/views/linking/LinkMenu.scss index 77c16a28f..80cf93ed8 100644 --- a/src/client/views/linking/LinkMenu.scss +++ b/src/client/views/linking/LinkMenu.scss @@ -1,4 +1,4 @@ -@import "../global/globalCssVariables"; +@import '../global/globalCssVariables'; .linkMenu { width: auto; @@ -13,7 +13,6 @@ border: 1px solid #e4e4e4; box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23); background: white; - min-width: 170px; max-height: 230px; overflow-y: scroll; z-index: 10; @@ -22,7 +21,7 @@ .linkMenu-list { white-space: nowrap; overflow-x: hidden; - width: 240px; + width: 100%; scrollbar-color: white; &:last-child { @@ -39,7 +38,6 @@ border-bottom: 0.5px solid lightgray; //@extend: 5px 0; - &:last-child { border-bottom: none; } @@ -76,4 +74,4 @@ display: none; } } -}
\ No newline at end of file +} diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx index 0096a58bd..c9112eec3 100644 --- a/src/client/views/linking/LinkMenu.tsx +++ b/src/client/views/linking/LinkMenu.tsx @@ -1,19 +1,19 @@ -import { action, computed, observable } from 'mobx'; +import { action, observable } from 'mobx'; import { observer } from 'mobx-react'; import { Doc } from '../../../fields/Doc'; +import { DocCast } from '../../../fields/Types'; import { LinkManager } from '../../util/LinkManager'; import { DocumentView } from '../nodes/DocumentView'; import { LinkDocPreview } from '../nodes/LinkDocPreview'; -import { LinkEditor } from './LinkEditor'; import './LinkMenu.scss'; import { LinkMenuGroup } from './LinkMenuGroup'; import React = require('react'); interface Props { docView: DocumentView; - position?: { x?: number; y?: number }; + style?: { left: number; top: number }; itemHandler?: (doc: Doc) => void; - clearLinkEditor: () => void; + clearLinkEditor?: () => void; } /** @@ -22,23 +22,15 @@ interface Props { @observer export class LinkMenu extends React.Component<Props> { _editorRef = React.createRef<HTMLDivElement>(); - @observable _editingLink?: Doc; @observable _linkMenuRef = React.createRef<HTMLDivElement>(); - @computed get position() { - return this.props.position ?? (dv => ({ x: dv?.left || 0, y: (dv?.bottom || 0) + 15 }))(this.props.docView.getBounds()); - } - - clear = action(() => { - this.props.clearLinkEditor(); - this._editingLink = undefined; - }); + clear = () => this.props.clearLinkEditor?.(); componentDidMount() { - document.addEventListener('pointerdown', this.onPointerDown, true); + this.props.clearLinkEditor && document.addEventListener('pointerdown', this.onPointerDown, true); } componentWillUnmount() { - document.removeEventListener('pointerdown', this.onPointerDown, true); + this.props.clearLinkEditor && document.removeEventListener('pointerdown', this.onPointerDown, true); } onPointerDown = action((e: PointerEvent) => { @@ -55,32 +47,20 @@ export class LinkMenu extends React.Component<Props> { */ renderAllGroups = (groups: Map<string, Array<Doc>>): Array<JSX.Element> => { const linkItems = Array.from(groups.entries()).map(group => ( - <LinkMenuGroup - key={group[0]} - itemHandler={this.props.itemHandler} - docView={this.props.docView} - sourceDoc={this.props.docView.props.Document} - group={group[1]} - groupType={group[0]} - clearLinkEditor={this.clear} - showEditor={action(linkDoc => (this._editingLink = linkDoc))} - /> + <LinkMenuGroup key={group[0]} itemHandler={this.props.itemHandler} docView={this.props.docView} sourceDoc={this.props.docView.props.Document} group={group[1]} groupType={group[0]} clearLinkEditor={this.clear} /> )); - return linkItems.length ? linkItems : this.props.position ? [<></>] : [<p key="">No links have been created yet. Drag the linking button onto another document to create a link.</p>]; + return linkItems.length ? linkItems : this.props.style ? [<></>] : [<p key="">No links have been created yet. Drag the linking button onto another document to create a link.</p>]; }; render() { - const sourceDoc = this.props.docView.props.Document; + const sourceDoc = this.props.docView.rootDoc; + const sourceAnchor = this.props.docView.anchorViewDoc ?? sourceDoc; + const style = this.props.style ?? (dv => ({ left: dv?.left || 0, top: this.props.docView.topMost ? undefined : (dv?.bottom || 0) + 15, bottom: this.props.docView.topMost ? 20 : undefined, maxWidth: 200 }))(this.props.docView.getBounds()); + return ( - <div className="linkMenu" ref={this._linkMenuRef} style={{ left: this.position.x, top: this.props.docView.topMost ? undefined : this.position.y, bottom: this.props.docView.topMost ? 20 : undefined }}> - {this._editingLink ? ( - <div className="linkMenu-listEditor"> - <LinkEditor sourceDoc={sourceDoc} linkDoc={this._editingLink} showLinks={action(() => (this._editingLink = undefined))} /> - </div> - ) : ( - <div className="linkMenu-list">{this.renderAllGroups(LinkManager.Instance.getRelatedGroupedLinks(sourceDoc))}</div> - )} + <div className="linkMenu" ref={this._linkMenuRef} style={{ ...style }}> + <div className="linkMenu-list">{this.renderAllGroups(LinkManager.Instance.getRelatedGroupedLinks(sourceAnchor))}</div> </div> ); } diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx index fa6a2f506..d02a1c4eb 100644 --- a/src/client/views/linking/LinkMenuGroup.tsx +++ b/src/client/views/linking/LinkMenuGroup.tsx @@ -1,20 +1,20 @@ -import { observer } from "mobx-react"; -import { observable, action } from "mobx"; -import { Doc, StrListCast } from "../../../fields/Doc"; -import { Id } from "../../../fields/FieldSymbols"; -import { Cast } from "../../../fields/Types"; -import { LinkManager } from "../../util/LinkManager"; -import { DocumentView } from "../nodes/DocumentView"; +import { observer } from 'mobx-react'; +import { observable, action } from 'mobx'; +import { Doc, StrListCast } from '../../../fields/Doc'; +import { Id } from '../../../fields/FieldSymbols'; +import { Cast, DocCast } from '../../../fields/Types'; +import { LinkManager } from '../../util/LinkManager'; +import { DocumentView } from '../nodes/DocumentView'; import './LinkMenu.scss'; -import { LinkMenuItem } from "./LinkMenuItem"; -import React = require("react"); +import { LinkMenuItem } from './LinkMenuItem'; +import React = require('react'); +import { DocumentType } from '../../documents/DocumentTypes'; interface LinkMenuGroupProps { sourceDoc: Doc; group: Doc[]; groupType: string; - clearLinkEditor: () => void; - showEditor: (linkDoc: Doc) => void; + clearLinkEditor?: () => void; docView: DocumentView; itemHandler?: (doc: Doc) => void; } @@ -26,49 +26,60 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { getBackgroundColor = (): string => { const linkRelationshipList = StrListCast(Doc.UserDoc().linkRelationshipList); const linkColorList = StrListCast(Doc.UserDoc().linkColorList); - let color = "white"; + 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)"; + color = RGBcolor.slice(0, RGBcolor.length - 1) + ', 0.25)'; } } return color; - } + }; @observable _collapsed = false; render() { const set = new Set<Doc>(this.props.group); const groupItems = Array.from(set.keys()).map(linkDoc => { - const destination = LinkManager.getOppositeAnchor(linkDoc, this.props.sourceDoc) || - LinkManager.getOppositeAnchor(linkDoc, Cast(linkDoc.anchor2, Doc, null).annotationOn === this.props.sourceDoc ? Cast(linkDoc.anchor2, Doc, null) : Cast(linkDoc.anchor1, Doc, null)); - if (destination && this.props.sourceDoc) { - return <LinkMenuItem key={linkDoc[Id]} + const sourceDoc = + this.props.docView.anchorViewDoc ?? + (this.props.docView.rootDoc.type === DocumentType.LINK // + ? this.props.docView.props.LayoutTemplateString?.includes('anchor1') + ? DocCast(linkDoc.anchor1) + : DocCast(linkDoc.anchor2) + : this.props.sourceDoc); + const destDoc = !sourceDoc + ? undefined + : this.props.docView.rootDoc.type === DocumentType.LINK + ? this.props.docView.props.LayoutTemplateString?.includes('anchor1') + ? DocCast(linkDoc.anchor2) + : DocCast(linkDoc.anchor1) + : LinkManager.getOppositeAnchor(linkDoc, sourceDoc) || LinkManager.getOppositeAnchor(linkDoc, Cast(linkDoc.anchor2, Doc, null).annotationOn === sourceDoc ? Cast(linkDoc.anchor2, Doc, null) : Cast(linkDoc.anchor1, Doc, null)); + return !destDoc || !sourceDoc ? null : ( + <LinkMenuItem + key={linkDoc[Id]} itemHandler={this.props.itemHandler} groupType={this.props.groupType} docView={this.props.docView} linkDoc={linkDoc} - sourceDoc={this.props.sourceDoc} - destinationDoc={destination} + sourceDoc={sourceDoc} + destinationDoc={destDoc} clearLinkEditor={this.props.clearLinkEditor} - showEditor={this.props.showEditor} - menuRef={this._menuRef} />; - } + menuRef={this._menuRef} + /> + ); }); return ( <div className="linkMenu-group" ref={this._menuRef}> - <div className="linkMenu-group-name" onClick={action(() => this._collapsed = !this._collapsed)} style={{ background: this.getBackgroundColor() }}> - <p className={this.props.groupType === "*" || this.props.groupType === "" ? "" : "expand-one"}> {this.props.groupType}:</p> + <div className="linkMenu-group-name" onClick={action(() => (this._collapsed = !this._collapsed))} style={{ background: this.getBackgroundColor() }}> + <p className={this.props.groupType === '*' || this.props.groupType === '' ? '' : 'expand-one'}> {this.props.groupType}:</p> </div> - {this._collapsed ? (null) : <div className="linkMenu-group-wrapper"> - {groupItems} - </div>} - </div > + {this._collapsed ? null : <div className="linkMenu-group-wrapper">{groupItems}</div>} + </div> ); } -}
\ No newline at end of file +} diff --git a/src/client/views/linking/LinkMenuItem.scss b/src/client/views/linking/LinkMenuItem.scss index 8333aa374..806a2c381 100644 --- a/src/client/views/linking/LinkMenuItem.scss +++ b/src/client/views/linking/LinkMenuItem.scss @@ -1,7 +1,7 @@ -@import "../global/globalCssVariables"; +@import '../global/globalCssVariables'; .linkMenu-item { - // border-top: 0.5px solid $medium-gray; + // border-top: 0.5px solid $medium-gray; position: relative; display: flex; border-top: 0.5px solid #cdcdcd; @@ -14,7 +14,6 @@ background-color: white; - .linkMenu-name { position: relative; width: auto; @@ -22,9 +21,7 @@ display: flex; .linkMenu-text { - - // padding: 4px 2px; - //display: inline; + width: 100%; .linkMenu-source-title { text-decoration: none; @@ -35,9 +32,7 @@ margin-left: 20px; } - .linkMenu-title-wrapper { - display: flex; align-items: center; min-height: 20px; @@ -59,7 +54,7 @@ .linkMenu-destination-title { text-decoration: none; - color: #4476F7; + color: #4476f7; font-size: 13px; line-height: 0.9; padding-bottom: 2px; @@ -84,7 +79,6 @@ font-size: 9px; line-height: 0.9; margin-left: 20px; - max-width: 125px; height: auto; white-space: break-spaces; } @@ -96,9 +90,7 @@ //overflow-wrap: break-word; user-select: none; } - } - } .linkMenu-item-content { @@ -114,15 +106,13 @@ } &:hover { - background-color: rgb(201, 239, 252); .linkMenu-item-buttons { - display: flex; + opacity: 1; } .linkMenu-item-content { - .linkMenu-destination-title { text-decoration: underline; color: rgb(60, 90, 156); @@ -135,11 +125,9 @@ .linkMenu-item-buttons { //@extend: right; - position: absolute; - top: 50%; - right: 0; - transform: translateY(-50%); - display: none; + position: relative; + display: flex; + opacity: 0; .button { width: 20px; @@ -172,4 +160,4 @@ cursor: pointer; } } -}
\ No newline at end of file +} diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index 3f9db2612..91b63c1a6 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -1,17 +1,20 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; -import { action, observable } from 'mobx'; +import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, DocListCast } from '../../../fields/Doc'; -import { Cast, StrCast } from '../../../fields/Types'; +import { Cast, DocCast, StrCast } from '../../../fields/Types'; import { WebField } from '../../../fields/URLField'; -import { emptyFunction, setupMoveUpEvents } from '../../../Utils'; +import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils'; import { DocumentType } from '../../documents/DocumentTypes'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager } from '../../util/DragManager'; import { LinkFollower } from '../../util/LinkFollower'; import { LinkManager } from '../../util/LinkManager'; +import { SelectionManager } from '../../util/SelectionManager'; +import { SettingsManager } from '../../util/SettingsManager'; +import { undoBatch } from '../../util/UndoManager'; import { DocumentView } from '../nodes/DocumentView'; import { LinkDocPreview } from '../nodes/LinkDocPreview'; import './LinkMenuItem.scss'; @@ -23,8 +26,7 @@ interface LinkMenuItemProps { docView: DocumentView; sourceDoc: Doc; destinationDoc: Doc; - clearLinkEditor: () => void; - showEditor: (linkDoc: Doc) => void; + clearLinkEditor?: () => void; menuRef: React.Ref<HTMLDivElement>; itemHandler?: (doc: Doc) => void; } @@ -68,7 +70,6 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { private _drag = React.createRef<HTMLDivElement>(); _editRef = React.createRef<HTMLDivElement>(); - _buttonRef = React.createRef<HTMLDivElement>(); @observable private _showMore: boolean = false; @action toggleShowMore(e: React.PointerEvent) { @@ -76,8 +77,18 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { this._showMore = !this._showMore; } - onEdit = (e: React.PointerEvent): void => { - LinkManager.currentLink = this.props.linkDoc; + @computed get sourceAnchor() { + const ldoc = this.props.linkDoc; + if (this.props.sourceDoc !== ldoc.anchor1 && this.props.sourceDoc !== ldoc.anchor2) { + if (Doc.AreProtosEqual(DocCast(DocCast(ldoc.anchor1).annotationOn), this.props.sourceDoc)) return DocCast(ldoc.anchor1); + if (Doc.AreProtosEqual(DocCast(DocCast(ldoc.anchor2).annotationOn), this.props.sourceDoc)) return DocCast(ldoc.anchor2); + } + return this.props.sourceDoc; + } + @action + onEdit = (e: React.PointerEvent) => { + LinkManager.currentLink = this.props.linkDoc === LinkManager.currentLink ? undefined : this.props.linkDoc; + LinkManager.currentLinkAnchor = this.sourceAnchor; setupMoveUpEvents( this, e, @@ -88,7 +99,12 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { return true; }, emptyFunction, - () => this.props.showEditor(this.props.linkDoc) + action(() => { + SelectionManager.SelectView(this.props.docView, false); + if ((SettingsManager.propertiesWidth ?? 0) < 100) { + SettingsManager.propertiesWidth = 250; + } + }) ); }; @@ -100,12 +116,12 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { const eleClone: any = this._drag.current!.cloneNode(true); eleClone.style.transform = `translate(${e.x}px, ${e.y}px)`; StartLinkTargetsDrag(eleClone, this.props.docView, e.x, e.y, this.props.sourceDoc, [this.props.linkDoc]); - this.props.clearLinkEditor(); + this.props.clearLinkEditor?.(); return true; }, emptyFunction, () => { - this.props.clearLinkEditor(); + this.props.clearLinkEditor?.(); if (this.props.itemHandler) { this.props.itemHandler?.(this.props.linkDoc); } else { @@ -116,21 +132,20 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { ? Cast(this.props.linkDoc.anchor12, Doc, null) : undefined; - if (focusDoc) this.props.docView.ComponentView?.scrollFocus?.(focusDoc, true); + if (focusDoc) this.props.docView.ComponentView?.scrollFocus?.(focusDoc, { instant: true }); LinkFollower.FollowLink(this.props.linkDoc, this.props.sourceDoc, this.props.docView.props, false); } } ); }; + deleteLink = (e: React.PointerEvent): void => setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => LinkManager.Instance.deleteLink(this.props.linkDoc)))); + render() { const destinationIcon = Doc.toIcon(this.props.destinationDoc) as any as IconProp; const title = StrCast(this.props.destinationDoc.title).length > 18 ? StrCast(this.props.destinationDoc.title).substr(0, 14) + '...' : this.props.destinationDoc.title; - // ... - // from anika to bob: here's where the text that is specifically linked would show up (linkDoc.storedText) - // ... const source = this.props.sourceDoc.type === DocumentType.RTF ? this.props.linkDoc.storedText @@ -141,24 +156,34 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { : undefined; return ( - <div className="linkMenu-item"> + <div className="linkMenu-item" style={{ background: LinkManager.currentLink === this.props.linkDoc ? 'lightBlue' : undefined }}> <div className={'linkMenu-item-content expand-two'}> <div ref={this._drag} className="linkMenu-name" //title="drag to view target. click to customize." - onPointerLeave={LinkDocPreview.Clear} - onPointerEnter={e => - this.props.linkDoc && - LinkDocPreview.SetLinkInfo({ - docProps: this.props.docView.props, - linkSrc: this.props.sourceDoc, - linkDoc: this.props.linkDoc, - showHeader: false, - location: [this._drag.current?.getBoundingClientRect().right ?? 100, this._drag.current?.getBoundingClientRect().top ?? e.clientY], - }) - } onPointerDown={this.onLinkButtonDown}> - <div className="linkMenu-text"> + <div className="linkMenu-item-buttons"> + <Tooltip title={<div className="dash-tooltip">Edit Link</div>}> + <div className="button" style={{ background: LinkManager.currentLink === this.props.linkDoc ? 'black' : 'gray' }} ref={this._editRef} onPointerDown={this.onEdit} onClick={e => e.stopPropagation()}> + <FontAwesomeIcon className="fa-icon" icon="edit" size="sm" /> + </div> + </Tooltip> + </div> + <div + className="linkMenu-text" + onPointerLeave={LinkDocPreview.Clear} + onPointerEnter={e => + this.props.linkDoc && + this.props.clearLinkEditor && + LinkDocPreview.SetLinkInfo({ + docProps: this.props.docView.props, + linkSrc: this.props.sourceDoc, + linkDoc: this.props.linkDoc, + showHeader: false, + location: [this._drag.current?.getBoundingClientRect().right ?? 100, this._drag.current?.getBoundingClientRect().top ?? e.clientY], + noPreview: false, + }) + }> {source ? ( <p className="linkMenu-source-title"> {' '} @@ -176,10 +201,10 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { {!this.props.linkDoc.description ? null : <p className="linkMenu-description">{StrCast(this.props.linkDoc.description)}</p>} </div> - <div className="linkMenu-item-buttons" ref={this._buttonRef}> - <Tooltip title={<div className="dash-tooltip">Edit Link</div>}> - <div className="button" ref={this._editRef} onPointerDown={this.onEdit} onClick={e => e.stopPropagation()}> - <FontAwesomeIcon className="fa-icon" icon="edit" size="sm" /> + <div className="linkMenu-item-buttons"> + <Tooltip title={<div className="dash-tooltip">Delete Link</div>}> + <div className="button" style={{ background: 'red' }} onPointerDown={this.deleteLink} onClick={e => e.stopPropagation()}> + <FontAwesomeIcon className="fa-icon" icon="trash" size="sm" /> </div> </Tooltip> </div> diff --git a/src/client/views/linking/LinkPopup.scss b/src/client/views/linking/LinkPopup.scss index 60c9ebfcd..b20ad9476 100644 --- a/src/client/views/linking/LinkPopup.scss +++ b/src/client/views/linking/LinkPopup.scss @@ -1,7 +1,7 @@ .linkPopup-container { background: white; box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23); - top: 35px; + top: 0; height: 200px; width: 200px; position: absolute; @@ -38,8 +38,7 @@ } } - .searchBox-container { background: pink; } -}
\ No newline at end of file +} diff --git a/src/client/views/linking/LinkPopup.tsx b/src/client/views/linking/LinkPopup.tsx index 0bcb68f82..7bdace2b6 100644 --- a/src/client/views/linking/LinkPopup.tsx +++ b/src/client/views/linking/LinkPopup.tsx @@ -11,10 +11,13 @@ import { SearchBox } from '../search/SearchBox'; import { DefaultStyleProvider } from '../StyleProvider'; import './LinkPopup.scss'; import React = require('react'); +import { OpenWhere } from '../nodes/DocumentView'; interface LinkPopupProps { showPopup: boolean; linkFrom?: () => Doc | undefined; + linkCreateAnchor?: () => Doc | undefined; + linkCreated?: (link: Doc) => void; // groupType: string; // linkDoc: Doc; // docView: DocumentView; @@ -32,14 +35,10 @@ export class LinkPopup extends React.Component<LinkPopupProps> { // TODO: should check for valid URL @undoBatch - makeLinkToURL = (target: string, lcoation: string) => { - ((this.view as any)?.TextView as FormattedTextBox).makeLinkAnchor(undefined, 'onRadd:rightight', target, target); - }; + makeLinkToURL = (target: string, lcoation: string) => ((this.view as any)?.TextView as FormattedTextBox).makeLinkAnchor(undefined, OpenWhere.addRight, target, target); @action - onLinkChange = (e: React.ChangeEvent<HTMLInputElement>) => { - this.linkURL = e.target.value; - }; + onLinkChange = (e: React.ChangeEvent<HTMLInputElement>) => (this.linkURL = e.target.value); getPWidth = () => 500; getPHeight = () => 500; @@ -67,7 +66,9 @@ export class LinkPopup extends React.Component<LinkPopupProps> { Document={Doc.MySearcher} DataDoc={Doc.MySearcher} linkFrom={linkDoc} + linkCreateAnchor={this.props.linkCreateAnchor} linkSearch={true} + linkCreated={this.props.linkCreated} fieldKey="data" dropAction="move" isSelected={returnTrue} |
