diff options
Diffstat (limited to 'src/client/views/nodes')
| -rw-r--r-- | src/client/views/nodes/CollectionFreeFormDocumentView.tsx | 3 | ||||
| -rw-r--r-- | src/client/views/nodes/DocumentIcon.tsx | 56 | ||||
| -rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 4 | ||||
| -rw-r--r-- | src/client/views/nodes/KeyValueBox.tsx | 6 | ||||
| -rw-r--r-- | src/client/views/nodes/KeyValuePair.scss | 64 | ||||
| -rw-r--r-- | src/client/views/nodes/KeyValuePair.tsx | 8 | ||||
| -rw-r--r-- | src/client/views/nodes/WebBox.tsx | 359 | ||||
| -rw-r--r-- | src/client/views/nodes/button/FontIconBox.tsx | 13 | ||||
| -rw-r--r-- | src/client/views/nodes/formattedText/DashFieldView.tsx | 150 | ||||
| -rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 3 | ||||
| -rw-r--r-- | src/client/views/nodes/formattedText/marks_rts.ts | 2 | ||||
| -rw-r--r-- | src/client/views/nodes/trails/PresBox.tsx | 18 |
12 files changed, 299 insertions, 387 deletions
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index a8c8a0d2e..862d0da8c 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -12,12 +12,11 @@ import { SelectionManager } from '../../util/SelectionManager'; import { Transform } from '../../util/Transform'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { DocComponent } from '../DocComponent'; +import { InkingStroke } from '../InkingStroke'; import { StyleProp } from '../StyleProvider'; import './CollectionFreeFormDocumentView.scss'; import { DocumentView, DocumentViewProps, OpenWhere } from './DocumentView'; import React = require('react'); -import { DocumentType } from '../../documents/DocumentTypes'; -import { InkingStroke } from '../InkingStroke'; export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { dataProvider?: (doc: Doc, replica: string) => { x: number; y: number; zIndex?: number; rotation?: number; color?: string; backgroundColor?: string; opacity?: number; highlight?: boolean; z: number; transition?: string } | undefined; diff --git a/src/client/views/nodes/DocumentIcon.tsx b/src/client/views/nodes/DocumentIcon.tsx index 56de2d1fc..6e2ed72b8 100644 --- a/src/client/views/nodes/DocumentIcon.tsx +++ b/src/client/views/nodes/DocumentIcon.tsx @@ -1,23 +1,39 @@ - -import { observer } from "mobx-react"; -import * as React from "react"; -import { DocumentView } from "./DocumentView"; -import { DocumentManager } from "../../util/DocumentManager"; -import { Transformer, ts } from "../../util/Scripting"; -import { Field } from "../../../fields/Doc"; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { DocumentView } from './DocumentView'; +import { DocumentManager } from '../../util/DocumentManager'; +import { Transformer, ts } from '../../util/Scripting'; +import { Field } from '../../../fields/Doc'; +import { Tooltip } from '@material-ui/core'; +import { action, observable } from 'mobx'; +import { Id } from '../../../fields/FieldSymbols'; +import { factory } from 'typescript'; +import { LightboxView } from '../LightboxView'; @observer -export class DocumentIcon extends React.Component<{ view: DocumentView, index: number }> { +export class DocumentIcon extends React.Component<{ view: DocumentView; index: number }> { + @observable _hovered = false; + static get DocViews() { + return LightboxView.LightboxDoc ? DocumentManager.Instance.DocumentViews.filter(v => LightboxView.IsLightboxDocView(v.props.docViewPath())) : DocumentManager.Instance.DocumentViews; + } render() { const view = this.props.view; const { left, top, right, bottom } = view.getBounds() || { left: 0, top: 0, right: 0, bottom: 0 }; return ( - <div className="documentIcon-outerDiv" style={{ - position: "absolute", - transform: `translate(${(left + right) / 2}px, ${top}px)`, - }}> - <p>d{this.props.index}</p> + <div + className="documentIcon-outerDiv" + onPointerEnter={action(e => (this._hovered = true))} + onPointerLeave={action(e => (this._hovered = false))} + style={{ + pointerEvents: 'all', + opacity: this._hovered ? 0.3 : 1, + position: 'absolute', + transform: `translate(${(left + right) / 2}px, ${top}px)`, + }}> + <Tooltip title={<>{this.props.view.rootDoc.title}</>}> + <p>d{this.props.index}</p> + </Tooltip> </div> ); } @@ -41,7 +57,9 @@ export class DocumentIconContainer extends React.Component { const match = node.text.match(/d([0-9]+)/); if (match) { const m = parseInt(match[1]); + const doc = DocumentIcon.DocViews[m].rootDoc; usedDocuments.add(m); + return factory.createIdentifier(`idToDoc("${doc[Id]}")`); } } } @@ -52,14 +70,14 @@ export class DocumentIconContainer extends React.Component { }; }, getVars() { - const docs = Array.from(DocumentManager.Instance.DocumentViews); + const docs = DocumentIcon.DocViews; const capturedVariables: { [name: string]: Field } = {}; - usedDocuments.forEach(index => capturedVariables[`d${index}`] = docs[index].props.Document); - return { capturedVariables }; - } + usedDocuments.forEach(index => (capturedVariables[`d${index}`] = docs.length > index ? docs[index].props.Document : `d${index}`)); + return capturedVariables; + }, }; } render() { - return Array.from(DocumentManager.Instance.DocumentViews).map((dv, i) => <DocumentIcon key={i} index={i} view={dv} />); + return DocumentIcon.DocViews.map((dv, i) => <DocumentIcon key={i} index={i} view={dv} />); } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index b61a468bd..ffbde18ca 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -48,6 +48,7 @@ import { PresEffect, PresEffectDirection } from './trails'; import { PinProps, PresBox } from './trails/PresBox'; import React = require('react'); import { InkingStroke } from '../InkingStroke'; +import { RefField } from '../../../fields/RefField'; const { Howl } = require('howler'); interface Window { @@ -127,6 +128,7 @@ export interface DocComponentView { IsPlaying?: () => boolean; // is a media document playing TogglePause?: (keep?: boolean) => void; // toggle media document playing state setFocus?: () => void; // sets input focus to the componentView + setData?: (data: Field | Promise<RefField | undefined>) => boolean; componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null; incrementalRendering?: () => void; fitWidth?: () => boolean; // whether the component always fits width (eg, KeyValueBox) @@ -434,7 +436,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps }, console.log ); UndoManager.RunInBatch(() => (func().result?.select === true ? this.props.select(false) : ''), 'on double click'); } else if (!Doc.IsSystem(this.rootDoc) && (defaultDblclick === undefined || defaultDblclick === 'default')) { - UndoManager.RunInBatch(() => this.props.addDocTab(this.rootDoc, OpenWhere.lightbox), 'double tap'); + UndoManager.RunInBatch(() => LightboxView.AddDocTab(this.rootDoc, OpenWhere.lightbox), 'double tap'); SelectionManager.DeselectAll(); Doc.UnBrushDoc(this.props.Document); } else { diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index e317de11e..f3c8ba62d 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -20,6 +20,10 @@ import { ImageBox } from './ImageBox'; import './KeyValueBox.scss'; import { KeyValuePair } from './KeyValuePair'; import React = require('react'); +import { DocumentManager } from '../../util/DocumentManager'; +import { ScriptingGlobals } from '../../util/ScriptingGlobals'; +import { ScriptingRepl } from '../ScriptingRepl'; +import { DocumentIconContainer } from './DocumentIcon'; export type KVPScript = { script: CompiledScript; @@ -75,7 +79,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { value = dubEq ? value.substring(2) : value; const options: ScriptOptions = { addReturn: true, typecheck: false, params: { this: Doc.name, self: Doc.name, _last_: 'any', _readOnly_: 'boolean' }, editable: true }; if (dubEq) options.typecheck = false; - const script = CompileScript(value, options); + const script = CompileScript(value, { ...options, transformer: DocumentIconContainer.getTransformer() }); return !script.compiled ? undefined : { script, type: dubEq, onDelegate: eq }; } diff --git a/src/client/views/nodes/KeyValuePair.scss b/src/client/views/nodes/KeyValuePair.scss index 5b660e582..57d36932e 100644 --- a/src/client/views/nodes/KeyValuePair.scss +++ b/src/client/views/nodes/KeyValuePair.scss @@ -1,60 +1,52 @@ -@import "../global/globalCssVariables"; - +@import '../global/globalCssVariables'; .keyValuePair-td-key { - display:inline-block; + display: inline-block; - .keyValuePair-td-key-container{ - width:100%; - height:100%; - display: flex; - flex-direction: row; - flex-wrap: nowrap; - justify-content: space-between; - align-items: center; - .keyValuePair-td-key-delete{ + .keyValuePair-td-key-container { + width: 100%; + height: 100%; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: space-between; + .keyValuePair-td-key-delete { position: relative; background-color: transparent; - color:red; + color: red; } .keyValuePair-td-key-check { position: relative; margin: 0; } .keyValuePair-keyField { - width:100%; + width: 100%; margin-left: 20px; - margin-top: -1px; font-family: monospace; - // text-align: center; - align-self: center; position: relative; overflow: auto; + display: inline; } } } .keyValuePair-td-value { - display:inline-block; + display: inline-block; overflow: scroll; font-family: monospace; height: 30px; - .keyValuePair-td-value-container { - display: flex; - align-items: center; - align-content: center; - flex-direction: row; - justify-content: space-between; - flex-wrap: nowrap; - width: 100%; - height: 100%; + .keyValuePair-td-value-container { + display: inline; + justify-content: space-between; + width: 100%; + height: 100%; - img { - max-height: 36px; - width: auto; - } - .videoBox-cont{ - width: auto; - max-height: 36px; - } + img { + max-height: 36px; + width: auto; + } + .videoBox-cont { + width: auto; + max-height: 36px; + } } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index 7ea6d42ff..85641124f 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -118,13 +118,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { </td> <td className="keyValuePair-td-value" style={{ width: `${100 - this.props.keyWidth}%` }} onContextMenu={this.onContextMenu}> <div className="keyValuePair-td-value-container"> - <EditableView - contents={contents} - maxHeight={36} - height={'auto'} - GetValue={() => Field.toKeyValueString(props.Document, props.fieldKey)} - SetValue={(value: string) => KeyValueBox.SetField(props.Document, props.fieldKey, value)} - /> + <EditableView contents={contents} GetValue={() => Field.toKeyValueString(props.Document, props.fieldKey)} SetValue={(value: string) => KeyValueBox.SetField(props.Document, props.fieldKey, value)} /> </div> </td> </tr> diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index e05b48c0b..82c8e796d 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -2,16 +2,16 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as WebRequest from 'web-request'; -import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; +import { Doc, DocListCast, Field, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { HtmlField } from '../../../fields/HtmlField'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; -import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types'; +import { Cast, ImageCast, NumCast, StrCast, WebCast } from '../../../fields/Types'; import { ImageField, WebField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, getWordAtPoint, returnFalse, returnOne, returnZero, setupMoveUpEvents, smoothScroll, StopEvent, Utils } from '../../../Utils'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, getWordAtPoint, removeStyleSheetRule, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents, smoothScroll, StopEvent, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager } from '../../util/DragManager'; @@ -31,12 +31,13 @@ import { Annotation } from '../pdf/Annotation'; import { GPTPopup } from '../pdf/GPTPopup/GPTPopup'; import { SidebarAnnos } from '../SidebarAnnos'; import { StyleProp } from '../StyleProvider'; -import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere } from './DocumentView'; +import { DocComponentView, DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { LinkDocPreview } from './LinkDocPreview'; import { PinProps, PresBox } from './trails'; import './WebBox.scss'; import React = require('react'); +import { RefField } from '../../../fields/RefField'; const { CreateImage } = require('./WebBoxRenderer'); const _global = (window /* browser */ || global) /* node */ as any; const htmlToText = require('html-to-text'); @@ -47,6 +48,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } public static openSidebarWidth = 250; public static sidebarResizerWidth = 5; + static webStyleSheet = addStyleSheet(); private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void); private _setBrushViewer: undefined | ((view: { width: number; height: number; panX: number; panY: number }) => void); private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); @@ -58,15 +60,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps private _sidebarRef = React.createRef<SidebarAnnos>(); private _searchRef = React.createRef<HTMLInputElement>(); private _searchString = ''; + private _scrollTimer: any; private get _getAnchor() { return AnchorMenu.Instance?.GetAnchor; } - @observable private _webUrl = ''; // url of the src parameter of the embedded iframe but not necessarily the rendered page - eg, when following a link, the rendered page changes but we don't wan the src parameter to also change as that would cause an unnecessary re-render. + @observable private _webUrl = ''; // url of the src parameter of the embedded iframe but not necessarily the rendered page - eg, when following a link, the rendered page changes but we don't want the src parameter to also change as that would cause an unnecessary re-render. @observable private _hackHide = false; // apparently changing the value of the 'sandbox' prop doesn't necessarily apply it to the active iframe. so thisforces the ifrmae to be rebuilt when allowScripts is toggled @observable private _searching: boolean = false; @observable private _showSidebar = false; - @observable private _scrollTimer: any; @observable private _webPageHasBeenRendered = false; @observable private _overlayAnnoInfo: Opt<Doc>; @observable private _marqueeing: number[] | undefined; @@ -141,51 +143,40 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; updateThumb = async () => { - const imageBitmap = ImageCast(this.layoutDoc['thumb-frozen'])?.url.href; + if (!this._iframe) return; const scrollTop = NumCast(this.layoutDoc._scrollTop); const nativeWidth = NumCast(this.layoutDoc.nativeWidth); const nativeHeight = (nativeWidth * this.props.PanelHeight()) / this.props.PanelWidth(); - if ( - !this.props.isSelected(true) && - !Doc.IsBrushedDegree(this.rootDoc) && - !this.isAnyChildContentActive() && - !this.rootDoc.thumbLockout && - !this.props.dontRegisterView && - this._iframe && - !imageBitmap && - (scrollTop !== this.layoutDoc.thumbScrollTop || nativeWidth !== this.layoutDoc.thumbNativeWidth || nativeHeight !== this.layoutDoc.thumbNativeHeight) - ) { - var htmlString = this._iframe.contentDocument && new XMLSerializer().serializeToString(this._iframe.contentDocument); - if (!htmlString) { - htmlString = await (await fetch(Utils.CorsProxy(this.webField!.href))).text(); - } - this.layoutDoc.thumb = undefined; - this.rootDoc.thumbLockout = true; // lock to prevent multiple thumb updates. - CreateImage(this._webUrl.endsWith('/') ? this._webUrl.substring(0, this._webUrl.length - 1) : this._webUrl, this._iframe.contentDocument?.styleSheets ?? [], htmlString, nativeWidth, nativeHeight, scrollTop) - .then((data_url: any) => { - if (data_url.includes('<!DOCTYPE')) { - console.log('BAD DATA IN THUMB CREATION'); - return; - } - Utils.convertDataUri(data_url, this.layoutDoc[Id] + '-icon' + new Date().getTime(), true, this.layoutDoc[Id] + '-icon').then(returnedfilename => - setTimeout( - action(() => { - this.rootDoc.thumbLockout = false; - this.layoutDoc.thumb = new ImageField(returnedfilename); - this.layoutDoc.thumbScrollTop = scrollTop; - this.layoutDoc.thumbNativeWidth = nativeWidth; - this.layoutDoc.thumbNativeHeight = nativeHeight; - }), - 500 - ) - ); - }) - .catch(function (error: any) { - console.error('oops, something went wrong!', error); - }); + var htmlString = this._iframe.contentDocument && new XMLSerializer().serializeToString(this._iframe.contentDocument); + if (!htmlString) { + htmlString = await (await fetch(Utils.CorsProxy(this.webField!.href))).text(); } + this.layoutDoc.thumb = undefined; + this.rootDoc.thumbLockout = true; // lock to prevent multiple thumb updates. + CreateImage(this._webUrl.endsWith('/') ? this._webUrl.substring(0, this._webUrl.length - 1) : this._webUrl, this._iframe.contentDocument?.styleSheets ?? [], htmlString, nativeWidth, nativeHeight, scrollTop) + .then((data_url: any) => { + if (data_url.includes('<!DOCTYPE')) { + console.log('BAD DATA IN THUMB CREATION'); + return; + } + Utils.convertDataUri(data_url, this.layoutDoc[Id] + '-icon' + new Date().getTime(), true, this.layoutDoc[Id] + '-icon').then(returnedfilename => + setTimeout( + action(() => { + this.rootDoc.thumbLockout = false; + this.layoutDoc.thumb = new ImageField(returnedfilename); + this.layoutDoc.thumbScrollTop = scrollTop; + this.layoutDoc.thumbNativeWidth = nativeWidth; + this.layoutDoc.thumbNativeHeight = nativeHeight; + }), + 500 + ) + ); + }) + .catch(function (error: any) { + console.error('oops, something went wrong!', error); + }); }; - _thumbTimer: any; + async componentDidMount() { this.props.setContentView?.(this); // this tells the DocumentView that this WebBox is the "content" of the document. this allows the DocumentView to call WebBox relevant methods to configure the UI (eg, show back/forward buttons) @@ -196,24 +187,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps reqdFuncs[this.fieldKey + '-annotations'] = `copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-annotations"])`; reqdFuncs[this.fieldKey + '-annotations-setter'] = `this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-annotations"] = value`; reqdFuncs[this.fieldKey + '-sidebar'] = `copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-sidebar"])`; - DocUtils.AssignScripts(this.rootDoc, {}, reqdFuncs); + DocUtils.AssignScripts(this.dataDoc, {}, reqdFuncs); }); - reaction( - () => this.props.isSelected(true) || this.isAnyChildContentActive() || Doc.isBrushedHighlightedDegree(this.props.Document), - async selected => { - if (selected) { - this._thumbTimer && clearTimeout(this._thumbTimer); - this._webPageHasBeenRendered = true; - } else if ( - (!this.props.isContentActive(true) || SnappingManager.GetIsDragging()) && // update thumnail when unselected AND (no child annotation is active OR we've started dragging the document in which case no additional deselect will occur so this is the only chance to update the thumbnail) - LightboxView.LightboxDoc !== this.rootDoc - ) { - // don't create a thumbnail if entering Lightbox from maximize either, since thumb will be empty. - this._thumbTimer && clearTimeout(this._thumbTimer); - this._thumbTimer = setTimeout(this.updateThumb, 2000); - } - }, - { fireImmediately: this.props.isSelected(true) || this.isAnyChildContentActive() || (Doc.isBrushedHighlightedDegreeUnmemoized(this.props.Document) ? true : false) } + this._disposers.urlchange = reaction( + () => WebCast(this.rootDoc.data), + url => { + this.submitURL(url.url.href, false, false); + } ); this._disposers.autoHeight = reaction( @@ -296,8 +276,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return this._savedAnnotations; }; - menuControls = () => this.urlEditor; // controls to be added to the top bar when a document of this type is selected - setBrushViewer = (func?: (view: { width: number; height: number; panX: number; panY: number }) => void) => (this._setBrushViewer = func); brushView = (view: { width: number; height: number; panX: number; panY: number }) => this._setBrushViewer?.(view); focus = (anchor: Doc, options: DocFocusOptions) => { @@ -316,9 +294,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } }; - getView = async (doc: Doc) => { + @action + getView = (doc: Doc) => { if (this.rootDoc.layoutKey === 'layout_icon') this.props.DocumentView?.().iconify(); - if (this._url && StrCast(doc.webUrl) !== this._url) this.submitURL(StrCast(doc.webUrl)); + if (this._url && WebCast(doc.presData).url.href !== this._url) this.setData(WebCast(doc.presData).url.href); if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar(false); return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv))); }; @@ -341,13 +320,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } catch (e) {} const anchor = this._getAnchor(this._savedAnnotations, false) ?? - Docs.Create.WebanchorDocument(this._url, { + Docs.Create.WebanchorDocument({ title: StrCast(this.rootDoc.title + ' ' + this.layoutDoc._scrollTop), y: NumCast(this.layoutDoc._scrollTop), unrendered: true, annotationOn: this.rootDoc, }); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true, pannable: true } }, this.rootDoc); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: pinProps?.pinData ? true : false, pannable: true } }, this.rootDoc); anchor.text = ele?.textContent ?? ''; anchor.textHtml = ele?.innerHTML; //addAsAnnotation && @@ -413,7 +392,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps iframeClick = () => this._iframeClick; iframeScaling = () => 1 / this.props.ScreenToLocalTransform().Scale; - addStyleSheet(document: any, styleType: string = 'text/css') { + addWebStyleSheet(document: any, styleType: string = 'text/css') { if (document) { const style = document.createElement('style'); style.type = styleType; @@ -421,7 +400,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return (sheets as any).sheet; } } - addStyleSheetRule(sheet: any, selector: any, css: any, selectorPrefix = '.') { + addWebStyleSheetRule(sheet: any, selector: any, css: any, selectorPrefix = '.') { const propText = typeof css === 'string' ? css @@ -438,8 +417,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (this._initialScroll !== undefined) { this.setScrollPos(this._initialScroll); } - - this.addStyleSheetRule(this.addStyleSheet(this._iframe?.contentDocument), '::selection', { color: 'white', background: 'orange' }, ''); + this._scrollHeight = this._iframe?.contentDocument?.body?.scrollHeight ?? 0; + this.addWebStyleSheetRule(this.addWebStyleSheet(this._iframe?.contentDocument), '::selection', { color: 'white', background: 'orange' }, ''); let href: Opt<string>; try { @@ -463,12 +442,19 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps .replace('search&', 'search?') .replace('?gbv=1', ''); } - this.submitURL(requrlraw, undefined, true); + this.setData(requrlraw); } const iframeContent = iframe?.contentDocument; if (iframeContent) { iframeContent.addEventListener('pointerup', this.iframeUp); iframeContent.addEventListener('pointerdown', this.iframeDown); + // iframeContent.addEventListener( + // 'wheel', + // e => { + // e.ctrlKey && e.preventDefault(); + // }, + // { passive: false } + // ); const initHeights = () => { this._scrollHeight = Math.max(this._scrollHeight, (iframeContent.body.children[0] as any)?.scrollHeight || 0); if (this._scrollHeight) { @@ -482,11 +468,11 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps action(() => initHeights), 5000 ); - iframe.setAttribute('enable-annotation', 'true'); iframeContent.addEventListener( 'click', undoBatch( action((e: MouseEvent) => { + const batch = UndoManager.StartBatch('webclick'); let href = ''; for (let ele = e.target as any; ele; ele = ele.parentElement) { href = (typeof ele.href === 'string' ? ele.href : ele.href?.baseVal) || ele.parentElement?.href || href; @@ -494,7 +480,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const origin = this.webField?.origin; if (href && origin) { e.stopPropagation(); - setTimeout(() => this.submitURL(href.replace(Utils.prepend(''), origin))); + setTimeout(() => { + this.setData(href.replace(Utils.prepend(''), origin)); + batch.end(); + }); if (this._outerRef.current) { this._outerRef.current.scrollTop = NumCast(this.layoutDoc._scrollTop); this._outerRef.current.scrollLeft = 0; @@ -503,39 +492,49 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }) ) ); - iframe.contentDocument.addEventListener('wheel', this.iframeWheel, false); - //iframe.contentDocument.addEventListener('scroll', () => !this.active() && this._iframe && (this._iframe.scrollTop = NumCast(this.layoutDoc._scrollTop), false)); + iframe.contentDocument.addEventListener('wheel', this.iframeWheel, { passive: false }); } }; @action - iframeWheel = (e: any) => { + iframeWheel = (e: WheelEvent) => { if (!this._scrollTimer) { - this._scrollTimer = setTimeout( - action(() => (this._scrollTimer = undefined)), - 250 - ); // this turns events off on the iframe which allows scrolling to change direction smoothly + addStyleSheetRule(WebBox.webStyleSheet, 'webBox-iframe', { 'pointer-events': 'none' }); + this._scrollTimer = setTimeout(() => { + this._scrollTimer = undefined; + clearStyleSheetRules(WebBox.webStyleSheet); + }, 250); // this turns events off on the iframe which allows scrolling to change direction smoothly + } + if (e.ctrlKey) { + if (this._innerCollectionView) { + this._innerCollectionView.zoom(e.screenX, e.screenY, e.deltaY); + const offset = e.clientY - NumCast(this.layoutDoc._scrollTop); + this.layoutDoc.panY = offset - offset / NumCast(this.layoutDoc._viewScale) + NumCast(this.layoutDoc._scrollTop) - NumCast(this.layoutDoc._scrollTop) / NumCast(this.layoutDoc._viewScale); + } + e.preventDefault(); } }; @action setDashScrollTop = (scrollTop: number, timeout: number = 250) => { const iframeHeight = Math.max(scrollTop, this._scrollHeight - this.panelHeight()); - this._scrollTimer && clearTimeout(this._scrollTimer); - this._scrollTimer = setTimeout( - action(() => { - this._scrollTimer = undefined; - const newScrollTop = scrollTop > iframeHeight ? iframeHeight : scrollTop; - if (!LinkDocPreview.LinkInfo && this._outerRef.current && newScrollTop !== this.layoutDoc.thumbScrollTop && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath()))) { - this.layoutDoc.thumb = undefined; - this.layoutDoc.thumbScrollTop = undefined; - this.layoutDoc.thumbNativeWidth = undefined; - this.layoutDoc.thumbNativeHeight = undefined; - this.layoutDoc.scrollTop = this._outerRef.current.scrollTop = newScrollTop; - } else if (this._outerRef.current) this._outerRef.current.scrollTop = newScrollTop; - }), - timeout - ); + if (this._scrollTimer) { + clearTimeout(this._scrollTimer); + clearStyleSheetRules(WebBox.webStyleSheet); + } + addStyleSheetRule(WebBox.webStyleSheet, 'webBox-iframe', { 'pointer-events': 'none' }); + this._scrollTimer = setTimeout(() => { + clearStyleSheetRules(WebBox.webStyleSheet); + this._scrollTimer = undefined; + const newScrollTop = scrollTop > iframeHeight ? iframeHeight : scrollTop; + if (!LinkDocPreview.LinkInfo && this._outerRef.current && newScrollTop !== this.layoutDoc.thumbScrollTop && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath()))) { + this.layoutDoc.thumb = undefined; + this.layoutDoc.thumbScrollTop = undefined; + this.layoutDoc.thumbNativeWidth = undefined; + this.layoutDoc.thumbNativeHeight = undefined; + this.layoutDoc.scrollTop = this._outerRef.current.scrollTop = newScrollTop; + } else if (this._outerRef.current) this._outerRef.current.scrollTop = newScrollTop; + }, timeout); }; goTo = (scrollTop: number, duration: number, easeFunc: 'linear' | 'ease' | undefined) => { @@ -557,8 +556,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps runInAction(() => { if (future.length) { const curUrl = this._url; - this.rootDoc[this.fieldKey + '-history'] = new List<string>([...history, this._url]); - this.rootDoc[this.fieldKey] = new WebField(new URL(future.pop()!)); + this.dataDoc[this.fieldKey + '-history'] = new List<string>([...history, this._url]); + this.dataDoc[this.fieldKey] = new WebField(new URL(future.pop()!)); + this._scrollHeight = 0; if (this._webUrl === this._url) { this._webUrl = curUrl; setTimeout(action(() => (this._webUrl = this._url))); @@ -578,9 +578,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps runInAction(() => { if (history.length) { const curUrl = this._url; - if (future === undefined) this.rootDoc[this.fieldKey + '-future'] = new List<string>([this._url]); - else this.rootDoc[this.fieldKey + '-future'] = new List<string>([...future, this._url]); - this.layoutDoc[this.fieldKey] = new WebField(new URL(history.pop()!)); + if (future === undefined) this.dataDoc[this.fieldKey + '-future'] = new List<string>([this._url]); + else this.dataDoc[this.fieldKey + '-future'] = new List<string>([...future, this._url]); + this.dataDoc[this.fieldKey] = new WebField(new URL(history.pop()!)); + this._scrollHeight = 0; if (this._webUrl === this._url) { this._webUrl = curUrl; setTimeout(action(() => (this._webUrl = this._url))); @@ -607,23 +608,18 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (!newUrl) return; if (!newUrl.startsWith('http')) newUrl = 'http://' + newUrl; try { - const future = Cast(this.rootDoc[this.fieldKey + '-future'], listSpec('string')); - const history = Cast(this.rootDoc[this.fieldKey + '-history'], listSpec('string')); - const url = this.webField?.toString(); - if (url && !preview) { - this.rootDoc[this.fieldKey + '-history'] = new List<string>([...(history || []), url]); - this.layoutDoc._scrollTop = 0; + if (!preview) { if (this._webPageHasBeenRendered) { this.layoutDoc.thumb = undefined; this.layoutDoc.thumbScrollTop = undefined; this.layoutDoc.thumbNativeWidth = undefined; this.layoutDoc.thumbNativeHeight = undefined; } - future && (future.length = 0); } if (!preview) { - this.layoutDoc[this.fieldKey] = new WebField(new URL(newUrl)); - !dontUpdateIframe && (this._webUrl = this._url); + if (!dontUpdateIframe) { + this._webUrl = this._url; + } } } catch (e) { console.log('WebBox URL error:' + this._url); @@ -637,48 +633,28 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const uri = dataTransfer.getData('text/uri-list'); const url = uri || html || this._url || ''; const newurl = url.startsWith(window.location.origin) ? url.replace(window.location.origin, this._url?.match(/http[s]?:\/\/[^\/]*/)?.[0] || '') : url; - this.submitURL(newurl); + this.setData(newurl); e.stopPropagation(); }; + + @action + setData = (data: Field | Promise<RefField | undefined>) => { + if (!(typeof data === 'string') && !(data instanceof WebField)) return false; + if (Field.toString(data) === this._url) return false; + this._scrollHeight = 0; + const oldUrl = this._url; + const history = Cast(this.rootDoc[this.fieldKey + '-history'], listSpec('string'), []); + const weburl = new WebField(Field.toString(data)); + this.dataDoc[this.fieldKey + '-future'] = new List<string>([]); + this.dataDoc[this.fieldKey + '-history'] = new List<string>([...(history || []), oldUrl]); + this.dataDoc[this.fieldKey] = weburl; + return true; + }; onWebUrlValueKeyDown = (e: React.KeyboardEvent) => { - e.key === 'Enter' && this.submitURL(this._keyInput.current!.value); + if (e.key === 'Enter') this.setData(this._keyInput.current!.value); e.stopPropagation(); }; - @computed get urlEditor() { - return ( - <div className="collectionMenu-webUrlButtons" onDrop={this.onWebUrlDrop} onDragOver={e => e.preventDefault()}> - <input - className="collectionMenu-urlInput" - key={this._url} - placeholder="ENTER URL" - defaultValue={this._url} - onDrop={this.onWebUrlDrop} - onDragOver={e => e.preventDefault()} - onKeyDown={this.onWebUrlValueKeyDown} - onClick={e => { - this._keyInput.current!.select(); - e.stopPropagation(); - }} - ref={this._keyInput} - /> - <div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', maxWidth: '250px' }}> - <button className="submitUrl" onClick={() => this.submitURL(this._keyInput.current!.value)} onDragOver={e => e.stopPropagation()} onDrop={this.onWebUrlDrop}> - GO - </button> - <button className="submitUrl" onClick={() => this.back}> - {' '} - <FontAwesomeIcon icon="caret-left" size="lg" />{' '} - </button> - <button className="submitUrl" onClick={() => this.forward}> - {' '} - <FontAwesomeIcon icon="caret-right" size="lg" />{' '} - </button> - </div> - </div> - ); - } - specificContextMenu = (e: React.MouseEvent | PointerEvent): void => { const cm = ContextMenu.Instance; const funcs: ContextMenuProps[] = []; @@ -706,6 +682,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }, icon: 'snowflake', }); + funcs.push({ description: 'Create Thumbnail', event: () => this.updateThumb(), icon: 'portrait' }); cm.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' }); } }; @@ -746,19 +723,23 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; @computed get urlContent() { - if (this._hackHide || (this.webThumb && !this._webPageHasBeenRendered && LightboxView.LightboxDoc !== this.rootDoc)) return null; - this.props.thumbShown?.(); + setTimeout( + action(() => { + if (this._initialScroll === undefined && !this._webPageHasBeenRendered) { + this.setScrollPos(NumCast(this.layoutDoc.thumbScrollTop, NumCast(this.layoutDoc.scrollTop))); + } + this._webPageHasBeenRendered = true; + }) + ); const field = this.rootDoc[this.props.fieldKey]; - let view; if (field instanceof HtmlField) { - view = <span className="webBox-htmlSpan" contentEditable onPointerDown={e => e.stopPropagation()} dangerouslySetInnerHTML={{ __html: field.html }} />; - } else if (field instanceof WebField) { + return <span className="webBox-htmlSpan" contentEditable onPointerDown={e => e.stopPropagation()} dangerouslySetInnerHTML={{ __html: field.html }} />; + } + if (field instanceof WebField) { const url = this.layoutDoc.useCors ? Utils.CorsProxy(this._webUrl) : this._webUrl; - view = ( + return ( <iframe className="webBox-iframe" - enable-annotation={'true'} - style={{ pointerEvents: this._scrollTimer ? 'none' : undefined }} ref={action((r: HTMLIFrameElement | null) => (this._iframe = r))} src={url} onLoad={this.iframeLoaded} @@ -767,32 +748,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps sandbox={`${this.layoutDoc.allowScripts ? 'allow-scripts' : ''} allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin`} /> ); - } else { - view = ( - <iframe - className="webBox-iframe" - enable-annotation={'true'} - style={{ pointerEvents: this._scrollTimer ? 'none' : undefined }} // if we allow pointer events when scrolling is on, then reversing direction does not work smoothly - ref={action((r: HTMLIFrameElement | null) => (this._iframe = r))} - src={'https://crossorigin.me/https://cs.brown.edu'} - /> - ); } - setTimeout( - action(() => { - this._scrollHeight = Math.max(this._scrollHeight, this._iframe && this._iframe.contentDocument && this._iframe.contentDocument.body ? this._iframe.contentDocument.body.scrollHeight : 0); - if (this._initialScroll === undefined && !this._webPageHasBeenRendered) { - this.setScrollPos(NumCast(this.layoutDoc.thumbScrollTop, NumCast(this.layoutDoc.scrollTop))); - } - this._webPageHasBeenRendered = true; - }) - ); - return view; + return <iframe className="webBox-iframe" ref={action((r: HTMLIFrameElement | null) => (this._iframe = r))} src={'https://crossorigin.me/https://cs.brown.edu'} />; } addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => { - console.log(annotationKey); - (doc instanceof Doc ? [doc] : doc).forEach(doc => (doc.webUrl = this._url)); + (doc instanceof Doc ? [doc] : doc).forEach(doc => (doc.presData = new WebField(this._url))); return this.addDocument(doc, annotationKey); }; @@ -843,7 +804,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps title="Toggle Sidebar" style={{ display: !this.props.isContentActive() ? 'none' : undefined, - top: StrCast(this.rootDoc._showTitle) === 'title' ? 20 : 5, + top: StrCast(this.layoutDoc._showTitle) === 'title' ? 20 : 5, backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, }} onPointerDown={e => this.sidebarBtnDown(e, true)}> @@ -878,17 +839,33 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } } }); + @action + onZoomWheel = (e: React.WheelEvent) => { + if (this.props.isContentActive(true)) { + e.stopPropagation(); + } + }; sidebarWidth = () => { if (!this.SidebarShown) return 0; if (this._previewWidth) return WebBox.sidebarResizerWidth + WebBox.openSidebarWidth; // return default sidebar if previewing (as in viewing a link target) const nativeDiff = NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc); return WebBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1); }; + _innerCollectionView: CollectionFreeFormView | undefined; + zoomScaling = () => this._innerCollectionView?.zoomScaling() ?? 1; + setInnerContent = (component: DocComponentView) => (this._innerCollectionView = component as CollectionFreeFormView); + @computed get content() { const interactive = this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && Doc.ActiveTool === InkTool.None; return ( - <div className={'webBox-cont' + (interactive ? '-interactive' : '')} onKeyDown={e => e.stopPropagation()} style={{ width: !this.layoutDoc.forceReflow ? NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']) || `100%` : '100%' }}> - {this.urlContent} + <div + className={'webBox-cont' + (interactive ? '-interactive' : '')} + onKeyDown={e => e.stopPropagation()} + style={{ + width: !this.layoutDoc.forceReflow ? NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']) || `100%` : '100%', + transform: `scale(${this.zoomScaling()}) translate(${-NumCast(this.layoutDoc.panX)}px, ${-NumCast(this.layoutDoc.panY)}px)`, + }}> + {this._hackHide || (this.webThumb && !this._webPageHasBeenRendered && LightboxView.LightboxDoc !== this.rootDoc) ? null : this.urlContent} </div> ); } @@ -896,7 +873,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @computed get annotationLayer() { TraceMobx(); return ( - <div className="webBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}> + <div + className="webBox-annotationLayer" + style={{ + transform: `scale(${this.zoomScaling()}) translate(${-NumCast(this.layoutDoc.panX)}px, ${-NumCast(this.layoutDoc.panY)}px)`, + height: Doc.NativeHeight(this.Document) || undefined, + }} + ref={this._annotationLayer}> {this.inlineTextAnnotations .sort((a, b) => NumCast(a.y) - NumCast(b.y)) .map(anno => ( @@ -916,9 +899,11 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const renderAnnotations = (docFilters: () => string[]) => ( <CollectionFreeFormView {...this.props} - setContentView={emptyFunction} + setContentView={this.setInnerContent} NativeWidth={returnZero} NativeHeight={returnZero} + originTopLeft={false} + isAnnotationOverlayScrollable={true} renderDepth={this.props.renderDepth + 1} isAnnotationOverlay={true} fieldKey={this.annotationKey} @@ -945,16 +930,17 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ); return ( <div - className={'webBox-outerContent'} + className="webBox-outerContent" ref={this._outerRef} style={{ height: `${100 / scale}%`, pointerEvents, }} - onWheel={StopEvent} // block wheel events from propagating since they're handled by the iframe + // when active, block wheel events from propagating since they're handled by the iframe + onWheel={this.onZoomWheel} onScroll={e => this.setDashScrollTop(this._outerRef.current?.scrollTop || 0)} onPointerDown={this.onMarqueeDown}> - <div className={'webBox-innerContent'} style={{ height: this._webPageHasBeenRendered && this._scrollHeight ? this.scrollHeight : '100%', pointerEvents }}> + <div className="webBox-innerContent" style={{ height: (this._webPageHasBeenRendered && this._scrollHeight) || '100%', pointerEvents }}> {this.content} {<div style={{ display: DragManager.docsBeingDragged.length ? 'none' : undefined, mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div>} {renderAnnotations(this.opaqueFilter)} @@ -1016,7 +1002,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps pointerEvents = () => (!this._draggingSidebar && this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance?.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none'); annotationPointerEvents = () => (this._isAnnotating || SnappingManager.GetIsDragging() || Doc.ActiveTool !== InkTool.None ? 'all' : 'none'); render() { - setTimeout(() => DocListCast(this.rootDoc[this.annotationKey]).forEach(doc => (doc.webUrl = this._url))); const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this.props.pointerEvents?.() as any); const scale = previewScale * (this.props.NativeDimScaling?.() || 1); diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx index 26515da30..a4968dcd0 100644 --- a/src/client/views/nodes/button/FontIconBox.tsx +++ b/src/client/views/nodes/button/FontIconBox.tsx @@ -505,7 +505,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { <div className="menuButton editableText"> <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={'lock'} /> <div style={{ width: 'calc(100% - .875em)', paddingLeft: '4px' }}> - <EditableView GetValue={() => script?.script.run({ value: '', _readOnly_: true }).result} SetValue={setValue} contents={checkResult} /> + <EditableView GetValue={() => script?.script.run({ value: '', _readOnly_: true }).result} SetValue={setValue} oneLine={true} contents={checkResult} /> </div> </div> ); @@ -889,7 +889,7 @@ ScriptingGlobals.add(function webSetURL(url: string, checkResult?: boolean) { if (checkResult) { return StrCast(selected.rootDoc.data, Cast(selected.rootDoc.data, WebField, null)?.url?.href); } - (selected.ComponentView as WebBox).submitURL(url); + selected.ComponentView?.setData?.(url); //selected.rootDoc.data = new WebField(url); } }); @@ -925,6 +925,15 @@ ScriptingGlobals.add(function toggleSchemaPreview(checkResult?: boolean) { } } }); +ScriptingGlobals.add(function toggleSingleLineSchema(checkResult?: boolean) { + const selected = SelectionManager.Docs().lastElement(); + if (checkResult && selected) { + return NumCast(selected._singleLine) > 0 ? Colors.MEDIUM_BLUE : 'transparent'; + } + if (selected) { + selected._singleLine = !selected._singleLine; + } +}); /** STACK * groupBy diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index bf6fa2ec6..a9e59ab1c 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -2,18 +2,17 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; import { action, computed, IReactionDisposer, observable } from 'mobx'; import { observer } from 'mobx-react'; -import { NodeSelection } from 'prosemirror-state'; import * as ReactDOM from 'react-dom/client'; -import { DataSym, Doc, DocListCast, Field } from '../../../../fields/Doc'; +import { DataSym, Doc, Field } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { listSpec } from '../../../../fields/Schema'; import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField'; -import { ComputedField } from '../../../../fields/ScriptField'; import { Cast, StrCast } from '../../../../fields/Types'; -import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../../Utils'; +import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; import { CollectionViewType } from '../../../documents/DocumentTypes'; import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; +import { SchemaTableCell } from '../../collections/collectionSchema/SchemaTableCell'; import { OpenWhere } from '../DocumentView'; import './DashFieldView.scss'; import { FormattedTextBox } from './FormattedTextBox'; @@ -70,10 +69,10 @@ export class DashFieldView { } catch {} }); } - deselectNode() { + @action deselectNode() { this.dom.classList.remove('ProseMirror-selectednode'); } - selectNode() { + @action selectNode() { this.dom.classList.add('ProseMirror-selectednode'); } } @@ -98,6 +97,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna _fieldKey: string; _fieldStringRef = React.createRef<HTMLSpanElement>(); @observable _dashDoc: Doc | undefined; + @observable _expanded = false; constructor(props: IDashFieldViewInternal) { super(props); @@ -114,126 +114,32 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna this._reactionDisposer?.(); } - public static multiValueDelimeter = ';'; - public static fieldContent(textBoxDoc: Doc, dashDoc: Doc, fieldKey: string) { - const dashVal = dashDoc[fieldKey] ?? dashDoc[DataSym][fieldKey] ?? ''; - const fval = dashVal instanceof List ? dashVal.join(DashFieldViewInternal.multiValueDelimeter) : StrCast(dashVal).startsWith(':=') || dashVal === '' ? Doc.Layout(textBoxDoc)[fieldKey] : dashVal; - return { boolVal: Cast(fval, 'boolean', null), strVal: Field.toString(fval as Field) || '' }; - } - // set the display of the field's value (checkbox for booleans, span of text for strings) @computed get fieldValueContent() { - if (this._dashDoc) { - const { boolVal, strVal } = DashFieldViewInternal.fieldContent(this._textBoxDoc, this._dashDoc, this._fieldKey); - // field value is a boolean, so use a checkbox or similar widget to display it - if (boolVal === true || boolVal === false) { - return ( - <input - className="dashFieldView-fieldCheck" - type="checkbox" - checked={boolVal} - onChange={e => { - if (this._fieldKey.startsWith('_')) Doc.Layout(this._textBoxDoc)[this._fieldKey] = e.target.checked; - Doc.SetInPlace(this._dashDoc!, this._fieldKey, e.target.checked, true); - }} - /> - ); - } // field value is a string, so display it as an editable span - else { - // bcz: this is unfortunate, but since this React component is nested within a non-React text box (prosemirror), we can't - // use React events. Essentially, React events occur after native events have been processed, so corresponding React events - // will never fire because Prosemirror has handled the native events. So we add listeners for native events here. - return ( - <span - className="dashFieldView-fieldSpan" - contentEditable={!this.props.unclickable()} - style={{ display: strVal.length < 2 ? 'inline-block' : undefined }} - suppressContentEditableWarning={true} - defaultValue={strVal} - ref={r => { - r?.addEventListener('keydown', e => this.fieldSpanKeyDown(e, r)); - r?.addEventListener('blur', e => r && this.updateText(r.textContent!, false)); - r?.addEventListener( - 'pointerdown', - action(e => { - // let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span> - // while (target && !target.dataset?.targethrefs) target = target.parentElement; - this.props.tbox.EditorView!.dispatch(this.props.tbox.EditorView!.state.tr.setSelection(new NodeSelection(this.props.tbox.EditorView!.state.doc.resolve(this.props.getPos())))); - // FormattedTextBoxComment.update(this.props.tbox, this.props.tbox.EditorView!, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc); - // e.stopPropagation(); - }) - ); - }}> - {strVal} - </span> - ); - } - } + return !this._dashDoc ? null : ( + <div onClick={action(e => (this._expanded = !this.props.editable ? !this._expanded : true))} style={{ fontSize: 'smaller', width: this.props.hideKey ? this.props.tbox.props.PanelWidth() - 20 : undefined }}> + <SchemaTableCell + Document={this._dashDoc} + col={0} + deselectCell={emptyFunction} + selectCell={emptyFunction} + maxWidth={this.props.hideKey ? undefined : () => 100} + columnWidth={this.props.hideKey ? () => this.props.tbox.props.PanelWidth() - 20 : returnZero} + selectedCell={() => [this._dashDoc!, 0]} + fieldKey={this._fieldKey} + rowHeight={returnZero} + isRowActive={() => this._expanded && this.props.editable} + padding={0} + getFinfo={emptyFunction} + setColumnValues={returnFalse} + allowCRs={true} + oneLine={!this._expanded} + finishEdit={action(() => (this._expanded = false))} + /> + </div> + ); } - // we need to handle all key events on the input span or else they will propagate to prosemirror. - @action - fieldSpanKeyDown = (e: KeyboardEvent, span: HTMLSpanElement) => { - if (e.key === 'c' && (e.ctrlKey || e.metaKey)) { - navigator.clipboard.writeText(window.getSelection()?.toString() || ''); - return; - } - if (e.key === 'Enter') { - // handle the enter key by "submitting" the current text to Dash's database. - this.updateText(span.textContent!, true); - e.preventDefault(); // prevent default to avoid a newline from being generated and wiping out this field view - } - if (e.key === 'a' && (e.ctrlKey || e.metaKey)) { - // handle ctrl-A to select all the text within the span - if (window.getSelection) { - const range = document.createRange(); - range.selectNodeContents(span); - window.getSelection()!.removeAllRanges(); - window.getSelection()!.addRange(range); - } - e.preventDefault(); //prevent default so that all the text in the prosemirror text box isn't selected - } - if (!this.props.editable) { - e.preventDefault(); - } - e.stopPropagation(); // we need to handle all events or else they will propagate to prosemirror. - }; - - @action - updateText = (nodeText: string, forceMatch: boolean) => { - if (nodeText) { - const newText = nodeText.startsWith(':=') || nodeText.startsWith('=:=') ? ':=-computed-' : nodeText; - // look for a document whose id === the fieldKey being displayed. If there's a match, then that document - // holds the different enumerated values for the field in the titles of its collected documents. - // if there's a partial match from the start of the input text, complete the text --- TODO: make this an auto suggest box and select from a drop down. - DocServer.GetRefField(this._fieldKey).then(options => { - let modText = ''; - options instanceof Doc && DocListCast(options.data).forEach(opt => (forceMatch ? StrCast(opt.title).startsWith(newText) : StrCast(opt.title) === newText) && (modText = StrCast(opt.title))); - if (modText) { - // elementfieldSpan.innerHTML = this._dashDoc![this._fieldKey as string] = modText; - Doc.SetInPlace(this._dashDoc!, this._fieldKey, modText, true); - } // if the text starts with a ':=' then treat it as an expression by making a computed field from its value storing it in the key - else if (nodeText.startsWith(':=')) { - this._dashDoc![DataSym][this._fieldKey] = ComputedField.MakeFunction(nodeText.substring(2)); - } else if (nodeText.startsWith('=:=')) { - Doc.Layout(this._textBoxDoc)[this._fieldKey] = ComputedField.MakeFunction(nodeText.substring(3)); - } else { - if (Number(newText).toString() === newText) { - if (this._fieldKey.startsWith('_')) Doc.Layout(this._textBoxDoc)[this._fieldKey] = Number(newText); - Doc.SetInPlace(this._dashDoc!, this._fieldKey, Number(newText), true); - } else { - const splits = newText.split(DashFieldViewInternal.multiValueDelimeter); - if (!this._textBoxDoc[this._fieldKey]) { - const strVal = splits.length > 1 ? new List<string>(splits) : newText; - if (this._fieldKey.startsWith('_')) Doc.Layout(this._textBoxDoc)[this._fieldKey] = strVal; - Doc.SetInPlace(this._dashDoc!, this._fieldKey, strVal, true); - } - } - } - }); - } - }; - createPivotForField = (e: React.MouseEvent) => { let container = this.props.tbox.props.DocumentView?.().props.docViewPath().lastElement(); if (container) { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index b5aa34a29..68160ac9b 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1575,7 +1575,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (!state.selection.empty && !(state.selection instanceof NodeSelection)) this.setupAnchorMenu(); else if (this.props.isContentActive(true)) { const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY }); - !this.props.isSelected(true) && editor.dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(pcords?.pos || 0)))); + // !this.props.isSelected(true) && + editor.dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(pcords?.pos || 0)))); let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span> while (target && !target.dataset?.targethrefs) target = target.parentElement; FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview === 'true'); diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index 5b47e8a70..7e17008bb 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -46,7 +46,7 @@ export const marks: { [index: string]: MarkSpec } = { toDOM(node: any) { const targethrefs = node.attrs.allAnchors.reduce((p: string, item: { href: string; title: string; anchorId: string }) => (p ? p + ' ' + item.href : item.href), ''); const anchorids = node.attrs.allAnchors.reduce((p: string, item: { href: string; title: string; anchorId: string }) => (p ? p + ' ' + item.anchorId : item.anchorId), ''); - return ['a', { class: anchorids, 'data-targethrefs': targethrefs, 'data-linkdoc': node.attrs.linkDoc, title: node.attrs.title, location: node.attrs.location, style: `background: lightBlue` }, 0]; + return ['a', { class: anchorids, 'data-targethrefs': targethrefs, 'data-noPreview': 'true', 'data-linkdoc': node.attrs.linkDoc, title: node.attrs.title, location: node.attrs.location, style: `background: lightBlue` }, 0]; }, }, noAutoLinkAnchor: { diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index bd2be8f11..c22107c03 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -5,7 +5,7 @@ import { action, computed, IReactionDisposer, observable, ObservableSet, reactio import { observer } from 'mobx-react'; import { AnimationSym, Doc, DocListCast, Field, FieldResult, Opt, StrListCast } from '../../../../fields/Doc'; import { Copy, Id } from '../../../../fields/FieldSymbols'; -import { InkField, InkTool } from '../../../../fields/InkField'; +import { InkField } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; import { ObjectField } from '../../../../fields/ObjectField'; import { listSpec } from '../../../../fields/Schema'; @@ -429,6 +429,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } } } + if ((pinDataTypes?.dataview && activeItem.presData !== undefined) || (!pinDataTypes && activeItem.presData !== undefined)) { + bestTarget._dataTransition = `all ${transTime}ms`; + const fkey = Doc.LayoutFieldKey(bestTarget); + const setData = bestTargetView?.ComponentView?.setData; + if (setData) setData(activeItem.presData); + else Doc.GetProto(bestTarget)[fkey] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData; + bestTarget[fkey + '-usePath'] = activeItem.presUsePath; + setTimeout(() => (bestTarget._dataTransition = undefined), transTime + 10); + } if (pinDataTypes?.datarange || (!pinDataTypes && activeItem.presXRange !== undefined)) { if (bestTarget.xRange !== activeItem.presXRange) { bestTarget.xRange = (activeItem.presXRange as ObjectField)?.[Copy](); @@ -521,13 +530,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const newList = new List<Doc>([...oldItems, ...hiddenItems, ...newItems]); Doc.GetProto(bestTarget)[fkey + '-annotations'] = newList; } - if ((pinDataTypes?.dataview && activeItem.presData !== undefined) || (!pinDataTypes && activeItem.presData !== undefined)) { - bestTarget._dataTransition = `all ${transTime}ms`; - const fkey = Doc.LayoutFieldKey(bestTarget); - Doc.GetProto(bestTarget)[fkey] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData; - bestTarget[fkey + '-usePath'] = activeItem.presUsePath; - setTimeout(() => (bestTarget._dataTransition = undefined), transTime + 10); - } if (pinDataTypes?.poslayoutview || (!pinDataTypes && activeItem.presPinLayoutData !== undefined)) { changed = true; const layoutField = Doc.LayoutFieldKey(bestTarget); |
