diff options
Diffstat (limited to 'src/client/views')
| -rw-r--r-- | src/client/views/DocumentButtonBar.tsx | 63 | ||||
| -rw-r--r-- | src/client/views/DocumentDecorations.tsx | 2 | ||||
| -rw-r--r-- | src/client/views/GlobalKeyHandler.ts | 2 | ||||
| -rw-r--r-- | src/client/views/MainView.tsx | 11 | ||||
| -rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 4 | ||||
| -rw-r--r-- | src/client/views/globalCssVariables.scss | 1 | ||||
| -rw-r--r-- | src/client/views/linking/LinkMenu.scss | 4 | ||||
| -rw-r--r-- | src/client/views/linking/LinkMenu.tsx | 37 | ||||
| -rw-r--r-- | src/client/views/nodes/AudioBox.scss | 71 | ||||
| -rw-r--r-- | src/client/views/nodes/AudioBox.tsx | 2 | ||||
| -rw-r--r-- | src/client/views/nodes/DocumentLinksButton.scss | 38 | ||||
| -rw-r--r-- | src/client/views/nodes/DocumentLinksButton.tsx | 104 | ||||
| -rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 28 | ||||
| -rw-r--r-- | src/client/views/nodes/LinkAnchorBox.tsx | 8 | ||||
| -rw-r--r-- | src/client/views/nodes/LinkDocPreview.tsx | 107 | ||||
| -rw-r--r-- | src/client/views/nodes/formattedText/RichTextMenu.tsx | 9 |
16 files changed, 344 insertions, 147 deletions
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 62a95116f..fcaea0f0b 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -1,28 +1,28 @@ import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; -import { faArrowAltCircleDown, faPhotoVideo, faArrowAltCircleUp, faArrowAltCircleRight, faCheckCircle, faCloudUploadAlt, faLink, faShare, faStopCircle, faSyncAlt, faTag, faTimes } from '@fortawesome/free-solid-svg-icons'; +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 { NumCast, StrCast, Cast } from "../../fields/Types"; +import { Cast, NumCast } 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 { UndoManager } from "../util/UndoManager"; import { CollectionDockingView, DockedFrameRenderer } from './collections/CollectionDockingView'; import { ParentDocSelector } from './collections/ParentDocumentSelector'; import './collections/ParentDocumentSelector.scss'; import './DocumentButtonBar.scss'; -import { LinkMenu } from "./linking/LinkMenu"; +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 { DragManager } from '../util/DragManager'; -import { MetadataEntryMenu } from './MetadataEntryMenu'; -import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager'; -import { Docs } from '../documents/Documents'; +import { DocumentLinksButton } from './nodes/DocumentLinksButton'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -113,39 +113,6 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV get view0() { return this.props.views()?.[0]; } - @action - onLinkButtonMoved = (e: PointerEvent) => { - if (this._linkButton.current !== null) { - const linkDrag = UndoManager.StartBatch("Drag Link"); - this.view0 && DragManager.StartLinkDrag(this._linkButton.current, this.view0.props.Document, e.pageX, e.pageY, { - dragComplete: dropEv => { - const linkDoc = dropEv.linkDragData?.linkDocument as Doc; // equivalent to !dropEve.aborted since linkDocument is only assigned on a completed drop - if (this.view0 && linkDoc) { - !linkDoc.linkRelationship && (Doc.GetProto(linkDoc).linkRelationship = "hyperlink"); - - // we want to allow specific views to handle the link creation in their own way (e.g., rich text makes text hyperlinks) - // the dragged view can regiser a linkDropCallback to be notified that the link was made and to update their data structures - // however, the dropped document isn't so accessible. What we do is set the newly created link document on the documentView - // The documentView passes a function prop returning this link doc to its descendants who can react to changes to it. - dropEv.linkDragData?.linkDropCallback?.(dropEv.linkDragData); - runInAction(() => this.view0!._link = linkDoc); - setTimeout(action(() => this.view0!._link = undefined), 0); - } - linkDrag?.end(); - }, - hideSource: false - }); - return true; - } - return false; - } - - - onLinkButtonDown = (e: React.PointerEvent): void => { - setupMoveUpEvents(this, e, this.onLinkButtonMoved, emptyFunction, emptyFunction); - } - - @computed get considerGoogleDocsPush() { const targetDoc = this.view0?.props.Document; @@ -236,20 +203,6 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV } @computed - get linkButton() { - const view0 = this.view0; - const linkCount = view0 && DocListCast(view0.props.Document.links).length; - return !view0 ? (null) : <div title="Drag(create link) Tap(view links)" className="documentButtonBar-linkFlyout" ref={this._linkButton}> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} - content={<LinkMenu docView={view0} addDocTab={view0.props.addDocTab} changeFlyout={emptyFunction} />}> - <div className={"documentButtonBar-linkButton-" + (linkCount ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} > - {linkCount ? linkCount : <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" />} - </div> - </Flyout> - </div>; - } - - @computed get metadataButton() { const view0 = this.view0; return !view0 ? (null) : <div title="Show metadata panel" className="documentButtonBar-linkFlyout"> @@ -318,7 +271,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV const considerPush = isText && this.considerGoogleDocsPush; return <div className="documentButtonBar"> <div className="documentButtonBar-button"> - {this.linkButton} + <DocumentLinksButton View={this.view0!} AlwaysOn={true} /> </div> <div className="documentButtonBar-button"> {this.templateButton} diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index beb6155ca..d7b0ab7a9 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -552,7 +552,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> onPointerDown={this.onRadiusDown} onContextMenu={(e) => e.preventDefault()}></div> </div > - <div className="link-button-container" style={{ left: bounds.x - this._resizeBorderWidth / 2, top: bounds.b + this._resizeBorderWidth / 2 }}> + <div className="link-button-container" style={{ left: bounds.x - this._resizeBorderWidth / 2 + 10, top: bounds.b + this._resizeBorderWidth / 2 }}> <DocumentButtonBar views={SelectionManager.SelectedDocuments} /> </div> </div > diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 27755737e..7bc8cf6a7 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -19,6 +19,7 @@ import { MarqueeView } from "./collections/collectionFreeForm/MarqueeView"; import { DocumentDecorations } from "./DocumentDecorations"; import { MainView } from "./MainView"; import { DocumentView } from "./nodes/DocumentView"; +import { DocumentLinksButton } from "./nodes/DocumentLinksButton"; const modifiers = ["control", "meta", "shift", "alt"]; type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise<KeyControlInfo>; @@ -77,6 +78,7 @@ export default class KeyManager { // MarqueeView.DragMarquee = !MarqueeView.DragMarquee; // bcz: this needs a better disclosure UI break; case "escape": + DocumentLinksButton.StartLink = undefined; const main = MainView.Instance; Doc.SetSelectedTool(InkTool.None); if (main.isPointerDown) { diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 97953452d..4e2c067ac 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -5,7 +5,7 @@ import { faQuestionCircle, faArrowLeft, faArrowRight, faArrowDown, faArrowUp, faBolt, faBullseye, faCaretUp, faCat, faCheck, faChevronRight, faClipboard, faClone, faCloudUploadAlt, faCommentAlt, faCompressArrowsAlt, faCut, faEllipsisV, faEraser, faExclamation, faFileAlt, faFileAudio, faFilePdf, faFilm, faFilter, faFont, faGlobeAsia, faHighlighter, faLongArrowAltRight, faMicrophone, faMousePointer, faMusic, faObjectGroup, faPause, faPen, faPenNib, faPhone, faPlay, faPortrait, faRedoAlt, faStamp, faStickyNote, - faThumbtack, faTree, faTv, faUndoAlt, faVideo, faAsterisk, faBrain, faImage, faPaintBrush, faTimes, faEye, faQuoteLeft + faThumbtack, faTree, faTv, faUndoAlt, faVideo, faAsterisk, faBrain, faImage, faPaintBrush, faTimes, faEye, faQuoteLeft, faSortAmountDown } from '@fortawesome/free-solid-svg-icons'; import { ANTIMODEMENU_HEIGHT } from './globalCssVariables.scss'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -55,6 +55,9 @@ import { TimelineMenu } from './animationtimeline/TimelineMenu'; import { SnappingManager } from '../util/SnappingManager'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; import { DocumentManager } from '../util/DocumentManager'; +import { DocumentLinksButton } from './nodes/DocumentLinksButton'; +import { LinkMenu } from './linking/LinkMenu'; +import { LinkDocPreview } from './nodes/LinkDocPreview'; @observer export class MainView extends React.Component { @@ -135,7 +138,7 @@ export class MainView extends React.Component { faQuestionCircle, faArrowLeft, faArrowRight, faArrowDown, faArrowUp, faBolt, faBullseye, faCaretUp, faCat, faCheck, faChevronRight, faClipboard, faClone, faCloudUploadAlt, faCommentAlt, faCompressArrowsAlt, faCut, faEllipsisV, faEraser, faExclamation, faFileAlt, faFileAudio, faFilePdf, faFilm, faFilter, faFont, faGlobeAsia, faHighlighter, faLongArrowAltRight, faMicrophone, faMousePointer, faMusic, faObjectGroup, faPause, faPen, faPenNib, faPhone, faPlay, faPortrait, faRedoAlt, faStamp, faStickyNote, faTrashAlt, faAngleRight, faBell, - faThumbtack, faTree, faTv, faUndoAlt, faVideo, faAsterisk, faBrain, faImage, faPaintBrush, faTimes, faEye, faQuoteLeft); + faThumbtack, faTree, faTv, faUndoAlt, faVideo, faAsterisk, faBrain, faImage, faPaintBrush, faTimes, faEye, faQuoteLeft, faSortAmountDown); this.initEventListeners(); this.initAuthenticationRouters(); } @@ -578,6 +581,10 @@ export class MainView extends React.Component { {this.mainContent} </GestureOverlay> <PreviewCursor /> + {DocumentLinksButton.EditLink ? <LinkMenu location={DocumentLinksButton.EditLinkLoc} docView={DocumentLinksButton.EditLink} addDocTab={DocumentLinksButton.EditLink.props.addDocTab} changeFlyout={emptyFunction} /> : (null)} + {LinkDocPreview.LinkInfo ? <LinkDocPreview location={LinkDocPreview.LinkInfo.Location} backgroundColor={this.defaultBackgroundColors} + linkDoc={LinkDocPreview.LinkInfo.linkDoc} linkSrc={LinkDocPreview.LinkInfo.linkSrc} href={LinkDocPreview.LinkInfo.href} + addDocTab={LinkDocPreview.LinkInfo.addDocTab} /> : (null)} <ContextMenu /> <RadialMenu /> <PDFMenu /> diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index dc62c48b7..42aa0f58e 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -47,6 +47,7 @@ import { Timeline } from "../../animationtimeline/Timeline"; import { SnappingManager } from "../../../util/SnappingManager"; import { ActiveInkColor, ActiveInkWidth, ActiveInkBezierApprox } from "../../InkingStroke"; import { DocumentType } from "../../../documents/DocumentTypes"; +import { DocumentLinksButton } from "../../nodes/DocumentLinksButton"; library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload); @@ -245,7 +246,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P // const source = Docs.Create.TextDocument("", { _width: 200, _height: 75, x: xp, y: yp, title: "dropped annotation" }); // this.props.addDocument(source); // linkDragData.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: linkDragData.linkSourceDocument }, "doc annotation"); // TODODO this is where in text links get passed - return false; + return false; } else { const source = Docs.Create.TextDocument("", { _width: 200, _height: 75, x: xp, y: yp, title: "dropped annotation" }); this.props.addDocument(source); @@ -589,6 +590,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P onClick = (e: React.MouseEvent) => { if (this.layoutDoc.targetScale && (Math.abs(e.pageX - this._downX) < 3 && Math.abs(e.pageY - this._downY) < 3)) { if (Date.now() - this._lastTap < 300) { + runInAction(() => DocumentLinksButton.StartLink = undefined); const docpt = this.getTransform().transformPoint(e.clientX, e.clientY); this.scaleAtPt(docpt, 1); e.stopPropagation(); diff --git a/src/client/views/globalCssVariables.scss b/src/client/views/globalCssVariables.scss index 4e85fe0ca..3e54d001b 100644 --- a/src/client/views/globalCssVariables.scss +++ b/src/client/views/globalCssVariables.scss @@ -12,6 +12,7 @@ $lighter-alt-accent: rgb(207, 220, 240); $darker-alt-accent: rgb(178, 206, 248); $intermediate-color: #9c9396; $dark-color: #121721; +$link-color: lightBlue; $antimodemenu-height: 35px; // fonts $sans-serif: "Noto Sans", diff --git a/src/client/views/linking/LinkMenu.scss b/src/client/views/linking/LinkMenu.scss index 7dee22f66..c372e7098 100644 --- a/src/client/views/linking/LinkMenu.scss +++ b/src/client/views/linking/LinkMenu.scss @@ -8,6 +8,10 @@ .linkMenu-list { max-height: 200px; overflow-y: scroll; + position: absolute; + z-index: 10; + background: $link-color; + min-width: 150px } .linkMenu-group { diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx index 56f40ad69..0fcc0f0b9 100644 --- a/src/client/views/linking/LinkMenu.tsx +++ b/src/client/views/linking/LinkMenu.tsx @@ -4,11 +4,12 @@ import { DocumentView } from "../nodes/DocumentView"; import { LinkEditor } from "./LinkEditor"; import './LinkMenu.scss'; import React = require("react"); -import { Doc } from "../../../fields/Doc"; +import { Doc, Opt } from "../../../fields/Doc"; import { LinkManager } from "../../util/LinkManager"; import { LinkMenuGroup } from "./LinkMenuGroup"; import { faTrash } from '@fortawesome/free-solid-svg-icons'; import { library } from "@fortawesome/fontawesome-svg-core"; +import { DocumentLinksButton } from "../nodes/DocumentLinksButton"; library.add(faTrash); @@ -16,16 +17,29 @@ interface Props { docView: DocumentView; changeFlyout: () => void; addDocTab: (document: Doc, where: string) => boolean; + location: number[]; } @observer export class LinkMenu extends React.Component<Props> { @observable private _editingLink?: Doc; + @observable private _linkMenuRef: Opt<HTMLDivElement | null>; @action + onClick = (e: PointerEvent) => { + if (!Array.from(this._linkMenuRef?.getElementsByTagName((e.target as HTMLElement).tagName) || []).includes(e.target as any)) { + DocumentLinksButton.EditLink = undefined; + } + } + @action componentDidMount() { this._editingLink = undefined; + document.addEventListener("pointerdown", this.onClick); + } + + componentWillUnmount() { + document.removeEventListener("pointerdown", this.onClick); } clearAllLinks = () => { @@ -56,20 +70,11 @@ export class LinkMenu extends React.Component<Props> { render() { const sourceDoc = this.props.docView.props.Document; const groups: Map<string, Doc[]> = LinkManager.Instance.getRelatedGroupedLinks(sourceDoc); - if (this._editingLink === undefined) { - return ( - <div className="linkMenu"> - {/* <button className="linkEditor-button linkEditor-clearButton" onClick={() => this.clearAllLinks()} title="Clear all links"><FontAwesomeIcon icon="trash" size="sm" /></button> */} - {/* <input id="linkMenu-searchBar" type="text" placeholder="Search..."></input> */} - <div className="linkMenu-list"> - {this.renderAllGroups(groups)} - </div> - </div> - ); - } else { - return ( - <LinkEditor sourceDoc={this.props.docView.props.Document} linkDoc={this._editingLink} showLinks={action(() => this._editingLink = undefined)}></LinkEditor> - ); - } + return <div className="linkMenu-list" ref={(r) => this._linkMenuRef = r} style={{ left: this.props.location[0], top: this.props.location[1] }}> + {!this._editingLink ? + this.renderAllGroups(groups) : + <LinkEditor sourceDoc={this.props.docView.props.Document} linkDoc={this._editingLink} showLinks={action(() => this._editingLink = undefined)} /> + } + </div>; } }
\ No newline at end of file diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss index 43dd698ad..b5d113ca0 100644 --- a/src/client/views/nodes/AudioBox.scss +++ b/src/client/views/nodes/AudioBox.scss @@ -175,76 +175,29 @@ @media only screen and (max-device-width: 480px) { - - .audiobox-container, - .audiobox-container-interactive { - width: max-content; - /* align-self: center; */ - left: 50%; - top: 40%; - transform: translate(-50%, 0); - height: 100px; - position: absolute; - display: flex; - pointer-events: all; - /* cursor: default; */ - } - - .audiobox-container .audiobox-buttons, - .audiobox-container-interactive .audiobox-buttons { - display: flex; - width: 500px; - /* left: 50%; */ - /* transform: translate(-50%,0); */ - align-items: center; - } - .audiobox-dictation { + font-size: 5em; + display: flex; width: 100; - height: 100; - font-size: 50; - /* background-color: white; */ + justify-content: center; + flex-direction: column; + align-items: center; } - .audiobox-container .audiobox-record-interactive, - .audiobox-container-interactive .audiobox-record-interactive { - width: 95%; - user-select: none; - font-size: 50px; - font-weight: 100; - left: 5%; + .audiobox-container .audiobox-record, + .audiobox-container-interactive .audiobox-record { + pointer-events: all; + font-size: 3em; + width: 100%; height: 100%; position: relative; + pointer-events: none; } .audiobox-container .audiobox-controls .audiobox-player .audiobox-playhead, .audiobox-container .audiobox-controls .audiobox-player .audiobox-dictation, .audiobox-container-interactive .audiobox-controls .audiobox-player .audiobox-playhead, .audiobox-container-interactive .audiobox-controls .audiobox-player .audiobox-dictation { - position: relative; - align-self: center; - width: 50px; - height: max-content; - padding: 0; - } - - .audiobox-container .audiobox-controls .audiobox-player, - .audiobox-container-interactive .audiobox-controls .audiobox-player { - margin-top: auto; - margin-bottom: auto; - width: 100%; - height: 100%; - position: relative; - padding-right: 5px; - display: flex; - } - - .audiobox-container .audiobox-controls, - .audiobox-container-interactive .audiobox-controls { - width: 500px; - height: 100%; - position: relative; - display: flex; - padding-left: 2px; + width: 70px; } }
\ No newline at end of file diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index cb0582d87..d5288fff6 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -193,7 +193,7 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument const newDoc = Docs.Create.TextDocument("", { title: "", _chromeStatus: "disabled", x: NumCast(this.props.Document.x), y: NumCast(this.props.Document.y) + NumCast(this.props.Document._height) + 10, - _width: NumCast(this.props.Document._width), _height: 3 * NumCast(this.props.Document._height) + _width: NumCast(this.props.Document._width), _height: 2 * NumCast(this.props.Document._height) }); Doc.GetProto(newDoc).recordingSource = this.dataDoc; Doc.GetProto(newDoc).recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.props.fieldKey}-recordingStart"]`); diff --git a/src/client/views/nodes/DocumentLinksButton.scss b/src/client/views/nodes/DocumentLinksButton.scss new file mode 100644 index 000000000..484f8c469 --- /dev/null +++ b/src/client/views/nodes/DocumentLinksButton.scss @@ -0,0 +1,38 @@ +@import "../globalCssVariables.scss"; + + +.documentLinksButton, +.documentLinksButton-endLink, +.documentLinksButton-startLink { + height: 20px; + width: 20px; + position: absolute; + border-radius: 50%; + opacity: 0.9; + pointer-events: auto; + color: black; + 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: deepskyblue; + transform: scale(1.05); + cursor: default; + } +} +.documentLinksButton { + background-color: $link-color; +} +.documentLinksButton-endLink { + border: red solid 2px; +} +.documentLinksButton-startLink { + border: red solid 2px; + background-color: rgba(255, 192, 203, 0.5); +}
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx new file mode 100644 index 000000000..f35b48ce4 --- /dev/null +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -0,0 +1,104 @@ +import { action, computed, observable, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import { Doc, DocListCast } from "../../../fields/Doc"; +import { emptyFunction, setupMoveUpEvents, returnFalse } from "../../../Utils"; +import { DragManager } from "../../util/DragManager"; +import { UndoManager } from "../../util/UndoManager"; +import './DocumentLinksButton.scss'; +import { DocumentView } from "./DocumentView"; +import React = require("react"); +import { DocUtils } from "../../documents/Documents"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { LinkDocPreview } from "./LinkDocPreview"; +const higflyout = require("@hig/flyout"); +export const { anchorPoints } = higflyout; +export const Flyout = higflyout.default; + +interface DocumentLinksButtonProps { + View: DocumentView; + Offset?: number[]; + AlwaysOn?: boolean; +} +@observer +export class DocumentLinksButton extends React.Component<DocumentLinksButtonProps, {}> { + private _linkButton = React.createRef<HTMLDivElement>(); + + @action + onLinkButtonMoved = (e: PointerEvent) => { + if (this._linkButton.current !== null) { + const linkDrag = UndoManager.StartBatch("Drag Link"); + this.props.View && DragManager.StartLinkDrag(this._linkButton.current, this.props.View.props.Document, e.pageX, e.pageY, { + dragComplete: dropEv => { + const linkDoc = dropEv.linkDragData?.linkDocument as Doc; // equivalent to !dropEve.aborted since linkDocument is only assigned on a completed drop + if (this.props.View && linkDoc) { + !linkDoc.linkRelationship && (Doc.GetProto(linkDoc).linkRelationship = "hyperlink"); + + // we want to allow specific views to handle the link creation in their own way (e.g., rich text makes text hyperlinks) + // the dragged view can regiser a linkDropCallback to be notified that the link was made and to update their data structures + // however, the dropped document isn't so accessible. What we do is set the newly created link document on the documentView + // The documentView passes a function prop returning this link doc to its descendants who can react to changes to it. + dropEv.linkDragData?.linkDropCallback?.(dropEv.linkDragData); + runInAction(() => this.props.View._link = linkDoc); + setTimeout(action(() => this.props.View._link = undefined), 0); + } + linkDrag?.end(); + }, + hideSource: false + }); + return true; + } + return false; + } + + @observable static StartLink: DocumentView | undefined; + onLinkButtonDown = (e: React.PointerEvent): void => { + setupMoveUpEvents(this, e, this.onLinkButtonMoved, emptyFunction, action((e, doubleTap) => { + if (doubleTap) { + DocumentLinksButton.StartLink = this.props.View; + } else { + DocumentLinksButton.EditLink = this.props.View; + DocumentLinksButton.EditLinkLoc = [e.clientX + 10, e.clientY]; + } + })); + } + completeLink = (e: React.PointerEvent): void => { + setupMoveUpEvents(this, e, returnFalse, emptyFunction, action((e, doubleTap) => { + if (doubleTap) { + if (DocumentLinksButton.StartLink === this.props.View) { + DocumentLinksButton.StartLink = undefined; + } else { + DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View && + DocUtils.MakeLink({ doc: DocumentLinksButton.StartLink.props.Document }, { doc: this.props.View.props.Document }, "long drag"); + } + } + })); + } + + @observable + public static EditLink: DocumentView | undefined; + public static EditLinkLoc: number[] = [0, 0]; + + @computed + get linkButton() { + const links = DocListCast(this.props.View.props.Document.links); + return (!links.length || links[0].hidden) && !this.props.AlwaysOn ? (null) : + <div title="Drag(create link) Tap(view links)" ref={this._linkButton} style={{ position: "absolute", left: this.props.Offset?.[0] }}> + <div className={"documentLinksButton"} style={{ backgroundColor: DocumentLinksButton.StartLink ? "transparent" : "" }} + onPointerDown={this.onLinkButtonDown} + onPointerLeave={action(() => LinkDocPreview.LinkInfo = undefined)} + onPointerEnter={action(e => LinkDocPreview.LinkInfo = { + addDocTab: this.props.View.props.addDocTab, + linkSrc: this.props.View.props.Document, + linkDoc: links[0], + Location: [e.clientX, e.clientY + 20] + })} > + {links.length ? links.length : <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" />} + </div> + {DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View ? <div className={"documentLinksButton-endLink"} onPointerDown={this.completeLink} /> : (null)} + {DocumentLinksButton.StartLink === this.props.View ? <div className={"documentLinksButton-startLink"} /> : (null)} + </div>; + } + render() { + return this.linkButton; + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index d31ec8c1a..afd28534e 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -42,6 +42,7 @@ import { LinkAnchorBox } from './LinkAnchorBox'; import { RadialMenu } from './RadialMenu'; import React = require("react"); import { MobileInterface } from '../../../mobile/MobileInterface'; +import { DocumentLinksButton } from './DocumentLinksButton'; library.add(fa.faEdit, fa.faTrash, fa.faShare, fa.faDownload, fa.faExpandArrowsAlt, fa.faCompressArrowsAlt, fa.faLayerGroup, fa.faExternalLinkAlt, fa.faAlignCenter, fa.faCaretSquareRight, fa.faSquare, fa.faConciergeBell, fa.faWindowRestore, fa.faFolder, fa.faMapPin, fa.faLink, fa.faFingerprint, fa.faCrosshairs, fa.faDesktop, fa.faUnlock, fa.faLock, fa.faLaptopCode, fa.faMale, @@ -585,10 +586,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu toggleLinkButtonBehavior = (): void => { if (this.Document.isLinkButton || this.layoutDoc.onClick || this.Document.ignoreClick) { this.Document.isLinkButton = false; + const first = DocListCast(this.Document.links).find(d => d instanceof Doc); + first && (first.hidden = false); this.Document.ignoreClick = false; this.layoutDoc.onClick = undefined; } else { this.Document.isLinkButton = true; + const first = DocListCast(this.Document.links).find(d => d instanceof Doc); + first && (first.hidden = true); this.Document.followLinkZoom = false; this.Document.followLinkLocation = undefined; } @@ -598,8 +603,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu toggleFollowInPlace = (): void => { if (this.Document.isLinkButton) { this.Document.isLinkButton = false; + const first = DocListCast(this.Document.links).find(d => d instanceof Doc); + first && (first.hidden = false); } else { this.Document.isLinkButton = true; + const first = DocListCast(this.Document.links).find(d => d instanceof Doc); + first && (first.hidden = true); this.Document.followLinkZoom = true; this.Document.followLinkLocation = "inPlace"; } @@ -609,6 +618,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu toggleFollowOnRight = (): void => { if (this.Document.isLinkButton) { this.Document.isLinkButton = false; + const first = DocListCast(this.Document.links).find(d => d instanceof Doc); + first && (first.hidden = false); } else { this.Document.isLinkButton = true; this.Document.followLinkZoom = false; @@ -737,10 +748,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : []; const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layoutKey)], Doc, null); templateDoc && optionItems.push({ description: "Open Template ", event: () => this.props.addDocTab(templateDoc, "onRight"), icon: "eye" }); - if (!options) { - options = { description: "Options...", subitems: optionItems, icon: "compass" }; - cm?.addItem(options); - } + optionItems.push({ description: "Toggle Show Each Link Dot", event: () => this.layoutDoc.showLinks = !this.layoutDoc.showLinks, icon: "eye" }); + !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "compass" }); const existingOnClick = cm?.findByDescription("OnClick..."); const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; @@ -1039,7 +1048,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu select={this.select} onClick={this.onClickHandler} layoutKey={this.finalLayoutKey} /> - {this.anchors} + {this.layoutDoc.showLinks ? this.anchors : (null)} + {this.props.forcedBackgroundColor?.(this.Document) === "transparent" || this.props.dontRegisterView ? (null) : <DocumentLinksButton View={this} Offset={[-15, 0]} />} </div> ); } @@ -1063,7 +1073,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu anchorPanelHeight = () => this.props.PanelHeight() || 1; @computed get anchors() { TraceMobx(); - return this.layoutDoc.presBox ? (null) : DocListCast(this.Document.links).filter(d => !d.hidden && this.isNonTemporalLink).map((d, i) => + return this.props.forcedBackgroundColor?.(this.Document) === "transparent" || this.layoutDoc.presBox || this.props.dontRegisterView ? (null) : DocListCast(this.Document.links).filter(d => !d.hidden && this.isNonTemporalLink).map((d, i) => <DocumentView {...this.props} key={i + 1} Document={d} ContainingCollectionView={this.props.ContainingCollectionView} @@ -1071,6 +1081,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu PanelWidth={this.anchorPanelWidth} PanelHeight={this.anchorPanelHeight} ContentScaling={returnOne} + dontRegisterView={false} forcedBackgroundColor={returnTransparent} removeDocument={this.hideLinkAnchor} pointerEvents={false} @@ -1194,7 +1205,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu color: StrCast(this.layoutDoc.color, "inherit"), outline: highlighting && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px", border: highlighting && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined, - boxShadow: this.props.Document.isTemplateForField ? "black 0.2vw 0.2vw 0.8vw" : undefined, + boxShadow: this.Document.isLinkButton && !this.props.dontRegisterView && this.props.forcedBackgroundColor?.(this.Document) !== "transparent" ? + StrCast(this.props.Document._linkButtonShadow, "lightblue 0em 0em 1em") : + this.props.Document.isTemplateForField ? "black 0.2vw 0.2vw 0.8vw" : + undefined, background: finalColor, opacity: finalOpacity, fontFamily: StrCast(this.Document._fontFamily, "inherit"), diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index 2b64cdab6..2bcc6168b 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -18,6 +18,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { SelectionManager } from "../../util/SelectionManager"; import { TraceMobx } from "../../../fields/util"; import { Id } from "../../../fields/FieldSymbols"; +import { LinkDocPreview } from "./LinkDocPreview"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -132,6 +133,13 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch ); const small = this.props.PanelWidth() <= 1; return <div className={`linkAnchorBox-cont${small ? "-small" : ""} ${this.rootDoc[Id]}`} + onPointerLeave={action(() => LinkDocPreview.LinkInfo = undefined)} + onPointerEnter={action(e => LinkDocPreview.LinkInfo = { + addDocTab: this.props.addDocTab, + linkSrc: this.props.ContainingCollectionDoc!, + linkDoc: this.rootDoc, + Location: [e.clientX, e.clientY + 20] + })} onPointerDown={this.onPointerDown} onClick={this.onClick} title={targetTitle} onContextMenu={this.specificContextMenu} ref={this._ref} style={{ background: c, diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx new file mode 100644 index 000000000..126dc240a --- /dev/null +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -0,0 +1,107 @@ +import { action, computed, observable, runInAction } from 'mobx'; +import { observer } from "mobx-react"; +import wiki from "wikijs"; +import { Doc, DocCastAsync, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; +import { Cast, FieldValue, NumCast } from "../../../fields/Types"; +import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnZero } from "../../../Utils"; +import { Docs } from "../../documents/Documents"; +import { DocumentManager } from "../../util/DocumentManager"; +import { Transform } from "../../util/Transform"; +import { ContentFittingDocumentView } from "./ContentFittingDocumentView"; +import React = require("react"); +import { DocumentView } from './DocumentView'; + +interface Props { + linkDoc?: Doc; + linkSrc?: Doc; + href?: string; + backgroundColor: (doc: Doc) => string; + addDocTab: (document: Doc, where: string) => boolean; + location: number[]; +} +@observer +export class LinkDocPreview extends React.Component<Props> { + @observable public static LinkInfo: Opt<{ linkDoc?: Doc; addDocTab: (document: Doc, where: string) => boolean, linkSrc: Doc; href?: string; Location: number[] }>; + @observable _targetDoc: Opt<Doc>; + @observable _toolTipText = ""; + + componentDidUpdate() { this.updatePreview() } + componentDidMount() { this.updatePreview() } + async updatePreview() { + const linkDoc = this.props.linkDoc; + const linkSrc = this.props.linkSrc; + if (this.props.href) { + if (this.props.href.startsWith("https://en.wikipedia.org/wiki/")) { + wiki().page(this.props.href.replace("https://en.wikipedia.org/wiki/", "")).then(page => page.summary().then(action(summary => this._toolTipText = summary.substring(0, 500)))); + } else { + runInAction(() => this._toolTipText = "external => " + this.props.href); + } + } else if (linkDoc && linkSrc) { + const anchor = FieldValue(Doc.AreProtosEqual(FieldValue(Cast(linkDoc.anchor1, Doc)), linkSrc) ? Cast(linkDoc.anchor2, Doc) : (Cast(linkDoc.anchor1, Doc)) || linkDoc); + const target = anchor?.annotationOn ? await DocCastAsync(anchor.annotationOn) : anchor; + runInAction(() => { + this._toolTipText = ""; + this._targetDoc = target; + if (anchor !== this._targetDoc && anchor && this._targetDoc) { + this._targetDoc._scrollY = NumCast(anchor?.y); + } + }); + } + } + pointerDown = (e: React.PointerEvent) => { + if (this.props.linkDoc && this.props.linkSrc) { + DocumentManager.Instance.FollowLink(this.props.linkDoc, this.props.linkSrc, + (doc: Doc, followLinkLocation: string) => this.props.addDocTab(doc, e.ctrlKey ? "inTab" : followLinkLocation)); + } else if (this.props.href) { + this.props.addDocTab(Docs.Create.WebDocument(this.props.href, { title: this.props.href, _width: 200, _height: 400, UseCors: true }), "onRight"); + } + } + width = () => Math.min(350, NumCast(this._targetDoc?.[WidthSym](), 350)); + height = () => Math.min(350, NumCast(this._targetDoc?.[HeightSym](), 350)); + @computed get targetDocView() { + return !this._targetDoc ? + <div style={{ pointerEvents: "all", maxWidth: 350, maxHeight: 250, width: "100%", height: "100%", overflow: "hidden" }}> + <div style={{ width: "100%", height: "100%", textOverflow: "ellipsis", }} onPointerDown={this.pointerDown}> + {this._toolTipText} + </div> + </div> : + <ContentFittingDocumentView + Document={this._targetDoc} + LibraryPath={emptyPath} + fitToBox={true} + backgroundColor={this.props.backgroundColor} + moveDocument={returnFalse} + rootSelected={returnFalse} + ScreenToLocalTransform={Transform.Identity} + parentActive={returnFalse} + addDocument={returnFalse} + removeDocument={returnFalse} + addDocTab={returnFalse} + pinToPres={returnFalse} + dontRegisterView={true} + docFilters={returnEmptyFilter} + ContainingCollectionDoc={undefined} + ContainingCollectionView={undefined} + renderDepth={0} + PanelWidth={this.width} + PanelHeight={this.height} + focus={emptyFunction} + whenActiveChanged={returnFalse} + bringToFront={returnFalse} + ContentScaling={returnOne} + NativeWidth={returnZero} + NativeHeight={returnZero} + />; + } + + render() { + return <div className="linkDocPreview" + style={{ + position: "absolute", left: this.props.location[0], + top: this.props.location[1], width: this.width(), height: this.height(), + boxShadow: "black 2px 2px 1em" + }}> + {this.targetDocView} + </div>; + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 5e32b3ef8..839943aac 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -11,7 +11,7 @@ import { EditorState, NodeSelection, TextSelection } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { Doc } from "../../../../fields/Doc"; import { DarkPastelSchemaPalette, PastelSchemaPalette } from '../../../../fields/SchemaHeaderField'; -import { Cast, StrCast } from "../../../../fields/Types"; +import { Cast, StrCast, BoolCast } from "../../../../fields/Types"; import { unimplementedFunction, Utils } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; import { LinkManager } from "../../../util/LinkManager"; @@ -72,7 +72,7 @@ export default class RichTextMenu extends AntimodeMenu { super(props); RichTextMenu.Instance = this; this._canFade = false; - this.Pinned = true; + this.Pinned = BoolCast(Doc.UserDoc()["menuRichText-pinned"]); this.fontSizeOptions = [ { mark: schema.marks.pFontSize.create({ fontSize: 7 }), title: "Set font size", label: "7pt", command: this.changeFontSize }, @@ -147,7 +147,6 @@ export default class RichTextMenu extends AntimodeMenu { this.updateFromDash(view, lastState, this.editorProps); } - public MakeLinkToSelection = (linkDocId: string, title: string, location: string, targetDocId: string): string => { if (this.view) { const link = this.view.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + linkDocId), title: title, location: location, linkId: linkDocId, targetId: targetDocId }); @@ -750,7 +749,7 @@ export default class RichTextMenu extends AntimodeMenu { @action toggleMenuPin = (e: React.MouseEvent) => { - this.Pinned = !this.Pinned; + Doc.UserDoc()["menuRichText-pinned"] = this.Pinned = !this.Pinned; if (!this.Pinned) { this.fadeOut(true); } @@ -779,7 +778,7 @@ export default class RichTextMenu extends AntimodeMenu { this.createHighlighterButton(), this.createLinkButton(), this.createBrushButton(), - this.createButton("indent", "Summarize", undefined, this.insertSummarizer), + this.createButton("sort-amount-down", "Summarize", undefined, this.insertSummarizer), this.createButton("quote-left", "Blockquote", undefined, this.insertBlockquote), ]}</div>; |
