import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; import { action, computed, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { Doc } from '../../fields/Doc'; import { RichTextField } from '../../fields/RichTextField'; import { Cast, DocCast, NumCast } from '../../fields/Types'; import { emptyFunction, returnFalse, setupMoveUpEvents, simulateMouseClick } from '../../Utils'; import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils'; import { Docs, DocUtils } from '../documents/Documents'; import { DragManager } from '../util/DragManager'; import { IsFollowLinkScript } from '../util/LinkFollower'; import { SelectionManager } from '../util/SelectionManager'; import { SharingManager } from '../util/SharingManager'; import { undoBatch, UndoManager } from '../util/UndoManager'; import { CollectionDockingView } from './collections/CollectionDockingView'; import { TabDocView } from './collections/TabDocView'; import './DocumentButtonBar.scss'; import { Colors } from './global/globalEnums'; import { LinkPopup } from './linking/LinkPopup'; import { MetadataEntryMenu } from './MetadataEntryMenu'; import { DocumentLinksButton } from './nodes/DocumentLinksButton'; import { DocumentView, DocumentViewInternal, OpenWhere, OpenWhereMod } from './nodes/DocumentView'; import { DashFieldView } from './nodes/formattedText/DashFieldView'; import { GoogleRef } from './nodes/formattedText/FormattedTextBox'; import { PinProps } from './nodes/trails'; import { TemplateMenu } from './TemplateMenu'; import React = require('react'); const higflyout = require('@hig/flyout'); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; const cloud: IconProp = 'cloud-upload-alt'; const fetch: IconProp = 'sync-alt'; enum UtilityButtonState { Default, OpenRight, OpenExternally, } @observer export class DocumentButtonBar extends React.Component<{ views: () => (DocumentView | undefined)[]; stack?: any }, {}> { private _dragRef = React.createRef(); private _pullAnimating = false; private _pushAnimating = false; private _pullColorAnimating = false; @observable private pushIcon: IconProp = 'arrow-alt-circle-up'; @observable private pullIcon: IconProp = 'arrow-alt-circle-down'; @observable private pullColor: string = 'white'; @observable public isAnimatingFetch = false; @observable public isAnimatingPulse = false; @observable private openHover: UtilityButtonState = UtilityButtonState.Default; @observable public static Instance: DocumentButtonBar; public static hasPushedHack = false; public static hasPulledHack = false; constructor(props: { views: () => (DocumentView | undefined)[] }) { super(props); runInAction(() => (DocumentButtonBar.Instance = this)); } public startPullOutcome = action((success: boolean) => { if (!this._pullAnimating) { this._pullAnimating = true; this.pullIcon = success ? 'check-circle' : 'stop-circle'; setTimeout( () => runInAction(() => { this.pullIcon = 'arrow-alt-circle-down'; this._pullAnimating = false; }), 1000 ); } }); public startPushOutcome = action((success: boolean) => { this.isAnimatingPulse = false; if (!this._pushAnimating) { this._pushAnimating = true; this.pushIcon = success ? 'check-circle' : 'stop-circle'; setTimeout( () => runInAction(() => { this.pushIcon = 'arrow-alt-circle-up'; this._pushAnimating = false; }), 1000 ); } }); public setPullState = action((unchanged: boolean) => { this.isAnimatingFetch = false; if (!this._pullColorAnimating) { this._pullColorAnimating = true; this.pullColor = unchanged ? 'lawngreen' : 'red'; setTimeout(this.clearPullColor, 1000); } }); private clearPullColor = action(() => { this.pullColor = 'white'; this._pullColorAnimating = false; }); get view0() { return this.props.views()?.[0]; } @computed get considerGoogleDocsPush() { const targetDoc = this.view0?.props.Document; const published = targetDoc && Doc.GetProto(targetDoc)[GoogleRef] !== undefined; const animation = this.isAnimatingPulse ? 'shadow-pulse 1s linear infinite' : 'none'; return !targetDoc ? null : (
{`${published ? 'Push' : 'Publish'} to Google Docs`}
}>
{ await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(); !published && runInAction(() => (this.isAnimatingPulse = true)); DocumentButtonBar.hasPushedHack = false; targetDoc[Pushes] = NumCast(targetDoc[Pushes]) + 1; }}>
); } @computed get considerGoogleDocsPull() { const targetDoc = this.view0?.props.Document; const dataDoc = targetDoc && Doc.GetProto(targetDoc); const animation = this.isAnimatingFetch ? 'spin 0.5s linear infinite' : 'none'; const title = (() => { switch (this.openHover) { default: case UtilityButtonState.Default: return `${!dataDoc?.googleDocUnchanged ? 'Pull from' : 'Fetch'} Google Docs`; case UtilityButtonState.OpenRight: return 'Open in Right Split'; case UtilityButtonState.OpenExternally: return 'Open in new Browser Tab'; } })(); return !targetDoc || !dataDoc || !dataDoc[GoogleRef] ? null : ( {title}}>
{ if (e.altKey) { this.openHover = UtilityButtonState.OpenExternally; } else if (e.shiftKey) { this.openHover = UtilityButtonState.OpenRight; } })} onPointerLeave={action(() => (this.openHover = UtilityButtonState.Default))} onClick={async e => { const googleDocUrl = `https://docs.google.com/document/d/${dataDoc[GoogleRef]}/edit`; if (e.shiftKey) { e.preventDefault(); let googleDoc = await Cast(dataDoc.googleDoc, Doc); if (!googleDoc) { const options = { _width: 600, _nativeWidth: 960, _nativeHeight: 800, data_useCors: false }; googleDoc = Docs.Create.WebDocument(googleDocUrl, options); dataDoc.googleDoc = googleDoc; } CollectionDockingView.AddSplit(googleDoc, OpenWhereMod.right); } else if (e.altKey) { e.preventDefault(); window.open(googleDocUrl); } else { this.clearPullColor(); DocumentButtonBar.hasPulledHack = false; targetDoc[Pulls] = NumCast(targetDoc[Pulls]) + 1; dataDoc.googleDocUnchanged && runInAction(() => (this.isAnimatingFetch = true)); } }}> { // prettier-ignore switch (this.openHover) { default: case UtilityButtonState.Default: return dataDoc.googleDocUnchanged === false ? (this.pullIcon as any) : fetch; case UtilityButtonState.OpenRight: return 'arrow-alt-circle-right'; case UtilityButtonState.OpenExternally: return 'share'; } })()} />
); } @observable subFollow = ''; @computed get followLinkButton() { const targetDoc = this.view0?.props.Document; const followBtn = (allDocs: boolean, click: (doc: Doc) => void, isSet: (doc?: Doc) => boolean, icon: IconProp) => { const tooltip = `Follow ${this.subPin}documents`; return !tooltip ? null : ( {tooltip}}>
(this.subPin = allDocs ? 'All ' : ''))} onPointerLeave={action(e => (this.subPin = ''))} onClick={e => { this.props.views().forEach(dv => click(dv!.rootDoc)); e.stopPropagation(); }} />
); }; const followLink = IsFollowLinkScript(targetDoc?.onClick); return !targetDoc ? null : ( Set onClick to follow primary link}>
this.props.views().map(view => view?.docView?.toggleFollowLink(undefined, false)))}>
{followBtn( true, (doc: Doc) => (doc.followAllLinks = !doc.followAllLinks), (doc?: Doc) => (doc?.followAllLinks ? true : false), 'window-maximize' )}
); } @observable subLink = ''; @computed get linkButton() { const targetDoc = this.view0?.props.Document; return !targetDoc || !this.view0 ? null : (
search for target
}>
open linked trail
}>
); } @observable subEndLink = ''; @computed get endLinkButton() { const linkBtn = (pinLayout: boolean, pinContent: boolean, icon: IconProp) => { const tooltip = `Finish Link and Save ${this.subEndLink} data`; return !this.view0 ? null : ( {tooltip}}>
(this.subEndLink = (pinLayout ? 'Layout' : '') + (pinLayout && pinContent ? ' &' : '') + (pinContent ? ' Content' : '')))} onPointerLeave={action(e => (this.subEndLink = ''))} onClick={e => { this.view0 && DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.view0.props.Document, true, this.view0, { pinDocLayout: pinLayout, pinData: !pinContent ? {} : { poslayoutview: true, dataannos: true, dataview: pinContent }, } as PinProps); e.stopPropagation(); }} />
); }; return !this.view0 ? null : (
{linkBtn(true, false, 'window-maximize')} {linkBtn(false, true, 'address-card')} {linkBtn(true, true, 'id-card')}
); } @observable subPin = ''; @computed get pinButton() { const targetDoc = this.view0?.props.Document; const pinBtn = (pinLayoutView: boolean, pinContentView: boolean, icon: IconProp) => { const tooltip = `Pin Document and Save ${this.subPin} to trail`; return !tooltip ? null : ( {tooltip}}>
(this.subPin = (pinLayoutView ? 'Layout' : '') + (pinLayoutView && pinContentView ? ' &' : '') + (pinContentView ? ' Content View' : '') + (pinLayoutView && pinContentView ? '(shift+alt)' : pinLayoutView ? '(shift)' : pinContentView ? '(alt)' : '')) )} onPointerLeave={action(e => (this.subPin = ''))} onClick={e => { const docs = this.props .views() .filter(v => v) .map(dv => dv!.rootDoc); TabDocView.PinDoc(docs, { pinAudioPlay: true, pinDocLayout: pinLayoutView, pinData: { dataview: pinContentView }, activeFrame: Cast(docs.lastElement()?.activeFrame, 'number', null), currentFrame: Cast(docs.lastElement()?.currentFrame, 'number', null), }); e.stopPropagation(); }} />
); }; return !targetDoc ? null : ( {`Pin Document ${SelectionManager.Views().length > 1 ? 'multiple documents' : ''} to Trail`}}>
{ const docs = this.props .views() .filter(v => v) .map(dv => dv!.rootDoc); TabDocView.PinDoc(docs, { pinAudioPlay: true, pinDocLayout: e.shiftKey, pinData: { dataview: e.altKey }, activeFrame: Cast(docs.lastElement()?.activeFrame, 'number', null) }); e.stopPropagation(); }}>
{pinBtn(true, false, 'window-maximize')} {pinBtn(false, true, 'address-card')} {pinBtn(true, true, 'id-card')}
); } @computed get shareButton() { const targetDoc = this.view0?.props.Document; return !targetDoc ? null : ( {'Open Sharing Manager'}}>
SharingManager.Instance.open(this.view0, targetDoc)}>
); } @computed get menuButton() { const targetDoc = this.view0?.props.Document; return !targetDoc ? null : ( {`Open Context Menu`}}>
); } @computed get metadataButton() { const view0 = this.view0; return !view0 ? null : ( Show metadata panel}>
dv) .map(dv => dv!.props.Document)} suggestWithFunction /> /* tfs: @bcz This might need to be the data document? */ }>
e.stopPropagation()}> {}
); } @observable _isRecording = false; _stopFunc: () => void = emptyFunction; @computed get recordButton() { const targetDoc = this.view0?.props.Document; return !targetDoc ? null : ( Press to record audio annotation}>
{ this._isRecording = true; this.props.views().map(view => view && DocumentViewInternal.recordAudioAnnotation(view.dataDoc, view.LayoutFieldKey, stopFunc => (this._stopFunc = stopFunc), emptyFunction)); const b = UndoManager.StartBatch('Recording'); setupMoveUpEvents( this, e, returnFalse, action(() => { this._isRecording = false; this._stopFunc(); b.end(); }), emptyFunction ); })}>
); } @observable _embedDown = false; onTemplateButton = action((e: React.PointerEvent): void => { this._tooltipOpen = false; setupMoveUpEvents(this, e, this.onEmbedButtonMoved, emptyFunction, emptyFunction); }); onEmbedButtonMoved = () => { if (this._dragRef.current) { const dragDocView = this.view0!; const dragData = new DragManager.DocumentDragData([dragDocView.props.Document]); const [left, top] = dragDocView.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); dragData.defaultDropAction = 'embed'; dragData.canEmbed = true; DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, left, top, { hideSource: false }); return true; } return false; }; _ref = React.createRef(); @observable _tooltipOpen: boolean = false; @computed get templateButton() { const view0 = this.view0; const views = this.props.views(); return !view0 ? null : ( Tap to Customize Layout. Drag an embedding} open={this._tooltipOpen} onClose={action(() => (this._tooltipOpen = false))} placement="bottom">
!this._ref.current?.getBoundingClientRect().width && (this._tooltipOpen = true))}> (this._embedDown = true))} onClose={action(() => (this._embedDown = false))} content={ !this._embedDown ? null : (
{' '} v).map(v => v as DocumentView)} />
) }>
); } openContextMenu = (e: React.MouseEvent) => { let child = SelectionManager.Views()[0].ContentDiv!.children[0]; while (child.children.length) { const next = Array.from(child.children).find(c => c.className?.toString().includes('SVGAnimatedString') || typeof c.className === 'string'); if (next?.className?.toString().includes(DocumentView.ROOT_DIV)) break; if (next?.className?.toString().includes(DashFieldView.name)) break; if (next) child = next; else break; } simulateMouseClick(child, e.clientX, e.clientY - 30, e.screenX, e.screenY - 30); }; @observable _showLinkPopup = false; @action toggleLinkSearch = (e: React.PointerEvent) => { this._showLinkPopup = !this._showLinkPopup; e.stopPropagation(); }; @observable _captureEndLinkLayout = false; @action captureEndLinkLayout = (e: React.PointerEvent) => { this._captureEndLinkLayout = !this._captureEndLinkLayout; }; @observable _captureEndLinkContent = false; @action captureEndLinkContent = (e: React.PointerEvent) => { this._captureEndLinkContent = !this._captureEndLinkContent; }; @action captureEndLinkState = (e: React.PointerEvent) => { this._captureEndLinkContent = this._captureEndLinkLayout = !this._captureEndLinkLayout; }; @action toggleTrail = (e: React.PointerEvent) => { const rootView = this.props.views()[0]; const rootDoc = rootView?.rootDoc; if (rootDoc) { const anchor = rootView.ComponentView?.getAnchor?.(true) ?? rootDoc; const trail = DocCast(anchor.presentationTrail) ?? Doc.MakeCopy(DocCast(Doc.UserDoc().emptyTrail), true); if (trail !== anchor.presentationTrail) { DocUtils.MakeLink(anchor, trail, { link_relationship: 'link trail' }); anchor.presentationTrail = trail; } Doc.ActivePresentation = trail; this.props.views().lastElement()?.props.addDocTab(trail, OpenWhere.replaceRight); } e.stopPropagation(); }; render() { if (!this.view0) return null; const isText = this.view0.props.Document[this.view0.LayoutFieldKey] instanceof RichTextField; const doc = this.view0?.props.Document; const considerPull = isText && this.considerGoogleDocsPull; const considerPush = isText && this.considerGoogleDocsPush; return (
{this._showLinkPopup ? (
(link.link_displayLine = !IsFollowLinkScript(this.props.views().lastElement()?.rootDoc.onClick))} linkCreateAnchor={() => this.props.views().lastElement()?.ComponentView?.getAnchor?.(true)} linkFrom={() => this.props.views().lastElement()?.rootDoc} />
) : (
{this.linkButton}
)} {DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== doc ?
{this.endLinkButton}
: null} { Doc.noviceMode ? null :
{this.templateButton}
/*
{this.metadataButton}
*/ } {!SelectionManager.Views()?.some(v => v.allLinks.length) ? null :
{this.followLinkButton}
}
{this.pinButton}
{this.recordButton}
{!Doc.UserDoc()['documentLinksButton-fullMenu'] ? null :
{this.shareButton}
} {!Doc.UserDoc()['documentLinksButton-fullMenu'] ? null : (
{this.considerGoogleDocsPush}
)}
{this.considerGoogleDocsPull}
{this.menuButton}
); } }