@webspellchecker/wproofreader-ckeditor5
Version:
Multilingual spelling and grammar checking plugin for CKEditor 5
739 lines (727 loc) • 23.6 kB
JavaScript
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