import { action, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { lightOrDark, returnFalse } from '../../ClientUtils'; import { Doc, Opt } from '../../fields/Doc'; import { Docs, DocumentOptions } from '../documents/Documents'; import { DocUtils } from '../documents/DocUtils'; import { ImageUtils } from '../util/Import & Export/ImageUtils'; import { Transform } from '../util/Transform'; import { UndoManager, undoBatch } from '../util/UndoManager'; import { ObservableReactComponent } from './ObservableReactComponent'; import './PreviewCursor.scss'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; @observer export class PreviewCursor extends ObservableReactComponent<{}> { // eslint-disable-next-line no-use-before-define static _instance: PreviewCursor; public static get Instance() { return PreviewCursor._instance; } _onKeyDown?: (e: KeyboardEvent) => void; _getTransform?: () => Transform; _addDocument?: (doc: Doc | Doc[]) => boolean; _addLiveTextDoc?: (doc: Doc) => void; _nudge?: undefined | ((x: number, y: number) => boolean); _slowLoadDocuments?: (files: File[] | string, options: DocumentOptions, generatedDocuments: Doc[], text: string, completed: ((doc: Doc[]) => void) | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => Promise; @observable _clickPoint: number[] = []; @observable public Visible = false; public Doc: Opt; constructor(props: any) { super(props); makeObservable(this); PreviewCursor._instance = this; this._clickPoint = observable([0, 0]); document.addEventListener('keydown', this.onKeyDown); document.addEventListener('paste', this.paste, true); } paste = async (e: ClipboardEvent) => { if (this.Visible && e.clipboardData) { const newPoint = this._getTransform?.().transformPoint(this._clickPoint[0], this._clickPoint[1]); runInAction(() => { this.Visible = false; }); // tests for URL and makes web document const re: any = /^https?:\/\//g; const plain = e.clipboardData.getData('text/plain'); if (plain && newPoint) { // tests for youtube and makes video document if (plain.indexOf('www.youtube.com/watch') !== -1) { const batch = UndoManager.StartBatch('youtube upload'); const generatedDocuments: Doc[] = []; const options = { title: plain, _width: 400, _height: 315, x: newPoint[0], y: newPoint[1], }; this._slowLoadDocuments?.(plain.split('v=')[1].split('&')[0], options, generatedDocuments, '', undefined, this._addDocument ?? returnFalse).then(batch.end); } else if (re.test(plain)) { const url = plain; if (!url.startsWith(window.location.href)) { undoBatch(() => this._addDocument?.( Docs.Create.WebDocument(url, { title: url, _width: 500, _height: 300, data_useCors: true, x: newPoint[0], y: newPoint[1], }) ) )(); } else alert('cannot paste dash into itself'); } else if (plain.startsWith('__DashDocId(') || plain.startsWith('__DashCloneId(')) { const clone = plain.startsWith('__DashCloneId('); const docids = plain.split(':'); const strs = docids[0].split(','); // hack! docids[0] is the top left of the selection rectangle const ptx = Number(strs[0].substring((clone ? '__DashCloneId(' : '__DashDocId(').length)); const pty = Number(strs[1].substring(0, strs[1].length - 1)); this._addDocument && Doc.Paste(docids.slice(1), clone, this._addDocument, ptx, pty, newPoint); e.stopPropagation(); } else { FormattedTextBox.PasteOnLoad = e; if (e.clipboardData.getData('dash/pdfAnchor')) e.preventDefault(); UndoManager.RunInBatch(() => this._addLiveTextDoc?.(DocUtils.GetNewTextDoc('', newPoint[0], newPoint[1], 500, undefined, undefined)), 'paste'); } } // pasting in images else if (e.clipboardData.getData('text/html') !== '' && e.clipboardData.getData('text/html').includes(' { const doc = Docs.Create.ImageDocument(arr[1], { _width: 300, title: arr[1], x: newPoint[0], y: newPoint[1], }); ImageUtils.ExtractImgInfo(doc); this._addDocument?.(doc); })(); } } else if (e.clipboardData.items.length && newPoint) { const batch = UndoManager.StartBatch('collection view drop'); const files: File[] = []; Array.from(e.clipboardData.items).forEach(item => { const file = item.getAsFile(); file && files.push(file); }); const generatedDocuments = await DocUtils.uploadFilesToDocs(files, { x: newPoint[0], y: newPoint[1] }); this._addDocument && generatedDocuments.forEach(this._addDocument); batch.end(); } } }; @action onKeyDown = (e: KeyboardEvent) => { // Mixing events between React and Native is finicky. // if not these keys, make a textbox if preview cursor is active! if ( e.key !== 'Escape' && e.key !== 'Backspace' && e.key !== 'Delete' && e.key !== 'CapsLock' && e.key !== 'Alt' && e.key !== 'Shift' && e.key !== 'Meta' && e.key !== 'Control' && e.key !== 'Insert' && e.key !== 'Home' && e.key !== 'End' && e.key !== 'PageUp' && e.key !== 'PageDown' && e.key !== 'NumLock' && e.key !== ' ' && (e.keyCode < 112 || e.keyCode > 123) && // F1 thru F12 keys (e.keyCode < 173 || e.keyCode > 183 || e.key === '-') && // mute, volume up/down etc, - is there specifically because its keycode is 173 in Firefox so shouldn't be avoided !e.key.startsWith('Arrow') && !e.defaultPrevented ) { if ((!e.metaKey && !e.ctrlKey) || (e.keyCode >= 48 && e.keyCode <= 57) || (e.keyCode >= 65 && e.keyCode <= 90)) { // /^[a-zA-Z0-9$*^%#@+-=_|}{[]"':;?/><.,}]$/.test(e.key)) { this.Visible && this._onKeyDown?.(e); ((!e.ctrlKey && !e.metaKey) || e.key !== 'v') && (this.Visible = false); } } else if (this.Visible) { if (e.key === 'ArrowRight') { this._nudge?.(1 * (e.shiftKey ? 2 : 1), 0) && e.stopPropagation(); } else if (e.key === 'ArrowLeft') { this._nudge?.(-1 * (e.shiftKey ? 2 : 1), 0) && e.stopPropagation(); } else if (e.key === 'ArrowUp') { this._nudge?.(0, 1 * (e.shiftKey ? 2 : 1)) && e.stopPropagation(); } else if (e.key === 'ArrowDown') { this._nudge?.(0, -1 * (e.shiftKey ? 2 : 1)) && e.stopPropagation(); } } }; // when focus is lost, this will remove the preview cursor @action onBlur = (): void => { this.Visible = false; }; @action public static Show( x: number, y: number, onKeyDown: (e: KeyboardEvent) => void, addLiveText: (doc: Doc) => void, getTransform: () => Transform, addDocument: undefined | ((doc: Doc | Doc[]) => boolean), nudge: undefined | ((nudgeX: number, nudgeY: number) => boolean), slowLoadDocuments: (files: File[] | string, options: DocumentOptions, generatedDocuments: Doc[], text: string, completed: ((doc: Doc[]) => void) | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => Promise ) { const self = PreviewCursor.Instance; if (self) { self._clickPoint = [x, y]; self._onKeyDown = onKeyDown; self._addLiveTextDoc = addLiveText; self._getTransform = getTransform; self._addDocument = addDocument || returnFalse; self._nudge = nudge; self._slowLoadDocuments = slowLoadDocuments; self.Visible = true; } } render() { return !this._clickPoint || !this.Visible ? null : ( // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
e?.focus()} style={{ color: lightOrDark(this.Doc?.backgroundColor ?? 'white'), transform: `translate(${this._clickPoint[0]}px, ${this._clickPoint[1]}px)` }}> I
); } }