import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Utils, intersectRect, lightOrDark, returnFalse } from '../../../../Utils'; import { Doc, Opt } from '../../../../fields/Doc'; import { AclAdmin, AclAugment, AclEdit, DocData } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { InkData, InkField, InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; import { RichTextField } from '../../../../fields/RichTextField'; import { Cast, FieldValue, NumCast, StrCast } from '../../../../fields/Types'; import { ImageField } from '../../../../fields/URLField'; import { GetEffectiveAcl } from '../../../../fields/util'; import { CognitiveServices } from '../../../cognitive_services/CognitiveServices'; import { DocumentType } from '../../../documents/DocumentTypes'; import { DocUtils, Docs, DocumentOptions } from '../../../documents/Documents'; import { SelectionManager } from '../../../util/SelectionManager'; import { freeformScrollMode } from '../../../util/SettingsManager'; import { SnappingManager } from '../../../util/SnappingManager'; import { Transform } from '../../../util/Transform'; import { UndoManager, undoBatch } from '../../../util/UndoManager'; import { ContextMenu } from '../../ContextMenu'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import { PreviewCursor } from '../../PreviewCursor'; import { OpenWhere } from '../../nodes/DocumentView'; import { pasteImageBitmap } from '../../nodes/WebBoxRenderer'; import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; import { SubCollectionViewProps } from '../CollectionSubView'; import { MarqueeOptionsMenu } from './MarqueeOptionsMenu'; import './MarqueeView.scss'; interface MarqueeViewProps { getContainerTransform: () => Transform; getTransform: () => Transform; activeDocuments: () => Doc[]; selectDocuments: (docs: Doc[]) => void; addLiveTextDocument: (doc: Doc) => void; isSelected: () => boolean; panXFieldKey: string; panYFieldKey: string; trySelectCluster: (addToSel: boolean) => boolean; nudge?: (x: number, y: number, nudgeTime?: number) => boolean; ungroup?: () => void; setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt) => void) => void; slowLoadDocuments: (files: File[] | string, options: DocumentOptions, generatedDocuments: Doc[], text: string, completed: ((doc: Doc[]) => void) | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => Promise; } export interface MarqueeViewBounds { left: number; top: number; width: number; height: number; } @observer export class MarqueeView extends ObservableReactComponent { public static CurViewBounds(pinDoc: Doc, panelWidth: number, panelHeight: number) { const ps = NumCast(pinDoc._freeform_scale, 1); return { left: NumCast(pinDoc._freeform_panX) - panelWidth / 2 / ps, top: NumCast(pinDoc._freeform_panY) - panelHeight / 2 / ps, width: panelWidth / ps, height: panelHeight / ps }; } constructor(props: any) { super(props); makeObservable(this); } private _commandExecuted = false; @observable _lastX: number = 0; @observable _lastY: number = 0; @observable _downX: number = 0; @observable _downY: number = 0; @observable _visible: boolean = false; // selection rentangle for marquee selection/free hand lasso is visible @observable _lassoPts: [number, number][] = []; @observable _lassoFreehand: boolean = false; @computed get Transform() { return this._props.getTransform(); } @computed get Bounds() { // nda - ternary argument to transformPoint is returning the lower of the downX/Y and lastX/Y and passing in as args x,y const topLeft = this.Transform.transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY); // nda - args to transformDirection is just x and y diff btw downX/Y and lastX/Y const size = this.Transform.transformDirection(this._lastX - this._downX, this._lastY - this._downY); const bounds: MarqueeViewBounds = { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) }; return bounds; } componentDidMount() { this._props.setPreviewCursor?.(this.setPreviewCursor); } @action cleanupInteractions = (all: boolean = false, hideMarquee: boolean = true) => { if (all) { document.removeEventListener('pointerup', this.onPointerUp, true); document.removeEventListener('pointermove', this.onPointerMove, true); } document.removeEventListener('keydown', this.marqueeCommand, true); hideMarquee && this.hideMarquee(); this._lassoPts = []; }; @action onKeyDown = (e: KeyboardEvent) => { //make textbox and add it to this collection // tslint:disable-next-line:prefer-const const cm = ContextMenu.Instance; const [x, y] = this.Transform.transformPoint(this._downX, this._downY); if (e.key === '?') { cm.setDefaultItem('?', (str: string) => this._props.addDocTab(Docs.Create.WebDocument(`https://wikipedia.org/wiki/${str}`, { _width: 400, x, y, _height: 512, _nativeWidth: 850, title: `wiki:${str}`, data_useCors: true }), OpenWhere.addRight) ); cm.displayMenu(this._downX, this._downY, undefined, true); e.stopPropagation(); } else if (e.key === 'u' && this._props.ungroup) { e.stopPropagation(); this._props.ungroup(); } else if (e.key === ':') { DocUtils.addDocumentCreatorMenuItems(this._props.addLiveTextDocument, this._props.addDocument || returnFalse, x, y); cm.displayMenu(this._downX, this._downY, undefined, true); e.stopPropagation(); } else if (e.key === 'a' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); this._props.selectDocuments(this._props.activeDocuments()); e.stopPropagation(); } else if (e.key === 'q' && e.ctrlKey) { e.preventDefault(); (async () => { const text: string = await navigator.clipboard.readText(); const ns = text.split('\n').filter(t => t.trim() !== '\r' && t.trim() !== ''); for (let i = 0; i < ns.length - 1; i++) { while ( !(ns[i].trim() === '' || ns[i].endsWith('-\r') || ns[i].endsWith('-') || ns[i].endsWith(';\r') || ns[i].endsWith(';') || ns[i].endsWith('.\r') || ns[i].endsWith('.') || ns[i].endsWith(':\r') || ns[i].endsWith(':')) && i < ns.length - 1 ) { const sub = ns[i].endsWith('\r') ? 1 : 0; const br = ns[i + 1].trim() === ''; ns.splice(i, 2, ns[i].substr(0, ns[i].length - sub) + ns[i + 1].trimLeft()); if (br) break; } } let ypos = y; ns.map(line => { const indent = line.search(/\S|$/); const newBox = Docs.Create.TextDocument(line, { _width: 200, _height: 35, x: x + (indent / 3) * 10, y: ypos, title: line }); this._props.addDocument?.(newBox); ypos += 40 * this.Transform.Scale; }); })(); e.stopPropagation(); } else if (e.key === 'b' && e.ctrlKey) { document.body.focus(); // so that we can access the clipboard without an error setTimeout(() => pasteImageBitmap((data: any, error: any) => { error && console.log(error); data && Utils.convertDataUri(data, this._props.Document[Id] + '-thumb-frozen').then(returnedfilename => { this._props.Document['thumb-frozen'] = new ImageField(returnedfilename); }); }) ); } /* else if (e.key === 's' && e.ctrlKey) { e.preventDefault(); const slide = DocUtils.copyDragFactory(DocCast(Doc.UserDoc().emptySlide))!; slide.x = x; slide.y = y; FormattedTextBox.SelectOnLoad = slide[Id]; TreeView._editTitleOnLoad = { id: slide[Id], parent: undefined }; this._props.addDocument?.(slide); e.stopPropagation(); }*/ else if (e.key === 'p' && e.ctrlKey) { e.preventDefault(); (async () => { const text: string = await navigator.clipboard.readText(); const ns = text.split('\n').filter(t => t.trim() !== '\r' && t.trim() !== ''); this.pasteTable(ns, x, y); })(); e.stopPropagation(); } else if (!e.ctrlKey && !e.metaKey && SelectionManager.Views.length < 2) { FormattedTextBox.SelectOnLoadChar = Doc.UserDoc().defaultTextLayout && !this._props.childLayoutString ? e.key : ''; FormattedTextBox.LiveTextUndo = UndoManager.StartBatch('type new note'); this._props.addLiveTextDocument(DocUtils.GetNewTextDoc('-typed text-', x, y, 200, 100)); e.stopPropagation(); } }; //heuristically converts pasted text into a table. // assumes each entry is separated by a tab // skips all rows until it gets to a row with more than one entry // assumes that 1st row has header entry for each column // assumes subsequent rows have entries for each column header OR // any row that has only one column is a section header-- this header is then added as a column to subsequent rows until the next header // assumes each cell is a string or a number pasteTable(ns: string[], x: number, y: number) { let csvRows = []; const headers = ns[0].split('\t'); csvRows.push(headers.join(',')); ns[0] = ''; const eachCell = ns.join('\t').split('\t'); let eachRow = []; for (let i = 1; i < eachCell.length; i++) { eachRow.push(eachCell[i].replace(/\,/g, '')); if (i % headers.length == 0) { csvRows.push(eachRow); eachRow = []; } } const blob = new Blob([csvRows.join('\n')], { type: 'text/csv' }); const options = { x: x, y: y, title: 'droppedTable', _width: 300, _height: 100, type: 'text/csv' }; const file = new File([blob], 'droppedTable', options); const loading = Docs.Create.LoadingDocument(file, options); DocUtils.uploadFileToDoc(file, {}, loading); this._props.addDocument?.(loading); } @action onPointerDown = (e: React.PointerEvent): void => { this._downX = this._lastX = e.clientX; this._downY = this._lastY = e.clientY; const scrollMode = e.altKey ? (Doc.UserDoc().freeformScrollMode === freeformScrollMode.Pan ? freeformScrollMode.Zoom : freeformScrollMode.Pan) : Doc.UserDoc().freeformScrollMode; // allow marquee if right drag/meta drag, or pan mode if (e.button === 2 || e.metaKey || (this._props.isContentActive() && scrollMode === freeformScrollMode.Pan && Doc.ActiveTool === InkTool.None)) { this.setPreviewCursor(e.clientX, e.clientY, true, false, this._props.Document); e.preventDefault(); } else PreviewCursor.Instance.Visible = false; }; @action onPointerMove = (e: PointerEvent): void => { this._lastX = e.pageX; this._lastY = e.pageY; this._lassoPts.push([e.clientX, e.clientY]); if (!e.cancelBubble) { if (!Utils.isClick(this._lastX, this._lastY, this._downX, this._downY, Date.now())) { if (!this._commandExecuted) { this.showMarquee(); } e.stopPropagation(); e.preventDefault(); } } else { this.cleanupInteractions(true); // stop listening for events if another lower-level handle (e.g. another Marquee) has stopPropagated this } e.altKey && e.preventDefault(); }; @action onPointerUp = (e: PointerEvent): void => { if (this._visible) { const mselect = this.marqueeSelect(); if (!e.shiftKey) { SelectionManager.DeselectAll(mselect.length ? undefined : this._props.Document); } const docs = mselect.length ? mselect : [this._props.Document]; this._props.selectDocuments(docs); } const hideMarquee = () => { this.hideMarquee(); MarqueeOptionsMenu.Instance.fadeOut(true); document.removeEventListener('pointerdown', hideMarquee, true); document.removeEventListener('wheel', hideMarquee, true); }; if (!this._commandExecuted && Math.abs(this.Bounds.height * this.Bounds.width) > 100) { MarqueeOptionsMenu.Instance.createCollection = this.collection; MarqueeOptionsMenu.Instance.delete = this.delete; MarqueeOptionsMenu.Instance.summarize = this.summary; MarqueeOptionsMenu.Instance.showMarquee = this.showMarquee; MarqueeOptionsMenu.Instance.hideMarquee = this.hideMarquee; MarqueeOptionsMenu.Instance.jumpTo(e.clientX, e.clientY); MarqueeOptionsMenu.Instance.pinWithView = this.pinWithView; document.addEventListener('pointerdown', hideMarquee, true); document.addEventListener('wheel', hideMarquee, true); } else { this.hideMarquee(); } this.cleanupInteractions(true, this._commandExecuted); e.altKey && e.preventDefault(); }; clearSelection() { if (window.getSelection) { window.getSelection()?.removeAllRanges(); } else if (document.getSelection()) { document.getSelection()?.empty(); } } setPreviewCursor = action((x: number, y: number, drag: boolean, hide: boolean, doc: Opt) => { if (hide) { this._downX = this._lastX = x; this._downY = this._lastY = y; this._commandExecuted = false; PreviewCursor.Instance.Visible = false; PreviewCursor.Instance.Doc = undefined; } else if (drag) { this._downX = this._lastX = x; this._downY = this._lastY = y; this._commandExecuted = false; PreviewCursor.Instance.Visible = false; PreviewCursor.Instance.Doc = undefined; this.cleanupInteractions(true); document.addEventListener('pointermove', this.onPointerMove, true); document.addEventListener('pointerup', this.onPointerUp, true); document.addEventListener('keydown', this.marqueeCommand, true); } else { this._downX = x; this._downY = y; const effectiveAcl = GetEffectiveAcl(this._props.Document[DocData]); if ([AclAdmin, AclEdit, AclAugment].includes(effectiveAcl)) { PreviewCursor.Instance.Doc = doc; PreviewCursor.Show(x, y, this.onKeyDown, this._props.addLiveTextDocument, this._props.getTransform, this._props.addDocument, this._props.nudge, this._props.slowLoadDocuments); } this.clearSelection(); } }); @action onClick = (e: React.MouseEvent): void => { if (this._props.pointerEvents?.() === 'none') return; if (Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, Date.now())) { if (Doc.ActiveTool === InkTool.None) { if (!this._props.trySelectCluster(e.shiftKey)) { !SnappingManager.ExploreMode && this.setPreviewCursor(e.clientX, e.clientY, false, false, this._props.Document); } else e.stopPropagation(); } // let the DocumentView stopPropagation of this event when it selects this document } else { // why do we get a click event when the cursor have moved a big distance? // let's cut it off here so no one else has to deal with it. e.stopPropagation(); } }; @action showMarquee = () => (this._visible = true); @action hideMarquee = () => (this._visible = false); @undoBatch delete = action((e?: React.PointerEvent | KeyboardEvent | undefined, hide?: boolean) => { const selected = this.marqueeSelect(false); SelectionManager.DeselectAll(); selected.forEach(doc => (hide ? (doc.hidden = true) : this._props.removeDocument?.(doc))); this.cleanupInteractions(false); MarqueeOptionsMenu.Instance.fadeOut(true); this.hideMarquee(); }); getCollection = action((selected: Doc[], creator: Opt<(documents: Array, options: DocumentOptions, id?: string) => Doc>, makeGroup: Opt) => { const newCollection = creator ? creator(selected, { title: 'nested stack' }) : ((doc: Doc) => { const docData = doc[DocData]; docData.data = new List(selected); docData.isGroup = makeGroup; docData.title = makeGroup ? 'grouping' : 'nested freeform'; doc._freeform_panX = doc._freeform_panY = 0; return doc; })(Doc.MakeCopy(Doc.UserDoc().emptyCollection as Doc, true)); newCollection.isSystem = undefined; newCollection._width = this.Bounds.width; newCollection._height = this.Bounds.height; newCollection._dragWhenActive = makeGroup; newCollection.x = this.Bounds.left; newCollection.y = this.Bounds.top; newCollection.layout_fitWidth = true; selected.forEach(d => Doc.SetContainer(d, newCollection)); this.hideMarquee(); return newCollection; }); @undoBatch pileup = action((e: KeyboardEvent | React.PointerEvent | undefined) => { const selected = this.marqueeSelect(false); SelectionManager.DeselectAll(); selected.forEach(d => this._props.removeDocument?.(d)); const newCollection = DocUtils.pileup(selected, this.Bounds.left + this.Bounds.width / 2, this.Bounds.top + this.Bounds.height / 2)!; this._props.addDocument?.(newCollection); this._props.selectDocuments([newCollection]); MarqueeOptionsMenu.Instance.fadeOut(true); this.hideMarquee(); }); /** * This triggers the TabDocView.PinDoc method which is the universal method * used to pin documents to the currently active presentation trail. * * This one is unique in that it includes the bounds associated with marquee view. */ @undoBatch pinWithView = action(() => { this._props.pinToPres(this._props.Document, { pinViewport: this.Bounds }); MarqueeOptionsMenu.Instance.fadeOut(true); this.hideMarquee(); }); @undoBatch collection = action((e: KeyboardEvent | React.PointerEvent | undefined, group?: boolean, selection?: Doc[]) => { const selected = selection ?? this.marqueeSelect(false); const activeFrame = selected.reduce((v, d) => v ?? Cast(d._activeFrame, 'number', null), undefined as number | undefined); if (e instanceof KeyboardEvent ? 'cg'.includes(e.key) : true) { this._props.removeDocument?.(selected); } const newCollection = this.getCollection(selected, (e as KeyboardEvent)?.key === 't' ? Docs.Create.StackingDocument : undefined, group); newCollection._freeform_panX = this.Bounds.left + this.Bounds.width / 2; newCollection._freeform_panY = this.Bounds.top + this.Bounds.height / 2; newCollection._currentFrame = activeFrame; this._props.addDocument?.(newCollection); this._props.selectDocuments([newCollection]); MarqueeOptionsMenu.Instance.fadeOut(true); this.hideMarquee(); }); @undoBatch syntaxHighlight = action((e: KeyboardEvent | React.PointerEvent | undefined) => { const selected = this.marqueeSelect(false); if (e instanceof KeyboardEvent ? e.key === 'i' : true) { const inks = selected.filter(s => s.type === DocumentType.INK); const setDocs = selected.filter(s => s.type === DocumentType.RTF && s.color); const sets = setDocs.map(sd => Cast(sd.data, RichTextField)?.Text as string); const colors = setDocs.map(sd => FieldValue(sd.color) as string); const wordToColor = new Map(); sets.forEach((st: string, i: number) => st.split(',').forEach(word => wordToColor.set(word, colors[i]))); const strokes: InkData[] = []; inks.filter(i => Cast(i.data, InkField)).forEach(i => { const d = Cast(i.data, InkField, null); const left = Math.min(...(d?.inkData.map(pd => pd.X) ?? [0])); const top = Math.min(...(d?.inkData.map(pd => pd.Y) ?? [0])); strokes.push(d.inkData.map(pd => ({ X: pd.X + NumCast(i.x) - left, Y: pd.Y + NumCast(i.y) - top }))); }); CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then(results => { // const wordResults = results.filter((r: any) => r.category === "inkWord"); // for (const word of wordResults) { // const indices: number[] = word.strokeIds; // indices.forEach(i => { // if (wordToColor.has(word.recognizedText.toLowerCase())) { // inks[i].color = wordToColor.get(word.recognizedText.toLowerCase()); // } // else { // for (const alt of word.alternates) { // if (wordToColor.has(alt.recognizedString.toLowerCase())) { // inks[i].color = wordToColor.get(alt.recognizedString.toLowerCase()); // break; // } // } // } // }) // } // const wordResults = results.filter((r: any) => r.category === "inkWord"); // for (const word of wordResults) { // const indices: number[] = word.strokeIds; // indices.forEach(i => { // const otherInks: Doc[] = []; // indices.forEach(i2 => i2 !== i && otherInks.push(inks[i2])); // inks[i].relatedInks = new List(otherInks); // const uniqueColors: string[] = []; // Array.from(wordToColor.values()).forEach(c => uniqueColors.indexOf(c) === -1 && uniqueColors.push(c)); // inks[i].alternativeColors = new List(uniqueColors); // if (wordToColor.has(word.recognizedText.toLowerCase())) { // inks[i].color = wordToColor.get(word.recognizedText.toLowerCase()); // } // else if (word.alternates) { // for (const alt of word.alternates) { // if (wordToColor.has(alt.recognizedString.toLowerCase())) { // inks[i].color = wordToColor.get(alt.recognizedString.toLowerCase()); // break; // } // } // } // }); // } const lines = results.filter((r: any) => r.category === 'line'); const text = lines.map((l: any) => l.recognizedText).join('\r\n'); this._props.addDocument?.(Docs.Create.TextDocument(text, { _width: this.Bounds.width, _height: this.Bounds.height, x: this.Bounds.left + this.Bounds.width, y: this.Bounds.top, title: text })); }); } }); @undoBatch summary = action((e: KeyboardEvent | React.PointerEvent | undefined) => { const selected = this.marqueeSelect(false).map(d => { this._props.removeDocument?.(d); d.x = NumCast(d.x) - this.Bounds.left; d.y = NumCast(d.y) - this.Bounds.top; return d; }); const summary = Docs.Create.TextDocument('', { backgroundColor: '#e2ad32', x: this.Bounds.left, y: this.Bounds.top, followLinkToggle: true, _width: 200, _height: 200, _layout_showSidebar: true, title: 'overview', }); const portal = Docs.Create.FreeformDocument(selected, { title: 'summarized documents', x: this.Bounds.left + 200, y: this.Bounds.top, isGroup: true, backgroundColor: 'transparent' }); DocUtils.MakeLink(summary, portal, { link_relationship: 'summary of:summarized by' }); portal.hidden = true; this._props.addDocument?.(portal); this._props.addLiveTextDocument(summary); MarqueeOptionsMenu.Instance.fadeOut(true); }); @action marqueeCommand = (e: KeyboardEvent) => { if (this._commandExecuted || (e as any).propagationIsStopped) { return; } if (e.key === 'Backspace' || e.key === 'Delete' || e.key === 'd' || e.key === 'h') { this._commandExecuted = true; e.stopPropagation(); (e as any).propagationIsStopped = true; this.delete(e, e.key === 'h'); e.stopPropagation(); } if ('ctsSpg'.indexOf(e.key) !== -1) { this._commandExecuted = true; e.stopPropagation(); e.preventDefault(); (e as any).propagationIsStopped = true; if (e.key === 'g') this.collection(e, true); if (e.key === 'c' || e.key === 't') this.collection(e); if (e.key === 's' || e.key === 'S') this.summary(e); if (e.key === 'p') this.pileup(e); this.cleanupInteractions(false); } if (e.key === 'r' || e.key === ' ') { this._commandExecuted = true; e.stopPropagation(); e.preventDefault(); this._lassoFreehand = !this._lassoFreehand; } }; touchesLine(r1: { left: number; top: number; width: number; height: number }) { for (const lassoPt of this._lassoPts) { const topLeft = this.Transform.transformPoint(lassoPt[0], lassoPt[1]); if (r1.left < topLeft[0] && topLeft[0] < r1.left + r1.width && r1.top < topLeft[1] && topLeft[1] < r1.top + r1.height) { return true; } } return false; } boundingShape(r1: { left: number; top: number; width: number; height: number }) { const xs = this._lassoPts.map(pair => pair[0]); const ys = this._lassoPts.map(pair => pair[1]); const tl = this.Transform.transformPoint(Math.min(...xs), Math.min(...ys)); const br = this.Transform.transformPoint(Math.max(...xs), Math.max(...ys)); if (r1.left > tl[0] && r1.top > tl[1] && r1.left + r1.width < br[0] && r1.top + r1.height < br[1]) { let hasTop = false; let hasLeft = false; let hasBottom = false; let hasRight = false; for (const lassoPt of this._lassoPts) { const truePoint = this.Transform.transformPoint(lassoPt[0], lassoPt[1]); hasLeft = hasLeft || (truePoint[0] > tl[0] && truePoint[0] < r1.left && truePoint[1] > r1.top && truePoint[1] < r1.top + r1.height); hasTop = hasTop || (truePoint[1] > tl[1] && truePoint[1] < r1.top && truePoint[0] > r1.left && truePoint[0] < r1.left + r1.width); hasRight = hasRight || (truePoint[0] < br[0] && truePoint[0] > r1.left + r1.width && truePoint[1] > r1.top && truePoint[1] < r1.top + r1.height); hasBottom = hasBottom || (truePoint[1] < br[1] && truePoint[1] > r1.top + r1.height && truePoint[0] > r1.left && truePoint[0] < r1.left + r1.width); if (hasTop && hasLeft && hasBottom && hasRight) { return true; } } } return false; } marqueeSelect(selectBackgrounds: boolean = false) { const selection: Doc[] = []; const selectFunc = (doc: Doc) => { const layoutDoc = Doc.Layout(doc); const bounds = { left: NumCast(doc.x), top: NumCast(doc.y), width: NumCast(layoutDoc._width), height: NumCast(layoutDoc._height) }; if (!this._lassoFreehand) { intersectRect(bounds, this.Bounds) && selection.push(doc); } else { (this.touchesLine(bounds) || this.boundingShape(bounds)) && selection.push(doc); } }; this._props .activeDocuments() .filter(doc => !doc.z && !doc._lockedPosition) .map(selectFunc); if (!selection.length && selectBackgrounds) this._props .activeDocuments() .filter(doc => doc.z === undefined) .map(selectFunc); if (!selection.length) this._props .activeDocuments() .filter(doc => doc.z !== undefined) .map(selectFunc); return selection; } @computed get marqueeDiv() { const cpt = this._lassoFreehand || !this._visible ? [0, 0] : [this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY]; const p = this._props.getContainerTransform().transformPoint(cpt[0], cpt[1]); const v = this._lassoFreehand ? [0, 0] : this._props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); return (
{' '} {this._lassoFreehand ? ( s + pt[0] + ',' + pt[1] + ' ', '')} fill="none" stroke={lightOrDark(this._props.Document?.backgroundColor ?? 'white')} strokeWidth="1" strokeDasharray="3" /> ) : ( )}
); } MarqueeRef: HTMLDivElement | null = null; /** * This is called for every drag movement when a document is dragged over this collection. * If the document is dragged within 25 pixels of the edge of the collection and paused, this will * auto scroll the collection so that it can be dragged farther (unless auto panning has been disabled) */ @action onDragMovePause = (e: CustomEvent) => { if ((e as any).handlePan || this._props.isAnnotationOverlay) return; (e as any).handlePan = true; const bounds = this.MarqueeRef?.getBoundingClientRect(); if (!this._props.Document._freeform_noAutoPan && !this._props.renderDepth && bounds) { const dragX = e.detail.clientX; const dragY = e.detail.clientY; const deltaX = dragX - bounds.left < 25 ? -(25 + (bounds.left - dragX)) : bounds.right - dragX < 25 ? 25 - (bounds.right - dragX) : 0; const deltaY = dragY - bounds.top < 25 ? -(25 + (bounds.top - dragY)) : bounds.bottom - dragY < 25 ? 25 - (bounds.bottom - dragY) : 0; if (deltaX !== 0 || deltaY !== 0) { this._props.Document[this._props.panYFieldKey] = NumCast(this._props.Document[this._props.panYFieldKey]) + deltaY / 2; this._props.Document[this._props.panXFieldKey] = NumCast(this._props.Document[this._props.panXFieldKey]) + deltaX / 2; } } e.stopPropagation(); }; render() { return (
{ r?.addEventListener('dashDragMovePause', this.onDragMovePause as any); this.MarqueeRef = r; }} style={{ overflow: StrCast(this._props.Document._overflow), cursor: [InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool) || this._visible ? 'crosshair' : 'pointer', }} onDragOver={e => e.preventDefault()} onScroll={e => (e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0)} onClick={this.onClick} onPointerDown={this.onPointerDown}> {this._visible ? this.marqueeDiv : null} {this.props.children}
); } }