@ckeditor/ckeditor5-utils
Advanced tools
Comparing version 35.0.1 to 35.1.0
@@ -169,3 +169,3 @@ Changelog | ||
* Vairous fixes in the API docs. Thanks to [@denisname](https://github.com/denisname)! | ||
* Various fixes in the API docs. Thanks to [@denisname](https://github.com/denisname)! | ||
@@ -172,0 +172,0 @@ |
{ | ||
"name": "@ckeditor/ckeditor5-utils", | ||
"version": "35.0.1", | ||
"version": "35.1.0", | ||
"description": "Miscellaneous utilities used by CKEditor 5.", | ||
@@ -17,6 +17,6 @@ "keywords": [ | ||
"devDependencies": { | ||
"@ckeditor/ckeditor5-build-classic": "^35.0.1", | ||
"@ckeditor/ckeditor5-editor-classic": "^35.0.1", | ||
"@ckeditor/ckeditor5-core": "^35.0.1", | ||
"@ckeditor/ckeditor5-engine": "^35.0.1", | ||
"@ckeditor/ckeditor5-build-classic": "^35.1.0", | ||
"@ckeditor/ckeditor5-editor-classic": "^35.1.0", | ||
"@ckeditor/ckeditor5-core": "^35.1.0", | ||
"@ckeditor/ckeditor5-engine": "^35.1.0", | ||
"@types/lodash-es": "^4.17.6", | ||
@@ -23,0 +23,0 @@ "typescript": "^4.6.4" |
@@ -8,7 +8,6 @@ /** | ||
*/ | ||
import EmitterMixin from './emittermixin'; | ||
import { Emitter } from './emittermixin'; | ||
import CKEditorError from './ckeditorerror'; | ||
import uid from './uid'; | ||
import isIterable from './isiterable'; | ||
import mix from './mix'; | ||
/** | ||
@@ -26,3 +25,3 @@ * Collections are ordered sets of objects. Items in the collection can be retrieved by their indexes | ||
*/ | ||
class Collection { | ||
export default class Collection extends Emitter { | ||
/** | ||
@@ -65,2 +64,3 @@ * Creates a new Collection instance. | ||
constructor(initialItemsOrOptions = {}, options = {}) { | ||
super(); | ||
const hasInitialItems = isIterable(initialItemsOrOptions); | ||
@@ -621,3 +621,1 @@ if (!hasInitialItems) { | ||
} | ||
mix(Collection, EmitterMixin); | ||
export default Collection; |
@@ -5,11 +5,10 @@ /** | ||
*/ | ||
/* eslint-disable new-cap */ | ||
/** | ||
* @module utils/dom/emittermixin | ||
*/ | ||
import { default as EmitterMixin, _getEmitterListenedTo, _setEmitterId } from '../emittermixin'; | ||
import { _getEmitterListenedTo, _setEmitterId, Emitter as BaseEmitter } from '../emittermixin'; | ||
import uid from '../uid'; | ||
import isNode from './isnode'; | ||
import isWindow from './iswindow'; | ||
import { extend } from 'lodash-es'; | ||
import mix from '../mix'; | ||
/** | ||
@@ -24,5 +23,5 @@ * Mixin that injects the DOM events API into its host. It provides the API | ||
* import DomEmitterMixin from '../utils/dom/emittermixin.js'; | ||
* import { Emitter } from '../utils/emittermixin.js'; | ||
* | ||
* class SomeView {} | ||
* mix( SomeView, DomEmitterMixin ); | ||
* class SomeView extends DomEmitterMixin( Emitter ) {} | ||
* | ||
@@ -38,95 +37,75 @@ * const view = new SomeView(); | ||
*/ | ||
const DomEmitterMixin = extend({}, EmitterMixin, { | ||
/** | ||
* Registers a callback function to be executed when an event is fired in a specific Emitter or DOM Node. | ||
* It is backwards compatible with {@link module:utils/emittermixin~EmitterMixin#listenTo}. | ||
* | ||
* @param {module:utils/emittermixin~Emitter|Node|Window} emitter The object that fires the event. | ||
* @param {String} event The name of the event. | ||
* @param {Function} callback The function to be called on event. | ||
* @param {Object} [options={}] Additional options. | ||
* @param {module:utils/priorities~PriorityString|Number} [options.priority='normal'] The priority of this event callback. The higher | ||
* the priority value the sooner the callback will be fired. Events having the same priority are called in the | ||
* order they were added. | ||
* @param {Boolean} [options.useCapture=false] Indicates that events of this type will be dispatched to the registered | ||
* listener before being dispatched to any EventTarget beneath it in the DOM tree. | ||
* @param {Boolean} [options.usePassive=false] Indicates that the function specified by listener will never call preventDefault() | ||
* and prevents blocking browser's main thread by this event handler. | ||
*/ | ||
listenTo(emitter, event, callback, options = {}) { | ||
// Check if emitter is an instance of DOM Node. If so, use corresponding ProxyEmitter (or create one if not existing). | ||
if (isNode(emitter) || isWindow(emitter)) { | ||
const proxyOptions = { | ||
capture: !!options.useCapture, | ||
passive: !!options.usePassive | ||
}; | ||
const proxyEmitter = this._getProxyEmitter(emitter, proxyOptions) || new ProxyEmitter(emitter, proxyOptions); | ||
this.listenTo(proxyEmitter, event, callback, options); | ||
export default function DomEmitterMixin(base) { | ||
class Mixin extends base { | ||
listenTo(emitter, event, callback, options = {}) { | ||
// Check if emitter is an instance of DOM Node. If so, use corresponding ProxyEmitter (or create one if not existing). | ||
if (isNode(emitter) || isWindow(emitter)) { | ||
const proxyOptions = { | ||
capture: !!options.useCapture, | ||
passive: !!options.usePassive | ||
}; | ||
const proxyEmitter = this._getProxyEmitter(emitter, proxyOptions) || new ProxyEmitter(emitter, proxyOptions); | ||
this.listenTo(proxyEmitter, event, callback, options); | ||
} | ||
else { | ||
// Execute parent class method with Emitter (or ProxyEmitter) instance. | ||
BaseEmitter.prototype.listenTo.call(this, emitter, event, callback, options); | ||
} | ||
} | ||
else { | ||
// Execute parent class method with Emitter (or ProxyEmitter) instance. | ||
EmitterMixin.listenTo.call(this, emitter, event, callback, options); | ||
} | ||
}, | ||
/** | ||
* Stops listening for events. It can be used at different levels: | ||
* It is backwards compatible with {@link module:utils/emittermixin~EmitterMixin#listenTo}. | ||
* | ||
* * To stop listening to a specific callback. | ||
* * To stop listening to a specific event. | ||
* * To stop listening to all events fired by a specific object. | ||
* * To stop listening to all events fired by all object. | ||
* | ||
* @param {module:utils/emittermixin~Emitter|Node|Window} [emitter] The object to stop listening to. | ||
* If omitted, stops it for all objects. | ||
* @param {String} [event] (Requires the `emitter`) The name of the event to stop listening to. If omitted, stops it | ||
* for all events from `emitter`. | ||
* @param {Function} [callback] (Requires the `event`) The function to be removed from the call list for the given | ||
* `event`. | ||
*/ | ||
stopListening(emitter, event, callback) { | ||
// Check if the emitter is an instance of DOM Node. If so, forward the call to the corresponding ProxyEmitters. | ||
if (isNode(emitter) || isWindow(emitter)) { | ||
const proxyEmitters = this._getAllProxyEmitters(emitter); | ||
for (const proxy of proxyEmitters) { | ||
this.stopListening(proxy, event, callback); | ||
stopListening(emitter, event, callback) { | ||
// Check if the emitter is an instance of DOM Node. If so, forward the call to the corresponding ProxyEmitters. | ||
if (isNode(emitter) || isWindow(emitter)) { | ||
const proxyEmitters = this._getAllProxyEmitters(emitter); | ||
for (const proxy of proxyEmitters) { | ||
this.stopListening(proxy, event, callback); | ||
} | ||
} | ||
else { | ||
// Execute parent class method with Emitter (or ProxyEmitter) instance. | ||
BaseEmitter.prototype.stopListening.call(this, emitter, event, callback); | ||
} | ||
} | ||
else { | ||
// Execute parent class method with Emitter (or ProxyEmitter) instance. | ||
EmitterMixin.stopListening.call(this, emitter, event, callback); | ||
/** | ||
* Retrieves ProxyEmitter instance for given DOM Node residing in this Host and given options. | ||
* | ||
* @private | ||
* @param {Node|Window} node DOM Node of the ProxyEmitter. | ||
* @param {Object} [options] Additional options. | ||
* @param {Boolean} [options.useCapture=false] Indicates that events of this type will be dispatched to the registered | ||
* listener before being dispatched to any EventTarget beneath it in the DOM tree. | ||
* @param {Boolean} [options.usePassive=false] Indicates that the function specified by listener will never call preventDefault() | ||
* and prevents blocking browser's main thread by this event handler. | ||
* @returns {module:utils/dom/emittermixin~ProxyEmitter|null} ProxyEmitter instance bound to the DOM Node. | ||
*/ | ||
_getProxyEmitter(node, options) { | ||
return _getEmitterListenedTo(this, getProxyEmitterId(node, options)); | ||
} | ||
}, | ||
/** | ||
* Retrieves ProxyEmitter instance for given DOM Node residing in this Host and given options. | ||
* | ||
* @private | ||
* @param {Node|Window} node DOM Node of the ProxyEmitter. | ||
* @param {Object} [options] Additional options. | ||
* @param {Boolean} [options.useCapture=false] Indicates that events of this type will be dispatched to the registered | ||
* listener before being dispatched to any EventTarget beneath it in the DOM tree. | ||
* @param {Boolean} [options.usePassive=false] Indicates that the function specified by listener will never call preventDefault() | ||
* and prevents blocking browser's main thread by this event handler. | ||
* @returns {module:utils/dom/emittermixin~ProxyEmitter|null} ProxyEmitter instance bound to the DOM Node. | ||
*/ | ||
_getProxyEmitter(node, options) { | ||
return _getEmitterListenedTo(this, getProxyEmitterId(node, options)); | ||
}, | ||
/** | ||
* Retrieves all the ProxyEmitter instances for given DOM Node residing in this Host. | ||
* | ||
* @private | ||
* @param {Node|Window} node DOM Node of the ProxyEmitter. | ||
* @returns {Array.<module:utils/dom/emittermixin~ProxyEmitter>} | ||
*/ | ||
_getAllProxyEmitters(node) { | ||
return [ | ||
{ capture: false, passive: false }, | ||
{ capture: false, passive: true }, | ||
{ capture: true, passive: false }, | ||
{ capture: true, passive: true } | ||
].map(options => this._getProxyEmitter(node, options)).filter(proxy => !!proxy); | ||
/** | ||
* Retrieves all the ProxyEmitter instances for given DOM Node residing in this Host. | ||
* | ||
* @private | ||
* @param {Node|Window} node DOM Node of the ProxyEmitter. | ||
* @returns {Array.<module:utils/dom/emittermixin~ProxyEmitter>} | ||
*/ | ||
_getAllProxyEmitters(node) { | ||
return [ | ||
{ capture: false, passive: false }, | ||
{ capture: false, passive: true }, | ||
{ capture: true, passive: false }, | ||
{ capture: true, passive: true } | ||
].map(options => this._getProxyEmitter(node, options)).filter(proxy => !!proxy); | ||
} | ||
} | ||
return Mixin; | ||
} | ||
export const Emitter = DomEmitterMixin(BaseEmitter); | ||
// Backward compatibility with `mix` | ||
([ | ||
'_getProxyEmitter', '_getAllProxyEmitters', | ||
'on', 'once', 'off', 'listenTo', | ||
'stopListening', 'fire', 'delegate', 'stopDelegating', | ||
'_addEventListener', '_removeEventListener' | ||
]).forEach(key => { | ||
DomEmitterMixin[key] = Emitter.prototype[key]; | ||
}); | ||
export default DomEmitterMixin; | ||
/** | ||
@@ -164,3 +143,3 @@ * Creates a ProxyEmitter instance. Such an instance is a bridge between a DOM Node firing events | ||
*/ | ||
class ProxyEmitter { | ||
class ProxyEmitter extends BaseEmitter { | ||
/** | ||
@@ -175,2 +154,3 @@ * @param {Node|Window} node DOM Node that fires events. | ||
constructor(node, options) { | ||
super(); | ||
// Set emitter ID to match DOM Node "expando" property. | ||
@@ -243,3 +223,3 @@ _setEmitterId(this, getProxyEmitterId(node, options)); | ||
this.attach(event); | ||
EmitterMixin._addEventListener.call(this, event, callback, options); | ||
BaseEmitter.prototype._addEventListener.call(this, event, callback, options); | ||
} | ||
@@ -255,3 +235,3 @@ /** | ||
_removeEventListener(event, callback) { | ||
EmitterMixin._removeEventListener.call(this, event, callback); | ||
BaseEmitter.prototype._removeEventListener.call(this, event, callback); | ||
this.detach(event); | ||
@@ -283,3 +263,2 @@ } | ||
} | ||
mix(ProxyEmitter, EmitterMixin); | ||
// Gets an unique DOM Node identifier. The identifier will be set if not defined. | ||
@@ -286,0 +265,0 @@ // |
@@ -23,2 +23,16 @@ /** | ||
*/ | ||
export default { window, document }; | ||
let global; | ||
// In some environments window and document API might not be available. | ||
try { | ||
global = { window, document }; | ||
} | ||
catch (e) { | ||
// It's not possible to mock a window object to simulate lack of a window object without writing extremely convoluted code. | ||
/* istanbul ignore next */ | ||
// Let's cast it to not change module's API. | ||
// We only handle this so loading editor in environments without window and document doesn't fail. | ||
// For better DX we shouldn't introduce mixed types and require developers to check the type manually. | ||
// This module should not be used on purpose in any environment outside browser. | ||
global = { window: {}, document: {} }; | ||
} | ||
export default global; |
@@ -8,2 +8,3 @@ /** | ||
*/ | ||
/* eslint-disable new-cap */ | ||
import EventInfo from './eventinfo'; | ||
@@ -30,247 +31,219 @@ import uid from './uid'; | ||
*/ | ||
const EmitterMixin = { | ||
/** | ||
* @inheritDoc | ||
*/ | ||
on(event, callback, options = {}) { | ||
this.listenTo(this, event, callback, options); | ||
}, | ||
/** | ||
* @inheritDoc | ||
*/ | ||
once(event, callback, options) { | ||
let wasFired = false; | ||
const onceCallback = (event, ...args) => { | ||
// Ensure the callback is called only once even if the callback itself leads to re-firing the event | ||
// (which would call the callback again). | ||
if (!wasFired) { | ||
wasFired = true; | ||
// Go off() at the first call. | ||
event.off(); | ||
// Go with the original callback. | ||
callback.call(this, event, ...args); | ||
} | ||
}; | ||
// Make a similar on() call, simply replacing the callback. | ||
this.listenTo(this, event, onceCallback, options); | ||
}, | ||
/** | ||
* @inheritDoc | ||
*/ | ||
off(event, callback) { | ||
this.stopListening(this, event, callback); | ||
}, | ||
/** | ||
* @inheritDoc | ||
*/ | ||
listenTo(emitter, event, callback, options = {}) { | ||
let emitterInfo, eventCallbacks; | ||
// _listeningTo contains a list of emitters that this object is listening to. | ||
// This list has the following format: | ||
// | ||
// _listeningTo: { | ||
// emitterId: { | ||
// emitter: emitter, | ||
// callbacks: { | ||
// event1: [ callback1, callback2, ... ] | ||
// .... | ||
// } | ||
// }, | ||
// ... | ||
// } | ||
if (!this[_listeningTo]) { | ||
this[_listeningTo] = {}; | ||
export default function EmitterMixin(base) { | ||
class Mixin extends base { | ||
on(event, callback, options) { | ||
this.listenTo(this, event, callback, options); | ||
} | ||
const emitters = this[_listeningTo]; | ||
if (!_getEmitterId(emitter)) { | ||
_setEmitterId(emitter); | ||
} | ||
const emitterId = _getEmitterId(emitter); | ||
if (!(emitterInfo = emitters[emitterId])) { | ||
emitterInfo = emitters[emitterId] = { | ||
emitter, | ||
callbacks: {} | ||
once(event, callback, options) { | ||
let wasFired = false; | ||
const onceCallback = (event, ...args) => { | ||
// Ensure the callback is called only once even if the callback itself leads to re-firing the event | ||
// (which would call the callback again). | ||
if (!wasFired) { | ||
wasFired = true; | ||
// Go off() at the first call. | ||
event.off(); | ||
// Go with the original callback. | ||
callback.call(this, event, ...args); | ||
} | ||
}; | ||
// Make a similar on() call, simply replacing the callback. | ||
this.listenTo(this, event, onceCallback, options); | ||
} | ||
if (!(eventCallbacks = emitterInfo.callbacks[event])) { | ||
eventCallbacks = emitterInfo.callbacks[event] = []; | ||
off(event, callback) { | ||
this.stopListening(this, event, callback); | ||
} | ||
eventCallbacks.push(callback); | ||
// Finally register the callback to the event. | ||
addEventListener(this, emitter, event, callback, options); | ||
}, | ||
/** | ||
* @inheritDoc | ||
*/ | ||
stopListening(emitter, event, callback) { | ||
const emitters = this[_listeningTo]; | ||
let emitterId = emitter && _getEmitterId(emitter); | ||
const emitterInfo = (emitters && emitterId) ? emitters[emitterId] : undefined; | ||
const eventCallbacks = (emitterInfo && event) ? emitterInfo.callbacks[event] : undefined; | ||
// Stop if nothing has been listened. | ||
if (!emitters || (emitter && !emitterInfo) || (event && !eventCallbacks)) { | ||
return; | ||
} | ||
// All params provided. off() that single callback. | ||
if (callback) { | ||
removeEventListener(this, emitter, event, callback); | ||
// We must remove callbacks as well in order to prevent memory leaks. | ||
// See https://github.com/ckeditor/ckeditor5/pull/8480 | ||
const index = eventCallbacks.indexOf(callback); | ||
if (index !== -1) { | ||
if (eventCallbacks.length === 1) { | ||
delete emitterInfo.callbacks[event]; | ||
} | ||
else { | ||
removeEventListener(this, emitter, event, callback); | ||
} | ||
listenTo(emitter, event, callback, options = {}) { | ||
let emitterInfo, eventCallbacks; | ||
// _listeningTo contains a list of emitters that this object is listening to. | ||
// This list has the following format: | ||
// | ||
// _listeningTo: { | ||
// emitterId: { | ||
// emitter: emitter, | ||
// callbacks: { | ||
// event1: [ callback1, callback2, ... ] | ||
// .... | ||
// } | ||
// }, | ||
// ... | ||
// } | ||
if (!this[_listeningTo]) { | ||
this[_listeningTo] = {}; | ||
} | ||
} | ||
// Only `emitter` and `event` provided. off() all callbacks for that event. | ||
else if (eventCallbacks) { | ||
while ((callback = eventCallbacks.pop())) { | ||
removeEventListener(this, emitter, event, callback); | ||
const emitters = this[_listeningTo]; | ||
if (!_getEmitterId(emitter)) { | ||
_setEmitterId(emitter); | ||
} | ||
delete emitterInfo.callbacks[event]; | ||
} | ||
// Only `emitter` provided. off() all events for that emitter. | ||
else if (emitterInfo) { | ||
for (event in emitterInfo.callbacks) { | ||
this.stopListening(emitter, event); | ||
const emitterId = _getEmitterId(emitter); | ||
if (!(emitterInfo = emitters[emitterId])) { | ||
emitterInfo = emitters[emitterId] = { | ||
emitter, | ||
callbacks: {} | ||
}; | ||
} | ||
delete emitters[emitterId]; | ||
if (!(eventCallbacks = emitterInfo.callbacks[event])) { | ||
eventCallbacks = emitterInfo.callbacks[event] = []; | ||
} | ||
eventCallbacks.push(callback); | ||
// Finally register the callback to the event. | ||
addEventListener(this, emitter, event, callback, options); | ||
} | ||
// No params provided. off() all emitters. | ||
else { | ||
for (emitterId in emitters) { | ||
this.stopListening(emitters[emitterId].emitter); | ||
stopListening(emitter, event, callback) { | ||
const emitters = this[_listeningTo]; | ||
let emitterId = emitter && _getEmitterId(emitter); | ||
const emitterInfo = (emitters && emitterId) ? emitters[emitterId] : undefined; | ||
const eventCallbacks = (emitterInfo && event) ? emitterInfo.callbacks[event] : undefined; | ||
// Stop if nothing has been listened. | ||
if (!emitters || (emitter && !emitterInfo) || (event && !eventCallbacks)) { | ||
return; | ||
} | ||
delete this[_listeningTo]; | ||
} | ||
}, | ||
/** | ||
* @inheritDoc | ||
*/ | ||
fire(eventOrInfo, ...args) { | ||
try { | ||
const eventInfo = eventOrInfo instanceof EventInfo ? eventOrInfo : new EventInfo(this, eventOrInfo); | ||
const event = eventInfo.name; | ||
let callbacks = getCallbacksForEvent(this, event); | ||
// Record that the event passed this emitter on its path. | ||
eventInfo.path.push(this); | ||
// Handle event listener callbacks first. | ||
if (callbacks) { | ||
// Arguments passed to each callback. | ||
const callbackArgs = [eventInfo, ...args]; | ||
// Copying callbacks array is the easiest and most secure way of preventing infinite loops, when event callbacks | ||
// are added while processing other callbacks. Previous solution involved adding counters (unique ids) but | ||
// failed if callbacks were added to the queue before currently processed callback. | ||
// If this proves to be too inefficient, another method is to change `.on()` so callbacks are stored if same | ||
// event is currently processed. Then, `.fire()` at the end, would have to add all stored events. | ||
callbacks = Array.from(callbacks); | ||
for (let i = 0; i < callbacks.length; i++) { | ||
callbacks[i].callback.apply(this, callbackArgs); | ||
// Remove the callback from future requests if off() has been called. | ||
if (eventInfo.off.called) { | ||
// Remove the called mark for the next calls. | ||
delete eventInfo.off.called; | ||
this._removeEventListener(event, callbacks[i].callback); | ||
// All params provided. off() that single callback. | ||
if (callback) { | ||
removeEventListener(this, emitter, event, callback); | ||
// We must remove callbacks as well in order to prevent memory leaks. | ||
// See https://github.com/ckeditor/ckeditor5/pull/8480 | ||
const index = eventCallbacks.indexOf(callback); | ||
if (index !== -1) { | ||
if (eventCallbacks.length === 1) { | ||
delete emitterInfo.callbacks[event]; | ||
} | ||
// Do not execute next callbacks if stop() was called. | ||
if (eventInfo.stop.called) { | ||
break; | ||
else { | ||
removeEventListener(this, emitter, event, callback); | ||
} | ||
} | ||
} | ||
// Delegate event to other emitters if needed. | ||
const delegations = this[_delegations]; | ||
if (delegations) { | ||
const destinations = delegations.get(event); | ||
const passAllDestinations = delegations.get('*'); | ||
if (destinations) { | ||
fireDelegatedEvents(destinations, eventInfo, args); | ||
// Only `emitter` and `event` provided. off() all callbacks for that event. | ||
else if (eventCallbacks) { | ||
while ((callback = eventCallbacks.pop())) { | ||
removeEventListener(this, emitter, event, callback); | ||
} | ||
if (passAllDestinations) { | ||
fireDelegatedEvents(passAllDestinations, eventInfo, args); | ||
delete emitterInfo.callbacks[event]; | ||
} | ||
// Only `emitter` provided. off() all events for that emitter. | ||
else if (emitterInfo) { | ||
for (event in emitterInfo.callbacks) { | ||
this.stopListening(emitter, event); | ||
} | ||
delete emitters[emitterId]; | ||
} | ||
return eventInfo.return; | ||
// No params provided. off() all emitters. | ||
else { | ||
for (emitterId in emitters) { | ||
this.stopListening(emitters[emitterId].emitter); | ||
} | ||
delete this[_listeningTo]; | ||
} | ||
} | ||
catch (err) { | ||
// @if CK_DEBUG // throw err; | ||
/* istanbul ignore next */ | ||
CKEditorError.rethrowUnexpectedError(err, this); | ||
} | ||
}, | ||
/** | ||
* @inheritDoc | ||
*/ | ||
delegate(...events) { | ||
return { | ||
to: (emitter, nameOrFunction) => { | ||
if (!this[_delegations]) { | ||
this[_delegations] = new Map(); | ||
fire(eventOrInfo, ...args) { | ||
try { | ||
const eventInfo = eventOrInfo instanceof EventInfo ? eventOrInfo : new EventInfo(this, eventOrInfo); | ||
const event = eventInfo.name; | ||
let callbacks = getCallbacksForEvent(this, event); | ||
// Record that the event passed this emitter on its path. | ||
eventInfo.path.push(this); | ||
// Handle event listener callbacks first. | ||
if (callbacks) { | ||
// Arguments passed to each callback. | ||
const callbackArgs = [eventInfo, ...args]; | ||
// Copying callbacks array is the easiest and most secure way of preventing infinite loops, when event callbacks | ||
// are added while processing other callbacks. Previous solution involved adding counters (unique ids) but | ||
// failed if callbacks were added to the queue before currently processed callback. | ||
// If this proves to be too inefficient, another method is to change `.on()` so callbacks are stored if same | ||
// event is currently processed. Then, `.fire()` at the end, would have to add all stored events. | ||
callbacks = Array.from(callbacks); | ||
for (let i = 0; i < callbacks.length; i++) { | ||
callbacks[i].callback.apply(this, callbackArgs); | ||
// Remove the callback from future requests if off() has been called. | ||
if (eventInfo.off.called) { | ||
// Remove the called mark for the next calls. | ||
delete eventInfo.off.called; | ||
this._removeEventListener(event, callbacks[i].callback); | ||
} | ||
// Do not execute next callbacks if stop() was called. | ||
if (eventInfo.stop.called) { | ||
break; | ||
} | ||
} | ||
} | ||
// Originally there was a for..of loop which unfortunately caused an error in Babel that didn't allow | ||
// build an application. See: https://github.com/ckeditor/ckeditor5-react/issues/40. | ||
events.forEach(eventName => { | ||
const destinations = this[_delegations].get(eventName); | ||
if (!destinations) { | ||
this[_delegations].set(eventName, new Map([[emitter, nameOrFunction]])); | ||
// Delegate event to other emitters if needed. | ||
const delegations = this[_delegations]; | ||
if (delegations) { | ||
const destinations = delegations.get(event); | ||
const passAllDestinations = delegations.get('*'); | ||
if (destinations) { | ||
fireDelegatedEvents(destinations, eventInfo, args); | ||
} | ||
else { | ||
destinations.set(emitter, nameOrFunction); | ||
if (passAllDestinations) { | ||
fireDelegatedEvents(passAllDestinations, eventInfo, args); | ||
} | ||
}); | ||
} | ||
return eventInfo.return; | ||
} | ||
}; | ||
}, | ||
/** | ||
* @inheritDoc | ||
*/ | ||
stopDelegating(event, emitter) { | ||
if (!this[_delegations]) { | ||
return; | ||
catch (err) { | ||
// @if CK_DEBUG // throw err; | ||
/* istanbul ignore next */ | ||
CKEditorError.rethrowUnexpectedError(err, this); | ||
} | ||
} | ||
if (!event) { | ||
this[_delegations].clear(); | ||
delegate(...events) { | ||
return { | ||
to: (emitter, nameOrFunction) => { | ||
if (!this[_delegations]) { | ||
this[_delegations] = new Map(); | ||
} | ||
// Originally there was a for..of loop which unfortunately caused an error in Babel that didn't allow | ||
// build an application. See: https://github.com/ckeditor/ckeditor5-react/issues/40. | ||
events.forEach(eventName => { | ||
const destinations = this[_delegations].get(eventName); | ||
if (!destinations) { | ||
this[_delegations].set(eventName, new Map([[emitter, nameOrFunction]])); | ||
} | ||
else { | ||
destinations.set(emitter, nameOrFunction); | ||
} | ||
}); | ||
} | ||
}; | ||
} | ||
else if (!emitter) { | ||
this[_delegations].delete(event); | ||
stopDelegating(event, emitter) { | ||
if (!this[_delegations]) { | ||
return; | ||
} | ||
if (!event) { | ||
this[_delegations].clear(); | ||
} | ||
else if (!emitter) { | ||
this[_delegations].delete(event); | ||
} | ||
else { | ||
const destinations = this[_delegations].get(event); | ||
if (destinations) { | ||
destinations.delete(emitter); | ||
} | ||
} | ||
} | ||
else { | ||
const destinations = this[_delegations].get(event); | ||
if (destinations) { | ||
destinations.delete(emitter); | ||
_addEventListener(event, callback, options) { | ||
createEventNamespace(this, event); | ||
const lists = getCallbacksListsForNamespace(this, event); | ||
const priority = priorities.get(options.priority); | ||
const callbackDefinition = { | ||
callback, | ||
priority | ||
}; | ||
// Add the callback to all callbacks list. | ||
for (const callbacks of lists) { | ||
// Add the callback to the list in the right priority position. | ||
insertToPriorityArray(callbacks, callbackDefinition); | ||
} | ||
} | ||
}, | ||
/** | ||
* @inheritDoc | ||
*/ | ||
_addEventListener(event, callback, options) { | ||
createEventNamespace(this, event); | ||
const lists = getCallbacksListsForNamespace(this, event); | ||
const priority = priorities.get(options.priority); | ||
const callbackDefinition = { | ||
callback, | ||
priority | ||
}; | ||
// Add the callback to all callbacks list. | ||
for (const callbacks of lists) { | ||
// Add the callback to the list in the right priority position. | ||
insertToPriorityArray(callbacks, callbackDefinition); | ||
} | ||
}, | ||
/** | ||
* @inheritDoc | ||
*/ | ||
_removeEventListener(event, callback) { | ||
const lists = getCallbacksListsForNamespace(this, event); | ||
for (const callbacks of lists) { | ||
for (let i = 0; i < callbacks.length; i++) { | ||
if (callbacks[i].callback == callback) { | ||
// Remove the callback from the list (fixing the next index). | ||
callbacks.splice(i, 1); | ||
i--; | ||
_removeEventListener(event, callback) { | ||
const lists = getCallbacksListsForNamespace(this, event); | ||
for (const callbacks of lists) { | ||
for (let i = 0; i < callbacks.length; i++) { | ||
if (callbacks[i].callback == callback) { | ||
// Remove the callback from the list (fixing the next index). | ||
callbacks.splice(i, 1); | ||
i--; | ||
} | ||
} | ||
@@ -280,4 +253,13 @@ } | ||
} | ||
}; | ||
export default EmitterMixin; | ||
return Mixin; | ||
} | ||
export const Emitter = EmitterMixin(Object); | ||
// Backward compatibility with `mix` | ||
([ | ||
'on', 'once', 'off', 'listenTo', | ||
'stopListening', 'fire', 'delegate', 'stopDelegating', | ||
'_addEventListener', '_removeEventListener' | ||
]).forEach(key => { | ||
EmitterMixin[key] = Emitter.prototype[key]; | ||
}); | ||
/** | ||
@@ -461,3 +443,3 @@ * Checks if `listeningEmitter` listens to an emitter with given `listenedToEmitterId` and if so, returns that emitter. | ||
// This is needed in some tests that are using mocks instead of the real objects with EmitterMixin mixed. | ||
listener._addEventListener.call(emitter, event, callback, options); | ||
(listener._addEventListener).call(emitter, event, callback, options); | ||
} | ||
@@ -464,0 +446,0 @@ } |
@@ -9,4 +9,19 @@ /** | ||
*/ | ||
const userAgent = navigator.userAgent.toLowerCase(); | ||
/** | ||
* Safely returns `userAgent` from browser's navigator API in a lower case. | ||
* If navigator API is not available it will return an empty string. | ||
* | ||
* @returns {String} | ||
*/ | ||
export function getUserAgent() { | ||
// In some environments navigator API might not be available. | ||
try { | ||
return navigator.userAgent.toLowerCase(); | ||
} | ||
catch (e) { | ||
return ''; | ||
} | ||
} | ||
const userAgent = getUserAgent(); | ||
/** | ||
* A namespace containing environment and browser information. | ||
@@ -13,0 +28,0 @@ * |
@@ -6,2 +6,3 @@ /** | ||
/* global setTimeout, clearTimeout */ | ||
/* eslint-disable new-cap */ | ||
/** | ||
@@ -11,5 +12,4 @@ * @module utils/focustracker | ||
import DomEmitterMixin from './dom/emittermixin'; | ||
import ObservableMixin from './observablemixin'; | ||
import { Observable } from './observablemixin'; | ||
import CKEditorError from './ckeditorerror'; | ||
import mix from './mix'; | ||
/** | ||
@@ -30,4 +30,5 @@ * Allows observing a group of `Element`s whether at least one of them is focused. | ||
*/ | ||
class FocusTracker { | ||
export default class FocusTracker extends DomEmitterMixin(Observable) { | ||
constructor() { | ||
super(); | ||
this.set('isFocused', false); | ||
@@ -104,4 +105,1 @@ this.set('focusedElement', null); | ||
} | ||
mix(FocusTracker, DomEmitterMixin); | ||
mix(FocusTracker, ObservableMixin); | ||
export default FocusTracker; |
@@ -28,2 +28,3 @@ /** | ||
* | ||
* @depreciated Use mixin pattern, see: https://www.typescriptlang.org/docs/handbook/mixins.html. | ||
* @param {Function} [baseClass] Class which prototype will be extended. | ||
@@ -40,2 +41,5 @@ * @param {Object} [...mixins] Objects from which to get properties. | ||
} | ||
if (typeof mixin == 'function' && (key == 'length' || key == 'name' || key == 'prototype')) { | ||
return; | ||
} | ||
const sourceDescriptor = Object.getOwnPropertyDescriptor(mixin, key); | ||
@@ -42,0 +46,0 @@ sourceDescriptor.enumerable = false; |
@@ -5,7 +5,7 @@ /** | ||
*/ | ||
/* eslint-disable @typescript-eslint/unified-signatures */ | ||
/* eslint-disable @typescript-eslint/unified-signatures, new-cap */ | ||
/** | ||
* @module utils/observablemixin | ||
*/ | ||
import EmitterMixin from './emittermixin'; | ||
import { Emitter } from './emittermixin'; | ||
import CKEditorError from './ckeditorerror'; | ||
@@ -31,213 +31,212 @@ import { isObject } from 'lodash-es'; | ||
*/ | ||
const ObservableMixin = { | ||
/** | ||
* @inheritDoc | ||
*/ | ||
set(name, value) { | ||
// If the first parameter is an Object, iterate over its properties. | ||
if (isObject(name)) { | ||
Object.keys(name).forEach(property => { | ||
this.set(property, name[property]); | ||
}, this); | ||
return; | ||
} | ||
initObservable(this); | ||
const properties = this[observablePropertiesSymbol]; | ||
if ((name in this) && !properties.has(name)) { | ||
/** | ||
* Cannot override an existing property. | ||
* | ||
* This error is thrown when trying to {@link ~Observable#set set} a property with | ||
* a name of an already existing property. For example: | ||
* | ||
* let observable = new Model(); | ||
* observable.property = 1; | ||
* observable.set( 'property', 2 ); // throws | ||
* | ||
* observable.set( 'property', 1 ); | ||
* observable.set( 'property', 2 ); // ok, because this is an existing property. | ||
* | ||
* @error observable-set-cannot-override | ||
*/ | ||
throw new CKEditorError('observable-set-cannot-override', this); | ||
} | ||
Object.defineProperty(this, name, { | ||
enumerable: true, | ||
configurable: true, | ||
get() { | ||
return properties.get(name); | ||
}, | ||
set(value) { | ||
const oldValue = properties.get(name); | ||
// Fire `set` event before the new value will be set to make it possible | ||
// to override observable property without affecting `change` event. | ||
// See https://github.com/ckeditor/ckeditor5-utils/issues/171. | ||
let newValue = this.fire('set:' + name, name, value, oldValue); | ||
if (newValue === undefined) { | ||
newValue = value; | ||
} | ||
// Allow undefined as an initial value like A.define( 'x', undefined ) (#132). | ||
// Note: When properties map has no such own property, then its value is undefined. | ||
if (oldValue !== newValue || !properties.has(name)) { | ||
properties.set(name, newValue); | ||
this.fire('change:' + name, name, newValue, oldValue); | ||
} | ||
export default function ObservableMixin(base) { | ||
class Mixin extends base { | ||
set(name, value) { | ||
// If the first parameter is an Object, iterate over its properties. | ||
if (isObject(name)) { | ||
Object.keys(name).forEach(property => { | ||
this.set(property, name[property]); | ||
}, this); | ||
return; | ||
} | ||
}); | ||
this[name] = value; | ||
}, | ||
/** | ||
* @inheritDoc | ||
*/ | ||
bind(...bindProperties) { | ||
if (!bindProperties.length || !isStringArray(bindProperties)) { | ||
/** | ||
* All properties must be strings. | ||
* | ||
* @error observable-bind-wrong-properties | ||
*/ | ||
throw new CKEditorError('observable-bind-wrong-properties', this); | ||
} | ||
if ((new Set(bindProperties)).size !== bindProperties.length) { | ||
/** | ||
* Properties must be unique. | ||
* | ||
* @error observable-bind-duplicate-properties | ||
*/ | ||
throw new CKEditorError('observable-bind-duplicate-properties', this); | ||
} | ||
initObservable(this); | ||
const boundProperties = this[boundPropertiesSymbol]; | ||
bindProperties.forEach(propertyName => { | ||
if (boundProperties.has(propertyName)) { | ||
initObservable(this); | ||
const properties = this[observablePropertiesSymbol]; | ||
if ((name in this) && !properties.has(name)) { | ||
/** | ||
* Cannot bind the same property more than once. | ||
* Cannot override an existing property. | ||
* | ||
* @error observable-bind-rebind | ||
* This error is thrown when trying to {@link ~Observable#set set} a property with | ||
* a name of an already existing property. For example: | ||
* | ||
* let observable = new Model(); | ||
* observable.property = 1; | ||
* observable.set( 'property', 2 ); // throws | ||
* | ||
* observable.set( 'property', 1 ); | ||
* observable.set( 'property', 2 ); // ok, because this is an existing property. | ||
* | ||
* @error observable-set-cannot-override | ||
*/ | ||
throw new CKEditorError('observable-bind-rebind', this); | ||
throw new CKEditorError('observable-set-cannot-override', this); | ||
} | ||
}); | ||
const bindings = new Map(); | ||
// @typedef {Object} Binding | ||
// @property {Array} property Property which is bound. | ||
// @property {Array} to Array of observable–property components of the binding (`{ observable: ..., property: .. }`). | ||
// @property {Array} callback A function which processes `to` components. | ||
bindProperties.forEach(a => { | ||
const binding = { property: a, to: [] }; | ||
boundProperties.set(a, binding); | ||
bindings.set(a, binding); | ||
}); | ||
// @typedef {Object} BindChain | ||
// @property {Function} to See {@link ~ObservableMixin#_bindTo}. | ||
// @property {Function} toMany See {@link ~ObservableMixin#_bindToMany}. | ||
// @property {module:utils/observablemixin~Observable} _observable The observable which initializes the binding. | ||
// @property {Array} _bindProperties Array of `_observable` properties to be bound. | ||
// @property {Array} _to Array of `to()` observable–properties (`{ observable: toObservable, properties: ...toProperties }`). | ||
// @property {Map} _bindings Stores bindings to be kept in | ||
// {@link ~ObservableMixin#_boundProperties}/{@link ~ObservableMixin#_boundObservables} | ||
// initiated in this binding chain. | ||
return { | ||
to: bindTo, | ||
toMany: bindToMany, | ||
_observable: this, | ||
_bindProperties: bindProperties, | ||
_to: [], | ||
_bindings: bindings | ||
}; | ||
}, | ||
/** | ||
* @inheritDoc | ||
*/ | ||
unbind(...unbindProperties) { | ||
// Nothing to do here if not inited yet. | ||
if (!(this[observablePropertiesSymbol])) { | ||
return; | ||
Object.defineProperty(this, name, { | ||
enumerable: true, | ||
configurable: true, | ||
get() { | ||
return properties.get(name); | ||
}, | ||
set(value) { | ||
const oldValue = properties.get(name); | ||
// Fire `set` event before the new value will be set to make it possible | ||
// to override observable property without affecting `change` event. | ||
// See https://github.com/ckeditor/ckeditor5-utils/issues/171. | ||
let newValue = this.fire(`set:${name}`, name, value, oldValue); | ||
if (newValue === undefined) { | ||
newValue = value; | ||
} | ||
// Allow undefined as an initial value like A.define( 'x', undefined ) (#132). | ||
// Note: When properties map has no such own property, then its value is undefined. | ||
if (oldValue !== newValue || !properties.has(name)) { | ||
properties.set(name, newValue); | ||
this.fire(`change:${name}`, name, newValue, oldValue); | ||
} | ||
} | ||
}); | ||
this[name] = value; | ||
} | ||
const boundProperties = this[boundPropertiesSymbol]; | ||
const boundObservables = this[boundObservablesSymbol]; | ||
if (unbindProperties.length) { | ||
if (!isStringArray(unbindProperties)) { | ||
bind(...bindProperties) { | ||
if (!bindProperties.length || !isStringArray(bindProperties)) { | ||
/** | ||
* Properties must be strings. | ||
* All properties must be strings. | ||
* | ||
* @error observable-unbind-wrong-properties | ||
* @error observable-bind-wrong-properties | ||
*/ | ||
throw new CKEditorError('observable-unbind-wrong-properties', this); | ||
throw new CKEditorError('observable-bind-wrong-properties', this); | ||
} | ||
unbindProperties.forEach(propertyName => { | ||
const binding = boundProperties.get(propertyName); | ||
// Nothing to do if the binding is not defined | ||
if (!binding) { | ||
return; | ||
if ((new Set(bindProperties)).size !== bindProperties.length) { | ||
/** | ||
* Properties must be unique. | ||
* | ||
* @error observable-bind-duplicate-properties | ||
*/ | ||
throw new CKEditorError('observable-bind-duplicate-properties', this); | ||
} | ||
initObservable(this); | ||
const boundProperties = this[boundPropertiesSymbol]; | ||
bindProperties.forEach(propertyName => { | ||
if (boundProperties.has(propertyName)) { | ||
/** | ||
* Cannot bind the same property more than once. | ||
* | ||
* @error observable-bind-rebind | ||
*/ | ||
throw new CKEditorError('observable-bind-rebind', this); | ||
} | ||
binding.to.forEach(([toObservable, toProperty]) => { | ||
const toProperties = boundObservables.get(toObservable); | ||
const toPropertyBindings = toProperties[toProperty]; | ||
toPropertyBindings.delete(binding); | ||
if (!toPropertyBindings.size) { | ||
delete toProperties[toProperty]; | ||
}); | ||
const bindings = new Map(); | ||
// @typedef {Object} Binding | ||
// @property {Array} property Property which is bound. | ||
// @property {Array} to Array of observable–property components of the binding (`{ observable: ..., property: .. }`). | ||
// @property {Array} callback A function which processes `to` components. | ||
bindProperties.forEach(a => { | ||
const binding = { property: a, to: [] }; | ||
boundProperties.set(a, binding); | ||
bindings.set(a, binding); | ||
}); | ||
// @typedef {Object} BindChain | ||
// @property {Function} to See {@link ~ObservableMixin#_bindTo}. | ||
// @property {Function} toMany See {@link ~ObservableMixin#_bindToMany}. | ||
// @property {module:utils/observablemixin~Observable} _observable The observable which initializes the binding. | ||
// @property {Array} _bindProperties Array of `_observable` properties to be bound. | ||
// @property {Array} _to Array of `to()` observable–properties (`{ observable: toObservable, properties: ...toProperties }`). | ||
// @property {Map} _bindings Stores bindings to be kept in | ||
// {@link ~ObservableMixin#_boundProperties}/{@link ~ObservableMixin#_boundObservables} | ||
// initiated in this binding chain. | ||
return { | ||
to: bindTo, | ||
toMany: bindToMany, | ||
_observable: this, | ||
_bindProperties: bindProperties, | ||
_to: [], | ||
_bindings: bindings | ||
}; | ||
} | ||
unbind(...unbindProperties) { | ||
// Nothing to do here if not inited yet. | ||
if (!(this[observablePropertiesSymbol])) { | ||
return; | ||
} | ||
const boundProperties = this[boundPropertiesSymbol]; | ||
const boundObservables = this[boundObservablesSymbol]; | ||
if (unbindProperties.length) { | ||
if (!isStringArray(unbindProperties)) { | ||
/** | ||
* Properties must be strings. | ||
* | ||
* @error observable-unbind-wrong-properties | ||
*/ | ||
throw new CKEditorError('observable-unbind-wrong-properties', this); | ||
} | ||
unbindProperties.forEach(propertyName => { | ||
const binding = boundProperties.get(propertyName); | ||
// Nothing to do if the binding is not defined | ||
if (!binding) { | ||
return; | ||
} | ||
if (!Object.keys(toProperties).length) { | ||
boundObservables.delete(toObservable); | ||
this.stopListening(toObservable, 'change'); | ||
} | ||
binding.to.forEach(([toObservable, toProperty]) => { | ||
const toProperties = boundObservables.get(toObservable); | ||
const toPropertyBindings = toProperties[toProperty]; | ||
toPropertyBindings.delete(binding); | ||
if (!toPropertyBindings.size) { | ||
delete toProperties[toProperty]; | ||
} | ||
if (!Object.keys(toProperties).length) { | ||
boundObservables.delete(toObservable); | ||
this.stopListening(toObservable, 'change'); | ||
} | ||
}); | ||
boundProperties.delete(propertyName); | ||
}); | ||
boundProperties.delete(propertyName); | ||
}); | ||
} | ||
else { | ||
boundObservables.forEach((bindings, boundObservable) => { | ||
this.stopListening(boundObservable, 'change'); | ||
}); | ||
boundObservables.clear(); | ||
boundProperties.clear(); | ||
} | ||
} | ||
else { | ||
boundObservables.forEach((bindings, boundObservable) => { | ||
this.stopListening(boundObservable, 'change'); | ||
decorate(methodName) { | ||
initObservable(this); | ||
const originalMethod = this[methodName]; | ||
if (!originalMethod) { | ||
/** | ||
* Cannot decorate an undefined method. | ||
* | ||
* @error observablemixin-cannot-decorate-undefined | ||
* @param {Object} object The object which method should be decorated. | ||
* @param {String} methodName Name of the method which does not exist. | ||
*/ | ||
throw new CKEditorError('observablemixin-cannot-decorate-undefined', this, { object: this, methodName }); | ||
} | ||
this.on(methodName, (evt, args) => { | ||
evt.return = originalMethod.apply(this, args); | ||
}); | ||
boundObservables.clear(); | ||
boundProperties.clear(); | ||
this[methodName] = function (...args) { | ||
return this.fire(methodName, args); | ||
}; | ||
this[methodName][decoratedOriginal] = originalMethod; | ||
if (!this[decoratedMethods]) { | ||
this[decoratedMethods] = []; | ||
} | ||
this[decoratedMethods].push(methodName); | ||
} | ||
}, | ||
/** | ||
* @inheritDoc | ||
*/ | ||
decorate(methodName) { | ||
initObservable(this); | ||
const originalMethod = this[methodName]; | ||
if (!originalMethod) { | ||
/** | ||
* Cannot decorate an undefined method. | ||
* | ||
* @error observablemixin-cannot-decorate-undefined | ||
* @param {Object} object The object which method should be decorated. | ||
* @param {String} methodName Name of the method which does not exist. | ||
*/ | ||
throw new CKEditorError('observablemixin-cannot-decorate-undefined', this, { object: this, methodName }); | ||
// Override the EmitterMixin stopListening method to be able to clean (and restore) decorated methods. | ||
// This is needed in case of: | ||
// 1. Have x.foo() decorated. | ||
// 2. Call x.stopListening() | ||
// 3. Call x.foo(). Problem: nothing happens (the original foo() method is not executed) | ||
stopListening(emitter, event, callback) { | ||
// Removing all listeners so let's clean the decorated methods to the original state. | ||
if (!emitter && this[decoratedMethods]) { | ||
for (const methodName of this[decoratedMethods]) { | ||
this[methodName] = this[methodName][decoratedOriginal]; | ||
} | ||
delete this[decoratedMethods]; | ||
} | ||
Emitter.prototype.stopListening.call(this, emitter, event, callback); | ||
} | ||
this.on(methodName, (evt, args) => { | ||
evt.return = originalMethod.apply(this, args); | ||
}); | ||
this[methodName] = function (...args) { | ||
return this.fire(methodName, args); | ||
}; | ||
this[methodName][decoratedOriginal] = originalMethod; | ||
if (!this[decoratedMethods]) { | ||
this[decoratedMethods] = []; | ||
} | ||
this[decoratedMethods].push(methodName); | ||
}, | ||
...EmitterMixin | ||
}; | ||
// Override the EmitterMixin stopListening method to be able to clean (and restore) decorated methods. | ||
// This is needed in case of: | ||
// 1. Have x.foo() decorated. | ||
// 2. Call x.stopListening() | ||
// 3. Call x.foo(). Problem: nothing happens (the original foo() method is not executed) | ||
ObservableMixin.stopListening = function (emitter, event, callback) { | ||
// Removing all listeners so let's clean the decorated methods to the original state. | ||
if (!emitter && this[decoratedMethods]) { | ||
for (const methodName of this[decoratedMethods]) { | ||
this[methodName] = this[methodName][decoratedOriginal]; | ||
} | ||
delete this[decoratedMethods]; | ||
} | ||
EmitterMixin.stopListening.call(this, emitter, event, callback); | ||
}; | ||
export default ObservableMixin; | ||
return Mixin; | ||
} | ||
export const Observable = ObservableMixin(Emitter); | ||
// Backward compatibility with `mix` | ||
([ | ||
'set', 'bind', 'unbind', 'decorate', | ||
'on', 'once', 'off', 'listenTo', | ||
'stopListening', 'fire', 'delegate', 'stopDelegating', | ||
'_addEventListener', '_removeEventListener' | ||
]).forEach(key => { | ||
ObservableMixin[key] = Observable.prototype[key]; | ||
}); | ||
// Init symbol properties needed for the observable mechanism to work. | ||
@@ -244,0 +243,0 @@ // |
@@ -5,3 +5,2 @@ /** | ||
*/ | ||
/* globals window */ | ||
/* eslint-disable no-var */ | ||
@@ -12,5 +11,6 @@ /** | ||
import CKEditorError from './ckeditorerror'; | ||
import global from './dom/global'; | ||
/* istanbul ignore else */ | ||
if (!window.CKEDITOR_TRANSLATIONS) { | ||
window.CKEDITOR_TRANSLATIONS = {}; | ||
if (!global.window.CKEDITOR_TRANSLATIONS) { | ||
global.window.CKEDITOR_TRANSLATIONS = {}; | ||
} | ||
@@ -69,11 +69,11 @@ /** | ||
* function addTranslations( language, translations, getPluralForm ) { | ||
* if ( !window.CKEDITOR_TRANSLATIONS ) { | ||
* window.CKEDITOR_TRANSLATIONS = {}; | ||
* if ( !global.window.CKEDITOR_TRANSLATIONS ) { | ||
* global.window.CKEDITOR_TRANSLATIONS = {}; | ||
* } | ||
* if ( !window.CKEDITOR_TRANSLATIONS[ language ] ) { | ||
* window.CKEDITOR_TRANSLATIONS[ language ] = {}; | ||
* if ( !global.window.CKEDITOR_TRANSLATIONS[ language ] ) { | ||
* global.window.CKEDITOR_TRANSLATIONS[ language ] = {}; | ||
* } | ||
* | ||
* const languageTranslations = window.CKEDITOR_TRANSLATIONS[ language ]; | ||
* const languageTranslations = global.window.CKEDITOR_TRANSLATIONS[ language ]; | ||
* | ||
@@ -94,6 +94,6 @@ * languageTranslations.dictionary = languageTranslations.dictionary || {}; | ||
export function add(language, translations, getPluralForm) { | ||
if (!window.CKEDITOR_TRANSLATIONS[language]) { | ||
window.CKEDITOR_TRANSLATIONS[language] = {}; | ||
if (!global.window.CKEDITOR_TRANSLATIONS[language]) { | ||
global.window.CKEDITOR_TRANSLATIONS[language] = {}; | ||
} | ||
const languageTranslations = window.CKEDITOR_TRANSLATIONS[language]; | ||
const languageTranslations = global.window.CKEDITOR_TRANSLATIONS[language]; | ||
languageTranslations.dictionary = languageTranslations.dictionary || {}; | ||
@@ -149,3 +149,3 @@ languageTranslations.getPluralForm = getPluralForm || languageTranslations.getPluralForm; | ||
// This can't be done in the `Locale` class, because the translations comes after the `Locale` class initialization. | ||
language = Object.keys(window.CKEDITOR_TRANSLATIONS)[0]; | ||
language = Object.keys(global.window.CKEDITOR_TRANSLATIONS)[0]; | ||
} | ||
@@ -160,4 +160,4 @@ const messageId = message.id || message.string; | ||
} | ||
const dictionary = window.CKEDITOR_TRANSLATIONS[language].dictionary; | ||
const getPluralForm = window.CKEDITOR_TRANSLATIONS[language].getPluralForm || (n => n === 1 ? 0 : 1); | ||
const dictionary = global.window.CKEDITOR_TRANSLATIONS[language].dictionary; | ||
const getPluralForm = global.window.CKEDITOR_TRANSLATIONS[language].getPluralForm || (n => n === 1 ? 0 : 1); | ||
const translation = dictionary[messageId]; | ||
@@ -177,11 +177,11 @@ if (typeof translation === 'string') { | ||
export function _clear() { | ||
window.CKEDITOR_TRANSLATIONS = {}; | ||
global.window.CKEDITOR_TRANSLATIONS = {}; | ||
} | ||
// Checks whether the dictionary exists and translation in that dictionary exists. | ||
function hasTranslation(language, messageId) { | ||
return (!!window.CKEDITOR_TRANSLATIONS[language] && | ||
!!window.CKEDITOR_TRANSLATIONS[language].dictionary[messageId]); | ||
return (!!global.window.CKEDITOR_TRANSLATIONS[language] && | ||
!!global.window.CKEDITOR_TRANSLATIONS[language].dictionary[messageId]); | ||
} | ||
function getNumberOfLanguages() { | ||
return Object.keys(window.CKEDITOR_TRANSLATIONS).length; | ||
return Object.keys(global.window.CKEDITOR_TRANSLATIONS).length; | ||
} |
@@ -10,3 +10,3 @@ /** | ||
import CKEditorError from './ckeditorerror'; | ||
const version = '35.0.1'; | ||
const version = '35.1.0'; | ||
export default version; | ||
@@ -13,0 +13,0 @@ /* istanbul ignore next */ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
268898
0
6021