diff options
author | bobzel <zzzman@gmail.com> | 2022-11-29 12:05:29 -0500 |
---|---|---|
committer | bobzel <zzzman@gmail.com> | 2022-11-29 12:05:29 -0500 |
commit | c6d1059e24f362a167b9ac24e6f13d1e45361da9 (patch) | |
tree | a0136cee04608c73f54ac3ecd63bed0307a37fd4 /src | |
parent | 1f5db9cfc594dbf337d752ec94dab5fca7d8b6f7 (diff) |
changes to streamline link editing UI (got rid of LinkEditor). cleaned up link (un)highlighting.
Diffstat (limited to 'src')
21 files changed, 194 insertions, 1031 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index c5e08eeea..eed839520 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1375,7 +1375,6 @@ export namespace DocUtils { 'acl-Public': SharingPermissions.Augment, '_acl-Public': SharingPermissions.Augment, linkDisplay: true, - _hidden: true, _linkAutoMove: true, linkRelationship, _showCaption: 'description', diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 7120eeb88..4f02a8202 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -1,7 +1,7 @@ import { action, observable, runInAction } from 'mobx'; import { Doc, DocListCast, DocListCastAsync, Opt } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; -import { Cast } from '../../fields/Types'; +import { Cast, DocCast } from '../../fields/Types'; import { returnFalse } from '../../Utils'; import { DocumentType } from '../documents/DocumentTypes'; import { LightboxView } from '../views/LightboxView'; @@ -36,15 +36,14 @@ export class DocumentManager { //console.log("MOUNT " + view.props.Document.title + "/" + view.props.LayoutTemplateString); if (view.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { const viewAnchorIndex = view.props.LayoutTemplateString.includes('anchor2') ? 'anchor2' : 'anchor1'; - DocListCast(view.rootDoc.links).forEach(link => { - this.LinkAnchorBoxViews?.filter(dv => Doc.AreProtosEqual(dv.rootDoc, link) && !dv.props.LayoutTemplateString?.includes(viewAnchorIndex)).forEach(otherView => - this.LinkedDocumentViews.push({ - a: viewAnchorIndex === 'anchor2' ? otherView : view, - b: viewAnchorIndex === 'anchor2' ? view : otherView, - l: link, - }) - ); - }); + const link = view.rootDoc; + this.LinkAnchorBoxViews?.filter(dv => Doc.AreProtosEqual(dv.rootDoc, link) && !dv.props.LayoutTemplateString?.includes(viewAnchorIndex)).forEach(otherView => + this.LinkedDocumentViews.push({ + a: viewAnchorIndex === 'anchor2' ? otherView : view, + b: viewAnchorIndex === 'anchor2' ? view : otherView, + l: link, + }) + ); this.LinkAnchorBoxViews.push(view); // this.LinkedDocumentViews.forEach(view => console.log(" LV = " + view.a.props.Document.title + "/" + view.a.props.LayoutTemplateString + " --> " + // view.b.props.Document.title + "/" + view.b.props.LayoutTemplateString)); diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts index 5374bf44b..282116f1b 100644 --- a/src/client/util/LinkFollower.ts +++ b/src/client/util/LinkFollower.ts @@ -67,7 +67,7 @@ export class LinkFollower { docViewProps.ContainingCollectionDoc, action(() => { batch.end(); - DocumentDecorations.Instance.overrideBounds = false; + Doc.AddUnlightWatcher(action(() => (DocumentDecorations.Instance.overrideBounds = false))); }), altKey ? true : undefined ); diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 49cc3218d..01f4df723 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -44,7 +44,7 @@ export class LinkManager { if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) { Doc.GetProto(a1)[DirectLinksSym].add(link); Doc.GetProto(a2)[DirectLinksSym].add(link); - Doc.GetProto(link)[DirectLinksSym].add(link); + //Doc.GetProto(link)[DirectLinksSym].add(link); // bcz: links are not linked to themself, so this was a hack } }) ); diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 02d672a65..a3d6f5227 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -4,7 +4,6 @@ import { Doc, Opt } from '../../fields/Doc'; import { DocCast } from '../../fields/Types'; import { CollectionViewType, DocumentType } from '../documents/DocumentTypes'; import { DocumentView } from '../views/nodes/DocumentView'; -import { LinkManager } from './LinkManager'; import { ScriptingGlobals } from './ScriptingGlobals'; export namespace SelectionManager { diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 3efb5fb37..d1f0bf2ac 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -27,7 +27,7 @@ import { Colors } from './global/globalEnums'; import { InkingStroke } from './InkingStroke'; import { InkStrokeProperties } from './InkStrokeProperties'; import { LightboxView } from './LightboxView'; -import { DocumentView } from './nodes/DocumentView'; +import { DocumentView, OpenWhereMod } from './nodes/DocumentView'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; import { ImageBox } from './nodes/ImageBox'; import React = require('react'); @@ -253,17 +253,17 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P if (selectedDocs.length) { if (e.ctrlKey) { // open an alias in a new tab with Ctrl Key - CollectionDockingView.AddSplit(Doc.BestAlias(selectedDocs[0].props.Document), 'right'); + CollectionDockingView.AddSplit(Doc.BestAlias(selectedDocs[0].props.Document), OpenWhereMod.right); } else if (e.shiftKey) { // open centered in a new workspace with Shift Key const alias = Doc.MakeAlias(selectedDocs[0].props.Document); alias.context = undefined; alias.x = -alias[WidthSym]() / 2; alias.y = -alias[HeightSym]() / 2; - CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([alias], { title: 'Tab for ' + alias.title }), 'right'); + CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([alias], { title: 'Tab for ' + alias.title }), OpenWhereMod.right); } else if (e.altKey) { // open same document in new tab - CollectionDockingView.ToggleSplit(selectedDocs[0].props.Document, 'right'); + CollectionDockingView.ToggleSplit(selectedDocs[0].props.Document, OpenWhereMod.right); } else { var openDoc = selectedDocs[0].props.Document; if (openDoc.layoutKey === 'layout_icon') { @@ -720,13 +720,15 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P const hideTitle = hideDecorations || seldocview.props.hideDecorationTitle || seldocview.rootDoc.hideDecorationTitle || this._isRounding || this._isRotating; const hideDocumentButtonBar = hideDecorations || seldocview.props.hideDocumentButtonBar || seldocview.rootDoc.hideDocumentButtonBar || this._isRounding || this._isRotating; // if multiple documents have been opened at the same time, then don't show open button - const hideOpenButton =hideDecorations || + const hideOpenButton = + hideDecorations || seldocview.props.hideOpenButton || seldocview.rootDoc.hideOpenButton || SelectionManager.Views().some(docView => docView.props.Document._stayInCollection || docView.props.Document.isGroup || docView.props.Document.hideOpenButton) || this._isRounding || this._isRotating; - const hideDeleteButton =hideDecorations || + const hideDeleteButton = + hideDecorations || this._isRounding || this._isRotating || seldocview.props.hideDeleteButton || diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 98d0378be..09063901d 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -484,7 +484,7 @@ export class MainView extends React.Component { } globalPointerDown = action((e: PointerEvent) => { - runInAction(() => (Doc.HighlightBrush.linkFollowEffect = undefined)); + Doc.linkFollowUnhighlight(); AudioBox.Enabled = true; const targets = document.elementsFromPoint(e.x, e.y); if (targets.length) { diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss index 30806f718..897be9a32 100644 --- a/src/client/views/PropertiesView.scss +++ b/src/client/views/PropertiesView.scss @@ -4,8 +4,13 @@ height: 100%; width: 250; font-family: 'Roboto'; + font-size: 12px; cursor: auto; + .slider-text { + font-size: 8px; + } + overflow-x: hidden; overflow-y: auto; @@ -865,7 +870,15 @@ } .propertiesButton { - width: 4rem; + width: 2rem; + height: 2rem; + display: flex; + justify-content: center; + align-items: center; + > svg { + width: 15px; + height: 15px; + } } } diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index ad3f62990..e8fd540a8 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -55,7 +55,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { } @computed get selectedDoc() { - return LinkManager.currentLink || SelectionManager.SelectedSchemaDoc() || this.selectedDocumentView?.rootDoc || Doc.ActiveDashboard; + return SelectionManager.SelectedSchemaDoc() || this.selectedDocumentView?.rootDoc || Doc.ActiveDashboard; } @computed get selectedDocumentView() { if (SelectionManager.Views().length) return SelectionManager.Views()[0]; @@ -306,7 +306,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { } @computed get links() { - return !this.selectedDoc ? null : <PropertiesDocBacklinksSelector Document={this.selectedDoc} hideTitle={true} addDocTab={this.props.addDocTab} />; + const selAnchor = this.selectedDocumentView?.anchorViewDoc; + return !selAnchor ? null : <PropertiesDocBacklinksSelector Document={selAnchor} hideTitle={true} addDocTab={this.props.addDocTab} />; } @computed get layoutPreview() { @@ -1426,11 +1427,11 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @undoBatch animationDirection = (direction: PresEffectDirection, icon: string, gridColumn: number, gridRow: number, opts: object) => { const lanch = this.sourceAnchor; - const color = lanch?.presEffectDirection === direction || (direction === PresEffectDirection.Center && !lanch?.presEffectDirection) ? Colors.LIGHT_BLUE : 'black'; + const color = lanch?.presEffectDirection === direction || (direction === PresEffectDirection.Center && !lanch?.presEffectDirection) ? Colors.MEDIUM_BLUE : ''; return ( <Tooltip title={<div className="dash-tooltip">{direction}</div>}> <div - style={{ ...opts, border: direction === PresEffectDirection.Center ? `solid 2px ${color}` : undefined, borderRadius: '100%', cursor: 'pointer', gridColumn, gridRow, justifySelf: 'center', color }} + style={{ ...opts, border: direction === PresEffectDirection.Center ? `solid 2px ${color}` : undefined, borderRadius: '20%', cursor: 'pointer', gridColumn, gridRow, justifySelf: 'center', background: color, color: 'black' }} onClick={() => this.updateEffectDirection(direction)}> {icon ? <FontAwesomeIcon icon={icon as any} /> : null} </div> @@ -1470,19 +1471,21 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { } }; - toggleProp = (e: React.PointerEvent, prop: string) => { - setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => this.selectedDoc && (this.selectedDoc[prop] = !this.selectedDoc[prop])))); + toggleLinkProp = (e: React.PointerEvent, prop: string) => { + setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => LinkManager.currentLink && (LinkManager.currentLink[prop] = !LinkManager.currentLink[prop])))); }; @computed get destinationAnchor() { const ldoc = LinkManager.currentLink; - const lanch = LinkManager.currentLinkAnchor; + const lanch = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor; if (ldoc && lanch) return LinkManager.getOppositeAnchor(ldoc, lanch) ?? lanch; return ldoc ? DocCast(ldoc.anchor2) : ldoc; } @computed get sourceAnchor() { - return LinkManager.currentLinkAnchor ?? (LinkManager.currentLink && this.destinationAnchor ? LinkManager.getOppositeAnchor(LinkManager.currentLink, this.destinationAnchor) : LinkManager.currentLink); + const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor; + + return selAnchor ?? (LinkManager.currentLink && this.destinationAnchor ? LinkManager.getOppositeAnchor(LinkManager.currentLink, this.destinationAnchor) : LinkManager.currentLink); } toggleAnchorProp = (e: React.PointerEvent, prop: string, anchor?: Doc) => { @@ -1545,9 +1548,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { const isNovice = Doc.noviceMode; const zoom = Number((NumCast(this.sourceAnchor?.presZoom, 1) * 100).toPrecision(3)); const targZoom = this.sourceAnchor?.followLinkZoom; - const selectedDoc = this.selectedDoc; const indent = 30; - if (!selectedDoc && !this.isPres) { + if (!this.selectedDoc && !this.isPres) { return ( <div className="propertiesView" style={{ width: this.props.width }}> <div className="propertiesView-title" style={{ width: this.props.width }}> @@ -1556,7 +1558,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div> ); } else { - if (selectedDoc && !this.isPres) { + if (this.selectedDoc && !this.isPres) { return ( <div className="propertiesView" @@ -1573,9 +1575,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { {this.contextsSubMenu} {this.linksSubMenu} - {!selectedDoc || !LinkManager.currentLink || !SelectionManager.Views().some(dv => DocListCast(dv.rootDoc.links).includes(LinkManager.currentLink!)) ? null : ( + {!this.selectedDoc || !LinkManager.currentLink || !SelectionManager.Views().some(dv => DocListCast(this.sourceAnchor?.links).includes(LinkManager.currentLink!)) ? null : ( <> - <div className="propertiesView-section"> + <div className="propertiesView-section" style={{ background: 'darkgray' }}> <div className="propertiesView-input first" style={{ display: 'grid', gridTemplateColumns: '84px auto' }}> <p>Relationship</p> {this.editRelationship} @@ -1584,11 +1586,41 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <p>Description</p> {this.editDescription} </div> + <div className="propertiesView-input inline"> + <p>Show link</p> + <button + style={{ background: !LinkManager.currentLink?.linkDisplay ? '' : '#4476f7', borderRadius: 3 }} + onPointerDown={e => this.toggleLinkProp(e, 'linkDisplay')} + onClick={e => e.stopPropagation()} + className="propertiesButton"> + <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" /> + </button> + </div> + <div className="propertiesView-input inline" style={{ marginLeft: 10 }}> + <p>Auto-move anchors</p> + <button + style={{ background: !LinkManager.currentLink?.linkAutoMove ? '' : '#4476f7', borderRadius: 3 }} + onPointerDown={e => this.toggleLinkProp(e, 'linkAutoMove')} + onClick={e => e.stopPropagation()} + className="propertiesButton"> + <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" /> + </button> + </div> + <div className="propertiesView-input inline" style={{ marginLeft: 10 }}> + <p>Display arrow</p> + <button + style={{ background: !LinkManager.currentLink?.linkDisplayArrow ? '' : '#4476f7', borderRadius: 3 }} + onPointerDown={e => this.toggleLinkProp(e, 'linkDisplayArrow')} + onClick={e => e.stopPropagation()} + className="propertiesButton"> + <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" /> + </button> + </div> </div> <div className="propertiesView-section"> <div className="propertiesView-input inline first" style={{ display: 'grid', gridTemplateColumns: '84px calc(100% - 84px)' }}> <p>Follow by</p> - <select onChange={e => this.changeFollowBehavior(e.currentTarget.value)} value={StrCast(selectedDoc.followLinkLocation, 'default')}> + <select onChange={e => this.changeFollowBehavior(e.currentTarget.value)} value={StrCast(this.sourceAnchor?.followLinkLocation, 'default')}> <option value="default">Default</option> <option value="add:left">Opening in new left pane</option> <option value="add:right">Opening in new right pane</option> @@ -1598,7 +1630,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <option value="add">Opening in new tab</option> <option value="replace">Replacing current tab</option> <option value="inPlace">Opening in place</option> - {selectedDoc.linksToAnnotation ? <option value="openExternal">Open in external page</option> : null} + {LinkManager.currentLink?.linksToAnnotation ? <option value="openExternal">Open in external page</option> : null} </select> </div> <div className="propertiesView-input inline first" style={{ display: 'grid', gridTemplateColumns: '84px calc(100% - 134px) 50px' }}> @@ -1609,7 +1641,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <option value={effect.toString()}>{effect.toString()}</option> ))} </select> - <div className="effectDirection" style={{ marginLeft: '10px', display: 'grid', width: 40, gridColumn: 3 }}> + <div className="effectDirection" style={{ marginLeft: '10px', display: 'grid', width: 40, height: 36, gridColumn: 3, gridTemplateRows: '12px 12px 12px' }}> {this.animationDirection(PresEffectDirection.Left, 'angle-right', 1, 2, {})} {this.animationDirection(PresEffectDirection.Right, 'angle-left', 3, 2, {})} {this.animationDirection(PresEffectDirection.Top, 'angle-down', 2, 1, {})} @@ -1650,34 +1682,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" /> </button> </div> - <div className="propertiesView-input inline"> - <p>Show link</p> - <button style={{ background: !selectedDoc.linkDisplay ? '' : '#4476f7', borderRadius: 3 }} onPointerDown={e => this.toggleProp(e, 'linkDisplay')} onClick={e => e.stopPropagation()} className="propertiesButton"> - <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" /> - </button> - </div> - <div className="propertiesView-input inline" style={{ marginLeft: 10 }}> - <p>Auto-move anchor</p> - <button - style={{ background: !selectedDoc.linkAutoMove ? '' : '#4476f7', borderRadius: 3 }} - onPointerDown={e => this.toggleProp(e, 'linkAutoMove')} - onClick={e => e.stopPropagation()} - className="propertiesButton"> - <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" /> - </button> - </div> - <div className="propertiesView-input inline" style={{ marginLeft: 10 }}> - <p>Display arrow</p> - <button - style={{ background: !selectedDoc.linkDisplayArrow ? '' : '#4476f7', borderRadius: 3 }} - onPointerDown={e => this.toggleProp(e, 'linkDisplayArrow')} - onClick={e => e.stopPropagation()} - className="propertiesButton"> - <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" /> - </button> - </div> - <div className="propertiesView-input inline" style={{ display: 'grid', gridTemplateColumns: '78px calc(100% - 128px) 50px' }}> - <p>Zoom % screen</p> + <div className="propertiesView-input inline" style={{ display: 'grid', gridTemplateColumns: '78px calc(100% - 108px) 50px' }}> + <p>Zoom %</p> <div className="ribbon-property" style={{ display: !targZoom ? 'none' : 'inline-flex' }}> <input className="presBox-input" style={{ width: '100%' }} type="number" value={zoom} /> <div className="ribbon-propertyUpDown" style={{ display: 'flex', flexDirection: 'column' }}> diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index b8344dc0c..9e360f557 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -2,10 +2,10 @@ import { computed } from 'mobx'; import { observer } from 'mobx-react'; import { Id } from '../../../../fields/FieldSymbols'; import { DocumentManager } from '../../../util/DocumentManager'; +import { LightboxView } from '../../LightboxView'; import './CollectionFreeFormLinksView.scss'; import { CollectionFreeFormLinkView } from './CollectionFreeFormLinkView'; import React = require('react'); -import { LightboxView } from '../../LightboxView'; @observer export class CollectionFreeFormLinksView extends React.Component<React.PropsWithChildren<{}>> { diff --git a/src/client/views/global/globalCssVariables.scss b/src/client/views/global/globalCssVariables.scss index e68f9abe3..430a36dce 100644 --- a/src/client/views/global/globalCssVariables.scss +++ b/src/client/views/global/globalCssVariables.scss @@ -84,4 +84,5 @@ $TREE_BULLET_WIDTH: 20px; LEFT_MENU_WIDTH: $LEFT_MENU_WIDTH; TREE_BULLET_WIDTH: $TREE_BULLET_WIDTH; INK_MASK_SIZE: $INK_MASK_SIZE; + MEDIUM_GRAY: $medium-gray; } diff --git a/src/client/views/global/globalCssVariables.scss.d.ts b/src/client/views/global/globalCssVariables.scss.d.ts index 76259113c..3375579d6 100644 --- a/src/client/views/global/globalCssVariables.scss.d.ts +++ b/src/client/views/global/globalCssVariables.scss.d.ts @@ -11,6 +11,7 @@ interface IGlobalScss { LEFT_MENU_WIDTH: string; TREE_BULLET_WIDTH: string; INK_MASK_SIZE: number; + MEDIUM_GRAY: string; } declare const globalCssVariables: IGlobalScss; 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 01e33708a..000000000 --- a/src/client/views/linking/LinkEditor.tsx +++ /dev/null @@ -1,454 +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, DocCast, NumCast } 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'; -import { PresBox, PresEffect } from '../nodes/trails'; - -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 openEffectDropdown: 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 - 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 by:</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 opening in new left pane - </div> - <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('add:right')}> - Always opening in new right pane - </div> - <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('replace:right')}> - Always replacing right tab - </div> - <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('replace:left')}> - Always replacing left tab - </div> - <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('fullScreen')}> - Always opening full screen - </div> - <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('add')}> - Always opening in a new tab - </div> - <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('replace')}> - Replacing Tab - </div> - <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('inPlace')}> - Opening in Place - </div> - {this.props.linkDoc.linksToAnnotation ? ( - <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('openExternal')}> - Always open in external page - </div> - ) : null} - </div> - </div> - </div> - ); - } - - @computed get destinationAnchor() { - 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.anchor2); - if (Doc.AreProtosEqual(DocCast(DocCast(ldoc.anchor2).annotationOn), this.props.sourceDoc)) return DocCast(ldoc.anchor1); - } - return LinkManager.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc) ?? this.props.sourceDoc; - } - @action - changeEffectDropdown = () => { - this.openEffectDropdown = !this.openEffectDropdown; - }; - - @undoBatch - changeEffect = action((follow: string) => { - this.openEffectDropdown = false; - this.destinationAnchor.presEffect = follow; - }); - - @computed - get effectDropdown() { - return ( - <div className="linkEditor-followingDropdown"> - <div className="linkEditor-followingDropdown-label">Animation:</div> - <div className="linkEditor-followingDropdown-dropdown"> - <div className="linkEditor-followingDropdown-header" onPointerDown={this.changeEffectDropdown}> - {StrCast(this.destinationAnchor.presEffect, 'default')} - <FontAwesomeIcon className="linkEditor-followingDropdown-icon" icon={this.openEffectDropdown ? 'chevron-up' : 'chevron-down'} size={'lg'} /> - </div> - <div className="linkEditor-followingDropdown-optionsList" style={{ display: this.openEffectDropdown ? '' : 'none' }}> - {[PresEffect.None, PresEffect.Zoom, PresEffect.Lightspeed, PresEffect.Fade, PresEffect.Flip, PresEffect.Rotate, PresEffect.Bounce, PresEffect.Roll].map(effect => ( - <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeEffect(effect.toString())}> - {effect.toString()} - </div> - ))} - </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} - {this.effectDropdown} - {PresBox.inputter('0.1', '0.1', '10', NumCast(this.destinationAnchor.presTransition) / 1000, true, (val: string) => PresBox.SetTransitionTime(val, (timeInMS: number) => (this.destinationAnchor.presTransition = timeInMS)))} - <div - className={'slider-headers'} - style={{ - marginLeft: 30, - marginRight: 30, - display: 'grid', - justifyContent: 'space-between', - width: '100%', - gridTemplateColumns: 'auto auto auto', - }}> - <div className="slider-text">Fast</div> - <div className="slider-text">Medium</div> - <div className="slider-text">Slow</div> - </div>{' '} - </div> - ); - } -} diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx index 0c46a6d96..c9112eec3 100644 --- a/src/client/views/linking/LinkMenu.tsx +++ b/src/client/views/linking/LinkMenu.tsx @@ -1,14 +1,13 @@ -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'); -import { emptyFunction } from '../../../Utils'; interface Props { docView: DocumentView; @@ -23,15 +22,9 @@ interface Props { @observer export class LinkMenu extends React.Component<Props> { _editorRef = React.createRef<HTMLDivElement>(); - @observable _editingLink?: Doc; @observable _linkMenuRef = React.createRef<HTMLDivElement>(); - clear = !this.props.clearLinkEditor - ? undefined - : action(() => { - this.props.clearLinkEditor?.(); - this._editingLink = undefined; - }); + clear = () => this.props.clearLinkEditor?.(); componentDidMount() { this.props.clearLinkEditor && document.addEventListener('pointerdown', this.onPointerDown, true); @@ -43,7 +36,7 @@ export class LinkMenu extends React.Component<Props> { onPointerDown = action((e: PointerEvent) => { LinkDocPreview.Clear(); if (!this._linkMenuRef.current?.contains(e.target as any) && !this._editorRef.current?.contains(e.target as any)) { - this.clear?.(); + this.clear(); } }); @@ -54,34 +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.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={{ ...style }}> - {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-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 9d2082e21..d02a1c4eb 100644 --- a/src/client/views/linking/LinkMenuGroup.tsx +++ b/src/client/views/linking/LinkMenuGroup.tsx @@ -2,19 +2,19 @@ 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 { 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 { DocumentType } from '../../documents/DocumentTypes'; interface LinkMenuGroupProps { sourceDoc: Doc; group: Doc[]; groupType: string; clearLinkEditor?: () => void; - showEditor: (linkDoc: Doc) => void; docView: DocumentView; itemHandler?: (doc: Doc) => void; } @@ -44,25 +44,33 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { 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]} - itemHandler={this.props.itemHandler} - groupType={this.props.groupType} - docView={this.props.docView} - linkDoc={linkDoc} - sourceDoc={this.props.sourceDoc} - destinationDoc={destination} - clearLinkEditor={this.props.clearLinkEditor} - showEditor={this.props.showEditor} - menuRef={this._menuRef} - /> - ); - } + 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={sourceDoc} + destinationDoc={destDoc} + clearLinkEditor={this.props.clearLinkEditor} + menuRef={this._menuRef} + /> + ); }); return ( diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index c3705b0e1..fb4c6873e 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -18,6 +18,7 @@ import { DocumentView } from '../nodes/DocumentView'; import { LinkDocPreview } from '../nodes/LinkDocPreview'; import './LinkMenuItem.scss'; import React = require('react'); +import { SelectionManager } from '../../util/SelectionManager'; interface LinkMenuItemProps { groupType: string; @@ -26,7 +27,6 @@ interface LinkMenuItemProps { sourceDoc: Doc; destinationDoc: Doc; clearLinkEditor?: () => void; - showEditor: (linkDoc: Doc) => void; menuRef: React.Ref<HTMLDivElement>; itemHandler?: (doc: Doc) => void; } @@ -100,10 +100,10 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { }, emptyFunction, action(() => { + SelectionManager.SelectView(this.props.docView, false); if ((SettingsManager.propertiesWidth ?? 0) < 100) { SettingsManager.propertiesWidth = 250; } - //this.props.showEditor(this.props.linkDoc); }) ); }; diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index 627487a9e..99fa62fa7 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -305,7 +305,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp top: 0, pointerEvents: 'none', }}> - <Tooltip title={!DocumentLinksButton.LinkEditorDocView ? <div className="dash-tooltip">{title}</div> : <></>}>{this.linkButtonInner}</Tooltip> + {!DocumentLinksButton.LinkEditorDocView ? this.linkButtonInner : <Tooltip title={<div className="dash-tooltip">{title}</div>}>{this.linkButtonInner}</Tooltip>} </div> ); } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 2729a5047..a8bea61c9 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -3,7 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; import { action, computed, IReactionDisposer, observable, reaction, runInAction, trace } from 'mobx'; import { observer, renderReporter } from 'mobx-react'; -import { AclAdmin, AclEdit, AclPrivate, DataSym, Doc, DocListCast, Field, Opt, StrListCast, WidthSym } from '../../../fields/Doc'; +import { AclAdmin, AclEdit, AclPrivate, AnimationSym, DataSym, Doc, DocListCast, Field, Opt, StrListCast, WidthSym } from '../../../fields/Doc'; import { Document } from '../../../fields/documentSchemas'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; @@ -11,7 +11,7 @@ import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; -import { BoolCast, Cast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { AudioField } from '../../../fields/URLField'; import { GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util'; import { MobileInterface } from '../../../mobile/MobileInterface'; @@ -1156,7 +1156,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps anchorPanelHeight = () => this.props.PanelHeight() || 1; anchorStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => { // prettier-ignore - switch (property) { + switch (property.split(':')[0]) { case StyleProp.ShowTitle: return ''; case StyleProp.PointerEvents: return 'none'; case StyleProp.LinkSource: return this.props.Document; // pass the LinkSource to the LinkAnchorBox @@ -1188,7 +1188,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps TraceMobx(); if (this.layoutDoc.unrendered || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return null; if (this.rootDoc.type === DocumentType.PRES || this.rootDoc.type === DocumentType.LINK || this.props.dontRegisterView) return null; - const filtered = DocUtils.FilterDocs(this.directLinks, this.props.docFilters?.() ?? [], []).filter(d => !d.hidden); + const filtered = DocUtils.FilterDocs(this.directLinks, this.props.docFilters?.() ?? [], []).filter(d => d.linkDisplay); return filtered.map((link, i) => ( <div className="documentView-anchorCont" key={link[Id]}> <DocumentView @@ -1415,7 +1415,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps clipPath: borderPath.path ? `path('${borderPath.path}')` : undefined, }); - const animRenderDoc = Doc.IsHighlighted(this.rootDoc) || PresBox.Instance?.isActiveItemTarget(this.layoutDoc) ? PresBox.AnimationEffect(renderDoc, PresBox.Instance?.activeItem ?? Doc.HighlightBrush.linkFollowEffect, this.rootDoc) : renderDoc; + const animRenderDoc = Doc.IsHighlighted(this.rootDoc) || PresBox.Instance?.isActiveItemTarget(this.layoutDoc) ? PresBox.AnimationEffect(renderDoc, PresBox.Instance?.activeItem ?? this.rootDoc[AnimationSym], this.rootDoc) : renderDoc; return ( <div className={`${DocumentView.ROOT_DIV} docView-hack`} @@ -1535,8 +1535,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { linkButtonInverseScaling = () => (this.props.NativeDimScaling?.() || 1) * this.screenToLocalTransform().Scale; @computed get linkCountView() { - return (this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (this.isSelected() && this.props.renderDepth) || !this._isHovering || this.hideLinkButton) && - DocumentLinksButton.LinkEditorDocView?.rootDoc !== this.rootDoc ? null : ( + return this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (this.isSelected() && this.props.renderDepth) || !this._isHovering || this.hideLinkButton ? null : ( <DocumentLinksButton View={this} scaling={this.linkButtonInverseScaling} OnHover={true} Bottom={this.topMost} ShowCount={true} /> ); } @@ -1654,6 +1653,9 @@ export class DocumentView extends React.Component<DocumentViewProps> { startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this.docView?.startDragging(x, y, dropAction, hideSource); + @computed get anchorViewDoc() { + return this.props.LayoutTemplateString?.includes('anchor2') ? DocCast(this.rootDoc['anchor2']) : this.props.LayoutTemplateString?.includes('anchor1') ? DocCast(this.rootDoc['anchor1']) : this.rootDoc; + } docViewPathFunc = () => this.docViewPath; isSelected = (outsideReaction?: boolean) => SelectionManager.IsSelected(this, outsideReaction); select = (extendSelection: boolean) => SelectionManager.SelectView(this, !SelectionManager.Views().some(v => v.props.Document === this.props.ContainingCollectionDoc) && extendSelection); diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index be9565452..e89076c1f 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -1,4 +1,3 @@ -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, observable } from 'mobx'; import { observer } from 'mobx-react'; import { Doc } from '../../../fields/Doc'; @@ -7,20 +6,17 @@ import { TraceMobx } from '../../../fields/util'; import { emptyFunction, setupMoveUpEvents, Utils } from '../../../Utils'; import { DragManager } from '../../util/DragManager'; import { LinkFollower } from '../../util/LinkFollower'; -import { SelectionManager } from '../../util/SelectionManager'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxBaseComponent } from '../DocComponent'; -import { LinkEditor } from '../linking/LinkEditor'; import { StyleProp } from '../StyleProvider'; import { FieldView, FieldViewProps } from './FieldView'; import './LinkAnchorBox.scss'; import { LinkDocPreview } from './LinkDocPreview'; import React = require('react'); -import { OpenWhere } from './DocumentView'; -const higflyout = require('@hig/flyout'); -export const { anchorPoints } = higflyout; -export const Flyout = higflyout.default; +import { LinkManager } from '../../util/LinkManager'; +import globalCssVariables = require('../global/globalCssVariables.scss'); +import { SelectionManager } from '../../util/SelectionManager'; @observer export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { @@ -34,15 +30,23 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { _timeout: NodeJS.Timeout | undefined; @observable _x = 0; @observable _y = 0; - @observable _selected = false; - @observable _editing = false; - @observable _forceOpen = false; onPointerDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, false); + const anchorContainerDoc = this.props.styleProvider?.(this.dataDoc, this.props, StyleProp.LinkSource); + setupMoveUpEvents( + this, + e, + this.onPointerMove, + emptyFunction, + (e, doubleTap) => { + if (doubleTap) LinkFollower.FollowLink(this.rootDoc, anchorContainerDoc, this.props, false); + else this.props.select(false); + }, + false + ); }; onPointerMove = action((e: PointerEvent, down: number[], delta: number[]) => { - const cdiv = this._ref && this._ref.current && this._ref.current.parentElement; + const cdiv = this._ref?.current?.parentElement; if (!this._isOpen && cdiv) { const bounds = cdiv.getBoundingClientRect(); const pt = Utils.getNearestPointInPerimeter(bounds.left, bounds.top, bounds.width, bounds.height, e.clientX, e.clientY); @@ -60,58 +64,8 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { } return false; }); - @action - onClick = (e: React.MouseEvent) => { - if (e.button === 2 || e.ctrlKey || !this.layoutDoc.isLinkButton) { - this.props.select(false); - } - if (!this._doubleTap && !e.ctrlKey && e.button < 2) { - const anchorContainerDoc = this.props.styleProvider?.(this.dataDoc, this.props, StyleProp.LinkSource); - this._editing = true; - anchorContainerDoc && this.props.bringToFront(anchorContainerDoc, false); - if (anchorContainerDoc && !this.layoutDoc.onClick && !this._isOpen) { - this._timeout = setTimeout( - action(() => { - LinkFollower.FollowLink(this.rootDoc, anchorContainerDoc, this.props, false); - this._editing = false; - }), - 300 - (Date.now() - this._lastTap) - ); - e.stopPropagation(); - } - } else { - this._timeout && clearTimeout(this._timeout); - this._timeout = undefined; - this._doubleTap = false; - this.openLinkEditor(e); - e.stopPropagation(); - } - }; - openLinkDocOnRight = (e: React.MouseEvent) => { - this.props.addDocTab(this.rootDoc, OpenWhere.addRight); - }; - openLinkTargetOnRight = (e: React.MouseEvent) => { - const alias = Doc.MakeAlias(Cast(this.layoutDoc[this.fieldKey], Doc, null)); - alias._isLinkButton = undefined; - alias.layoutKey = 'layout'; - this.props.addDocTab(alias, OpenWhere.addRight); - }; - @action - openLinkEditor = action((e: React.MouseEvent) => { - SelectionManager.DeselectAll(); - this._editing = this._forceOpen = true; - }); - - specificContextMenu = (e: React.MouseEvent): void => { - const funcs: ContextMenuProps[] = []; - funcs.push({ description: 'Open Link Target on Right', event: () => this.openLinkTargetOnRight(e), icon: 'eye' }); - funcs.push({ description: 'Open Link on Right', event: () => this.openLinkDocOnRight(e), icon: 'eye' }); - funcs.push({ description: 'Open Link Editor', event: () => this.openLinkEditor(e), icon: 'eye' }); - funcs.push({ description: 'Toggle Always Show Link', event: () => (this.props.Document.linkDisplay = !this.props.Document.linkDisplay), icon: 'eye' }); - - ContextMenu.Instance.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' }); - }; + specificContextMenu = (e: React.MouseEvent): void => {}; render() { TraceMobx(); @@ -122,22 +76,13 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { const background = this.props.styleProvider?.(this.dataDoc, this.props, StyleProp.BackgroundColor + ':anchor'); const anchor = this.fieldKey === 'anchor1' ? 'anchor2' : 'anchor1'; const anchorScale = !this.dataDoc[this.fieldKey + '-useLinkSmallAnchor'] && (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : 0.25; - const targetTitle = StrCast((this.dataDoc[anchor] as Doc)?.title); - const flyout = ( - <div className="linkAnchorBoxBox-flyout" title=" " onPointerOver={() => Doc.UnBrushDoc(this.rootDoc)}> - <LinkEditor sourceDoc={Cast(this.dataDoc[this.fieldKey], Doc, null)} hideback={true} linkDoc={this.rootDoc} showLinks={action(() => {})} /> - {!this._forceOpen ? null : ( - <div className="linkAnchorBox-linkCloser" onPointerDown={action(() => (this._isOpen = this._editing = this._forceOpen = false))}> - <FontAwesomeIcon color="dimgray" icon={'times'} size={'sm'} /> - </div> - )} - </div> - ); + const selView = SelectionManager.Views().lastElement()?.props.LayoutTemplateString?.includes('anchor1') ? 'anchor1' : SelectionManager.Views().lastElement()?.props.LayoutTemplateString?.includes('anchor2') ? 'anchor2' : ''; return ( <div + ref={this._ref} + title={targetTitle} className={`linkAnchorBox-cont${small ? '-small' : ''}`} - //onPointerLeave={} //LinkDocPreview.Clear} onPointerEnter={e => LinkDocPreview.SetLinkInfo({ docProps: this.props, @@ -149,24 +94,15 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { }) } onPointerDown={this.onPointerDown} - onClick={this.onClick} - title={targetTitle} onContextMenu={this.specificContextMenu} - ref={this._ref} style={{ + border: selView && this.rootDoc[selView] === this.rootDoc[this.fieldKey] ? `solid ${globalCssVariables.MEDIUM_GRAY} 2px` : undefined, background, left: `calc(${x}% - ${small ? 2.5 : 7.5}px)`, top: `calc(${y}% - ${small ? 2.5 : 7.5}px)`, transform: `scale(${anchorScale})`, - }}> - {!this._editing && !this._forceOpen ? null : ( - <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout} open={this._forceOpen ? true : undefined} onOpen={() => (this._isOpen = true)} onClose={action(() => (this._isOpen = this._forceOpen = this._editing = false))}> - <span className="linkAnchorBox-button"> - <FontAwesomeIcon icon={'eye'} size={'lg'} /> - </span> - </Flyout> - )} - </div> + }} + /> ); } } diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index c10751698..c6cabe269 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -94,6 +94,8 @@ export function DocListCastOrNull(field: FieldResult) { export const WidthSym = Symbol('Width'); export const HeightSym = Symbol('Height'); +export const AnimationSym = Symbol('Animation'); +export const HighlightSym = Symbol('Highlight'); export const DataSym = Symbol('Data'); export const LayoutSym = Symbol('Layout'); export const FieldsSym = Symbol('Fields'); @@ -329,6 +331,8 @@ export class Doc extends RefField { @observable private ___fieldKeys: any = {}; @observable public [AclSym]: { [key: string]: symbol } = {}; @observable public [DirectLinksSym]: Set<Doc> = new Set(); + @observable public [AnimationSym]: Opt<Doc>; + @observable public [HighlightSym]: boolean = false; private [UpdatingFromServer]: boolean = false; private [ForceServerWrite]: boolean = false; @@ -1261,53 +1265,55 @@ export namespace Doc { } export function linkFollowUnhighlight() { - Doc.UnhighlightAll(); + UnhighlightWatchers.length = 0; + highlightedDocs.forEach(doc => Doc.UnHighlightDoc(doc)); document.removeEventListener('pointerdown', linkFollowUnhighlight); - runInAction(() => (HighlightBrush.linkFollowEffect = undefined)); } - let _lastDate = 0; + let UnhighlightWatchers: (() => void)[] = []; + let UnhighlightTimer: any; + export function AddUnlightWatcher(watcher: () => void) { + if (UnhighlightTimer) { + UnhighlightWatchers.push(watcher); + } else watcher(); + } export function linkFollowHighlight(destDoc: Doc | Doc[], dataAndDisplayDocs = true, presEffect?: Doc) { - //linkFollowUnhighlight(); - // runInAction(() => presEffect && (HighlightBrush.linkFollowEffect = undefined)); - // setTimeout(() => runInAction(() => presEffect && (HighlightBrush.linkFollowEffect = presEffect))); - runInAction(() => presEffect && (HighlightBrush.linkFollowEffect = presEffect)); - (destDoc instanceof Doc ? [destDoc] : destDoc).forEach(doc => Doc.HighlightDoc(doc, dataAndDisplayDocs)); + linkFollowUnhighlight(); + (destDoc instanceof Doc ? [destDoc] : destDoc).forEach(doc => Doc.HighlightDoc(doc, dataAndDisplayDocs, presEffect)); document.removeEventListener('pointerdown', linkFollowUnhighlight); document.addEventListener('pointerdown', linkFollowUnhighlight); - const lastDate = (_lastDate = Date.now()); - window.setTimeout(() => _lastDate === lastDate && linkFollowUnhighlight(), 5000); + if (UnhighlightTimer) clearTimeout(UnhighlightTimer); + UnhighlightTimer = window.setTimeout(() => { + UnhighlightWatchers.forEach(watcher => watcher()); + linkFollowUnhighlight(); + UnhighlightTimer = 0; + }, 5000); } - export class HighlightBrush { - @observable HighlightedDoc: Map<Doc, boolean> = new Map(); - @observable static linkFollowEffect: Doc | undefined; - } - const highlightManager = new HighlightBrush(); + var highlightedDocs = new Set<Doc>(); export function IsHighlighted(doc: Doc) { if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate || doc.opacity === 0) return false; - return highlightManager.HighlightedDoc.get(doc) || highlightManager.HighlightedDoc.get(Doc.GetProto(doc)); + return doc[HighlightSym] || Doc.GetProto(doc)[HighlightSym]; } - export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true) { + export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true, presEffect?: Doc) { runInAction(() => { - highlightManager.HighlightedDoc.set(doc, true); - dataAndDisplayDocs && highlightManager.HighlightedDoc.set(Doc.GetProto(doc), true); + doc[AnimationSym] = presEffect; + highlightedDocs.add(doc); + doc[HighlightSym] = true; + if (dataAndDisplayDocs) { + highlightedDocs.add(Doc.GetProto(doc)); + Doc.GetProto(doc)[HighlightSym] = true; + } }); } export function UnHighlightDoc(doc: Doc) { runInAction(() => { - highlightManager.HighlightedDoc.set(doc, false); - highlightManager.HighlightedDoc.set(Doc.GetProto(doc), false); + highlightedDocs.delete(doc); + highlightedDocs.delete(Doc.GetProto(doc)); + doc[HighlightSym] = Doc.GetProto(doc)[HighlightSym] = false; + doc[AnimationSym] = undefined; }); } - export function UnhighlightAll() { - const mapEntries = highlightManager.HighlightedDoc.keys(); - let docEntry: IteratorResult<Doc>; - while (!(docEntry = mapEntries.next()).done) { - const targetDoc = docEntry.value; - targetDoc && Doc.UnHighlightDoc(targetDoc); - } - } export function UnBrushAllDocs() { brushManager.BrushedDoc.clear(); } |