UNPKG

devexpress-richedit

Version:

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

280 lines (279 loc) 15.6 kB
import { ListLevelDisplayFormatStringHistoryItem } from '../model/history/items/list-level-properties-history-items'; import { InsertTextManipulatorParams } from '../model/manipulators/text-manipulator/insert-text-manipulator-params'; import { NumberingType } from '../model/numbering-lists/numbering-list'; import { RichUtils } from '../model/rich-utils'; import { RunType } from '../model/runs/run-type'; import { SubDocumentInterval, SubDocumentPosition } from '../model/sub-document'; import { InputPositionBase } from '../selection/input-position-base'; import { SelectionIntervalsInfo } from '../selection/selection-intervals-info'; import { ModelWordPositionHelper } from '../spelling/helpers'; import { FixedInterval } from '@devexpress/utils/lib/intervals/fixed'; import { KeyCode } from '@devexpress/utils/lib/utils/key'; import { ListUtils } from '@devexpress/utils/lib/utils/list'; import { SearchUtils } from '@devexpress/utils/lib/utils/search'; import { InsertParagraphManipulatorParams } from '../model/manipulators/paragraph-manipulator/insert-paragraph-manipulator-params'; import { RichEditClientCommand } from '../commands/client-command'; import { CommandOptions, CommandBase } from '../commands/command-base'; import { IntervalCommandState } from '../commands/command-states'; import { SelectionHistoryItem } from '../model/history/selection/selection-history-item'; import { splitByLines } from '../utils/utils'; export class AutoCorrectProviderBase { get subDocument() { return this.control.selection.activeSubDocument; } constructor(control) { this.control = control; } revise() { const pos = this.control.selection.lastSelectedInterval.start; const lastInsertedChar = this.control.selection.activeSubDocument.getText(new FixedInterval(pos - 1, 1)); if (this.isTriggerChar(lastInsertedChar)) { const wordStartPos = ModelWordPositionHelper.getWordStartPositionByCharCondition(this.control.selection.activeSubDocument, pos - 1, (char) => { return this.isSeparator(char); }); const wordEndPos = (this.includeTriggerChar(lastInsertedChar) ? pos : pos - 1) - wordStartPos; const interval = new FixedInterval(wordStartPos, wordEndPos); const text = this.control.selection.activeSubDocument.getText(interval); let selectionStateInfo = null; if (!this.includeTriggerChar(lastInsertedChar)) selectionStateInfo = this.control.selection.getFloatingState(); const autoCorrectResult = this.reviseCore(text, interval); if (selectionStateInfo) if (autoCorrectResult) this.control.selection.restoreFloatingState(selectionStateInfo); else selectionStateInfo.finalize(); return autoCorrectResult; } return false; } isSeparator(char) { return !RichUtils.isAlphanumeric.test(char); } isTriggerChar(char) { return this.isSeparator(char); } includeTriggerChar(_char) { return false; } } export class EventAutoCorrectProvider extends AutoCorrectProviderBase { reviseCore(text, interval) { this.control.modelManager.history.beginTransaction(); const handled = this.control.clientSideEvents.raiseAutoCorrect(text, interval); this.control.modelManager.history.endTransaction(); return handled; } isTriggerChar(_char) { return true; } includeTriggerChar(_char) { return true; } } export class TwoInitialCapitalsAutoCorrectProvider extends AutoCorrectProviderBase { reviseCore(text, interval) { if (text.length > 2 && text[0] === text[0].toUpperCase() && text[1] === text[1].toUpperCase()) { for (let i = 2; i < text.length; i++) if (text[i] !== text[i].toLowerCase() || text[i] == text[i].toUpperCase()) return false; const replaceWith = text.substring(0, 1) + text[1].toLowerCase() + text.slice(2); this.control.modelManager.history.beginTransaction(); this.control.modelManager.modelManipulator.range.removeInterval(new SubDocumentInterval(this.control.selection.activeSubDocument, interval), false, false); this.control.modelManager.modelManipulator.text.insertTextViaHistory(new InsertTextManipulatorParams(new SubDocumentPosition(this.control.selection.activeSubDocument, interval.start), new InputPositionBase().setIntervals(SelectionIntervalsInfo.fromPosition(this.control.selection.activeSubDocument, interval.start)) .charPropsBundle, RunType.TextRun, replaceWith)); this.control.modelManager.history.endTransaction(); return true; } return false; } } export class BulletedListAutoCorrectProvider extends AutoCorrectProviderBase { constructor() { super(...arguments); this.numberingRegEx = /^[0-9]+[\.\)]$/; } reviseCore(text, interval) { const subDocument = this.control.selection.activeSubDocument; const position = interval.end; const paragraphIndex = subDocument.getParagraphIndexByPosition(position); const paragraph = subDocument.paragraphs[paragraphIndex]; if (paragraph.isInList()) return false; const parStartPosition = paragraph.startLogPosition.value; if (parStartPosition !== interval.start) return false; const targetNumberingListType = this.getTargetNumberingListType(text); if (targetNumberingListType === undefined) return false; this.control.modelManager.history.beginTransaction(); this.control.modelManager.modelManipulator.range.removeInterval(new SubDocumentInterval(subDocument, FixedInterval.fromPositions(parStartPosition, position + 1)), false, false); this.control.selection.changeState(newState => newState.setPosition(parStartPosition)); if (targetNumberingListType === NumberingType.Bullet) this.control.commandManager.getCommand(RichEditClientCommand.ToggleBulletedListItem).execute(this.control.commandManager.isPublicApiCall); else { const targetIndex = parseInt(text.slice(0, -1)); this.control.commandManager.getCommand(RichEditClientCommand.ToggleNumberingListItem).execute(this.control.commandManager.isPublicApiCall, targetIndex); const numberingListIndex = this.control.modelManager.model.numberingLists.length - 1; const separator = text[text.length - 1]; for (var i = 0, length = this.control.modelManager.model.numberingLists[numberingListIndex].levels.length; i < length; i++) this.control.modelManager.history.addAndRedo(new ListLevelDisplayFormatStringHistoryItem(this.control.modelManager.modelManipulator, false, numberingListIndex, i, `{${i}}${separator}`)); } this.control.modelManager.history.endTransaction(); return true; } isSeparator(char) { return RichUtils.isWhitespace.test(char); } isTriggerChar(char) { return char.charCodeAt(0) != KeyCode.Enter && RichUtils.isWhitespace.test(char); } getTargetNumberingListType(paragraphText) { if (paragraphText.length === 1 && paragraphText.indexOf("*") === 0) return NumberingType.Bullet; if (this.numberingRegEx.test(paragraphText)) return NumberingType.Simple; return undefined; } } export class UrlAutoCorrectProvider extends AutoCorrectProviderBase { constructor() { super(...arguments); this.urlRegex = /^(?:[a-z][\w-]+:(?:\/{1,3}([^./]*:[^./]*@){0,1})|www\d{0,3}[.]|ftp[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?=((?:[^\s()<>]+|\([^\s<>]*\))+(?:\([^\s<>]*\)|[^\s`!()\[\]{};:'"".,<>?«»“”‘’])))\2$/i; this.emailRegex = /^(mailto:)?[-\w!#$%&'*+/=?^_`{|}~]+(?:\.[-\w!#$%&'*+/=?^_`{|}~]+)*@(?:\w+([-\w]*\w)\.)[\w]+$/; this.localRegex = /^(([a-zA-Z][:][\\])|(\\\\))(((?![<>:"\\|?*]).)+((?![ .])\\)?)*$/; } reviseCore(text, interval) { if (this.urlRegex.test(text) || this.emailRegex.test(text) || this.localRegex.test(text)) { this.control.modelManager.history.beginTransaction(); this.control.modelManager.history.addAndRedo(new SelectionHistoryItem(this.control.modelManager.modelManipulator, this.control.selection, this.control.selection.getState(), this.control.selection.getState().setIntervals([interval]))); const command = this.control.commandManager .getCommand(RichEditClientCommand.ShowHyperlinkForm); const options = new CommandOptions(this.control).setIntervalsInfo(SelectionIntervalsInfo.fromInterval(this.subDocument, interval)); const state = command.getState(options); const parameters = command.createParameters(options); const initParameters = parameters.clone(); parameters.text = text; parameters.url = this.createNavigateUri(text); const result = command.applyParameters(new IntervalCommandState(state.enabled, interval), parameters, initParameters); this.control.modelManager.history.endTransaction(); return result; } return false; } createNavigateUri(url) { if (url.indexOf(":") == -1 && url.indexOf("@") > 0) return "mailto:" + url; if (url.indexOf("www") == 0) return "http://" + url; if (url.indexOf("\\") == 0) return "file://" + url.replace(/\\/g, "\\\\"); return url; } isSeparator(char) { return RichUtils.isWhitespace.test(char); } } export class TableBasedAutoCorrectProviderBase extends AutoCorrectProviderBase { constructor(control, settings) { super(control); this.replaceInfoCollection = []; this.caseSensitiveReplacement = settings.caseSensitiveReplacement; settings.replaceInfoCollection.sort((a, b) => this.compare(a.replace, b.replace)); this.initReplaceInfo(settings.replaceInfoCollection); } compare(origin, target) { if (!this.caseSensitiveReplacement) { origin = origin.toLocaleLowerCase(); target = target.toLocaleLowerCase(); } return origin === target ? 0 : (origin < target ? -1 : 1); } performReplace(interval, text) { const replaceWithStrings = splitByLines(text); const charPropsBundle = this.control.inputPosition.charPropsBundle; this.control.modelManager.modelManipulator.range.removeInterval(new SubDocumentInterval(this.subDocument, interval), true, false); let endOfInsertedInterval = interval.start; for (let i = 0; i < replaceWithStrings.length; i++) { if (replaceWithStrings[i]) { const subDocumentPosition = new SubDocumentPosition(this.subDocument, endOfInsertedInterval); const insertTextManipulatorParams = new InsertTextManipulatorParams(subDocumentPosition, charPropsBundle, RunType.TextRun, replaceWithStrings[i]); const result = this.control.modelManager.modelManipulator.text.insertTextViaHistory(insertTextManipulatorParams); endOfInsertedInterval = result.insertedInterval.end; } if (i < replaceWithStrings.length - 1) { const subDocumentPosition = new SubDocumentPosition(this.subDocument, endOfInsertedInterval); const insertTextManipulatorParams = new InsertParagraphManipulatorParams(subDocumentPosition, charPropsBundle); const result = this.control.modelManager.modelManipulator.paragraph.insertParagraphViaHistory(insertTextManipulatorParams); endOfInsertedInterval = result.end; } } return endOfInsertedInterval; } } export class TableBasedSimpleAutoCorrectProvider extends TableBasedAutoCorrectProviderBase { initReplaceInfo(replaceInfoCollection) { ListUtils.forEach(replaceInfoCollection, val => { if (RichUtils.isAlphanumeric.test(val.replace)) this.replaceInfoCollection.push(val); }); } reviseCore(text, interval) { const replaceWordIndex = SearchUtils.binaryIndexOf(this.replaceInfoCollection, a => this.compare(a.replace, text)); if (replaceWordIndex >= 0) { this.control.modelManager.history.beginTransaction(); CommandBase.addSelectionBefore(this.control); const endOfInsertedInterval = this.performReplace(interval, this.replaceInfoCollection[replaceWordIndex].with); CommandBase.addSelectionAfter(this.control, endOfInsertedInterval + 1); this.control.modelManager.history.endTransaction(); return true; } return false; } } export class TableBasedCompositeAutoCorrectProvider extends TableBasedAutoCorrectProviderBase { initReplaceInfo(replaceInfoCollection) { ListUtils.forEach(replaceInfoCollection, val => { const lastSymbol = val.replace[val.replace.length - 1]; if (!RichUtils.isAlphanumeric.test(val.replace) && RichUtils.isAlphanumeric.test(lastSymbol)) this.replaceInfoCollection.push(val); }); } reviseCore(text, interval) { for (let val of this.replaceInfoCollection) { if (this.isEndOf(val.replace, text)) { const diff = val.replace.length - text.length; const start = interval.start - diff; if (start >= 0) { const intervalToCheck = new FixedInterval(start, interval.end - start); const textToCheck = this.control.selection.activeSubDocument.getText(intervalToCheck); if (this.isEndOf(val.replace, textToCheck)) { this.control.modelManager.history.beginTransaction(); const endOfInsertedInterval = this.performReplace(intervalToCheck, val.with); this.control.modelManager.history.addAndRedo(new SelectionHistoryItem(this.control.modelManager.modelManipulator, this.control.selection, this.control.selection.getState(), this.control.selection.getState().setPosition(endOfInsertedInterval))); this.control.modelManager.history.endTransaction(); return true; } } } } return false; } isEndOf(origin, target) { return this.compare(origin.substr(target.length * -1, target.length), target) == 0; } } export class TableBasedImmediateAutoCorrectProvider extends TableBasedCompositeAutoCorrectProvider { initReplaceInfo(replaceInfoCollection) { this.triggeredChar = []; ListUtils.forEach(replaceInfoCollection, val => { const lastSymbol = val.replace[val.replace.length - 1]; if (!RichUtils.isAlphanumeric.test(lastSymbol)) { this.triggeredChar.push(lastSymbol); this.replaceInfoCollection.push(val); } }); } isTriggerChar(char) { return ListUtils.unsafeAnyOf(this.triggeredChar, (currVal) => currVal == char); } includeTriggerChar(_char) { return true; } }