UNPKG

substance

Version:

Substance is a JavaScript library for web-based content editing. It provides building blocks for realizing custom text editors and web-based publishing system. It is developed to power our online editing platform [Substance](http://substance.io).

217 lines (199 loc) 6.07 kB
import forEach from './forEach' import isObject from './isObject' import hasOwnProperty from './hasOwnProperty' // for debugging const DEBUG = false let count = 0 const COUNT_MSG = '%s listeners registered in the whole system.' /** Event support. */ class EventEmitter { /** Emit an event. @param {String} event @param ...arguments @return true if a listener was notified, false otherwise. */ emit (event) { if (event in this.__events__) { // console.log("Emitting event %s (%d listeners) on", event, this.__events__[event].length, this) // Clone the list of bindings so that handlers can remove or add handlers during the call. var bindings = this.__events__[event].slice() var args = Array.prototype.slice.call(arguments, 1) for (var i = 0, len = bindings.length; i < len; i++) { var binding = bindings[i] // console.log("- triggering %s on %s", event, binding.context.constructor.name) binding.method.apply(binding.context, args) } return true } return false } /** Subscribe a listener to an event. Optionally, a `priority` can be provided to control the order of all bindings. The default priority is 0. All listeners with the same priority remain in order of registration. A lower priority will make the listener be called later, a higher priority earlier. @param {String} event @param {Function} method @param {Object} context */ on (event, method, context) { // TODO: we could add options like 'once' _on.call(this, event, method, context) } /** Unsubscribe a listener from an event. @param {String} event @param {Function} method @param {Object} context */ off (event, method, context) { // eslint-disable-line no-unused-vars if (arguments.length === 1 && isObject(arguments[0])) { _disconnect.call(this, arguments[0]) } else { _off.apply(this, arguments) } } _debugEvents () { /* eslint-disable no-console */ console.log('### EventEmitter: ', this) forEach(this.__events__, (handlers, name) => { console.log('- %s listeners for %s: ', handlers.length, name, handlers) }) /* eslint-enable no-console */ } get __events__ () { if (!this.___events___) { this.___events___ = {} } return this.___events___ } } /* Internal implementation for registering a listener. @param {String} event @param {Function} method @param {Object} context */ function _on (event, method, context) { /* eslint-disable no-invalid-this */ var bindings validateMethod(method, context) if (hasOwnProperty(this.__events__, event)) { bindings = this.__events__[event] } else { // Auto-initialize bindings list bindings = this.__events__[event] = [] } // Add binding bindings.push({ method: method, context: context || null }) if (DEBUG) { count++ console.info('_on()', event, method.name, context, this) console.info(COUNT_MSG, count) } return this /* eslint-enable no-invalid-this */ } /* Remove a listener. @param {String} event @param {Function} method @param {Object} context */ function _off (event, method, context) { /* eslint-disable no-invalid-this */ if (arguments.length === 0) { if (DEBUG) { forEach(this.__events__, (bindings) => { bindings.forEach((b) => { console.info('_off()', b.method.name, b.context, this) }) count -= bindings.length }) console.info(COUNT_MSG, count) } this.___events___ = {} return this } if (arguments.length === 1) { // Remove all bindings for event if (DEBUG) { count -= (this.__events__[event] || []).length console.info(COUNT_MSG, count) } delete this.__events__[event] return this } validateMethod(method, context) if (!(event in this.__events__) || !this.__events__[event].length) { if (DEBUG) console.info('NO MATCHING BINDINGS') // No matching bindings return this } // Default to null context if (arguments.length < 3) { context = null } // Remove matching handlers const bindings = this.__events__[event] for (let i = bindings.length - 1; i >= 0; i--) { const b = bindings[i] if (b.method === method && b.context === context) { bindings.splice(i, 1) if (DEBUG) count-- } } // Cleanup if now empty if (bindings.length === 0) { delete this.__events__[event] } if (DEBUG) console.info(COUNT_MSG, count) return this /* eslint-enable no-invalid-this */ } // removes a listener from all events function _disconnect (context) { /* eslint-disable no-invalid-this */ // Remove all connections to the context forEach(this.__events__, (bindings, event) => { for (let i = bindings.length - 1; i >= 0; i--) { // bindings[i] may have been removed by the previous steps // so check it still exists if (bindings[i] && bindings[i].context === context) { _off.call(this, event, bindings[i].method, context) } } }) return this /* eslint-enable no-invalid-this */ } function validateMethod (method, context) { // Validate method and context if (typeof method === 'string') { // Validate method if (context === undefined || context === null) { throw new Error('Method name "' + method + '" has no context.') } if (!(method in context)) { // Technically the method does not need to exist yet: it could be // added before call time. But this probably signals a typo. throw new Error('Method not found: "' + method + '"') } if (typeof context[method] !== 'function') { // Technically the property could be replaced by a function before // call time. But this probably signals a typo. throw new Error('Property "' + method + '" is not a function') } } else if (typeof method !== 'function') { throw new Error('Invalid callback. Function or method name expected.') } } export default EventEmitter