UNPKG

@webspellchecker/wproofreader-ckeditor5

Version:

Multilingual spelling and grammar checking plugin for CKEditor 5

739 lines (727 loc) 23.6 kB
import { Command, Plugin } from '@ckeditor/ckeditor5-core/dist/index.js'; import { createDropdown, addListToDropdown, ViewModel } from '@ckeditor/ckeditor5-ui/dist/index.js'; import { Collection } from '@ckeditor/ckeditor5-utils/dist/index.js'; var wproofreaderIcon = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\"><polygon points=\"10.46 17.747 7.014 14.292 8.076 13.232 10.461 15.624 16.377 9.708 17.437 10.768 10.46 17.747\"/><path d=\"M10.147,12.276c.063.153.138.331.226.531a2.7,2.7,0,0,0,.144.286L12,11.614l-.01-.027L9.4,5.189c-.122-.321-.222-.576-.3-.768A2.871,2.871,0,0,0,8.8,3.883a1.355,1.355,0,0,0-.453-.409,1.4,1.4,0,0,0-.7-.159,1.387,1.387,0,0,0-.693.159,1.314,1.314,0,0,0-.453.416,3.4,3.4,0,0,0-.325.646L5.92,5.2,3.383,11.641c-.1.257-.178.457-.223.6a1.4,1.4,0,0,0-.067.419.852.852,0,0,0,.29.629.941.941,0,0,0,.67.277.756.756,0,0,0,.643-.26,4.432,4.432,0,0,0,.473-1l.473-1.272H9.673ZM6.17,9.524,7.638,5.432,9.132,9.524Z\"/></svg>\n"; /** * The {@code WProofreaderToggleCommand} to toggle the {@code WProofreader}. */ class WProofreaderToggleCommand extends Command { /** * Executes the {@code WProofreaderToggleCommand}. * @public * @inheritDoc */ execute() { const wproofreader = this.editor.plugins.get('WProofreader'); wproofreader.toggle(); } } /** * The {@code WProofreaderSettingsCommand} to open the {@code WProofreader} settings. */ class WProofreaderSettingsCommand extends Command { /** * Executes the {@code WProofreaderSettingsCommand}. * @public * @inheritDoc */ execute() { const wproofreader = this.editor.plugins.get('WProofreader'); wproofreader.openSettings(); } } /** * The {@code WProofreaderDialogCommand} to open the {@code WProofreader} dialog. */ class WProofreaderDialogCommand extends Command { /** * Executes the {@code WProofreaderDialogCommand}. * @public * @inheritDoc */ execute() { const wproofreader = this.editor.plugins.get('WProofreader'); wproofreader.openDialog(); } } /** * The {@code WProofreaderEditing} plugin. It introduces all {@code WProofreader} commands. */ class WProofreaderEditing extends Plugin { /** * @inheritDoc */ static get pluginName() { return 'WProofreaderEditing'; } /** * Initializes the {@code WProofreaderEditing} plugin. * @public */ init() { this._addCommands(); } /** * Initializes the {@code WProofreaderEditing} plugin in the third initialization stage. * @public */ afterInit() { this._enableInModes([ { modeName: 'TrackChanges', editingName: 'TrackChangesEditing' }, { modeName: 'RestrictedEditingMode', editingName: 'RestrictedEditingModeEditing' } ]); } /** * Adds the {@code WProofreader} commands to the editor. * @private */ _addCommands() { this.editor.commands.add('WProofreaderToggle', new WProofreaderToggleCommand(this.editor)); this.editor.commands.add('WProofreaderSettings', new WProofreaderSettingsCommand(this.editor)); this.editor.commands.add('WProofreaderDialog', new WProofreaderDialogCommand(this.editor)); } /** * Enables the {@code WProofreader} commands in a certain CKEditor 5 modes. * @private */ _enableInModes(modes) { modes.forEach((mode)=>{ this._enableInMode(mode.modeName, mode.editingName); }); } /** * Enables the {@code WProofreader} commands in a certain CKEditor 5 mode. * @private */ _enableInMode(modeName, editingName) { const isModeLoaded = this.editor.plugins.has(modeName); if (isModeLoaded) { const editing = this.editor.plugins.get(editingName); const commands = [ 'WProofreaderToggle', 'WProofreaderSettings', 'WProofreaderDialog' ]; commands.forEach((command)=>editing.enableCommand(command)); } } } /** * The {@code WProofreaderUI} plugin. It introduces the {@code WProofreader} toolbar button. */ class WProofreaderUI extends Plugin { /** * @inheritDoc */ static get pluginName() { return 'WProofreaderUI'; } /** * @inheritDoc */ constructor(editor){ super(editor); this._commands = { toggle: 'WProofreaderToggle', settings: 'WProofreaderSettings', proofreadDialog: 'WProofreaderDialog' }; } /** * Initializes the {@code WProofreaderUI} plugin. * @public */ init() { this._registerDropdown(); } /** * Registers the {@code WProofreader} dropdown among the UI components of the editor. * @private */ _registerDropdown() { const editor = this.editor; const wproofreader = editor.plugins.get('WProofreader'); editor.ui.componentFactory.add('wproofreader', (locale)=>{ const dropdownView = createDropdown(locale); let dropdownItemsDefinitions, actions; dropdownView.buttonView.set({ label: 'WProofreader text checker', icon: wproofreaderIcon, tooltip: true }); dropdownView.on('change:isOpen', ()=>{ const ready = wproofreader.isInstancesReady(); const enabled = wproofreader.isInstancesEnabled(); if (!ready) { dropdownView.class = 'ck-wproofreader-empty'; return; } if (!dropdownItemsDefinitions) { actions = wproofreader.getStaticActions(); dropdownItemsDefinitions = this._getDropdownItemsDefinitions(actions); addListToDropdown(dropdownView, dropdownItemsDefinitions); } dropdownView.class = ''; dropdownItemsDefinitions.map((item)=>{ item.model.class = enabled ? '' : 'ck-hidden'; if (item.model.commandParam === 'WProofreaderToggle') { item.model.label = enabled ? item.model.localization.disable : item.model.localization.enable; item.model.class = ''; } }); }); // Execute the command when the dropdown item is clicked (executed). dropdownView.on('execute', (evt)=>{ editor.execute(evt.source.commandParam); }); // The dropdown view should be disabled if the WProofreaderToggle command is disabled. dropdownView.bind('isEnabled').to(editor.commands.get('WProofreaderToggle')); return dropdownView; }); } /** * Creates dropdown items for the {@code WProofreader} actions. * @private */ _getDropdownItemsDefinitions(actions) { const itemDefinitions = new Collection(); actions.forEach((action)=>{ const definition = { type: 'button', model: new ViewModel({ commandParam: this._commands[action.name], label: action.localization.default, localization: action.localization, class: '', withText: true }) }; // Add the item definition to the collection. itemDefinitions.add(definition); }); return itemDefinitions; } } /** * The global storage of the script src. */ class SrcStorage { /** * Creates an instance of the {@code SrcStorage}. * @public */ constructor(){ this._create(); } /** * Creates the storage of the src of the script. * @private */ _create() { window.WPROOFREADER_SRCSTORAGE = window.WPROOFREADER_SRCSTORAGE || {}; this._storage = window.WPROOFREADER_SRCSTORAGE; } /** * Checks if the src of the script exists. * @public * * @param {String} src - a source of the script * @returns {Boolean} - {@code True} if src of the scripts exists {@code False} otherwise */ has(src) { return this._storage[src] ? true : false; } /** * Adds a src of the script to the storage. * @public * * @param {String} src - a source of the script */ add(src) { this._storage[src] = { onLoad: [], onError: [] }; } /** * Adds executable callbacks to the storage. * @public * * @param {String} src - a source of the script * @param {Function} resolve - a {@code resolve} function of the {@code Promise} * @param {Function} reject - a {@code reject} function of the {@code Promise} */ addCallbacks(src, resolve, reject) { this._storage[src].onLoad.push(resolve); this._storage[src].onError.push(reject); } /** * Executes a provided callback function once for each {@code onLoad} element. * @public * * @param {String} src - a source of the script * @param {Function} callback - a function to be executed for each {@code onLoad} element */ eachOnLoad(src, callback) { this._storage[src].onLoad.forEach(callback); } /** * Executes a provided callback function once for each {@code onError} element. * @public * * @param {String} src - a source of the script * @param {Function} callback - a function to be executed for each {@code onError} element */ eachOnError(src, callback) { this._storage[src].onError.forEach(callback); } /** * Deletes the {@code WPROOFREADER_SRCSTORAGE} field by the passed src. * @public * * @param {String} src - a source of the script */ delete(src) { delete this._storage[src]; } /** * Returns the {@code WPROOFREADER_SRCSTORAGE} field by the passed src. * @public * * @param {String} src - a source of the script */ get(src) { return this._storage[src]; } } /** * Loads script asynchronously. */ class ScriptLoader { /** * Creates an instance of the {@code ScriptLoader}. * @public * * @param {String} src - url of the script */ constructor(src){ this._validateSrc(src); this._src = src; this._globalSrcStorage = new SrcStorage(); } /** * Loads script by the passed url. * @public */ load() { return new Promise((resolve, reject)=>{ if (!this._isScriptOnPage()) { this._createScript(resolve, reject); } else { this._processExistingScript(resolve, reject); } }); } /** * Checks if the src of the script exists otherwise throws an {@code Error}. * @private */ _validateSrc(src) { if (!src) { throw new Error('Path to the script is not specified.'); } } /** * Checks if the script exists on the page. * @private */ _isScriptOnPage() { return document.querySelector('script[src="' + this._src + '"]') ? true : false; } /** * Creates the script and add listeners for it. * @private */ _createScript(resolve, reject) { this._script = this._createElement(); this._globalSrcStorage.add(this._src); this._globalSrcStorage.addCallbacks(this._src, resolve, reject); this._subscribeOnScriptLoad(); this._subscribeOnScriptError(); this._appendScript(this._script); } /** * Creates a script html element. * @private */ _createElement() { const script = document.createElement('script'); script.type = 'text/javascript'; script.charset = 'UTF-8'; script.src = this._src; return script; } /** * Subscribes on a load event of the script. * @private */ _subscribeOnScriptLoad() { this._script.onload = ()=>{ this._globalSrcStorage.eachOnLoad(this._src, (callback)=>{ callback(); }); this._destroy(); }; } /** * Subscribes on an error event of the script. * @private */ _subscribeOnScriptError() { this._script.onerror = ()=>{ const error = new Error(`${this._src} failed to load.`); this._globalSrcStorage.eachOnError(this._src, (callback)=>{ callback(error); }); this._destroy(); }; } /** * Destroys the {@code SrciptLoader}. * @private */ _destroy() { this._removeListeners(); this._globalSrcStorage.delete(this._src); this._src = null; this._script = null; } /** * Removes script listeners. * @private */ _removeListeners() { this._script.onload = null; this._script.onerror = null; } /** * Appends the script to the {@code head} block of a html page. * @private */ _appendScript(script) { const head = document.getElementsByTagName('head')[0]; head.appendChild(script); } /** * Processes existing script. It is not known whether it is fully loaded. * @private */ _processExistingScript(resolve, reject) { if (this._globalSrcStorage.has(this._src)) { this._addCallbacks(resolve, reject); } else { this._processLoadedScript(resolve); } } /** * Adds callbacks to the storage. Called when the script is on the page, but is not loaded yet. * @private */ _addCallbacks(resolve, reject) { this._globalSrcStorage.addCallbacks(this._src, resolve, reject); } /** * Processes loaded script. Called when the script on the page is fully loaded and ready to use. * @private */ _processLoadedScript(resolve) { resolve(); } } const DISABLE_COMMAND_ID = 'WProofreaderToggleCommandDisabling'; const DISABLE_INSTANCES_ID = 'InstancesDisabling'; /** * Initializes and creates instances of the {@code WEBSPELLCHECKER}. */ class WProofreader extends Plugin { /** * @inheritdoc */ static get requires() { return [ WProofreaderEditing, WProofreaderUI ]; } /** * @inheritdoc */ static get pluginName() { return 'WProofreader'; } /** * @inheritDoc */ constructor(editor){ super(editor); /** * Flag indicating whether the {@code WProofreaderToggle} command is enabled. * * @observable * @member {Boolean} */ this.set('isToggleCommandEnabled', true); this._instances = []; this._restrictedEditingName = 'RestrictedEditingMode'; } /** * Initializes the {@code WProofreader} plugin. * @public */ init() { this._userOptions = this._getUserOptions(); this._setTheme(); this._setAutoStartup(); this._setBadgeOffset(); this._setIsEnabled(this._userOptions.autoStartup, DISABLE_INSTANCES_ID); this._loadWscbundle().then(()=>{ this._handleWscbundleLoaded(); }).catch((error)=>{ throw new Error(error); }); this.bind('isToggleCommandEnabled').to(this.editor.commands.get('WProofreaderToggle'), 'isEnabled', (isEnabled)=>this._handleToggleCommandEnabled(isEnabled)); } /** * {@inheritdoc} * * Destroys the {@code WProofreader} plugin. * @public */ destroy() { super.destroy(); this._instances.forEach((instance)=>instance.destroy()); this._instances = null; } /** * Gets the configuration of the {@code WEBSPELLCHECKER} from the {@code CKEditor 5} config. * @private */ _getUserOptions() { const config = this.editor.config.get('wproofreader'); if (!config) { throw new Error('No WProofreader configuration.'); } return config; } /** * Checks if the theme option exists otherwise sets ckeditor5 theme. * @private */ _setTheme() { if (!this._userOptions.theme) { this._userOptions.theme = 'ckeditor5'; } } /** * Checks if the autoStartup option exists otherwise sets {@code true} value. * @private */ _setAutoStartup() { if (!Object.prototype.hasOwnProperty.call(this._userOptions, 'autoStartup')) { this._userOptions.autoStartup = true; } } /** * Checks if the badgeOffsetX/badgeOffsetY and fullSizeBadge options exist otherwise sets values by default. * @private */ _setBadgeOffset() { const badgeOffset = 11; if (this._userOptions.fullSizeBadge) { return; } if (!Object.prototype.hasOwnProperty.call(this._userOptions, 'badgeOffsetX')) { this._userOptions.badgeOffsetX = badgeOffset; } if (!Object.prototype.hasOwnProperty.call(this._userOptions, 'badgeOffsetY')) { this._userOptions.badgeOffsetY = badgeOffset; } } /** * Configures the {@code isEnabled} state of the plugin. * @private */ _setIsEnabled(enable, disableId) { enable ? this.clearForceDisabled(disableId) : this.forceDisabled(disableId); } /** * Loads {@code wscbundle} script. * @private */ _loadWscbundle() { const scriptLoader = new ScriptLoader(this._userOptions.srcUrl); return scriptLoader.load().then(()=>{ if (!window.WEBSPELLCHECKER) { throw new Error('WEBSPELLCHECKER is not defined.'); } }); } /** * Handles the {@code wscbundle} loaded state. * @private */ _handleWscbundleLoaded() { if (this.editor.state === 'ready') { this._createInstances(); } else { this._subscribeOnEditorReady(); } } /** * Creates {@code WEBSPELLCHECKER} intances. * @private */ _createInstances() { const roots = this.editor.editing.view.domRoots.values(); this._setFields(); for (const root of roots){ this._createInstance(root); } } /** * Sets extra fields related to the {@code WEBSPELLCHECKER} instance creating. * @private */ _setFields() { this._isRestrictedEditingMode = this._checkRestrictedEditingMode(); this._options = this._createOptions(); } /** * Checks if the current editor in the restricted editing mode. * @private */ _checkRestrictedEditingMode() { return this.editor.plugins.has(this._restrictedEditingName); } /** * Creates options for the {@code WEBSPELLCHECKER} initialization. * @private */ _createOptions() { return { appType: 'proofreader_ck5', restrictedEditingMode: this._isRestrictedEditingMode, disableBadgePulsing: true, onCommitOptions: this._onCommitOptions.bind(this), onToggle: this._onToggle.bind(this) }; } /** * Handles the {@code commitOptions} behavior of the {@code WEBSPELLCHECKER} instance. * @private */ _onCommitOptions(changedOptions) { this._syncOptions(changedOptions); } /** * Synchronizes the changed options between each instance of the {@code WEBSPELLCHECKER}. * @private */ _syncOptions(changedOptions) { this._instances.forEach((instance)=>{ instance.commitOption(changedOptions, { ignoreCallback: true }); }); } /** * Handles the {@code toggle} behavior of the {@code WEBSPELLCHECKER} instance. * @private */ _onToggle(instance) { const enable = !instance.isDisabled(); this._setIsEnabled(enable, DISABLE_INSTANCES_ID); this._syncToggle(enable); } /** * Synchronizes the toggle state between each instance of the {@code WEBSPELLCHECKER}. * @private */ _syncToggle(enable) { this._instances.forEach((instance)=>{ enable ? this._enableInstance(instance) : this._disableInstance(instance); }); } /** * Enables an instance of the {@code WEBSPELLCHECKER}. * @private */ _enableInstance(instance) { const options = { ignoreCallback: true }; if (!this.isEnabled) { return; } instance.enable(options); } /** * Disables an instance of the {@code WEBSPELLCHECKER}. * @private */ _disableInstance(instance) { const options = { ignoreCallback: true }; instance.disable(options); } /** * Creates an instance of the {@code WEBSPELLCHECKER}. * @private */ _createInstance(root) { window.WEBSPELLCHECKER.init(this._mergeOptions(root), this._handleInstanceCreated.bind(this)); } /** * Creates the configuration of the {@code WEBSPELLCHECKER}. * @private */ _mergeOptions(container) { return Object.assign({}, this._userOptions, this._options, { container: container }); } /** * Handles a state when an instance of the {@code WEBSPELLCHECKER} completely created. * @private */ _handleInstanceCreated(instance) { if (!instance) { return; } if (this.editor.state === 'destroyed') { instance.destroy(); return; } if (!this.isEnabled) { this._disableInstance(instance); } this._instances.push(instance); } /** * Subscribes on the ready state of the {@code CKEditor 5}. * @private */ _subscribeOnEditorReady() { this.editor.on('ready', ()=>{ this._createInstances(); }); } /** * Handles an enabled state of the {@code WProofreaderToggle} command. * @private */ _handleToggleCommandEnabled(isEnabled) { this._setIsEnabled(isEnabled, DISABLE_COMMAND_ID); this._syncToggle(isEnabled); // Method should return a boolean value to correct work of the bind functionality. return isEnabled; } /** * Returns available static actions of the {@code WEBSPELLCHECKER}. * @public * * @returns {Array} Static actions. */ getStaticActions() { if (this._instances.length === 0) { return []; } return this._instances[0].getStaticActions(); } /** * Toggles instances state of the {@code WEBSPELLCHECKER}. * @public */ toggle() { if (this._instances.length === 0) { return; } const enable = this.isInstancesEnabled(); this._setIsEnabled(!enable, DISABLE_INSTANCES_ID); this._syncToggle(!enable); } /** * Opens settings of the {@code WEBSPELLCHECKER}. * @public */ openSettings() { if (this._instances.length === 0) { return; } this._instances[0].openSettings(); } /** * Opens the proofread Dialog of the {@code WEBSPELLCHECKER}. * @public */ openDialog() { if (this._instances.length === 0) { return; } this._instances[0].openDialog(); } /** * Indicates that instances of the {@code WEBSPELLCHECKER} are ready to use. * @public * * @returns {Boolean} {@code true} if instances are ready, {@code false} otherwise. */ isInstancesReady() { return this._instances.length > 0; } /** * Indicates that instances of the {@code WEBSPELLCHECKER} are enabled. * @public * * @returns {Boolean} {@code true} if instances are enabled, {@code false} otherwise. */ isInstancesEnabled() { if (this._instances.length === 0) { return false; } return !this._instances[0].isDisabled(); } } const icons = { wproofreaderIcon }; export { WProofreader, icons }; //# sourceMappingURL=index.js.map