UNPKG

devexpress-richedit

Version:

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

458 lines (457 loc) 20.6 kB
import { __awaiter } from "tslib"; 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 { 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(); } tryWriteToClipboard() { return __awaiter(this, void 0, void 0, function* () { try { yield 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(); } } ClipboardCommand.additionalWaitingTimeForMac = 10; ClipboardCommand.timeout = Browser.Firefox ? 10 : (Browser.MacOSPlatform && (Browser.WebKitFamily || Browser.Opera) ? 10 : 0); 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 { 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 { 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); } insertClipboardItem(item, type, insert) { return __awaiter(this, void 0, void 0, function* () { const blob = yield item.getType(type); const text = yield this.readAsText(blob); return yield 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 (_a) { reject(ClipboardHelper.clipboardItemCannotBeInsertedMessage); } }); } tryWriteToClipboard(clipboardData) { return __awaiter(this, void 0, void 0, function* () { if (this.canWriteToClipboard()) yield this.writeToClipboard(clipboardData); }); } canWriteToClipboard() { var _a; return !!((_a = this.clipboard) === null || _a === void 0 ? void 0 : _a.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((blob) => __awaiter(this, void 0, void 0, function* () { let error = null; try { const item = this.createClipboardItem(blob); yield this.clipboard.write([item]); } catch (err) { error = err; } if (this.useWithBuildInClipboard) { const text = yield 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()); } } ClipboardHelper.browserDoesNotSupportReadingFromClipboard = 'The browser does not support reading from the clipboard.'; ClipboardHelper.noDataInClipboardMessage = 'There is no any supported data in the clipboard.'; ClipboardHelper.clipboardItemCannotBeInsertedMessage = 'The clipboard item cannot be inserted.'; ClipboardHelper.lastWrittenTextHash = -1; 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 = {}));