aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/DocumentView.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/DocumentView.tsx')
-rw-r--r--src/client/views/nodes/DocumentView.tsx175
1 files changed, 130 insertions, 45 deletions
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index da665a502..3d6b53ccc 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,4 +1,5 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { Dropdown, DropdownType, Type } from 'browndash-components';
import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import { computedFn } from 'mobx-utils';
@@ -17,7 +18,7 @@ import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
import { emptyFunction, isTargetChildOf as isParentOf, lightOrDark, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick, Utils } from '../../../Utils';
import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
import { DocServer } from '../../DocServer';
-import { Docs, DocUtils } from '../../documents/Documents';
+import { DocOptions, Docs, DocUtils, FInfo } from '../../documents/Documents';
import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
import { Networking } from '../../Network';
import { DictationManager } from '../../util/DictationManager';
@@ -38,7 +39,6 @@ import { ContextMenuProps } from '../ContextMenuItem';
import { DocComponent } from '../DocComponent';
import { EditableView } from '../EditableView';
import { GestureOverlay } from '../GestureOverlay';
-import { InkingStroke } from '../InkingStroke';
import { LightboxView } from '../LightboxView';
import { StyleProp } from '../StyleProvider';
import { UndoStack } from '../UndoStack';
@@ -48,11 +48,11 @@ import { DocumentLinksButton } from './DocumentLinksButton';
import './DocumentView.scss';
import { FieldViewProps } from './FieldView';
import { FormattedTextBox } from './formattedText/FormattedTextBox';
+import { KeyValueBox } from './KeyValueBox';
import { LinkAnchorBox } from './LinkAnchorBox';
import { PresEffect, PresEffectDirection } from './trails';
import { PinProps, PresBox } from './trails/PresBox';
import React = require('react');
-import { KeyValueBox } from './KeyValueBox';
const { Howl } = require('howler');
interface Window {
@@ -120,7 +120,6 @@ export interface DocComponentView {
addDocTab?: (doc: Doc, where: OpenWhere) => boolean; // determines how to add a document - used in following links to open the target ina local lightbox
addDocument?: (doc: Doc | Doc[], annotationKey?: string) => boolean; // add a document (used only by collections)
reverseNativeScaling?: () => boolean; // DocumentView's setup screenToLocal based on the doc having a nativeWidth/Height. However, some content views (e.g., FreeFormView w/ fitContentsToBox set) may ignore the native dimensions so this flags the DocumentView to not do Nativre scaling.
- shrinkWrap?: () => void; // requests a document to display all of its contents with no white space. currently only implemented (needed?) for freeform views
select?: (ctrlKey: boolean, shiftKey: boolean) => void;
focus?: (textAnchor: Doc, options: DocFocusOptions) => Opt<number>;
menuControls?: () => JSX.Element; // controls to display in the top menu bar when the document is selected.
@@ -142,6 +141,7 @@ export interface DocComponentView {
annotationKey?: string;
getTitle?: () => string;
getCenter?: (xf: Transform) => { X: number; Y: number };
+ screenBounds?: () => { left: number; top: number; right: number; bottom: number; center?: { X: number; Y: number } };
ptToScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number };
ptFromScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number };
snapPt?: (pt: { X: number; Y: number }, excludeSegs?: number[]) => { nearestPt: { X: number; Y: number }; distance: number };
@@ -154,8 +154,8 @@ export interface DocumentViewSharedProps {
renderDepth: number;
Document: Doc;
DataDoc?: Doc;
- contentBounds?: () => undefined | { x: number; y: number; r: number; b: number };
fitContentsToBox?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _freeform_fitContentsToBox property on a Document
+ isGroupActive?: () => string | undefined; // is this document part of a group that is active
suppressSetHeight?: boolean;
setContentView?: (view: DocComponentView) => any;
CollectionFreeFormDocumentView?: () => CollectionFreeFormDocumentView;
@@ -219,7 +219,7 @@ export interface DocumentViewProps extends DocumentViewSharedProps {
hideLinkAnchors?: boolean;
isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events
isContentActive: () => boolean | undefined; // whether document contents should handle pointer events
- contentPointerEvents?: string; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents
+ contentPointerEvents?: 'none' | 'all' | undefined; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents
radialMenu?: String[];
LayoutTemplateString?: string;
dontCenter?: 'x' | 'y' | 'xy';
@@ -425,7 +425,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
public static addDocTabFunc: (doc: Doc, location: OpenWhere) => boolean = returnFalse;
onClick = action((e: React.MouseEvent | React.PointerEvent) => {
- if (!this.Document.ignoreClick && this.pointerEvents !== 'none' && this.props.renderDepth >= 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) {
+ if (this.props.isGroupActive?.() === 'child' && !this.props.isDocumentActive?.()) return;
+ if (!this.Document.ignoreClick && this.props.renderDepth >= 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) {
let stopPropagate = true;
let preventDefault = true;
!this.rootDoc._keepZWhenDragged && this.props.bringToFront(this.rootDoc);
@@ -496,7 +497,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
// prettier-ignore
clickFunc ?? (() => (sendToBack ? this.props.DocumentView().props.bringToFront(this.rootDoc, true) :
this._componentView?.select?.(e.ctrlKey || e.metaKey, e.shiftKey) ??
- this.props.select(e.ctrlKey || e.metaKey || e.shiftKey)));
+ this.props.select(e.ctrlKey||e.shiftKey, e.metaKey)));
const waitFordblclick = this.props.waitForDoubleClickToClick?.() ?? this.Document.waitForDoubleClickToClick;
if ((clickFunc && waitFordblclick !== 'never') || waitFordblclick === 'always') {
this._doubleClickTimeout && clearTimeout(this._doubleClickTimeout);
@@ -513,6 +514,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@action
onPointerDown = (e: React.PointerEvent): void => {
+ if (this.props.isGroupActive?.() === 'child' && !this.props.isDocumentActive?.()) return;
this._longPressSelector = setTimeout(() => {
if (DocumentView.LongPress) {
if (this.rootDoc.undoIgnoreFields) {
@@ -536,7 +538,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
!this.props.onBrowseClick?.() &&
!this.Document.ignoreClick &&
e.button === 0 &&
- this.pointerEvents !== 'none' &&
!Doc.IsInMyOverlay(this.layoutDoc)
) {
e.stopPropagation();
@@ -622,7 +623,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
if (this.props.Document === Doc.ActiveDashboard) {
e.stopPropagation();
e.preventDefault();
- alert((e.target as any)?.closest?.('*.lm_content') ? "You can't perform this move most likely because you don't have permission to modify the destination." : 'Linking to document tabs not yet supported. Drop link on document content.');
+ alert(
+ (e.target as any)?.closest?.('*.lm_content')
+ ? "You can't perform this move most likely because you didn't drag the document's title bar to enable embedding in a different document."
+ : 'Linking to document tabs not yet supported. Drop link on document content.'
+ );
return true;
}
const linkdrag = de.complete.annoDragData ?? de.complete.linkDragData;
@@ -635,6 +640,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
if (de.complete.annoDragData || this.rootDoc !== linkdrag.linkSourceDoc.embedContainer) {
const dropDoc = de.complete.annoDragData?.dropDocument ?? this._componentView?.getAnchor?.(true) ?? this.rootDoc;
de.complete.linkDocument = DocUtils.MakeLink(linkdrag.linkSourceDoc, dropDoc, {}, undefined, [de.x, de.y - 50]);
+ if (de.complete.linkDocument) {
+ de.complete.linkDocument.layout_isSvg = true;
+ this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView.addDocument(de.complete.linkDocument);
+ }
}
e.stopPropagation();
return true;
@@ -889,17 +898,19 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
/// disable pointer events on content when there's an enabled onClick script (but not the browse script) and the contents aren't forced active, or if contents are marked inactive
@computed get _contentPointerEvents() {
+ if (this.props.contentPointerEvents) return this.props.contentPointerEvents;
return (!this.disableClickScriptFunc && this.onClickHandler && !this.props.onBrowseClick?.() && this.isContentActive() !== true) || this.isContentActive() === false ? 'none' : this.pointerEvents;
}
contentPointerEvents = () => this._contentPointerEvents;
@computed get contents() {
TraceMobx();
- const isInk = StrCast(this.layoutDoc.layout).includes(InkingStroke.name) && !this.props.LayoutTemplateString;
+ const isInk = this.layoutDoc._layout_isSvg && !this.props.LayoutTemplateString;
+ const noBackground = this.rootDoc._isGroup && (!this.rootDoc.backgroundColor || this.rootDoc.backgroundColor === 'transparent');
return (
<div
className="documentView-contentsView"
style={{
- pointerEvents: (isInk ? 'none' : this.contentPointerEvents()) ?? 'all',
+ pointerEvents: (isInk || noBackground ? 'none' : this.contentPointerEvents()) ?? 'all',
height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
}}>
<DocumentContentsView
@@ -1063,6 +1074,41 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
};
captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string) => this.props?.styleProvider?.(doc, props, property + ':caption');
+ @observable _changingTitleField = false;
+ @observable _dropDownInnerWidth = 0;
+ fieldsDropdown = (inputOptions: string[], dropdownWidth: number, placeholder: string, onChange: (val: string | number) => void, onClose: () => void) => {
+ const filteredOptions = new Set(inputOptions);
+ const scaling = this.titleHeight / 30; /* height of Dropdown */
+ Object.entries(DocOptions)
+ .filter(opts => opts[1].filterable)
+ .forEach((pair: [string, FInfo]) => filteredOptions.add(pair[0]));
+ filteredOptions.add(StrCast(this.layoutDoc.layout_showTitle));
+ const options = Array.from(filteredOptions)
+ .filter(f => f)
+ .map(facet => ({ val: facet, text: facet }));
+ return (
+ <div style={{ width: dropdownWidth }}>
+ <div
+ ref={action((r: any) => r && (this._dropDownInnerWidth = Number(getComputedStyle(r).width.replace('px', ''))))}
+ onPointerDown={action(e => (this._changingTitleField = true))}
+ style={{ width: 'max-content', transformOrigin: 'left', transform: `scale(${scaling})` }}>
+ <Dropdown
+ activeChanged={action(isOpen => !isOpen && (this._changingTitleField = false))}
+ selectedVal={placeholder}
+ setSelectedVal={onChange}
+ color={SettingsManager.userColor}
+ background={SettingsManager.userVariantColor}
+ type={Type.TERT}
+ closeOnSelect={true}
+ dropdownType={DropdownType.SELECT}
+ items={options}
+ width={100}
+ fillWidth
+ />
+ </div>
+ </div>
+ );
+ };
@computed get innards() {
TraceMobx();
const showTitle = this.layout_showTitle?.split(':')[0];
@@ -1090,8 +1136,15 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
</div>
);
const targetDoc = showTitle?.startsWith('_') ? this.layoutDoc : this.rootDoc;
- const background = StrCast(SharingManager.Instance.users.find(u => u.user.email === this.dataDoc.author)?.sharingDoc.headingColor, StrCast(Doc.SharingDoc().headingColor, SettingsManager.userBackgroundColor));
+ const background = StrCast(
+ this.layoutDoc.layout_headingColor,
+ StrCast(SharingManager.Instance.users.find(u => u.user.email === this.dataDoc.author)?.sharingDoc.headingColor, StrCast(this.layoutDoc.layout_headingColor, StrCast(Doc.SharingDoc().headingColor, SettingsManager.userBackgroundColor)))
+ );
+ const dropdownWidth = this._titleRef.current?._editing || this._changingTitleField ? Math.max(10, (this._dropDownInnerWidth * this.titleHeight) / 30) : 0;
const sidebarWidthPercent = +StrCast(this.layoutDoc.layout_sidebarWidthPercent).replace('%', '');
+ // displays a 'title' at the top of a document. The title contents default to the 'title' field, but can be changed to one or more fields by
+ // setting layout_showTitle using the format: field1[;field2[...][:hover]]
+ // from the UI, this is done by clicking the title field and prefixin the format with '#'. eg., #field1[;field2;...][:hover]
const titleView = !showTitle ? null : (
<div
className={`documentView-titleWrapper${showTitleHover ? '-hover' : ''}`}
@@ -1099,39 +1152,58 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
style={{
position: this.headerMargin ? 'relative' : 'absolute',
height: this.titleHeight,
- width: !this.headerMargin ? `calc(${sidebarWidthPercent || 100}% - 18px)` : (sidebarWidthPercent || 100) + '%', // leave room for annotation button
+ width: 100 - sidebarWidthPercent + '%',
color: background === 'transparent' ? SettingsManager.userColor : lightOrDark(background),
background,
pointerEvents: (!this.disableClickScriptFunc && this.onClickHandler) || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined,
}}>
- <EditableView
- ref={this._titleRef}
- contents={showTitle
- .split(';')
- .map(field => field.trim())
- .map(field => targetDoc[field]?.toString())
- .join('\\')}
- display={'block'}
- fontSize={10}
- GetValue={() => {
- return showTitle.split(';').length === 1 ? showTitle + '=' + Field.toString(targetDoc[showTitle.split(';')[0]] as any as Field) : '#' + showTitle;
- }}
- SetValue={undoBatch((input: string) => {
- if (input?.startsWith('#')) {
- if (this.rootDoc.layout_showTitle) {
- this.rootDoc._layout_showTitle = input?.substring(1) ? input.substring(1) : undefined;
- } else if (!this.props.layout_showTitle) {
- Doc.UserDoc().layout_showTitle = input?.substring(1) ? input.substring(1) : 'author_date';
+ {!dropdownWidth
+ ? null
+ : this.fieldsDropdown(
+ [],
+ dropdownWidth,
+ StrCast(this.layoutDoc.layout_showTitle).split(':')[0],
+ action((field: string | number) => {
+ if (this.rootDoc.layout_showTitle) {
+ this.rootDoc._layout_showTitle = field;
+ } else if (!this.props.layout_showTitle) {
+ Doc.UserDoc().layout_showTitle = field;
+ }
+ this._changingTitleField = false;
+ }),
+ action(() => (this._changingTitleField = false))
+ )}
+ <div
+ style={{
+ width: `calc(100% - ${dropdownWidth}px)`,
+ minWidth: '100px',
+ color: this._titleRef.current?._editing || this._changingTitleField ? 'black' : undefined,
+ background: this._titleRef.current?._editing || this._changingTitleField ? 'yellow' : undefined,
+ }}>
+ <EditableView
+ ref={this._titleRef}
+ contents={showTitle
+ .split(';')
+ .map(field => targetDoc[field.trim()]?.toString())
+ .join(' \\ ')}
+ display="block"
+ oneLine={true}
+ fontSize={(this.titleHeight / 15) * 10}
+ GetValue={() => (showTitle.split(';').length !== 1 ? '#' + showTitle : Field.toKeyValueString(this.rootDoc, showTitle.split(';')[0]))}
+ SetValue={undoBatch((input: string) => {
+ if (input?.startsWith('#')) {
+ if (this.rootDoc.layout_showTitle) {
+ this.rootDoc._layout_showTitle = input?.substring(1);
+ } else if (!this.props.layout_showTitle) {
+ Doc.UserDoc().layout_showTitle = input?.substring(1) ?? 'author_date';
+ }
+ } else if (showTitle && !showTitle.includes('Date') && showTitle !== 'author') {
+ KeyValueBox.SetField(targetDoc, showTitle, input);
}
- } else {
- var value = input.replace(new RegExp(showTitle + '='), '') as string | number;
- if (showTitle !== 'title' && Number(value).toString() === value) value = Number(value);
- if (showTitle.includes('Date') || showTitle === 'author') return true;
- Doc.SetInPlace(targetDoc, showTitle, value, true);
- }
- return true;
- })}
- />
+ return true;
+ })}
+ />
+ </div>
</div>
);
return this.props.hideTitle || (!showTitle && !this.layout_showCaption) ? (
@@ -1237,10 +1309,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
onPointerLeave={e => !isParentOf(this.ContentDiv, document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y)) && Doc.UnBrushDoc(this.rootDoc)}
style={{
borderRadius: this.borderRounding,
- pointerEvents: this.pointerEvents === 'visiblePainted' ? 'none' : this.pointerEvents,
+ pointerEvents: this.pointerEvents === 'visiblePainted' ? 'none' : this.pointerEvents, // visible painted means that the underlying doc contents are irregular and will process their own pointer events (otherwise, the contents are expected to fill the entire doc view box so we can handle pointer events here)
}}>
<>
- {DocumentViewInternal.AnimationEffect(renderDoc, this.rootDoc[Animation], this.rootDoc)}
+ {this._componentView instanceof KeyValueBox ? renderDoc : DocumentViewInternal.AnimationEffect(renderDoc, this.rootDoc[Animation], this.rootDoc)}
{borderPath?.jsx}
</>
</div>
@@ -1414,16 +1486,20 @@ export class DocumentView extends React.Component<DocumentViewProps> {
return this.props.dontCenter?.includes('y') ? 0 : this.Yshift;
}
- public toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.NativeDimScaling, this.props.PanelWidth(), this.props.PanelHeight());
+ public toggleNativeDimensions = () => this.docView && this.rootDoc.type !== DocumentType.INK && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.NativeDimScaling, this.props.PanelWidth(), this.props.PanelHeight());
public getBounds = () => {
if (!this.docView?.ContentDiv || this.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) {
return undefined;
}
+ if (this.docView._componentView?.screenBounds) {
+ return this.docView._componentView.screenBounds();
+ }
const xf = this.docView.props
.ScreenToLocalTransform()
.scale(this.trueNativeWidth() ? this.nativeScaling : 1)
.inverse();
const [[left, top], [right, bottom]] = [xf.transformPoint(0, 0), xf.transformPoint(this.panelWidth, this.panelHeight)];
+
if (this.docView.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) {
const docuBox = this.docView.ContentDiv.getElementsByClassName('linkAnchorBox-cont');
if (docuBox.length) return { ...docuBox[0].getBoundingClientRect(), center: undefined };
@@ -1482,7 +1558,16 @@ export class DocumentView extends React.Component<DocumentViewProps> {
scaleToScreenSpace = () => (1 / (this.props.NativeDimScaling?.() || 1)) * this.screenToLocalTransform().Scale;
docViewPathFunc = () => this.docViewPath;
isSelected = (outsideReaction?: boolean) => SelectionManager.IsSelected(this, outsideReaction);
- select = (extendSelection: boolean) => SelectionManager.SelectView(this, extendSelection);
+ select = (extendSelection: boolean, focusSelection?: boolean) => {
+ SelectionManager.SelectView(this, extendSelection);
+ if (focusSelection) {
+ DocumentManager.Instance.showDocument(this.rootDoc, {
+ willZoomCentered: true,
+ zoomScale: 0.9,
+ zoomTime: 500,
+ });
+ }
+ };
NativeWidth = () => this.effectiveNativeWidth;
NativeHeight = () => this.effectiveNativeHeight;
PanelWidth = () => this.panelWidth;