diff options
author | Aubrey Li <Aubrey-Li> | 2022-04-14 17:43:09 -0400 |
---|---|---|
committer | Aubrey Li <Aubrey-Li> | 2022-04-14 17:43:09 -0400 |
commit | cf379920876cafa84b0f342dbd981d60a66b6b4a (patch) | |
tree | 3059e2cbc4028abe0b308c5a0fc59744b60faeb5 | |
parent | 98567999b9882ed093bb0d750504a1bd10aa9a94 (diff) | |
parent | 9ec7503370229c17cf8a0d3cf77571010db04a9b (diff) |
Merge remote-tracking branch 'origin/presentation_group_test' into presentation_upgrade
21 files changed, 314 insertions, 74 deletions
diff --git a/src/Utils.ts b/src/Utils.ts index b280badd7..77095005b 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -603,6 +603,7 @@ export function DashColor(color: string) { } export function lightOrDark(color: any) { + if (color === "transparent") return "gray"; const nonAlphaColor = color.startsWith("#") ? (color as string).substring(0, 7) : color.startsWith("rgba") ? color.replace(/,.[^,]*\)/, ")").replace("rgba", "rgb") : color; const col = DashColor(nonAlphaColor).rgb(); diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 3bf81d374..be26fc561 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -631,7 +631,7 @@ export class MainView extends React.Component { <ComponentDecorations boundsLeft={this.leftScreenOffsetOfMainDocView} boundsTop={this.topOfMainDocContent} /> {this.topbar} {LinkDescriptionPopup.descriptionPopup ? <LinkDescriptionPopup /> : null} - {DocumentLinksButton.LinkEditorDocView ? <LinkMenu docView={DocumentLinksButton.LinkEditorDocView} changeFlyout={emptyFunction} /> : (null)} + {DocumentLinksButton.LinkEditorDocView ? <LinkMenu docView={DocumentLinksButton.LinkEditorDocView} /> : (null)} {LinkDocPreview.LinkInfo ? <LinkDocPreview {...LinkDocPreview.LinkInfo} /> : (null)} <div style={{ position: "relative", display: LightboxView.LightboxDoc ? "none" : undefined, zIndex: 2001 }} > <CollectionMenu panelWidth={this.topMenuWidth} panelHeight={this.topMenuHeight} /> diff --git a/src/client/views/PropertiesDocBacklinksSelector.scss b/src/client/views/PropertiesDocBacklinksSelector.scss new file mode 100644 index 000000000..4d2a61c3b --- /dev/null +++ b/src/client/views/PropertiesDocBacklinksSelector.scss @@ -0,0 +1,5 @@ +.propertiesView-contexts-content { + .linkMenu { + position: relative; + } +}
\ No newline at end of file diff --git a/src/client/views/PropertiesDocBacklinksSelector.tsx b/src/client/views/PropertiesDocBacklinksSelector.tsx index ea0d90e04..082492671 100644 --- a/src/client/views/PropertiesDocBacklinksSelector.tsx +++ b/src/client/views/PropertiesDocBacklinksSelector.tsx @@ -2,10 +2,12 @@ import { computed } from "mobx"; import { observer } from "mobx-react"; import * as React from "react"; import { Doc, DocListCast } from "../../fields/Doc"; -import { Id } from "../../fields/FieldSymbols"; -import { Cast, NumCast, StrCast } from "../../fields/Types"; +import { Cast } from "../../fields/Types"; +import { DocumentType } from "../documents/DocumentTypes"; import { LinkManager } from "../util/LinkManager"; -import './PropertiesDocContextSelector.scss'; +import { SelectionManager } from "../util/SelectionManager"; +import { LinkMenu } from "./linking/LinkMenu"; +import './PropertiesDocBacklinksSelector.scss'; type PropertiesDocBacklinksSelectorProps = { Document: Doc, @@ -22,7 +24,7 @@ export class PropertiesDocBacklinksSelector extends React.Component<PropertiesDo const collectedLinks = [] as Doc[]; links.map(link => { const other = LinkManager.getOppositeAnchor(link, linkSource); - const otherdoc = !other ? undefined : other.annotationOn ? Cast(other.annotationOn, Doc, null) : other; + const otherdoc = !other ? undefined : other.annotationOn && other.type !== DocumentType.RTF ? Cast(other.annotationOn, Doc, null) : other; if (otherdoc && !collectedLinks.some(d => Doc.AreProtosEqual(d, otherdoc))) { collectedLinks.push(otherdoc); } @@ -30,15 +32,20 @@ export class PropertiesDocBacklinksSelector extends React.Component<PropertiesDo return collectedLinks; } - getOnClick = (col: Doc) => { - col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col; - this.props.addDocTab(col, "toggle:right"); + getOnClick = (link: Doc) => { + const linkSource = this.props.Document; + const other = LinkManager.getOppositeAnchor(link, linkSource); + const otherdoc = !other ? undefined : other.annotationOn && other.type !== DocumentType.RTF ? Cast(other.annotationOn, Doc, null) : other; + + if (otherdoc) { + this.props.addDocTab(Doc.IsPrototype(otherdoc) ? Doc.MakeDelegate(otherdoc) : otherdoc, "toggle:right"); + } } render() { - return <div> + return !SelectionManager.Views().length ? (null) : <div> {this.props.hideTitle ? (null) : <p key="contexts">Contexts:</p>} - {this._docs.map(doc => <p key={doc[Id]}><a onClick={() => this.getOnClick(doc)}>{StrCast(doc.title)}</a></p>)} + <LinkMenu docView={SelectionManager.Views().lastElement()} itemHandler={this.getOnClick} position={{ x: 0 }} /> </div>; } }
\ No newline at end of file diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx index 62f6e388f..fae385660 100644 --- a/src/client/views/SidebarAnnos.tsx +++ b/src/client/views/SidebarAnnos.tsx @@ -61,7 +61,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { FormattedTextBox.SelectOnLoad = target[Id]; FormattedTextBox.DontSelectInitialText = true; this.allMetadata.map(tag => target[tag] = tag); - DocUtils.MakeLink({ doc: anchor }, { doc: target }, "inline markup", "annotation"); + DocUtils.MakeLink({ doc: anchor }, { doc: target }, "inline markup"); this.addDocument(target); this._stackRef.current?.focusDocument(target); } diff --git a/src/client/views/linking/LinkMenu.scss b/src/client/views/linking/LinkMenu.scss index 19c6463d3..77c16a28f 100644 --- a/src/client/views/linking/LinkMenu.scss +++ b/src/client/views/linking/LinkMenu.scss @@ -45,8 +45,11 @@ } .linkMenu-group-name { - padding: 10px; - + padding: 1px; + padding-left: 5px; + font-size: 10px; + font-style: italic; + font-weight: bold; &:hover { // p { @@ -64,7 +67,7 @@ p { width: 100%; - line-height: 12px; + line-height: 1; border-radius: 5px; text-transform: capitalize; } diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx index 53fe3f682..bc922be36 100644 --- a/src/client/views/linking/LinkMenu.tsx +++ b/src/client/views/linking/LinkMenu.tsx @@ -12,7 +12,8 @@ import React = require("react"); interface Props { docView: DocumentView; - changeFlyout: () => void; + position?: { x?: number, y?: number }; + itemHandler?: (doc: Doc) => void; } /** @@ -25,7 +26,7 @@ export class LinkMenu extends React.Component<Props> { @observable _linkMenuRef = React.createRef<HTMLDivElement>(); @computed get position() { - return ((dv) => ({ x: dv?.left || 0, y: dv?.top || 0, r: dv?.right || 0, b: dv?.bottom || 0 }))(this.props.docView.getBounds()); + return this.props.position ?? (dv => ({ x: dv?.left || 0, y: (dv?.bottom || 0) + 15 }))(this.props.docView.getBounds()); } componentDidMount() { document.addEventListener("pointerdown", this.onPointerDown); } @@ -48,19 +49,20 @@ export class LinkMenu extends React.Component<Props> { const linkItems = Array.from(groups.entries()).map(group => <LinkMenuGroup key={group[0]} + itemHandler={this.props.itemHandler} docView={this.props.docView} sourceDoc={this.props.docView.props.Document} group={group[1]} groupType={group[0]} showEditor={action(linkDoc => this._editingLink = linkDoc)} />); - return linkItems.length ? linkItems : [<p key="">No links have been created yet. Drag the linking button onto another document to create a link.</p>]; + return linkItems.length ? linkItems : this.props.position ? [<></>] : [<p key="">No links have been created yet. Drag the linking button onto another document to create a link.</p>]; } render() { const sourceDoc = this.props.docView.props.Document; return <div className="linkMenu" ref={this._linkMenuRef} - style={{ left: this.position.x, top: this.props.docView.topMost ? undefined : this.position.b + 15, bottom: this.props.docView.topMost ? 20 : undefined }} + style={{ left: this.position.x, top: this.props.docView.topMost ? undefined : this.position.y, bottom: this.props.docView.topMost ? 20 : undefined }} > {this._editingLink ? <div className="linkMenu-listEditor"> diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx index 03377ad4e..5b1e29e67 100644 --- a/src/client/views/linking/LinkMenuGroup.tsx +++ b/src/client/views/linking/LinkMenuGroup.tsx @@ -1,4 +1,5 @@ import { observer } from "mobx-react"; +import { observable, action } from "mobx"; import { Doc, StrListCast } from "../../../fields/Doc"; import { Id } from "../../../fields/FieldSymbols"; import { Cast } from "../../../fields/Types"; @@ -14,6 +15,7 @@ interface LinkMenuGroupProps { groupType: string; showEditor: (linkDoc: Doc) => void; docView: DocumentView; + itemHandler?: (doc: Doc) => void; } @observer @@ -36,6 +38,8 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { return color; } + @observable _collapsed = false; + render() { const set = new Set<Doc>(this.props.group); const groupItems = Array.from(set.keys()).map(linkDoc => { @@ -43,6 +47,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { LinkManager.getOppositeAnchor(linkDoc, Cast(linkDoc.anchor2, Doc, null).annotationOn === this.props.sourceDoc ? Cast(linkDoc.anchor2, Doc, null) : Cast(linkDoc.anchor1, Doc, null)); if (destination && this.props.sourceDoc) { return <LinkMenuItem key={linkDoc[Id]} + itemHandler={this.props.itemHandler} groupType={this.props.groupType} docView={this.props.docView} linkDoc={linkDoc} @@ -55,12 +60,12 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { return ( <div className="linkMenu-group" ref={this._menuRef}> - <div className="linkMenu-group-name" style={{ background: this.getBackgroundColor() }}> + <div className="linkMenu-group-name" onClick={action(() => this._collapsed = !this._collapsed)} style={{ background: this.getBackgroundColor() }}> <p className={this.props.groupType === "*" || this.props.groupType === "" ? "" : "expand-one"}> {this.props.groupType}:</p> </div> - <div className="linkMenu-group-wrapper"> + {this._collapsed ? (null) : <div className="linkMenu-group-wrapper"> {groupItems} - </div> + </div>} </div > ); } diff --git a/src/client/views/linking/LinkMenuItem.scss b/src/client/views/linking/LinkMenuItem.scss index 90722daf9..8333aa374 100644 --- a/src/client/views/linking/LinkMenuItem.scss +++ b/src/client/views/linking/LinkMenuItem.scss @@ -9,8 +9,8 @@ padding-left: 6.5px; padding-right: 2px; - padding-top: 6.5px; - padding-bottom: 6.5px; + padding-top: 1px; + padding-bottom: 1px; background-color: white; @@ -18,10 +18,12 @@ .linkMenu-name { position: relative; width: auto; + align-items: center; + display: flex; .linkMenu-text { - padding: 4px 2px; + // padding: 4px 2px; //display: inline; .linkMenu-source-title { @@ -37,6 +39,8 @@ .linkMenu-title-wrapper { display: flex; + align-items: center; + min-height: 20px; .destination-icon-wrapper { //border: 0.5px solid rgb(161, 161, 161); @@ -56,7 +60,8 @@ .linkMenu-destination-title { text-decoration: none; color: #4476F7; - font-size: 16px; + font-size: 13px; + line-height: 0.9; padding-bottom: 2px; padding-right: 4px; margin-right: 4px; @@ -77,6 +82,7 @@ font-style: italic; color: rgb(95, 97, 102); font-size: 9px; + line-height: 0.9; margin-left: 20px; max-width: 125px; height: auto; diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index 96cc6d600..d6400a6b3 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -1,12 +1,12 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; -import { action, observable, runInAction } from 'mobx'; +import { action, observable } from 'mobx'; import { observer } from "mobx-react"; import { Doc, DocListCast } from '../../../fields/Doc'; import { Cast, StrCast } from '../../../fields/Types'; import { WebField } from '../../../fields/URLField'; -import { emptyFunction, setupMoveUpEvents, returnFalse } from '../../../Utils'; +import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils'; import { DocumentType } from '../../documents/DocumentTypes'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager } from '../../util/DragManager'; @@ -14,11 +14,10 @@ import { Hypothesis } from '../../util/HypothesisUtils'; import { LinkManager } from '../../util/LinkManager'; import { undoBatch } from '../../util/UndoManager'; import { DocumentLinksButton } from '../nodes/DocumentLinksButton'; -import { DocumentView, DocumentViewSharedProps } from '../nodes/DocumentView'; +import { DocumentView } from '../nodes/DocumentView'; import { LinkDocPreview } from '../nodes/LinkDocPreview'; import './LinkMenuItem.scss'; import React = require("react"); -import { setup } from 'mocha'; interface LinkMenuItemProps { @@ -29,6 +28,7 @@ interface LinkMenuItemProps { destinationDoc: Doc; showEditor: (linkDoc: Doc) => void; menuRef: React.Ref<HTMLDivElement>; + itemHandler?: (doc: Doc) => void; } // drag links and drop link targets (aliasing them if needed) @@ -98,7 +98,12 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { emptyFunction, () => { DocumentLinksButton.ClearLinkEditor(); - LinkManager.FollowLink(this.props.linkDoc, this.props.sourceDoc, this.props.docView.props, false); + if (this.props.itemHandler) { + + this.props.itemHandler?.(this.props.linkDoc); + } else { + LinkManager.FollowLink(this.props.linkDoc, this.props.sourceDoc, this.props.docView.props, false); + } }); } diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 6f0f0074a..5149d370f 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -46,6 +46,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF get ZInd() { return this.dataProvider ? this.dataProvider.zIndex : NumCast(this.Document.zIndex); } get Opacity() { return this.dataProvider ? this.dataProvider.opacity : undefined; } get Highlight() { return this.dataProvider?.highlight; } + @computed get ShowTitle() { return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.ShowTitle) as (Opt<string>); } @computed get dataProvider() { return this.props.dataProvider?.(this.props.Document, this.props.replica); } @computed get sizeProvider() { return this.props.sizeProvider?.(this.props.Document, this.props.replica); } @@ -171,7 +172,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF style={{ outline: this.Highlight ? "orange solid 2px" : "", width: this.panelWidth(), - height: this.layoutDoc.autoHeight && this.rootDoc.type === DocumentType.RTF ? undefined : this.panelHeight(), + height: !this.ShowTitle && this.layoutDoc.autoHeight && this.rootDoc.type === DocumentType.RTF ? undefined : this.panelHeight(), transform: this.transform, transition: this.props.dataTransition ? this.props.dataTransition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.dataTransition), zIndex: this.ZInd, diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index c7f7cdb58..7361dba73 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -111,6 +111,7 @@ export interface DocumentViewSharedProps { ContainingCollectionView: Opt<CollectionView>; ContainingCollectionDoc: Opt<Doc>; thumbShown?: () => boolean; + isHovering?: () => boolean; setContentView?: (view: DocComponentView) => any; CollectionFreeFormDocumentView?: () => CollectionFreeFormDocumentView; PanelWidth: () => number; @@ -873,6 +874,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps {...this.props} docViewPath={this.props.viewPath} thumbShown={this.thumbShown} + isHovering={this.isHovering} setContentView={this.setContentView} scaling={this.contentScaling} PanelHeight={this.panelHeight} @@ -884,7 +886,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps focus={this.focus} layoutKey={this.finalLayoutKey} /> {this.layoutDoc.hideAllLinks ? (null) : this.allLinkEndpoints} - {this.hideLinkButton || this.props.renderDepth === -1 || SnappingManager.GetIsDragging() ? (null) : + {!this.props.isSelected() || this.hideLinkButton || this.props.renderDepth === -1 || SnappingManager.GetIsDragging() ? (null) : <DocumentLinksButton View={this.props.DocumentView()} ContentScaling={this.props.ContentScaling} Offset={[this.topMost ? 0 : -15, undefined, undefined, this.topMost ? 10 : -20]} /> @@ -1065,6 +1067,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps {captionView} </div>; } + isHovering = () => this._isHovering; + @observable _isHovering = false; @observable _: string = ""; @computed get renderDoc() { TraceMobx(); @@ -1074,6 +1078,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps return this.docContents ?? <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} id={this.props.Document[Id]} + onPointerEnter={action(() => this._isHovering = true)} + onPointerLeave={action(() => this._isHovering = false)} style={{ background: isButton || thumb ? undefined : this.backgroundColor, opacity: this.opacity, diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 4238f6d29..cd2b23f02 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -103,7 +103,7 @@ display: flex; height: 100%; - .imageBox-fadeBlocker { + .imageBox-fadeBlocker, .imageBox-fadeBlocker-hover{ width: 100%; height: 100%; position: absolute; @@ -133,12 +133,10 @@ transition: opacity 1s ease-in-out; } -.imageBox:hover { - .imageBox-fadeBlocker { - -webkit-transition: opacity 1s ease-in-out; - -moz-transition: opacity 1s ease-in-out; - -o-transition: opacity 1s ease-in-out; - transition: opacity 1s ease-in-out; - opacity: 0; - } +.imageBox-fadeBlocker-hover { + -webkit-transition: opacity 1s ease-in-out; + -moz-transition: opacity 1s ease-in-out; + -o-transition: opacity 1s ease-in-out; + transition: opacity 1s ease-in-out; + opacity: 0; }
\ No newline at end of file diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 0a4168698..17f95c1cc 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -45,7 +45,6 @@ const uploadIcons = { export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() { protected _multiTouchDisposer?: import("../../util/InteractionUtils").InteractionUtils.MultiTouchEventDisposer | undefined; public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ImageBox, fieldKey); } - private _imgRef: React.RefObject<HTMLImageElement> = React.createRef(); private _dropDisposer?: DragManager.DragDropDisposer; private _disposers: { [name: string]: IReactionDisposer } = {}; @observable _curSuffix = ""; @@ -78,7 +77,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp { fireImmediately: true, delay: 1000 }); this._disposers.path = reaction(() => ({ nativeSize: this.nativeSize, width: this.layoutDoc[WidthSym]() }), ({ nativeSize, width }) => { - if (!this.layoutDoc._height) { + if (true || !this.layoutDoc._height) { this.layoutDoc._height = width * nativeSize.nativeHeight / nativeSize.nativeWidth; } }, @@ -282,17 +281,18 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp return <div className="imageBox-cont" key={this.layoutDoc[Id]} ref={this.createDropTarget} onPointerDown={this.marqueeDown}> <div className="imageBox-fader" style={{ overflow: Array.from(this.props.docViewPath?.()).slice(-1)[0].fitWidth ? "auto" : undefined }} > - <img key="paths" ref={this._imgRef} + <img key="paths" src={srcpath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} /> - {fadepath === srcpath ? (null) : <div className="imageBox-fadeBlocker"> - <img className="imageBox-fadeaway" key={"fadeaway"} ref={this._imgRef} - src={fadepath} - style={{ transform, transformOrigin }} draggable={false} - width={nativeWidth} /> - </div>} + {fadepath === srcpath ? (null) : + <div className={`imageBox-fadeBlocker${this.props.isHovering?.() ? "-hover" : ""}`}> + <img className="imageBox-fadeaway" key={"fadeaway"} + src={fadepath} + style={{ transform, transformOrigin }} draggable={false} + width={nativeWidth} /> + </div>} </div> {this.considerDownloadIcon} {this.considerGooglePhotosLink()} diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index 7637d39be..d5f3f3b4e 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -46,7 +46,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { if (anchor1 && anchor2) { linkTarget = Doc.AreProtosEqual(anchor1, this._linkSrc) || Doc.AreProtosEqual(anchor1?.annotationOn as Doc, this._linkSrc) ? anchor2 : anchor1; } - if (linkTarget?.annotationOn) { + if (linkTarget?.annotationOn && linkTarget?.type !== DocumentType.RTF) { // want to show annotation context document if annotation is not text linkTarget && DocCastAsync(linkTarget.annotationOn).then(action(anno => this._targetDoc = anno)); } else { this._targetDoc = linkTarget; @@ -84,9 +84,16 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { anchorDoc && DocServer.GetRefField(anchorDoc).then(action(anchor => { if (anchor instanceof Doc && DocListCast(anchor.links).length) { this._linkDoc = this._linkDoc ?? DocListCast(anchor.links)[0]; - this._linkSrc = anchor; - const linkTarget = LinkManager.getOppositeAnchor(this._linkDoc, this._linkSrc); - this._targetDoc = linkTarget?.type === DocumentType.MARKER && linkTarget?.annotationOn ? Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget : linkTarget; + const automaticLink = this._linkDoc.linkRelationship === "automatic"; + if (automaticLink) { // automatic links specify the target in the link info, not the source + const linkTarget = anchor; + this._linkSrc = LinkManager.getOppositeAnchor(this._linkDoc, linkTarget); + this._targetDoc = linkTarget; + } else { + this._linkSrc = anchor; + const linkTarget = LinkManager.getOppositeAnchor(this._linkDoc, this._linkSrc); + this._targetDoc = linkTarget?.type === DocumentType.MARKER && linkTarget?.annotationOn ? Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget : linkTarget; + } this._toolTipText = ""; } })); @@ -99,7 +106,10 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(() => this._linkDoc && LinkManager.Instance.deleteLink(this._linkDoc))); } nextHref = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, returnFalse, emptyFunction, action(() => this._hrefInd = (this._hrefInd + 1) % (this.props.hrefs?.length || 1))); + setupMoveUpEvents(this, e, returnFalse, emptyFunction, action(() => { + this._linkDoc = undefined; + this._hrefInd = (this._hrefInd + 1) % (this.props.hrefs?.length || 1); + })); } followLink = (e: React.PointerEvent) => { @@ -152,10 +162,10 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { return (!this._linkDoc || !this._targetDoc || !this._linkSrc) && !this._toolTipText ? (null) : <div className="linkDocPreview-inner"> {!this.props.showHeader ? (null) : this.previewHeader} - <div className="linkDocPreview-preview-wrapper"> + <div className="linkDocPreview-preview-wrapper" style={{ maxHeight: this._toolTipText ? "100%" : undefined, overflow: "auto" }}> {this._toolTipText ? this._toolTipText : <DocumentView ref={(r) => { - const targetanchor = LinkManager.getOppositeAnchor(this._linkDoc!, this._linkSrc!); + const targetanchor = this._linkDoc && this._linkSrc && LinkManager.getOppositeAnchor(this._linkDoc, this._linkSrc); targetanchor && this._targetDoc !== targetanchor && r?.focus(targetanchor); }} Document={this._targetDoc!} diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 1f7e557d9..ec88b8d1d 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -381,8 +381,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp // creates links between terms in a document and documents which have a matching Id hyperlinkTerm = (tr: any, target: Doc, newAutoLinks: Set<Doc>) => { - if (this._editorView && (this._editorView as any).docView) { - const flattened1 = this.findInNode(this._editorView, this._editorView.state.doc, StrCast(target.title).replace(/^@/, "")); + const editorView = this._editorView; + if (editorView && (editorView as any).docView && !Doc.AreProtosEqual(target, this.rootDoc)) { + const flattened1 = this.findInNode(editorView, editorView.state.doc, StrCast(target.title).replace(/^@/, "")); var alink: Doc | undefined; flattened1.forEach((flat, i) => { const flattened = this.findInNode(this._editorView!, this._editorView!.state.doc, StrCast(target.title).replace(/^@/, "")); @@ -391,9 +392,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), this.rootDoc) && Doc.AreProtosEqual(Cast(link.anchor2, Doc, null), target)) || DocUtils.MakeLink({ doc: this.props.Document }, { doc: target }, "automatic")!); newAutoLinks.add(alink); - const allAnchors = [{ href: Doc.localServerPath(this.props.Document), title: "a link", anchorId: this.props.Document[Id] }]; - const link = this._editorView!.state.schema.marks.autoLinkAnchor.create({ allAnchors, linkDoc: alink[Id], title: "auto link", location }); - tr = tr.addMark(flattened[i].from, flattened[i].to, link); + const splitter = editorView.state.schema.marks.splitter.create({ id: Utils.GenerateGuid() }); + const sel = flattened[i]; + tr = tr.addMark(sel.from, sel.to, splitter); + tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => { + if (node.firstChild === null && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) { + const allAnchors = [{ href: Doc.localServerPath(target), title: "a link", anchorId: this.props.Document[Id] }]; + allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.autoLinkAnchor.name)?.attrs.allAnchors ?? [])); + const link = editorView.state.schema.marks.autoLinkAnchor.create({ allAnchors, title: "auto term", location: "add:right" }); + tr = tr.addMark(pos, pos + node.nodeSize, link); + } + }); + tr = tr.removeMark(sel.from, sel.to, splitter); }); } return tr; @@ -1443,6 +1453,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } catch (e: any) { console.log(e.message); } this._lastText = curText; } + if (StrCast(this.rootDoc.title).startsWith("@") && !this.dataDoc["title-custom"]) { + UndoManager.RunInBatch(() => { + this.dataDoc["title-custom"] = true; + this.dataDoc.showTitle = "title"; + const tr = this._editorView!.state.tr; + this._editorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(0), tr.doc.resolve(StrCast(this.rootDoc.title).length + 2))).deleteSelection()); + }, "titler"); + } } onKeyDown = (e: React.KeyboardEvent) => { // single line text boxes need to pass through tab/enter/backspace so that their containers can respond (eg, an outline container) diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index bafae84dc..00c03875b 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -294,6 +294,25 @@ export class RichTextRules { return state.tr.deleteRange(start, end).insert(start, fieldView); }), + + // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document + // wiki:title + new InputRule( + new RegExp(/wiki:([a-zA-Z_@:\.\?\-0-9]+ )$/), + (state, match, start, end) => { + const title = match[1]; + this.TextBox.EditorView?.dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end)))); + + this.TextBox.makeLinkAnchor(undefined, "add:right", `https://en.wikipedia.org/wiki/${title.trim()}`, "wikipedia reference"); + + const fstate = this.TextBox.EditorView?.state; + if (fstate) { + const tr = fstate?.tr.deleteRange(start, start + 5); + return tr.setSelection(new TextSelection(tr.doc.resolve(end - 5))).insertText(" "); + } + return state.tr; + }), + // create an inline view of a document {{ <layoutKey> : <Doc> }} // {{:Doc}} => show default view of document // {{<layout>}} => show layout for this doc diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index 52ef06b7f..1f6ce014f 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -19,13 +19,17 @@ export const marks: { [index: string]: MarkSpec } = { }, - // :: MarkSpec A linkAnchor. The anchor can have multiple links, where each link has an href URL and a title for use in menus and hover (Dash links have linkIDs & targetIDs). `title` - // defaults to the empty string. Rendered and parsed as an `<a>` + // :: MarkSpec an autoLinkAnchor. These are automatically generated anchors to "published" documents based on the anchor text matching the + // published document's title. + // NOTE: unlike linkAnchors, the autoLinkAnchor's href's indicate the target anchor of the hyperlink and NOT the source. This is because + // automatic links do not create a text selection Marker document for the source anchor, but use the text document itself. Since + // multiple automatic links can be created each having the same source anchor (the whole document), the target href of the link is needed to + // disambiguate links from one another. + // Rendered and parsed as an `<a>` // element. autoLinkAnchor: { attrs: { allAnchors: { default: [] as { href: string, title: string, anchorId: string }[] }, - linkDoc: { default: "" }, location: { default: null }, title: { default: null }, }, @@ -44,7 +48,8 @@ export const marks: { [index: string]: MarkSpec } = { return ["a", { class: anchorids, "data-targethrefs": targethrefs, "data-linkdoc": node.attrs.linkDoc, title: node.attrs.title, location: node.attrs.location, style: `background: lightBlue` }, 0]; } }, - // :: MarkSpec A linkAnchor. The anchor can have multiple links, where each link has an href URL and a title for use in menus and hover (Dash links have linkIDs & targetIDs). `title` + // :: MarkSpec A linkAnchor. The anchor can have multiple links, where each linkAnchor specifies an href to the URL of the source selection Marker text, + // and a title for use in menus and hover. `title` // defaults to the empty string. Rendered and parsed as an `<a>` // element. linkAnchor: { diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index 19de0fee9..0a91bf2ab 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -741,6 +741,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { //Regular click @action selectElement = async (doc: Doc) => { + console.log("got here", this.childDocs.indexOf(doc)) const context = Cast(doc.context, Doc, null); this.gotoDocument(this.childDocs.indexOf(doc), this.activeItem); if (doc.presPinView || doc.presentationTargetDoc === this.layoutDoc.presCollection) setTimeout(() => this.updateCurrentPresentation(context), 0); diff --git a/src/client/views/nodes/trails/PresElementBox.scss b/src/client/views/nodes/trails/PresElementBox.scss index 1ad4b820e..dd0520bad 100644 --- a/src/client/views/nodes/trails/PresElementBox.scss +++ b/src/client/views/nodes/trails/PresElementBox.scss @@ -195,6 +195,47 @@ $slide-active: #5B9FDD; box-shadow: 0 0 0px 1.5px $dark-blue; } +.presItem-slide.group { + border-radius: 5px; +} + +.presItem-slide.activeGroup { + border-radius: 5px; + box-shadow: 0 0 0px 1.5px $dark-blue; +} + +.presItem-groupSlideContainer { + position: absolute; + /* grid-row: 3; */ + /* grid-column: 1/8; */ + top: 28; + display: block; + background: #92adb9; + width: 100%; + height: 10px; + border-radius: 0px 0px 5px 5px; + height: calc(100% - 28px); + display: grid; + grid-template-rows: auto auto auto; + grid-template-columns: 100%; + justify-items: left; + align-items: center; +} + +.presItem-groupSlide { + position: relative; + background-color: #d5dce2; + border-radius: 5px; + height: calc(100% - 7px); + width: calc(100% - 20px); + left: 15px; + /* height: 20px; */ + /* width: calc(100% - 19px); */ + display: flex; + grid-template-rows: 16px 10px auto; + grid-template-columns: max-content max-content max-content max-content auto; +} + .presItem-multiDrag { font-family: Roboto; font-weight: 600; @@ -230,6 +271,37 @@ $slide-active: #5B9FDD; box-shadow: 0 0 0px 1.5px $dark-blue; } -// .presItem-slide:hover { -// background: $slide-hover; -// }
\ No newline at end of file +.expandButton { + cursor: pointer; + position: absolute; + border-radius: 100px; + bottom: 0; + left: -18; + z-index: 300; + width: 15px; + height: 15px; + display: flex; + font-size: 12px; + justify-self: center; + align-self: center; + background-color: #92adb9; + color: white; + justify-content: center; + align-items: center; + transition: 0.2s; + margin-right: 3px; +} + +.expandButton:hover { + background-color: rgba(0, 0, 0, 1); + transform: scale(1.2); +} + +.presItem-groupNum { + color: #d5dce2; + position: absolute; + left: -15px; + top: 1; + font-weight: 600; + font-size: 12; +}
\ No newline at end of file diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index a4ec559f5..1da7a596a 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -2,9 +2,9 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Tooltip } from "@material-ui/core"; import { action, computed, IReactionDisposer, observable, reaction } from "mobx"; import { observer } from "mobx-react"; -import { DataSym, Doc, Opt } from "../../../../fields/Doc"; +import { DataSym, Doc, DocListCast, Opt } from "../../../../fields/Doc"; import { Id } from "../../../../fields/FieldSymbols"; -import { Cast, NumCast, StrCast } from "../../../../fields/Types"; +import { BoolCast, Cast, NumCast, StrCast } from "../../../../fields/Types"; import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from "../../../../Utils"; import { DocUtils } from "../../../documents/Documents"; import { DocumentType } from "../../../documents/DocumentTypes"; @@ -106,6 +106,38 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { </div>; } + @computed get renderGroupSlides() { + const childDocs = DocListCast(this.targetDoc.data); + const groupSlides = childDocs.map((doc: Doc, ind: number) => + <div className="presItem-groupSlide" onClick={(e) => { + console.log("Clicked on slide with index: ", ind); + e.stopPropagation(); + e.preventDefault(); + PresBox.Instance.modifierSelect(doc, this._itemRef.current!, this._dragRef.current!, !e.shiftKey && !e.ctrlKey && !e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey); + }}> + <div className="presItem-groupNum"> + {`${ind + 1}.`} + </div> + {/* style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105, cursor: isSelected ? 'text' : 'grab' }} */} + <div className="presItem-name"> + <EditableView + ref={this._titleRef} + editing={undefined} + contents={doc.title} + overflow={'ellipsis'} + GetValue={() => StrCast(doc.title)} + SetValue={(value: string) => { + doc.title = !value.trim().length ? "-untitled-" : value; + return true; + }} + /> + </div> + </div> + ); + return ( + groupSlides + ); + } @computed get duration() { let durationInS: number; if (this.rootDoc.type === DocumentType.AUDIO || this.rootDoc.type === DocumentType.VID) { durationInS = NumCast(this.rootDoc.presEndTime) - NumCast(this.rootDoc.presStartTime); durationInS = Math.round(durationInS * 10) / 10; } @@ -284,6 +316,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { const presColorBool: boolean = presBoxColor ? (presBoxColor !== Colors.WHITE && presBoxColor !== "transparent") : false; const targetDoc: Doc = this.targetDoc; const activeItem: Doc = this.rootDoc; + const isGroup: boolean = BoolCast(targetDoc._isGroup); return ( <div className={`presItem-container`} key={this.props.Document[Id] + this.indexInPres} @@ -298,8 +331,12 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { PresBox.Instance.modifierSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, !e.shiftKey && !e.ctrlKey && !e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey); }} onDoubleClick={action(e => { - this.toggleProperties(); - PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, true); + if (isGroup) { + this.rootDoc.presExpandGroup = !this.rootDoc.presExpandGroup; + } else { + this.toggleProperties(); + PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, true); + } })} onPointerOver={this.onPointerOver} onPointerLeave={this.onPointerLeave} @@ -316,7 +353,46 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="presItem-number"> {`${this.indexInPres + 1}.`} </div>} - {miniView ? (null) : <div ref={miniView ? null : this._dragRef} className={`presItem-slide ${isSelected ? "active" : ""}`} + {isGroup ? + <div ref={miniView ? null : this._dragRef} className={`presItem-slide ${isSelected ? "activeGroup" : "group"}`} + style={{ + backgroundColor: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor), + boxShadow: presBoxColor && presBoxColor !== "white" && presBoxColor !== "transparent" ? isSelected ? "0 0 0px 1.5px" + presBoxColor : undefined : undefined + }}> + <div className="presItem-name" style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105, cursor: isSelected ? 'text' : 'grab' }}> + <EditableView + ref={this._titleRef} + editing={!isSelected ? false : undefined} + contents={activeItem.title} + overflow={'ellipsis'} + GetValue={() => StrCast(activeItem.title)} + SetValue={this.onSetValue} + /> + </div> + <div className={"presItem-slideButtons"}> + <Tooltip title={<><div className="dash-tooltip">{"Remove from presentation"}</div></>}><div + className={"slideButton"} + onClick={this.removeItem}> + <FontAwesomeIcon icon={"trash"} onPointerDown={e => e.stopPropagation()} /> + </div></Tooltip> + <div className="group"></div> + </div> + <div className="presItem-groupSlideContainer" style={{ top: 28, height: 'calc(100% - 28px)' }}> + {this.rootDoc.presExpandGroup ? this.renderGroupSlides : (null)} + </div> + <div className="presItem-docName" style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105 }}>{activeItem.presPinView ? (<><i>View of </i> {targetDoc.title}</>) : targetDoc.title}</div> + <div className="expandButton" + onClick={e => { + if (isGroup) { + this.rootDoc.presExpandGroup = !this.rootDoc.presExpandGroup; + } + }} + > + <FontAwesomeIcon icon={"caret-down"} style={{ transform: this.rootDoc.presExpandGroup ? "rotate(180deg)" : "rotate(0deg)" }} /> + </div> + </div> + : (null)} + {miniView || isGroup ? (null) : <div ref={miniView ? null : this._dragRef} className={`presItem-slide ${isSelected ? "active" : ""}`} style={{ backgroundColor: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor), boxShadow: presBoxColor && presBoxColor !== "white" && presBoxColor !== "transparent" ? isSelected ? "0 0 0px 1.5px" + presBoxColor : undefined : undefined |