diff options
-rw-r--r-- | src/client/util/DragManager.ts | 5 | ||||
-rw-r--r-- | src/client/views/DocumentButtonBar.scss | 129 | ||||
-rw-r--r-- | src/client/views/DocumentButtonBar.tsx | 368 | ||||
-rw-r--r-- | src/client/views/DocumentDecorations.tsx | 296 | ||||
-rw-r--r-- | src/client/views/collections/CollectionDockingView.tsx | 18 | ||||
-rw-r--r-- | src/client/views/collections/ParentDocumentSelector.scss | 11 | ||||
-rw-r--r-- | src/client/views/collections/ParentDocumentSelector.tsx | 46 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 3 | ||||
-rw-r--r-- | src/client/views/nodes/FormattedTextBox.tsx | 15 |
9 files changed, 585 insertions, 306 deletions
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index e3cdb3f39..56496c99b 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -66,7 +66,7 @@ export function SetupDrag( function moveLinkedDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean { const document = SelectionManager.SelectedDocuments()[0]; - document.props.removeDocument && document.props.removeDocument(doc); + document && document.props.removeDocument && document.props.removeDocument(doc); addDocument(doc); return true; } @@ -270,7 +270,8 @@ export namespace DragManager { let droppedDocuments: Doc[] = dragData.draggedDocuments.reduce((droppedDocs: Doc[], d) => { let dvs = DocumentManager.Instance.getDocumentViews(d); if (dvs.length) { - let inContext = dvs.filter(dv => dv.props.ContainingCollectionView === SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView); + let containingView = SelectionManager.SelectedDocuments()[0] ? SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView : undefined; + let inContext = dvs.filter(dv => dv.props.ContainingCollectionView === containingView); if (inContext.length) { inContext.forEach(dv => droppedDocs.push(dv.props.Document)); } else { diff --git a/src/client/views/DocumentButtonBar.scss b/src/client/views/DocumentButtonBar.scss new file mode 100644 index 000000000..8cd419bbe --- /dev/null +++ b/src/client/views/DocumentButtonBar.scss @@ -0,0 +1,129 @@ +@import "globalCssVariables"; + +$linkGap : 3px; + +.linkFlyout { + grid-column: 2/4; +} + +.linkButton-empty:hover { + background: $main-accent; + transform: scale(1.05); + cursor: pointer; +} + +.linkButton-nonempty:hover { + background: $main-accent; + transform: scale(1.05); + cursor: pointer; +} + +.documentButtonBar { + margin-top: $linkGap; + grid-column: 1/4; + width: max-content; + height: auto; + display: flex; + flex-direction: row; +} + +.linkButtonWrapper { + pointer-events: auto; + padding-right: 5px; + width: 25px; +} + +.linkButton-linker { + height: 20px; + width: 20px; + text-align: center; + border-radius: 50%; + pointer-events: auto; + color: $dark-color; + border: $dark-color 1px solid; +} + +.linkButton-linker:hover { + cursor: pointer; + transform: scale(1.05); +} + +.linkButton-empty, +.linkButton-nonempty { + height: 20px; + width: 20px; + border-radius: 50%; + opacity: 0.9; + pointer-events: auto; + background-color: $dark-color; + color: $light-color; + text-transform: uppercase; + letter-spacing: 2px; + font-size: 75%; + transition: transform 0.2s; + text-align: center; + display: flex; + justify-content: center; + align-items: center; + + &:hover { + background: $main-accent; + transform: scale(1.05); + cursor: pointer; + } +} + +.templating-menu { + position: absolute; + pointer-events: auto; + text-transform: uppercase; + letter-spacing: 2px; + font-size: 75%; + transition: transform 0.2s; + text-align: center; + display: flex; + justify-content: center; + align-items: center; +} + +.templating-button, +.docDecs-tagButton { + width: 20px; + height: 20px; + border-radius: 50%; + opacity: 0.9; + font-size: 14; + background-color: $dark-color; + color: $light-color; + text-align: center; + cursor: pointer; + + &:hover { + background: $main-accent; + transform: scale(1.05); + } +} + +#template-list { + position: absolute; + top: 25px; + left: 0px; + width: max-content; + font-family: $sans-serif; + font-size: 12px; + background-color: $light-color-secondary; + padding: 2px 12px; + list-style: none; + + .templateToggle, .chromeToggle { + text-align: left; + } + + input { + margin-right: 10px; + } +} + +@-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } } +@-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } } +@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } }
\ No newline at end of file diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx new file mode 100644 index 000000000..6c29a2dc7 --- /dev/null +++ b/src/client/views/DocumentButtonBar.tsx @@ -0,0 +1,368 @@ +import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; +import { faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faShare, faStopCircle, faSyncAlt, faTag, faTimes } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, observable, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import { Doc } from "../../new_fields/Doc"; +import { RichTextField } from '../../new_fields/RichTextField'; +import { NumCast } from "../../new_fields/Types"; +import { URLField } from '../../new_fields/URLField'; +import { emptyFunction } from "../../Utils"; +import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils'; +import { DragLinksAsDocuments, DragManager } from "../util/DragManager"; +import { LinkManager } from '../util/LinkManager'; +import { UndoManager } from "../util/UndoManager"; +import './DocumentButtonBar.scss'; +import './collections/ParentDocumentSelector.scss'; +import { LinkMenu } from "./linking/LinkMenu"; +import { MetadataEntryMenu } from './MetadataEntryMenu'; +import { FormattedTextBox, GoogleRef } from "./nodes/FormattedTextBox"; +import { TemplateMenu } from "./TemplateMenu"; +import { Template, Templates } from "./Templates"; +import React = require("react"); +import { DocumentView } from './nodes/DocumentView'; +import { ParentDocSelector } from './collections/ParentDocumentSelector'; +import { CollectionDockingView } from './collections/CollectionDockingView'; +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(faStopCircle); +library.add(faCheckCircle); +library.add(faCloudUploadAlt); +library.add(faSyncAlt); +library.add(faShare); + +const cloud: IconProp = "cloud-upload-alt"; +const fetch: IconProp = "sync-alt"; + +@observer +export class DocumentButtonBar extends React.Component<{ views: DocumentView[], stack?: any }, {}> { + private _linkButton = React.createRef<HTMLDivElement>(); + private _linkerButton = React.createRef<HTMLDivElement>(); + private _embedButton = React.createRef<HTMLDivElement>(); + private _tooltipoff = React.createRef<HTMLDivElement>(); + private _textDoc?: Doc; + private _linkDrag?: UndoManager.Batch; + public static Instance: DocumentButtonBar; + + constructor(props: { views: DocumentView[] }) { + super(props); + DocumentButtonBar.Instance = this; + } + + @observable public pushIcon: IconProp = "arrow-alt-circle-up"; + @observable public pullIcon: IconProp = "arrow-alt-circle-down"; + @observable public pullColor: string = "white"; + @observable public isAnimatingFetch = false; + @observable public openHover = false; + public pullColorAnimating = false; + + private pullAnimating = false; + private pushAnimating = false; + + 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) => { + 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; + }); + + onLinkerButtonDown = (e: React.PointerEvent): void => { + e.stopPropagation(); + e.preventDefault(); + document.removeEventListener("pointermove", this.onLinkerButtonMoved); + document.addEventListener("pointermove", this.onLinkerButtonMoved); + document.removeEventListener("pointerup", this.onLinkerButtonUp); + document.addEventListener("pointerup", this.onLinkerButtonUp); + } + + onEmbedButtonDown = (e: React.PointerEvent): void => { + e.stopPropagation(); + e.preventDefault(); + document.removeEventListener("pointermove", this.onEmbedButtonMoved); + document.addEventListener("pointermove", this.onEmbedButtonMoved); + document.removeEventListener("pointerup", this.onEmbedButtonUp); + document.addEventListener("pointerup", this.onEmbedButtonUp); + } + + onLinkerButtonUp = (e: PointerEvent): void => { + document.removeEventListener("pointermove", this.onLinkerButtonMoved); + document.removeEventListener("pointerup", this.onLinkerButtonUp); + e.stopPropagation(); + } + + onEmbedButtonUp = (e: PointerEvent): void => { + document.removeEventListener("pointermove", this.onEmbedButtonMoved); + document.removeEventListener("pointerup", this.onEmbedButtonUp); + e.stopPropagation(); + } + + @action + onLinkerButtonMoved = (e: PointerEvent): void => { + if (this._linkerButton.current !== null) { + document.removeEventListener("pointermove", this.onLinkerButtonMoved); + document.removeEventListener("pointerup", this.onLinkerButtonUp); + let selDoc = this.props.views[0]; + let container = selDoc.props.ContainingCollectionDoc ? selDoc.props.ContainingCollectionDoc.proto : undefined; + let dragData = new DragManager.LinkDragData(selDoc.props.Document, container ? [container] : []); + FormattedTextBox.InputBoxOverlay = undefined; + this._linkDrag = UndoManager.StartBatch("Drag Link"); + DragManager.StartLinkDrag(this._linkerButton.current, dragData, e.pageX, e.pageY, { + handlers: { + dragComplete: () => { + if (this._linkDrag) { + this._linkDrag.end(); + this._linkDrag = undefined; + } + }, + }, + hideSource: false + }); + } + e.stopPropagation(); + } + + @action + onEmbedButtonMoved = (e: PointerEvent): void => { + if (this._embedButton.current !== null) { + document.removeEventListener("pointermove", this.onEmbedButtonMoved); + document.removeEventListener("pointerup", this.onEmbedButtonUp); + + let dragDocView = this.props.views[0]; + let dragData = new DragManager.EmbedDragData(dragDocView.props.Document); + + DragManager.StartEmbedDrag(dragDocView.ContentDiv!, dragData, e.x, e.y, { + handlers: { + dragComplete: action(emptyFunction), + }, + hideSource: false + }); + } + e.stopPropagation(); + } + + onLinkButtonDown = (e: React.PointerEvent): void => { + e.stopPropagation(); + e.preventDefault(); + document.removeEventListener("pointermove", this.onLinkButtonMoved); + document.addEventListener("pointermove", this.onLinkButtonMoved); + document.removeEventListener("pointerup", this.onLinkButtonUp); + document.addEventListener("pointerup", this.onLinkButtonUp); + } + + onLinkButtonUp = (e: PointerEvent): void => { + document.removeEventListener("pointermove", this.onLinkButtonMoved); + document.removeEventListener("pointerup", this.onLinkButtonUp); + e.stopPropagation(); + } + + onLinkButtonMoved = async (e: PointerEvent) => { + if (this._linkButton.current !== null && (e.movementX > 1 || e.movementY > 1)) { + document.removeEventListener("pointermove", this.onLinkButtonMoved); + document.removeEventListener("pointerup", this.onLinkButtonUp); + DragLinksAsDocuments(this._linkButton.current, e.x, e.y, this.props.views[0].props.Document); + } + e.stopPropagation(); + } + + considerEmbed = () => { + let thisDoc = this.props.views[0].props.Document; + let canEmbed = thisDoc.data && thisDoc.data instanceof URLField; + if (!canEmbed) return (null); + return ( + <div className="linkButtonWrapper"> + <div title="Drag Embed" className="linkButton-linker" ref={this._embedButton} onPointerDown={this.onEmbedButtonDown}> + <FontAwesomeIcon className="documentdecorations-icon" icon="image" size="sm" /> + </div> + </div> + ); + } + + private get targetDoc() { + return this.props.views[0].props.Document; + } + + considerGoogleDocsPush = () => { + let canPush = this.targetDoc.data && this.targetDoc.data instanceof RichTextField; + if (!canPush) return (null); + let published = Doc.GetProto(this.targetDoc)[GoogleRef] !== undefined; + let icon: IconProp = published ? (this.pushIcon as any) : cloud; + return ( + <div className={"linkButtonWrapper"}> + <div title={`${published ? "Push" : "Publish"} to Google Docs`} className="linkButton-linker" onClick={() => { + DocumentDecorations.hasPushedHack = false; + this.targetDoc[Pushes] = NumCast(this.targetDoc[Pushes]) + 1; + }}> + <FontAwesomeIcon className="documentdecorations-icon" icon={icon} size={published ? "sm" : "xs"} /> + </div> + </div> + ); + } + + considerGoogleDocsPull = () => { + let canPull = this.targetDoc.data && this.targetDoc.data instanceof RichTextField; + let dataDoc = Doc.GetProto(this.targetDoc); + if (!canPull || !dataDoc[GoogleRef]) return (null); + let icon = dataDoc.unchanged === false ? (this.pullIcon as any) : fetch; + icon = this.openHover ? "share" : icon; + let animation = this.isAnimatingFetch ? "spin 0.5s linear infinite" : "none"; + let title = `${!dataDoc.unchanged ? "Pull from" : "Fetch"} Google Docs`; + return ( + <div className={"linkButtonWrapper"}> + <div + title={title} + className="linkButton-linker" + style={{ + backgroundColor: this.pullColor, + transition: "0.2s ease all" + }} + onPointerEnter={e => e.altKey && runInAction(() => this.openHover = true)} + onPointerLeave={() => runInAction(() => this.openHover = false)} + onClick={e => { + if (e.altKey) { + e.preventDefault(); + window.open(`https://docs.google.com/document/d/${dataDoc[GoogleRef]}/edit`); + } else { + this.clearPullColor(); + DocumentDecorations.hasPulledHack = false; + this.targetDoc[Pulls] = NumCast(this.targetDoc[Pulls]) + 1; + dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true); + } + }}> + <FontAwesomeIcon + style={{ + WebkitAnimation: animation, + MozAnimation: animation + }} + className="documentdecorations-icon" + icon={icon} + size="sm" + /> + </div> + </div> + ); + } + + public static hasPushedHack = false; + public static hasPulledHack = false; + + considerTooltip = () => { + let thisDoc = this.props.views[0].props.Document; + let isTextDoc = thisDoc.data && thisDoc.data instanceof RichTextField; + if (!isTextDoc) return null; + this._textDoc = thisDoc; + return ( + <div className="tooltipwrapper"> + <div title="Hide Tooltip" className="linkButton-linker" ref={this._tooltipoff} onPointerDown={this.onTooltipOff}> + {/* <FontAwesomeIcon className="fa-image" icon="image" size="sm" /> */} + </div> + </div> + + ); + } + + onTooltipOff = (e: React.PointerEvent): void => { + e.stopPropagation(); + if (this._textDoc) { + if (this._tooltipoff.current) { + if (this._tooltipoff.current.title === "Hide Tooltip") { + this._tooltipoff.current.title = "Show Tooltip"; + this._textDoc.tooltip = "hi"; + } + else { + this._tooltipoff.current.title = "Hide Tooltip"; + } + } + } + } + + get metadataMenu() { + return ( + <div className="linkButtonWrapper"> + <Flyout anchorPoint={anchorPoints.TOP_LEFT} + content={<MetadataEntryMenu docs={() => this.props.views.map(dv => dv.props.Document)} suggestWithFunction />}>{/* tfs: @bcz This might need to be the data document? */} + <div className="docDecs-tagButton" title="Add fields"><FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="sm" /></div> + </Flyout> + </div> + ); + } + + render() { + let linkButton = null; + if (this.props.views.length > 0) { + let selFirst = this.props.views[0]; + + let linkCount = LinkManager.Instance.getAllRelatedLinks(selFirst.props.Document).length; + linkButton = (<Flyout + anchorPoint={anchorPoints.RIGHT_TOP} + content={<LinkMenu docView={selFirst} + addDocTab={selFirst.props.addDocTab} + changeFlyout={emptyFunction} />}> + <div className={"linkButton-" + (linkCount ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} >{linkCount}</div> + </Flyout >); + } + + let templates: Map<Template, boolean> = new Map(); + Array.from(Object.values(Templates.TemplateList)).map(template => + templates.set(template, this.props.views.reduce((checked, doc) => checked || (doc.props.Document["show" + template.Name] ? true : false), false as boolean))); + + return (<div className="documentButtonBar"> + <div className="linkButtonWrapper"> + <div title="View Links" className="linkFlyout" ref={this._linkButton}> {linkButton} </div> + </div> + <div className="linkButtonWrapper"> + <div title="Drag Link" className="linkButton-linker" ref={this._linkerButton} onPointerDown={this.onLinkerButtonDown}> + <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" /> + </div> + </div> + <div className="linkButtonWrapper"> + <TemplateMenu docs={this.props.views} templates={templates} /> + </div> + {this.metadataMenu} + {this.considerEmbed()} + {this.considerGoogleDocsPush()} + {this.considerGoogleDocsPull()} + <ParentDocSelector Document={this.props.views[0].props.Document} addDocTab={(doc, data, where) => { + where === "onRight" ? CollectionDockingView.AddRightSplit(doc, data) : this.props.stack ? CollectionDockingView.Instance.AddTab(this.props.stack, doc, data) : this.props.views[0].props.addDocTab(doc, data, "onRight"); + return true; + }} /> + {/* {this.considerTooltip()} */} + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 2b121b32b..cbf812311 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -6,31 +6,23 @@ import { observer } from "mobx-react"; import { Doc, DocListCastAsync } from "../../new_fields/Doc"; import { List } from "../../new_fields/List"; import { ObjectField } from '../../new_fields/ObjectField'; -import { RichTextField } from '../../new_fields/RichTextField'; import { BoolCast, Cast, NumCast, StrCast } from "../../new_fields/Types"; -import { URLField } from '../../new_fields/URLField'; import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; import { emptyFunction, Utils } from "../../Utils"; -import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils'; import { Docs, DocUtils } from "../documents/Documents"; import { DocumentManager } from "../util/DocumentManager"; -import { DragLinksAsDocuments, DragManager } from "../util/DragManager"; -import { LinkManager } from '../util/LinkManager'; +import { DragManager } from "../util/DragManager"; import { SelectionManager } from "../util/SelectionManager"; import { undoBatch, UndoManager } from "../util/UndoManager"; import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss"; import { CollectionView } from "./collections/CollectionView"; +import { DocumentButtonBar } from './DocumentButtonBar'; import './DocumentDecorations.scss'; -import { LinkMenu } from "./linking/LinkMenu"; -import { MetadataEntryMenu } from './MetadataEntryMenu'; import { PositionDocument } from './nodes/CollectionFreeFormDocumentView'; import { DocumentView, swapViews } from "./nodes/DocumentView"; import { FieldView } from "./nodes/FieldView"; -import { FormattedTextBox, GoogleRef } from "./nodes/FormattedTextBox"; +import { FormattedTextBox } from "./nodes/FormattedTextBox"; import { IconBox } from "./nodes/IconBox"; -import { ImageBox } from './nodes/ImageBox'; -import { TemplateMenu } from "./TemplateMenu"; -import { Template, Templates } from "./Templates"; import React = require("react"); const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; @@ -47,28 +39,21 @@ library.add(faCloudUploadAlt); library.add(faSyncAlt); library.add(faShare); -const cloud: IconProp = "cloud-upload-alt"; -const fetch: IconProp = "sync-alt"; - @observer export class DocumentDecorations extends React.Component<{}, { value: string }> { static Instance: DocumentDecorations; private _isPointerDown = false; private _resizing = ""; - private keyinput: React.RefObject<HTMLInputElement>; + private _keyinput: React.RefObject<HTMLInputElement>; private _resizeBorderWidth = 16; private _linkBoxHeight = 20 + 3; // link button height + margin private _titleHeight = 20; - private _linkButton = React.createRef<HTMLDivElement>(); - private _linkerButton = React.createRef<HTMLDivElement>(); private _embedButton = React.createRef<HTMLDivElement>(); - private _tooltipoff = React.createRef<HTMLDivElement>(); - private _textDoc?: Doc; private _downX = 0; private _downY = 0; private _iconDoc?: Doc = undefined; private _resizeUndo?: UndoManager.Batch; - private _linkDrag?: UndoManager.Batch; + private _radiusDown = [0, 0]; @observable private _minimizedX = 0; @observable private _minimizedY = 0; @observable private _title: string = ""; @@ -78,58 +63,17 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> @observable private _opacity = 1; @observable private _removeIcon = false; @observable public Interacting = false; - @observable private _isMoving = false; @observable public pushIcon: IconProp = "arrow-alt-circle-up"; @observable public pullIcon: IconProp = "arrow-alt-circle-down"; @observable public pullColor: string = "white"; @observable public isAnimatingFetch = false; @observable public openHover = false; - public pullColorAnimating = false; - - private pullAnimating = false; - private pushAnimating = false; - - 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) => { - 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; - }); constructor(props: Readonly<{}>) { super(props); DocumentDecorations.Instance = this; - this.keyinput = React.createRef(); + this._keyinput = React.createRef(); reaction(() => SelectionManager.SelectedDocuments().slice(), docs => this._edtingTitle = false); } @@ -410,7 +354,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> iconDoc.y = where[1] + NumCast(selView.props.Document.y); } - _radiusDown = [0, 0]; @action onRadiusDown = (e: React.PointerEvent): void => { e.stopPropagation(); @@ -426,7 +369,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } onRadiusMove = (e: PointerEvent): void => { - this._isMoving = true; let dist = Math.sqrt((e.clientX - this._radiusDown[0]) * (e.clientX - this._radiusDown[0]) + (e.clientY - this._radiusDown[1]) * (e.clientY - this._radiusDown[1])); dist = dist < 3 ? 0 : dist; let usingRule = false; @@ -447,7 +389,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> e.preventDefault(); this._isPointerDown = false; this._resizeUndo && this._resizeUndo.end(); - this._isMoving = false; document.removeEventListener("pointermove", this.onRadiusMove); document.removeEventListener("pointerup", this.onRadiusUp); } @@ -467,13 +408,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } } - onLinkerButtonDown = (e: React.PointerEvent): void => { - e.stopPropagation(); - document.removeEventListener("pointermove", this.onLinkerButtonMoved); - document.addEventListener("pointermove", this.onLinkerButtonMoved); - document.removeEventListener("pointerup", this.onLinkerButtonUp); - document.addEventListener("pointerup", this.onLinkerButtonUp); - } onEmbedButtonDown = (e: React.PointerEvent): void => { e.stopPropagation(); @@ -483,11 +417,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> document.addEventListener("pointerup", this.onEmbedButtonUp); } - onLinkerButtonUp = (e: PointerEvent): void => { - document.removeEventListener("pointermove", this.onLinkerButtonMoved); - document.removeEventListener("pointerup", this.onLinkerButtonUp); - e.stopPropagation(); - } + onEmbedButtonUp = (e: PointerEvent): void => { document.removeEventListener("pointermove", this.onEmbedButtonMoved); @@ -496,31 +426,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } @action - onLinkerButtonMoved = (e: PointerEvent): void => { - if (this._linkerButton.current !== null) { - document.removeEventListener("pointermove", this.onLinkerButtonMoved); - document.removeEventListener("pointerup", this.onLinkerButtonUp); - let selDoc = SelectionManager.SelectedDocuments()[0]; - let container = selDoc.props.ContainingCollectionDoc ? selDoc.props.ContainingCollectionDoc.proto : undefined; - let dragData = new DragManager.LinkDragData(selDoc.props.Document, container ? [container] : []); - FormattedTextBox.InputBoxOverlay = undefined; - this._linkDrag = UndoManager.StartBatch("Drag Link"); - DragManager.StartLinkDrag(this._linkerButton.current, dragData, e.pageX, e.pageY, { - handlers: { - dragComplete: () => { - if (this._linkDrag) { - this._linkDrag.end(); - this._linkDrag = undefined; - } - }, - }, - hideSource: false - }); - } - e.stopPropagation(); - } - - @action onEmbedButtonMoved = (e: PointerEvent): void => { if (this._embedButton.current !== null) { document.removeEventListener("pointermove", this.onEmbedButtonMoved); @@ -539,29 +444,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> e.stopPropagation(); } - onLinkButtonDown = (e: React.PointerEvent): void => { - e.stopPropagation(); - document.removeEventListener("pointermove", this.onLinkButtonMoved); - document.addEventListener("pointermove", this.onLinkButtonMoved); - document.removeEventListener("pointerup", this.onLinkButtonUp); - document.addEventListener("pointerup", this.onLinkButtonUp); - } - - onLinkButtonUp = (e: PointerEvent): void => { - document.removeEventListener("pointermove", this.onLinkButtonMoved); - document.removeEventListener("pointerup", this.onLinkButtonUp); - e.stopPropagation(); - } - - onLinkButtonMoved = async (e: PointerEvent) => { - if (this._linkButton.current !== null && (e.movementX > 1 || e.movementY > 1)) { - document.removeEventListener("pointermove", this.onLinkButtonMoved); - document.removeEventListener("pointerup", this.onLinkButtonUp); - DragLinksAsDocuments(this._linkButton.current, e.x, e.y, SelectionManager.SelectedDocuments()[0].props.Document); - } - e.stopPropagation(); - } - onPointerMove = (e: PointerEvent): void => { e.stopPropagation(); e.preventDefault(); @@ -690,134 +572,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> return "-unset-"; } - changeFlyoutContent = (): void => { - } - // buttonOnPointerUp = (e: React.PointerEvent): void => { - // e.stopPropagation(); - // } - - considerEmbed = () => { - let thisDoc = SelectionManager.SelectedDocuments()[0].props.Document; - let canEmbed = thisDoc.data && thisDoc.data instanceof URLField; - if (!canEmbed) return (null); - return ( - <div className="linkButtonWrapper"> - <div title="Drag Embed" className="linkButton-linker" ref={this._embedButton} onPointerDown={this.onEmbedButtonDown}> - <FontAwesomeIcon className="documentdecorations-icon" icon="image" size="sm" /> - </div> - </div> - ); - } - - private get targetDoc() { - return SelectionManager.SelectedDocuments()[0].props.Document; - } - - considerGoogleDocsPush = () => { - let canPush = this.targetDoc.data && this.targetDoc.data instanceof RichTextField; - if (!canPush) return (null); - let published = Doc.GetProto(this.targetDoc)[GoogleRef] !== undefined; - let icon: IconProp = published ? (this.pushIcon as any) : cloud; - return ( - <div className={"linkButtonWrapper"}> - <div title={`${published ? "Push" : "Publish"} to Google Docs`} className="linkButton-linker" onClick={() => { - DocumentDecorations.hasPushedHack = false; - this.targetDoc[Pushes] = NumCast(this.targetDoc[Pushes]) + 1; - }}> - <FontAwesomeIcon className="documentdecorations-icon" icon={icon} size={published ? "sm" : "xs"} /> - </div> - </div> - ); - } - - considerGoogleDocsPull = () => { - let canPull = this.targetDoc.data && this.targetDoc.data instanceof RichTextField; - let dataDoc = Doc.GetProto(this.targetDoc); - if (!canPull || !dataDoc[GoogleRef]) return (null); - let icon = dataDoc.unchanged === false ? (this.pullIcon as any) : fetch; - icon = this.openHover ? "share" : icon; - let animation = this.isAnimatingFetch ? "spin 0.5s linear infinite" : "none"; - let title = `${!dataDoc.unchanged ? "Pull from" : "Fetch"} Google Docs`; - return ( - <div className={"linkButtonWrapper"}> - <div - title={title} - className="linkButton-linker" - style={{ - backgroundColor: this.pullColor, - transition: "0.2s ease all" - }} - onPointerEnter={e => e.altKey && runInAction(() => this.openHover = true)} - onPointerLeave={() => runInAction(() => this.openHover = false)} - onClick={e => { - if (e.altKey) { - e.preventDefault(); - window.open(`https://docs.google.com/document/d/${dataDoc[GoogleRef]}/edit`); - } else { - this.clearPullColor(); - DocumentDecorations.hasPulledHack = false; - this.targetDoc[Pulls] = NumCast(this.targetDoc[Pulls]) + 1; - dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true); - } - }}> - <FontAwesomeIcon - style={{ - WebkitAnimation: animation, - MozAnimation: animation - }} - className="documentdecorations-icon" - icon={icon} - size="sm" - /> - </div> - </div> - ); - } - - public static hasPushedHack = false; - public static hasPulledHack = false; - - considerTooltip = () => { - let thisDoc = SelectionManager.SelectedDocuments()[0].props.Document; - let isTextDoc = thisDoc.data && thisDoc.data instanceof RichTextField; - if (!isTextDoc) return null; - this._textDoc = thisDoc; - return ( - <div className="tooltipwrapper"> - <div title="Hide Tooltip" className="linkButton-linker" ref={this._tooltipoff} onPointerDown={this.onTooltipOff}> - {/* <FontAwesomeIcon className="fa-image" icon="image" size="sm" /> */} - </div> - </div> - - ); - } - - onTooltipOff = (e: React.PointerEvent): void => { - e.stopPropagation(); - if (this._textDoc) { - if (this._tooltipoff.current) { - if (this._tooltipoff.current.title === "Hide Tooltip") { - this._tooltipoff.current.title = "Show Tooltip"; - this._textDoc.tooltip = "hi"; - } - else { - this._tooltipoff.current.title = "Hide Tooltip"; - } - } - } - } - - get metadataMenu() { - return ( - <div className="linkButtonWrapper"> - <Flyout anchorPoint={anchorPoints.TOP_LEFT} - content={<MetadataEntryMenu docs={() => SelectionManager.SelectedDocuments().map(dv => dv.props.Document)} suggestWithFunction />}>{/* tfs: @bcz This might need to be the data document? */} - <div className="docDecs-tagButton" title="Add fields"><FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="sm" /></div> - </Flyout> - </div> - ); - } render() { var bounds = this.Bounds; @@ -830,24 +585,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> {SelectionManager.SelectedDocuments().length === 1 ? IconBox.DocumentIcon(StrCast(SelectionManager.SelectedDocuments()[0].props.Document.layout, "...")) : "..."} </div>); - let linkButton = null; - if (SelectionManager.SelectedDocuments().length > 0) { - let selFirst = SelectionManager.SelectedDocuments()[0]; - - let linkCount = LinkManager.Instance.getAllRelatedLinks(selFirst.props.Document).length; - linkButton = (<Flyout - anchorPoint={anchorPoints.RIGHT_TOP} - content={<LinkMenu docView={selFirst} - addDocTab={selFirst.props.addDocTab} - changeFlyout={this.changeFlyoutContent} />}> - <div className={"linkButton-" + (linkCount ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} >{linkCount}</div> - </Flyout >); - } - - let templates: Map<Template, boolean> = new Map(); - Array.from(Object.values(Templates.TemplateList)).map(template => - templates.set(template, SelectionManager.SelectedDocuments().reduce((checked, doc) => checked || (doc.props.Document["show" + template.Name] ? true : false), false as boolean))); - bounds.x = Math.max(0, bounds.x - this._resizeBorderWidth / 2) + this._resizeBorderWidth / 2; bounds.y = Math.max(0, bounds.y - this._resizeBorderWidth / 2 - this._titleHeight) + this._resizeBorderWidth / 2 + this._titleHeight; const borderRadiusDraggerWidth = 15; @@ -879,7 +616,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> {minimizeIcon} {this._edtingTitle ? - <input ref={this.keyinput} className="title" type="text" name="dynbox" value={this._title} onBlur={this.titleBlur} onChange={this.titleChanged} onKeyPress={this.titleEntered} /> : + <input ref={this._keyinput} className="title" type="text" name="dynbox" value={this._title} onBlur={this.titleBlur} onChange={this.titleChanged} onKeyPress={this.titleEntered} /> : <div className="title" onPointerDown={this.onTitleDown} ><span>{`${this.selectionTitle}`}</span></div>} <div className="documentDecorations-closeButton" title="Close Document" onPointerDown={this.onCloseDown}> <FontAwesomeIcon className="documentdecorations-times" icon={faTimes} size="lg" /> @@ -895,22 +632,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> <div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> <div id="documentDecorations-borderRadius" className="documentDecorations-radius" onPointerDown={this.onRadiusDown} onContextMenu={(e) => e.preventDefault()}><span className="borderRadiusTooltip" title="Drag Corner Radius"></span></div> <div className="link-button-container"> - <div className="linkButtonWrapper"> - <div title="View Links" className="linkFlyout" ref={this._linkButton}> {linkButton} </div> - </div> - <div className="linkButtonWrapper"> - <div title="Drag Link" className="linkButton-linker" ref={this._linkerButton} onPointerDown={this.onLinkerButtonDown}> - <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" /> - </div> - </div> - <div className="linkButtonWrapper"> - <TemplateMenu docs={SelectionManager.ViewsSortedVertically()} templates={templates} /> - </div> - {this.metadataMenu} - {this.considerEmbed()} - {this.considerGoogleDocsPush()} - {this.considerGoogleDocsPull()} - {/* {this.considerTooltip()} */} + <DocumentButtonBar views={SelectionManager.SelectedDocuments()} /> </div> </div > </div> diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 277fa0066..5ace1048d 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -28,8 +28,8 @@ import { MainView } from '../MainView'; import { DocumentView } from "../nodes/DocumentView"; import "./CollectionDockingView.scss"; import { SubCollectionViewProps } from "./CollectionSubView"; -import { ParentDocSelector } from './ParentDocumentSelector'; import React = require("react"); +import { ButtonSelector } from './ParentDocumentSelector'; library.add(faFile); @observer @@ -401,6 +401,10 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp dragSpan.style.bottom = "6px"; dragSpan.style.paddingLeft = "4px"; dragSpan.style.paddingRight = "2px"; + let gearSpan = document.createElement("span"); + gearSpan.style.position = "relative"; + gearSpan.style.paddingLeft = "0px"; + gearSpan.style.paddingRight = "12px"; let upDiv = document.createElement("span"); const stack = tab.contentItem.parent; // shifts the focus to this tab when another tab is dragged over it @@ -421,12 +425,14 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp hideSource: false }); }}><FontAwesomeIcon icon="file" size="lg" /></span>, dragSpan); - ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={(doc, data, where) => { - where === "onRight" ? CollectionDockingView.AddRightSplit(doc, dataDoc) : CollectionDockingView.Instance.AddTab(stack, doc, dataDoc); - return true; - }} />, upDiv); - tab.reactComponents = [dragSpan, upDiv]; + ReactDOM.render(<ButtonSelector Document={doc} Stack={stack} />, gearSpan); + // ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={(doc, data, where) => { + // where === "onRight" ? CollectionDockingView.AddRightSplit(doc, dataDoc) : CollectionDockingView.Instance.AddTab(stack, doc, dataDoc); + // return true; + // }} />, upDiv); + tab.reactComponents = [dragSpan, gearSpan, upDiv]; tab.element.append(dragSpan); + tab.element.append(gearSpan); tab.element.append(upDiv); tab.reactionDisposer = reaction(() => [doc.title, Doc.IsBrushedDegree(doc)], () => { tab.titleElement[0].textContent = doc.title, { fireImmediately: true }; diff --git a/src/client/views/collections/ParentDocumentSelector.scss b/src/client/views/collections/ParentDocumentSelector.scss index 2dd3e49f2..6f71bcc79 100644 --- a/src/client/views/collections/ParentDocumentSelector.scss +++ b/src/client/views/collections/ParentDocumentSelector.scss @@ -1,5 +1,5 @@ .PDS-flyout { - position: absolute; + position: relative; z-index: 9999; background-color: #eeeeee; box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); @@ -19,4 +19,13 @@ border-right: 0px; border-left: 0px; } +} +.parentDocumentSelector-button { + pointer-events: all; +} +.buttonSelector { + position: absolute; + display: inline-block; + padding-left: 5px; + padding-right: 5px; }
\ No newline at end of file diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx index d8475a467..7f2913214 100644 --- a/src/client/views/collections/ParentDocumentSelector.tsx +++ b/src/client/views/collections/ParentDocumentSelector.tsx @@ -8,8 +8,15 @@ import { SearchUtil } from "../../util/SearchUtil"; import { CollectionDockingView } from "./CollectionDockingView"; import { NumCast } from "../../../new_fields/Types"; import { CollectionViewType } from "./CollectionBaseView"; +import { DocumentButtonBar } from "../DocumentButtonBar"; +import { DocumentManager } from "../../util/DocumentManager"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faEdit } from "@fortawesome/free-solid-svg-icons"; +import { library } from "@fortawesome/fontawesome-svg-core"; -type SelectorProps = { Document: Doc, addDocTab(doc: Doc, dataDoc: Doc | undefined, location: string): void }; +library.add(faEdit); + +type SelectorProps = { Document: Doc, Stack?: any, addDocTab(doc: Doc, dataDoc: Doc | undefined, location: string): void }; @observer export class SelectorContextMenu extends React.Component<SelectorProps> { @observable private _docs: { col: Doc, target: Doc }[] = []; @@ -83,7 +90,7 @@ export class ParentDocSelector extends React.Component<SelectorProps> { ); } return ( - <span style={{ position: "relative", display: "inline-block", paddingLeft: "5px", paddingRight: "5px" }} + <span className="parentDocumentSelector-button" style={{ position: "relative", display: "inline-block", paddingLeft: "5px", paddingRight: "5px" }} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}> <p>^</p> @@ -92,3 +99,38 @@ export class ParentDocSelector extends React.Component<SelectorProps> { ); } } + +@observer +export class ButtonSelector extends React.Component<{ Document: Doc, Stack: any }> { + @observable hover = false; + + @action + onMouseLeave = () => { + this.hover = false; + } + + @action + onMouseEnter = () => { + this.hover = true; + } + + render() { + let flyout; + if (this.hover) { + let view = DocumentManager.Instance.getDocumentView(this.props.Document); + flyout = !view ? (null) : ( + <div className="PDS-flyout" title=" " onMouseLeave={this.onMouseLeave}> + <DocumentButtonBar views={[view]} stack={this.props.Stack} /> + </div> + ); + } + return ( + <span className="buttonSelector" + onMouseEnter={this.onMouseEnter} + onMouseLeave={this.onMouseLeave}> + {this.hover ? (null) : <FontAwesomeIcon icon={faEdit} size={"sm"} />} + {flyout} + </span> + ); + } +} diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 7f3ebe026..584b5d024 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -349,7 +349,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu e.stopPropagation(); // const docs = await SearchUtil.Search(`data_l:"${destDoc[Id]}"`, true); // const views = docs.map(d => DocumentManager.Instance.getDocumentView(d)).filter(d => d).map(d => d as DocumentView); - de.data.linkDocument = DocUtils.MakeLink(de.data.linkSourceDocument, this.props.Document, this.props.ContainingCollectionDoc); + de.data.linkSourceDocument !== this.props.Document && + (de.data.linkDocument = DocUtils.MakeLink(de.data.linkSourceDocument, this.props.Document, this.props.ContainingCollectionDoc)); } } diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index ab0412c37..031929bbd 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -39,6 +39,7 @@ import { ReplaceStep } from 'prosemirror-transform'; import { DocumentType } from '../../documents/DocumentTypes'; import { formattedTextBoxCommentPlugin, FormattedTextBoxComment } from './FormattedTextBoxComment'; import { inputRules } from 'prosemirror-inputrules'; +import { DocumentButtonBar } from '../DocumentButtonBar'; library.add(faEdit); library.add(faSmile, faTextHeight, faUpload); @@ -429,8 +430,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this._pullReactionDisposer = reaction( () => this.props.Document[Pulls], () => { - if (!DocumentDecorations.hasPulledHack) { - DocumentDecorations.hasPulledHack = true; + if (!DocumentButtonBar.hasPulledHack) { + DocumentButtonBar.hasPulledHack = true; let unchanged = this.dataDoc.unchanged; this.pullFromGoogleDoc(unchanged ? this.checkState : this.updateState); } @@ -440,8 +441,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this._pushReactionDisposer = reaction( () => this.props.Document[Pushes], () => { - if (!DocumentDecorations.hasPushedHack) { - DocumentDecorations.hasPushedHack = true; + if (!DocumentButtonBar.hasPushedHack) { + DocumentButtonBar.hasPushedHack = true; this.pushToGoogleDoc(); } } @@ -534,7 +535,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe response && (this.dataDoc[GoogleRef] = response.documentId); let pushSuccess = response !== undefined && !("errors" in response); dataDoc.unchanged = pushSuccess; - DocumentDecorations.Instance.startPushOutcome(pushSuccess); + DocumentButtonBar.Instance.startPushOutcome(pushSuccess); } }; let undo = () => { @@ -579,7 +580,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } else { delete dataDoc[GoogleRef]; } - DocumentDecorations.Instance.startPullOutcome(pullSuccess); + DocumentButtonBar.Instance.startPullOutcome(pullSuccess); } checkState = (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => { @@ -592,7 +593,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe let receivedTitle = exportState.title; let unchanged = storedPlainText === receivedPlainText && storedTitle === receivedTitle; dataDoc.unchanged = unchanged; - DocumentDecorations.Instance.setPullState(unchanged); + DocumentButtonBar.Instance.setPullState(unchanged); } } } |