UNPKG

devexpress-richedit

Version:

DevExpress Rich Text Editor is an advanced word-processing tool designed for working with rich text documents.

457 lines (456 loc) 20.3 kB
import { getHTMLElementsFromHtml } from '../../rich-utils/html-utils'; import { DocumentExporterOptions } from '../../formats/options'; import { ClientModelManager } from '../../model-manager'; import { RangeCopy } from '../../model/manipulators/range/create-range-copy-operation'; import { ControlOptions } from '../../model/options/control'; import { SubDocumentInterval, SubDocumentPosition } from '../../model/sub-document'; import { Browser } from '@devexpress/utils/lib/browser'; import { EmptyBatchUpdatableObject } from '@devexpress/utils/lib/class/batch-updatable'; import { Errors } from '@devexpress/utils/lib/errors'; import { DomUtils } from '@devexpress/utils/lib/utils/dom'; import { ListUtils } from '@devexpress/utils/lib/utils/list'; import { HtmlImporter } from '../../formats/html/import/html-importer'; import { TxtExporter } from '../../formats/txt/txt-exporter'; import { TxtImporter } from '../../formats/txt/txt-importer'; import { SelectionHistoryItem } from '../../model/history/selection/selection-history-item'; import { RichEditClientCommand } from '../client-command'; import { CommandBase, CommandSimpleOptions } from '../command-base'; import { SimpleCommandState } from '../command-states'; import { HtmlMimeType, PlainTextMimeType } from '@devexpress/utils/lib/utils/mime-type'; export class ClipboardCommand extends CommandBase { static additionalWaitingTimeForMac = 10; queryCommandId; static builtInClipboard; static timeout = Browser.Firefox ? 10 : (Browser.MacOSPlatform && (Browser.WebKitFamily || Browser.Opera) ? 10 : 0); clipboardHelper; constructor(control, queryCommandId) { super(control); this.queryCommandId = queryCommandId; if (this.isTouchMode()) ClipboardCommand.builtInClipboard = new BuiltInClipboard(this.control); this.clipboardHelper = new ClipboardHelper(this.control, this.isTouchMode()); } getState() { var state = new SimpleCommandState(this.isEnabled()); state.visible = this.isVisible(); return state; } isTouchMode() { return this.control.isTouchMode(); } isCommandSupported() { const editableDocument = this.control.inputController.getEditableDocument(); return !!editableDocument.queryCommandSupported && editableDocument.queryCommandSupported(this.queryCommandId); } execute(isPublicApiCall, parameter = this.control.shortcutManager.lastCommandExecutedByShortcut) { const isPublicApiCallPrevValue = this.control.commandManager.isPublicApiCall; this.control.commandManager.isPublicApiCall = isPublicApiCall; if (!this.isTouchMode() && !parameter && !this.isCommandSupported()) { this.control.commandManager.isPublicApiCall = isPublicApiCallPrevValue; if (this.clipboardHelper.canReadFromClipboard()) { this.clipboardHelper.readFromClipboard().catch((reason) => { console.warn(reason); this.executeShowErrorMessageCommand(); }); return true; } else return this.executeShowErrorMessageCommand(); } else { const options = this.convertToCommandOptions(parameter); this.beforeExecute(); this.executeCore(this.getState(), options); this.control.commandManager.isPublicApiCall = isPublicApiCallPrevValue; return true; } } executeCore(state, options) { if (!state.enabled || this.control.commandManager.clipboardTimerId != null) return false; if (!this.isTouchMode()) { if (!options.param) this.control.inputController.getEditableDocument().execCommand(this.queryCommandId, false, null); this.control.commandManager.clipboardTimerId = setTimeout(() => { this.executeFinalAction(); }, this.getTimeout()); } else this.executeBuiltInClipboardAction(this.getBuiltInClipboardActionType()); return true; } getTimeout() { return ClipboardCommand.timeout; } executeFinalAction() { if (this.control.isClosed() || this.control.commandManager.clipboardTimerId === null) return; this.control.beginUpdate(); this.changeModel(); this.clipboardHelper.clearAfterImport(); this.control.endUpdate(); this.control.commandManager.clipboardTimerId = null; } executeShowErrorMessageCommand() { return this.control.commandManager.getCommand(RichEditClientCommand.ShowErrorClipboardAccessDeniedMessageCommand).execute(this.control.commandManager.isPublicApiCall); } executeBuiltInClipboardAction(action) { this.control.beginUpdate(); switch (action) { case BuiltInClipboardAction.Copy: ClipboardCommand.builtInClipboard.copy(); this.tryWriteToClipboard(); break; case BuiltInClipboardAction.Cut: ClipboardCommand.builtInClipboard.cut(); this.tryWriteToClipboard(); break; case BuiltInClipboardAction.Paste: if (this.clipboardHelper.canReadFromClipboard()) { this.clipboardHelper.readFromClipboard().catch(reason => { ClipboardCommand.builtInClipboard.paste(); console.warn(reason); }); } else ClipboardCommand.builtInClipboard.paste(); break; } this.control.endUpdate(); } async tryWriteToClipboard() { try { await this.clipboardHelper.tryWriteToClipboard(ClipboardCommand.builtInClipboard.clipboardData); } catch (error) { console.log(error); } } isVisible() { return true; } getBuiltInClipboardActionType() { throw new Error(Errors.NotImplemented); } changeModel() { } beforeExecute() { if (!Browser.TouchUI) this.control.focusManager.captureFocus(); } } export class CopySelectionCommand extends ClipboardCommand { constructor(control) { super(control, "copy"); } copyEventRaised() { if (Browser.MacOSPlatform && this.control.commandManager.clipboardTimerId !== null) { setTimeout(() => { clearTimeout(this.control.commandManager.clipboardTimerId); this.executeFinalAction(); }, ClipboardCommand.additionalWaitingTimeForMac); } } getTimeout() { return Browser.MacOSPlatform ? 300 : super.getTimeout(); } isEnabled() { return super.isEnabled() && ControlOptions.isEnabled(this.control.modelManager.richOptions.control.copy) && !this.selection.isCollapsed(); } isVisible() { return ControlOptions.isVisible(this.control.modelManager.richOptions.control.copy); } getBuiltInClipboardActionType() { return BuiltInClipboardAction.Copy; } beforeExecute() { if (ControlOptions.isEnabled(this.control.modelManager.richOptions.control.copy)) { super.beforeExecute(); if (!this.isTouchMode()) this.control.inputController.renderSelectionToEditableDocument(); } } isEnabledInReadOnlyMode() { return true; } } export class CutSelectionCommand extends ClipboardCommand { constructor(control) { super(control, "cut"); } changeModel() { this.history.addTransaction(() => { const intervals = ListUtils.deepCopy(this.selection.intervalsInfo.intervals); ListUtils.reverseForEach(intervals, (interval) => this.modelManipulator.range.removeInterval(new SubDocumentInterval(this.selection.activeSubDocument, interval), true, true)); this.history.addAndRedo(new SelectionHistoryItem(this.modelManipulator, this.selection, this.selection.getState(), this.selection.getState().setPosition(intervals[0].start))); }); } isEnabled() { return super.isEnabled() && ControlOptions.isEnabled(this.control.modelManager.richOptions.control.cut) && !this.selection.isCollapsed(); } isVisible() { return ControlOptions.isVisible(this.control.modelManager.richOptions.control.cut); } getBuiltInClipboardActionType() { return BuiltInClipboardAction.Cut; } beforeExecute() { if (ControlOptions.isEnabled(this.control.modelManager.richOptions.control.cut)) { super.beforeExecute(); if (!this.isTouchMode()) this.control.inputController.renderSelectionToEditableDocument(); } } } export class PasteSelectionCommand extends ClipboardCommand { constructor(control) { super(control, "paste"); } getTimeout() { return Browser.MacOSPlatform ? 300 : super.getTimeout(); } pasteEventRaised() { if (Browser.MacOSPlatform && this.control.commandManager.clipboardTimerId !== null) { setTimeout(() => { clearTimeout(this.control.commandManager.clipboardTimerId); this.executeFinalAction(); }, ClipboardCommand.additionalWaitingTimeForMac); } } changeModel() { const elements = getHTMLElementsFromHtml(this.control.inputController, this.control.inputController.getEditableDocumentContent()); if (!this.control.isLoadingPictureFromClipboard) this.control.importHtml(elements); } isEnabled() { return super.isEnabled() && ControlOptions.isEnabled(this.control.modelManager.richOptions.control.paste); } isVisible() { return ControlOptions.isVisible(this.control.modelManager.richOptions.control.paste) || this.isTouchMode(); } getBuiltInClipboardActionType() { return BuiltInClipboardAction.Paste; } beforeExecute() { if (ControlOptions.isEnabled(this.control.modelManager.richOptions.control.paste)) { super.beforeExecute(); if (!this.isTouchMode()) { var selection = Browser.TouchUI ? window.getSelection() : this.control.inputController.getEditableDocument().getSelection(); selection.removeAllRanges(); var editableElement = this.control.inputController.getEditableDocument(); selection.selectAllChildren(editableElement.body || editableElement); } } } isCommandSupported() { return Browser.IE; } } export class BuiltInClipboard { _clipboardData; control; constructor(control) { this.control = control; } get clipboardData() { return this._clipboardData; } copy() { this._clipboardData = RangeCopy.create(this.control.selection.subDocumentIntervals); } paste() { if (this._clipboardData) { this.control.modelManager.modelManipulator.range.removeInterval(this.control.selection.subDocumentInterval, true, false); this._clipboardData.insertTo(this.control.modelManager.modelManipulator, this.control.selection.intervalsInfo.subDocPosition); } } cut() { this.control.modelManager.history.addTransaction(() => { this.copy(); ListUtils.reverseForEach(this.control.selection.intervalsInfo.intervals, (interval) => this.control.modelManager.modelManipulator.range.removeInterval(new SubDocumentInterval(this.control.selection.activeSubDocument, interval), true, true)); this.control.modelManager.history.addAndRedo(new SelectionHistoryItem(this.control.modelManager.modelManipulator, this.control.selection, this.control.selection.getState(), this.control.selection.getState() .setPosition(this.control.selection.intervalsInfo.intervals[0].start))); }); } } export class ClipboardHelper { control; useWithBuildInClipboard; static browserDoesNotSupportReadingFromClipboard = 'The browser does not support reading from the clipboard.'; static noDataInClipboardMessage = 'There is no any supported data in the clipboard.'; static clipboardItemCannotBeInsertedMessage = 'The clipboard item cannot be inserted.'; static lastWrittenTextHash = -1; constructor(control, useWithBuildInClipboard = false) { this.control = control; this.useWithBuildInClipboard = useWithBuildInClipboard; } get clipboard() { return navigator.clipboard; } canReadFromClipboard() { return !!(this.clipboard && (this.clipboard.read || this.clipboard.readText)); } readFromClipboard() { if (this.clipboard.read) return this.clipboard.read().then(items => this.insertClipboardItems(items)); else if (this.clipboard.readText) return this.clipboard.readText().then(text => this.insertPlainText(text)); return Promise.reject(ClipboardHelper.browserDoesNotSupportReadingFromClipboard); } clearAfterImport() { const editableDocument = this.control.inputController.getEditableDocument(); if (Browser.TouchUI) { getSelection().removeAllRanges(); DomUtils.clearInnerHtml(Browser.MSTouchUI ? editableDocument.body : editableDocument); } else { const selection = editableDocument.getSelection ? editableDocument.getSelection() : editableDocument.selection; if (selection.removeAllRanges) selection.removeAllRanges(); else if (selection.empty) selection.empty(); DomUtils.clearInnerHtml(editableDocument.body); } if (!Browser.TouchUI || Browser.IE && Browser.MSTouchUI) this.control.inputController.selectEditableDocumentContent(); else getSelection().selectAllChildren(editableDocument); } insertClipboardItems(items) { for (let item of items) { if (ListUtils.anyOf(item.types, type => type === HtmlMimeType)) return this.insertClipboardItem(item, HtmlMimeType, html => this.insertHtml(html)); if (ListUtils.anyOf(item.types, type => type === PlainTextMimeType)) return this.insertClipboardItem(item, PlainTextMimeType, text => this.insertPlainText(text)); } return Promise.reject(ClipboardHelper.noDataInClipboardMessage); } async insertClipboardItem(item, type, insert) { const blob = await item.getType(type); const text = await this.readAsText(blob); return await insert(text); } insertPlainText(text) { return new Promise((resolve, reject) => { if (ClipboardHelper.lastWrittenTextHash === this.calculateHash(text)) reject(); ClipboardHelper.lastWrittenTextHash = -1; const command = new InsertPlainTextCommand(this.control); if (command.execute(false, new CommandSimpleOptions(this.control, text))) resolve(); else reject(ClipboardHelper.clipboardItemCannotBeInsertedMessage); }); } insertHtml(html) { return new Promise((resolve, reject) => { try { this.control.beginUpdate(); this.control.importHtml(getHTMLElementsFromHtml(this.control.inputController, html)); this.clearAfterImport(); this.control.endUpdate(); resolve(); } catch { reject(ClipboardHelper.clipboardItemCannotBeInsertedMessage); } }); } async tryWriteToClipboard(clipboardData) { if (this.canWriteToClipboard()) await this.writeToClipboard(clipboardData); } canWriteToClipboard() { return !!this.clipboard?.write; } writeToClipboard(clipboardData) { ClipboardHelper.lastWrittenTextHash = -1; return new Promise((resolve, reject) => { const modelManager = this.createModelManager(clipboardData.model); const exporter = new TxtExporter(modelManager.modelManipulator, new DocumentExporterOptions()); exporter.exportToBlob(async (blob) => { let error = null; try { const item = this.createClipboardItem(blob); await this.clipboard.write([item]); } catch (err) { error = err; } if (this.useWithBuildInClipboard) { const text = await this.readAsText(blob); ClipboardHelper.lastWrittenTextHash = this.calculateHash(text); } if (error) reject(error); else resolve(); }); }); } createClipboardItem(blob) { return new ClipboardItem({ 'text/plain': blob }); } calculateHash(text) { let hash = 0; if (text.length === 0) return hash; for (let i = 0; i < text.length; i++) { hash = ((hash << 5) - hash) + text.charCodeAt(i); hash |= 0; } return hash; } readAsText(blob) { return blob.text(); } createModelManager(model) { return new ClientModelManager(model, this.control.modelManager.richOptions, new EmptyBatchUpdatableObject()); } } export class InsertHtmlCommand extends CommandBase { getState() { return new SimpleCommandState(this.isEnabled()); } executeCore(_state, options) { const elements = getHTMLElementsFromHtml(this.control.inputController, options.param); this.history.addTransaction(() => { const charPropsBundle = this.inputPosition.charPropsBundle; this.modelManipulator.range.removeInterval(this.selection.subDocumentInterval, true, false); new HtmlImporter(this.control.modelManager, this.control.measurer, this.selection.intervalsInfo.subDocPosition, elements, charPropsBundle).import(); }); this.control.inputController.setEditableDocumentContent(""); return true; } } class InsertPlainTextCommand extends CommandBase { getState() { return new SimpleCommandState(this.isEnabled()); } executeCore(_state, options) { const text = options.param; let result = false; const subDocument = this.selection.activeSubDocument; const position = this.selection.anchorPosition; const subDocumentPosition = new SubDocumentPosition(subDocument, position); const charPropsBundle = this.control.inputPosition.charPropsBundle; const parPropsBundle = this.control.inputPosition.parPropsBundle; this.history.addTransaction(() => { this.modelManipulator.range.removeInterval(this.selection.subDocumentInterval, true, false); new TxtImporter().importFromString(text, subDocument.documentModel.modelOptions, (model, _formatImagesImporter) => { new RangeCopy(model, false).insertTo(this.control.modelManager.modelManipulator, subDocumentPosition); result = true; }, (_reason) => { result = false; }, charPropsBundle, parPropsBundle); }); return result; } } export var BuiltInClipboardAction; (function (BuiltInClipboardAction) { BuiltInClipboardAction[BuiltInClipboardAction["Copy"] = 0] = "Copy"; BuiltInClipboardAction[BuiltInClipboardAction["Paste"] = 1] = "Paste"; BuiltInClipboardAction[BuiltInClipboardAction["Cut"] = 2] = "Cut"; })(BuiltInClipboardAction || (BuiltInClipboardAction = {}));