aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/PropertiesButtons.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/PropertiesButtons.tsx')
-rw-r--r--src/client/views/PropertiesButtons.tsx524
1 files changed, 524 insertions, 0 deletions
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
new file mode 100644
index 000000000..eecf670e6
--- /dev/null
+++ b/src/client/views/PropertiesButtons.tsx
@@ -0,0 +1,524 @@
+import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
+import { faArrowAltCircleDown, faArrowAltCircleRight, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faPhotoVideo, faShare, faStopCircle, faSyncAlt, faTag, faTimes } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { action, computed, observable, runInAction } from "mobx";
+import { observer } from "mobx-react";
+import { Doc, DocListCast } from "../../fields/Doc";
+import { RichTextField } from '../../fields/RichTextField';
+import { Cast, NumCast, BoolCast } from "../../fields/Types";
+import { emptyFunction, setupMoveUpEvents } 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 { CollectionDockingView, DockedFrameRenderer } from './collections/CollectionDockingView';
+import { ParentDocSelector } from './collections/ParentDocumentSelector';
+import './collections/ParentDocumentSelector.scss';
+import './PropertiesButtons.scss';
+import { MetadataEntryMenu } from './MetadataEntryMenu';
+import { DocumentView } from './nodes/DocumentView';
+import { GoogleRef } from "./nodes/formattedText/FormattedTextBox";
+import { TemplateMenu } from "./TemplateMenu";
+import { Template, Templates } from "./Templates";
+import React = require("react");
+import { Tooltip } from '@material-ui/core';
+import { SelectionManager } from '../util/SelectionManager';
+import SharingManager from '../util/SharingManager';
+import { GooglePhotos } from '../apis/google_docs/GooglePhotosClientUtils';
+import { ImageField } from '../../fields/URLField';
+import { undoBatch, UndoManager } from '../util/UndoManager';
+const higflyout = require("@hig/flyout");
+export const { anchorPoints } = higflyout;
+export const Flyout = higflyout.default;
+
+library.add(faLink);
+library.add(faTag);
+library.add(faTimes);
+library.add(faArrowAltCircleDown);
+library.add(faArrowAltCircleUp);
+library.add(faArrowAltCircleRight);
+library.add(faStopCircle);
+library.add(faCheckCircle);
+library.add(faCloudUploadAlt);
+library.add(faSyncAlt);
+library.add(faShare);
+library.add(faPhotoVideo);
+
+const cloud: IconProp = "cloud-upload-alt";
+const fetch: IconProp = "sync-alt";
+
+enum UtilityButtonState {
+ Default,
+ OpenRight,
+ OpenExternally
+}
+
+@observer
+export class PropertiesButtons extends React.Component<{}, {}> {
+ private _dragRef = React.createRef<HTMLDivElement>();
+ 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: PropertiesButtons;
+ public static hasPushedHack = false;
+ public static hasPulledHack = false;
+
+
+ @computed get selectedDocumentView() {
+ if (SelectionManager.SelectedDocuments().length) {
+ return SelectionManager.SelectedDocuments()[0];
+ } else { return undefined; }
+ }
+ @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; }
+ @computed get dataDoc() { return this.selectedDocumentView?.dataDoc; }
+
+ @computed get onClick() { return this.selectedDoc?.onClickBehavior ? this.selectedDoc?.onClickBehavior : "nothing"; }
+
+ 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;
+ });
+
+ @computed
+ get considerGoogleDocsPush() {
+ const targetDoc = this.selectedDoc;
+ const published = targetDoc && Doc.GetProto(targetDoc)[GoogleRef] !== undefined;
+ const animation = this.isAnimatingPulse ? "shadow-pulse 1s linear infinite" : "none";
+ return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{`${published ? "Push" : "Publish"} to Google Docs`}</div></>}>
+ <div
+ className="propertiesButtons-linker"
+ style={{ animation }}
+ onClick={async () => {
+ await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
+ !published && runInAction(() => this.isAnimatingPulse = true);
+ PropertiesButtons.hasPushedHack = false;
+ targetDoc[Pushes] = NumCast(targetDoc[Pushes]) + 1;
+ }}>
+ <FontAwesomeIcon className="documentdecorations-icon" icon={published ? (this.pushIcon as any) : cloud} size={published ? "sm" : "xs"} />
+ </div></Tooltip>;
+ }
+
+ @computed
+ get considerGoogleDocsPull() {
+ const targetDoc = this.selectedDoc;
+ 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?.unchanged ? "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) : <Tooltip
+ title={<><div className="dash-tooltip">{title}</div></>}>
+ <div className="propertiesButtons-linker"
+ style={{ backgroundColor: this.pullColor }}
+ onPointerEnter={action(e => {
+ 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, isAnnotating: false, UseCors: false };
+ googleDoc = Docs.Create.WebDocument(googleDocUrl, options);
+ dataDoc.googleDoc = googleDoc;
+ }
+ CollectionDockingView.AddRightSplit(googleDoc);
+ } else if (e.altKey) {
+ e.preventDefault();
+ window.open(googleDocUrl);
+ } else {
+ this.clearPullColor();
+ PropertiesButtons.hasPulledHack = false;
+ targetDoc[Pulls] = NumCast(targetDoc[Pulls]) + 1;
+ dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true);
+ }
+ }}>
+ <FontAwesomeIcon className="documentdecorations-icon" size="sm"
+ style={{ WebkitAnimation: animation, MozAnimation: animation }}
+ icon={(() => {
+ switch (this.openHover) {
+ default:
+ case UtilityButtonState.Default: return dataDoc.unchanged === false ? (this.pullIcon as any) : fetch;
+ case UtilityButtonState.OpenRight: return "arrow-alt-circle-right";
+ case UtilityButtonState.OpenExternally: return "share";
+ }
+ })()}
+ />
+ </div></Tooltip>;
+ }
+ @computed
+ get pinButton() {
+ const targetDoc = this.selectedDoc;
+ const isPinned = targetDoc && Doc.isDocPinned(targetDoc);
+ return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{Doc.isDocPinned(targetDoc) ? "Unpin from presentation" : "Pin to presentation"}</div></>}>
+ <div className="propertiesButtons-linker"
+ style={{ backgroundColor: isPinned ? "white" : "rgb(37, 43, 51)", color: isPinned ? "black" : "white" }}
+ onClick={e => DockedFrameRenderer.PinDoc(targetDoc, isPinned)}>
+ <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin"
+ />
+ </div></Tooltip>;
+ }
+
+ @computed
+ get metadataButton() {
+ //const view0 = this.view0;
+ if (this.selectedDoc) {
+ return <Tooltip title={<><div className="dash-tooltip">Show metadata panel</div></>}>
+ <div className="propertiesButtons-linkFlyout">
+ <Flyout anchorPoint={anchorPoints.LEFT_TOP}
+ content={<MetadataEntryMenu docs={[this.selectedDoc]} suggestWithFunction /> /* tfs: @bcz This might need to be the data document? */}>
+ <div className={"propertiesButtons-linkButton-" + "empty"} onPointerDown={e => e.stopPropagation()} >
+ {<FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="sm" />}
+ </div>
+ </Flyout>
+ </div></Tooltip>;
+ } else {
+ return null;
+ }
+
+ }
+
+ @observable _aliasDown = false;
+ onAliasButtonDown = (e: React.PointerEvent): void => {
+ setupMoveUpEvents(this, e, this.onAliasButtonMoved, emptyFunction, emptyFunction);
+ }
+ onAliasButtonMoved = () => {
+ if (this._dragRef.current) {
+ const dragDocView = this.selectedDocumentView!;
+ const dragData = new DragManager.DocumentDragData([dragDocView.props.Document]);
+ const [left, top] = dragDocView.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
+ dragData.dropAction = "alias";
+ DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, left, top, {
+ offsetX: dragData.offset[0],
+ offsetY: dragData.offset[1],
+ hideSource: false
+ });
+ return true;
+ }
+ return false;
+ }
+
+ @computed
+ get templateButton() {
+ const docView = this.selectedDocumentView;
+ const templates: Map<Template, boolean> = new Map();
+ const views = [this.selectedDocumentView];
+ Array.from(Object.values(Templates.TemplateList)).map(template =>
+ templates.set(template, views.reduce((checked, doc) => checked || doc?.props.Document["_show" + template.Name] ? true : false, false as boolean)));
+ return !docView ? (null) :
+ <Tooltip title={<><div className="dash-tooltip">Customize layout</div></>}>
+ <div className="propertiesButtons-linkFlyout">
+ <Flyout anchorPoint={anchorPoints.LEFT_TOP} //onOpen={action(() => this._aliasDown = true)} onClose={action(() => this._aliasDown = false)}
+ content={<TemplateMenu docViews={views.filter(v => v).map(v => v as DocumentView)} templates={templates} />}>
+ <div className={"propertiesButtons-linkButton-empty"} >
+ {<FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" />}
+ </div>
+ </Flyout>
+ </div></Tooltip>;
+ }
+
+ onCopy = () => {
+ if (this.selectedDoc && this.selectedDocumentView) {
+ const copy = Doc.MakeCopy(this.selectedDocumentView.props.Document, true);
+ copy.x = NumCast(this.selectedDoc.x) + NumCast(this.selectedDoc._width);
+ copy.y = NumCast(this.selectedDoc.y) + 30;
+ this.selectedDocumentView.props.addDocument?.(copy);
+ }
+ }
+
+ @computed
+ get copyButton() {
+ const targetDoc = this.selectedDoc;
+ return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Tap or Drag to create an alias"}</div></>}>
+ <div className={"propertiesButtons-linkButton-empty"}
+ ref={this._dragRef}
+ onPointerDown={this.onAliasButtonDown}
+ onClick={this.onCopy}>
+ {<FontAwesomeIcon className="documentdecorations-icon" icon="copy" size="sm" />}
+ </div>
+ </Tooltip>;
+ }
+
+ @action
+ onLock = () => {
+ this.selectedDocumentView?.toggleLockPosition();
+ }
+
+ @computed
+ get lockButton() {
+ const targetDoc = this.selectedDoc;
+ return !targetDoc ? (null) : <Tooltip
+ title={<><div className="dash-tooltip">{this.selectedDoc?.lockedPosition ?
+ "Unlock Position" : "Lock Position"}</div></>}>
+ <div className={"propertiesButtons-linkButton-empty"}
+ onPointerDown={this.onLock} >
+ {<FontAwesomeIcon className="documentdecorations-icon"
+ icon={BoolCast(this.selectedDoc?.lockedPosition) ? "unlock" : "lock"} size="sm" />}
+ </div>
+ </Tooltip>;
+ }
+
+ @computed
+ get downloadButton() {
+ const targetDoc = this.selectedDoc;
+ return !targetDoc ? (null) : <Tooltip
+ title={<><div className="dash-tooltip">{"Download Document"}</div></>}>
+ <div className={"propertiesButtons-linkButton-empty"}
+ onPointerDown={async () => {
+ if (this.selectedDocumentView?.props.Document) {
+ Doc.Zip(this.selectedDocumentView?.props.Document);
+ }
+ }}>
+ {<FontAwesomeIcon className="propertiesButtons-icon"
+ icon="download" size="sm" />}
+ </div>
+ </Tooltip>;
+ }
+
+ @computed
+ get deleteButton() {
+ const targetDoc = this.selectedDoc;
+ return !targetDoc ? (null) : <Tooltip
+ title={<><div className="dash-tooltip">{"Delete Document"}</div></>}>
+ <div className={"propertiesButtons-linkButton-empty"}
+ onPointerDown={() => {
+ this.selectedDocumentView?.props.ContainingCollectionView?.removeDocument(this.selectedDocumentView?.props.Document);
+ }}>
+ {<FontAwesomeIcon className="propertiesButtons-icon"
+ icon="trash-alt" size="sm" />}
+ </div>
+ </Tooltip>;
+ }
+
+ @computed
+ get sharingButton() {
+ const targetDoc = this.selectedDoc;
+ return !targetDoc ? (null) : <Tooltip
+ title={<><div className="dash-tooltip">{"Share Document"}</div></>}>
+ <div className={"propertiesButtons-linkButton-empty"}
+ onPointerDown={() => {
+ if (this.selectedDocumentView) {
+ SharingManager.Instance.open(this.selectedDocumentView);
+ }
+ }}>
+ {<FontAwesomeIcon className="propertiesButtons-icon"
+ icon="users" size="sm" />}
+ </div>
+ </Tooltip>;
+ }
+
+ @computed
+ get onClickButton() {
+ if (this.selectedDoc) {
+ return <Tooltip title={<><div className="dash-tooltip">Choose onClick behavior</div></>}>
+ <div className="propertiesButtons-linkFlyout">
+ <Flyout anchorPoint={anchorPoints.LEFT_TOP}
+ content={this.onClickFlyout}>
+ <div className={"propertiesButtons-linkButton-" + "empty"} onPointerDown={e => e.stopPropagation()} >
+ {<FontAwesomeIcon className="documentdecorations-icon" icon="mouse-pointer" size="sm" />}
+ </div>
+ </Flyout>
+ </div></Tooltip>;
+ } else {
+ return null;
+ }
+ }
+
+ @action
+ handleOptionChange = (e: any) => {
+ const value = e.target.value;
+ this.selectedDoc && (this.selectedDoc.onClickBehavior = e.target.value);
+ if (value === "nothing") {
+ this.selectedDocumentView?.noOnClick();
+ } else if (value === "enterPortal") {
+ this.selectedDocumentView?.noOnClick();
+ this.selectedDocumentView?.makeIntoPortal();
+ } else if (value === "toggleDetail") {
+ this.selectedDocumentView?.noOnClick();
+ this.selectedDocumentView?.toggleDetail();
+ } else if (value === "linkInPlace") {
+ this.selectedDocumentView?.noOnClick();
+ this.selectedDocumentView?.toggleFollowInPlace();
+ } else if (value === "linkOnRight") {
+ this.selectedDocumentView?.noOnClick();
+ this.selectedDocumentView?.toggleFollowOnRight();
+ }
+ }
+
+ @undoBatch @action
+ editOnClickScript = () => {
+ if (this.selectedDoc) {
+ DocUtils.makeCustomViewClicked(this.selectedDoc, undefined, "onClick");
+ }
+ }
+
+ @computed
+ get onClickFlyout() {
+ return <div><form>
+ <div className="radio">
+ <label>
+ <input type="radio" value="nothing"
+ checked={this.onClick === 'nothing'}
+ onChange={this.handleOptionChange} />
+ Select Document
+ </label>
+ </div>
+ <div className="radio">
+ <label>
+ <input type="radio" value="enterPortal"
+ checked={this.onClick === 'enterPortal'}
+ onChange={this.handleOptionChange} />
+ Enter Portal
+ </label>
+ </div>
+ <div className="radio">
+ <label>
+ <input type="radio" value="toggleDetail"
+ checked={this.onClick === 'toggleDetail'}
+ onChange={this.handleOptionChange} />
+ Toggle Detail
+ </label>
+ </div>
+ <div className="radio">
+ <label>
+ <input type="radio" value="linkInPlace"
+ checked={this.onClick === 'linkInPlace'}
+ onChange={this.handleOptionChange} />
+ Follow Link
+ </label>
+ </div>
+ <div className="radio">
+ <label>
+ <input type="radio" value="linkOnRight"
+ checked={this.onClick === 'linkOnRight'}
+ onChange={this.handleOptionChange} />
+ Open Link on Right
+ </label>
+ </div>
+ </form>
+ <div onPointerDown={this.editOnClickScript} className="onClickFlyout-editScript"> Edit onClick Script</div>
+ </div>;
+ }
+
+ @computed
+ get googlePhotosButton() {
+ const targetDoc = this.selectedDoc;
+ return !targetDoc ? (null) : <Tooltip
+ title={<><div className="dash-tooltip">{"Export to Google Photos"}</div></>}>
+ <div className={"propertiesButtons-linkButton-empty"}
+ onPointerDown={() => {
+ if (this.selectedDocumentView) {
+ GooglePhotos.Export.CollectionToAlbum({ collection: this.selectedDocumentView.Document }).then(console.log)
+ }
+ }}>
+ {<FontAwesomeIcon className="documentdecorations-icon"
+ icon="cloud-upload-alt" size="sm" />}
+ </div>
+ </Tooltip>;
+ }
+
+
+ render() {
+ if (!this.selectedDoc) return (null);
+
+ const isText = this.selectedDoc[Doc.LayoutFieldKey(this.selectedDoc)] instanceof RichTextField;
+ const considerPull = isText && this.considerGoogleDocsPull;
+ const considerPush = isText && this.considerGoogleDocsPush;
+ const isImage = this.selectedDoc[Doc.LayoutFieldKey(this.selectedDoc)] instanceof ImageField;
+ return <div><div className="propertiesButtons" style={{ paddingBottom: "5.5px" }}>
+ <div className="propertiesButtons-button">
+ {this.templateButton}
+ </div>
+ {/* <div className="propertiesButtons-button">
+ {this.metadataButton}
+ </div> */}
+ <div className="propertiesButtons-button">
+ {this.pinButton}
+ </div>
+ <div className="propertiesButtons-button">
+ {this.copyButton}
+ </div>
+ <div className="propertiesButtons-button">
+ {this.lockButton}
+ </div>
+ <div className="propertiesButtons-button">
+ {this.downloadButton}
+ </div>
+ </div>
+ <div className="propertiesButtons">
+ <div className="propertiesButtons-button">
+ {this.deleteButton}
+ </div>
+ <div className="propertiesButtons-button">
+ {this.onClickButton}
+ </div>
+ <div className="propertiesButtons-button">
+ {this.sharingButton}
+ </div>
+ <div className="propertiesButtons-button" style={{ display: !considerPush ? "none" : "" }}>
+ {this.considerGoogleDocsPush}
+ </div>
+ <div className="propertiesButtons-button" style={{ display: !considerPull ? "none" : "" }}>
+ {this.considerGoogleDocsPull}
+ </div>
+ <div className="propertiesButtons-button" style={{ display: !isImage ? "none" : "" }}>
+ {this.googlePhotosButton}
+ </div>
+ </div>
+ </div>;
+ }
+}