diff --git a/.docs/categories.json b/.docs/categories.json index 664ace6eb9..e8e6501856 100644 --- a/.docs/categories.json +++ b/.docs/categories.json @@ -181,6 +181,10 @@ "name": "jQuery", "classes": ["jQuery", "jQuery.Event", "jQuery.Promise", "jQuery.Deferred", "jQuery.jqXHR", "QUnit"] }, + { + "name": "OOJS", + "classes": ["OO", "OO.*"] + }, { "name": "JavaScript", "classes": [ diff --git a/.docs/config.json b/.docs/config.json index 0076e770a2..ec4084c24c 100644 --- a/.docs/config.json +++ b/.docs/config.json @@ -9,6 +9,7 @@ "--output": "../docs", "--": [ "./external.js", + "../modules/oojs", "../modules/unicodejs", "../modules/ve" ] diff --git a/.jshintrc b/.jshintrc index 1d0a4c3ae4..fe14209a31 100644 --- a/.jshintrc +++ b/.jshintrc @@ -37,6 +37,7 @@ "nomen": true, "predef": [ + "OO", "ve", "unicodeJS", "QUnit" diff --git a/VisualEditor.php b/VisualEditor.php index 08058e60e6..feaa99e0c6 100644 --- a/VisualEditor.php +++ b/VisualEditor.php @@ -80,6 +80,11 @@ $wgResourceModules += array( 'jquery/jquery.visibleText.js', ), ), + 'oojs' => $wgVisualEditorResourceTemplate + array( + 'scripts' => array( + 'oojs/oo.js', + ), + ), 'unicodejs.wordbreak' => $wgVisualEditorResourceTemplate + array( 'scripts' => array( 'unicodejs/unicodejs.js', @@ -202,6 +207,7 @@ $wgResourceModules += array( 've/ve.debug.js', ), 'dependencies' => array( + 'oojs', 'unicodejs.wordbreak', ), ), diff --git a/demos/ve/index.php b/demos/ve/index.php index f759d68fbe..93eafa1209 100644 --- a/demos/ve/index.php +++ b/demos/ve/index.php @@ -80,6 +80,7 @@ $html = file_get_contents( $page ); + diff --git a/maintenance/makeStaticLoader.php b/maintenance/makeStaticLoader.php index 1932ede6d6..f7ae144c87 100644 --- a/maintenance/makeStaticLoader.php +++ b/maintenance/makeStaticLoader.php @@ -84,6 +84,7 @@ class MakeStaticLoader extends Maintenance { 'scripts' => array( 'jquery/jquery.js', 'jquery/jquery.client.js', + 'oojs/oo.js', 'rangy/rangy-core-1.3.js', 'rangy/rangy-position-1.3.js', 'unicodejs/unicodejs.js', diff --git a/modules/oojs/oo.js b/modules/oojs/oo.js new file mode 100644 index 0000000000..963d0b8405 --- /dev/null +++ b/modules/oojs/oo.js @@ -0,0 +1,530 @@ +/*! + * Object Oriented JavaScript Library v1.0.1 + * https://github.com/trevorparscal/oojs + * + * Copyright 2011-2013 OOJS Team and other contributors. + * Released under the MIT license + * http://oojs.mit-license.org + * + * Date: Thu Jun 06 2013 17:25:38 GMT+0200 (CEST) + */ +( function ( global ) { + +'use strict'; +var + /** + * Namespace for all classes, static methods and static properties. + * @class OO + * @singleton + */ + oo = {}, + hasOwn = oo.hasOwnProperty, + toString = oo.toString; + +/* Class Methods */ + + +/** + * Assert whether a value is a plain object or not. + * + * @method + * @param {Mixed} obj + * @return {boolean} + */ +oo.isPlainObject = function ( obj ) { + // Any object or value whose internal [[Class]] property is not "[object Object]" + if ( toString.call( obj ) !== '[object Object]' ) { + return false; + } + + // The try/catch suppresses exceptions thrown when attempting to access + // the "constructor" property of certain host objects suich as window.location + // in Firefox < 20 (https://bugzilla.mozilla.org/814622) + try { + if ( obj.constructor && + !hasOwn.call( obj.constructor.prototype, 'isPrototypeOf' ) ) { + return false; + } + } catch ( e ) { + return false; + } + + return true; +}; + +/** + * Utility for common usage of Object#create for inheriting from one + * prototype to another. + * + * Beware: This redefines the prototype, call before setting your prototypes. + * Beware: This redefines the prototype, can only be called once on a function. + * If called multiple times on the same function, the previous prototype is lost. + * This is how prototypal inheritance works, it can only be one straight chain + * (just like classical inheritance in PHP for example). If you need to work with + * multiple constructors consider storing an instance of the other constructor in a + * property instead, or perhaps use a mixin (see oo.mixinClass). + * + * function Foo() {} + * Foo.prototype.jump = function () {}; + * + * function FooBar() {} + * oo.inheritClass( FooBar, Foo ); + * FooBar.prop.feet = 2; + * FooBar.prototype.walk = function () {}; + * + * function FooBarQuux() {} + * OO.inheritClass( FooBarQuux, FooBar ); + * FooBarQuux.prototype.jump = function () {}; + * + * FooBarQuux.prop.feet === 2; + * var fb = new FooBar(); + * fb.jump(); + * fb.walk(); + * fb instanceof Foo && fb instanceof FooBar && fb instanceof FooBarQuux; + * + * @method + * @param {Function} targetFn + * @param {Function} originFn + * @throws {Error} If target already inherits from origin + */ +oo.inheritClass = function ( targetFn, originFn ) { + if ( targetFn.prototype instanceof originFn ) { + throw new Error( 'Target already inherits from origin' ); + } + + var targetConstructor = targetFn.prototype.constructor; + + targetFn.prototype = Object.create( originFn.prototype ); + + // Restore constructor property of targetFn + targetFn.prototype.constructor = targetConstructor; + + // Extend static properties - always initialize both sides + originFn.static = originFn.static || {}; + targetFn.static = Object.create( originFn.static ); + + // Copy mixin tracking + targetFn.mixins = originFn.mixins ? originFn.mixins.slice( 0 ) : []; +}; + +/** + * Utility to copy over *own* prototype properties of a mixin. + * The 'constructor' (whether implicit or explicit) is not copied over. + * + * This does not create inheritance to the origin. If inheritance is needed + * use oo.inheritClass instead. + * + * Beware: This can redefine a prototype property, call before setting your prototypes. + * Beware: Don't call before oo.inheritClass. + * + * function Foo() {} + * function Context() {} + * + * // Avoid repeating this code + * function ContextLazyLoad() {} + * ContextLazyLoad.prototype.getContext = function () { + * if ( !this.context ) { + * this.context = new Context(); + * } + * return this.context; + * }; + * + * function FooBar() {} + * OO.inheritClass( FooBar, Foo ); + * OO.mixinClass( FooBar, ContextLazyLoad ); + * + * @method + * @param {Function} targetFn + * @param {Function} originFn + */ +oo.mixinClass = function ( targetFn, originFn ) { + var key; + + // Copy prototype properties + for ( key in originFn.prototype ) { + if ( key !== 'constructor' && hasOwn.call( originFn.prototype, key ) ) { + targetFn.prototype[key] = originFn.prototype[key]; + } + } + + // Copy static properties - always initialize both sides + targetFn.static = targetFn.static || {}; + if ( originFn.static ) { + for ( key in originFn.static ) { + if ( hasOwn.call( originFn.static, key ) ) { + targetFn.static[key] = originFn.static[key]; + } + } + } else { + originFn.static = {}; + } +}; + +/* Object Methods */ + +/** + * Create a new object that is an instance of the same + * constructor as the input, inherits from the same object + * and contains the same own properties. + * + * This makes a shallow non-recursive copy of own properties. + * To create a recursive copy of plain objects, use #copy. + * + * var foo = new Person( mom, dad ); + * foo.setAge( 21 ); + * var foo2 = OO.cloneObject( foo ); + * foo.setAge( 22 ); + * + * // Then + * foo2 !== foo; // true + * foo2 instanceof Person; // true + * foo2.getAge(); // 21 + * foo.getAge(); // 22 + * + * @method + * @param {Object} origin + * @return {Object} Clone of origin + */ +oo.cloneObject = function ( origin ) { + var key, r; + + r = Object.create( origin.constructor.prototype ); + + for ( key in origin ) { + if ( hasOwn.call( origin, key ) ) { + r[key] = origin[key]; + } + } + + return r; +}; + +/** + * Gets an array of all property values in an object. + * + * @method + * @param {Object} Object to get values from + * @returns {Array} List of object values + */ +oo.getObjectValues = function ( obj ) { + var key, values; + + if ( obj !== Object( obj ) ) { + throw new TypeError( 'Called on non-object' ); + } + + values = []; + for ( key in obj ) { + if ( hasOwn.call( obj, key ) ) { + values[values.length] = obj[key]; + } + } + + return values; +}; + +/** + * Recursively compares properties between two objects. + * + * A false result may be caused by property inequality or by properties in one object missing from + * the other. An asymmetrical test may also be performed, which checks only that properties in the + * first object are present in the second object, but not the inverse. + * + * @method + * @param {Object} a First object to compare + * @param {Object} b Second object to compare + * @param {boolean} [asymmetrical] Whether to check only that b contains values from a + * @returns {boolean} If the objects contain the same values as each other + */ +oo.compare = function ( a, b, asymmetrical ) { + var aValue, bValue, aType, bType, k; + for ( k in a ) { + aValue = a[k]; + bValue = b[k]; + aType = typeof aValue; + bType = typeof bValue; + if ( aType !== bType || + ( ( aType === 'string' || aType === 'number' ) && aValue !== bValue ) || + ( aValue === Object( aValue ) && !oo.compare( aValue, bValue, asymmetrical ) ) ) { + return false; + } + } + // If the check is not asymmetrical, recursing with the arguments swapped will verify our result + return asymmetrical ? true : oo.compare( b, a, true ); +}; + +/** + * Create a plain deep copy of any kind of object. + * + * Copies are deep, and will either be an object or an array depending on `source`. + * + * @method + * @param {Object} source Object to copy + * @param {Function} [callback] Applied to leaf values before they added to the clone + * @returns {Object} Copy of source object + */ +oo.copy = function ( source, callback ) { + var key, sourceValue, sourceType, destination; + + if ( typeof source.clone === 'function' ) { + return source.clone(); + } + + destination = Array.isArray( source ) ? new Array( source.length ) : {}; + + for ( key in source ) { + sourceValue = source[key]; + sourceType = typeof sourceValue; + if ( Array.isArray( sourceValue ) ) { + // Array + destination[key] = oo.copy( sourceValue, callback ); + } else if ( sourceValue && typeof sourceValue.clone === 'function' ) { + // Duck type object with custom clone method + destination[key] = callback ? + callback( sourceValue.clone() ) : sourceValue.clone(); + } else if ( sourceValue && typeof sourceValue.cloneNode === 'function' ) { + // DOM Node + destination[key] = callback ? + callback( sourceValue.cloneNode( true ) ) : sourceValue.cloneNode( true ); + } else if ( oo.isPlainObject( sourceValue ) ) { + // Plain objects + destination[key] = oo.copy( sourceValue, callback ); + } else { + // Non-plain objects (incl. functions) and primitive values + destination[key] = callback ? callback( sourceValue ) : sourceValue; + } + } + + return destination; +}; +/** + * Event emitter. + * + * @class OO.EventEmitter + * @constructor + * @property {Object} bindings + */ +oo.EventEmitter = function OoEventEmitter() { + // Properties + this.bindings = {}; +}; + +/* Methods */ + +/** + * Add a listener to events of a specific event. + * + * @method + * @param {string} event Type of event to listen to + * @param {Function} callback Function to call when event occurs + * @param {Array} [args] Arguments to pass to listener, will be prepended to emitted arguments + * @param {Object} [context=null] Object to use as context for callback function or call method on + * @throws {Error} Listener argument is not a function or method name + * @chainable + */ +oo.EventEmitter.prototype.on = function ( event, callback, args, context ) { + // Validate callback + if ( typeof callback !== 'function' ) { + throw new Error( 'Invalid callback. Function or method name expected.' ); + } + + // Auto-initialize binding + if ( !( event in this.bindings ) ) { + this.bindings[event] = []; + } + + // Add binding + this.bindings[event].push( { + 'callback': callback, + 'args': args, + 'context': context || null + } ); + return this; +}; + +/** + * Adds a one-time listener to a specific event. + * + * @method + * @param {string} event Type of event to listen to + * @param {Function} listener Listener to call when event occurs + * @chainable + */ +oo.EventEmitter.prototype.once = function ( event, listener ) { + var eventEmitter = this; + return this.on( event, function listenerWrapper() { + eventEmitter.off( event, listenerWrapper ); + listener.apply( eventEmitter, Array.prototype.slice.call( arguments, 0 ) ); + } ); +}; + +/** + * Remove a specific listener from a specific event. + * + * @method + * @param {string} event Type of event to remove listener from + * @param {Function} [callback] Listener to remove, omit to remove all + * @chainable + * @throws {Error} Listener argument is not a function + */ +oo.EventEmitter.prototype.off = function ( event, callback ) { + var i, bindings; + + if ( arguments.length === 1 ) { + // Remove all bindings for event + if ( event in this.bindings ) { + delete this.bindings[event]; + } + } else { + if ( typeof callback !== 'function' ) { + throw new Error( 'Invalid callback. Function expected.' ); + } + if ( !( event in this.bindings ) || !this.bindings[event].length ) { + // No matching bindings + return this; + } + // Remove matching handlers + bindings = this.bindings[event]; + i = bindings.length; + while ( i-- ) { + if ( bindings[i].callback === callback ) { + bindings.splice( i, 1 ); + } + } + // Cleanup if now empty + if ( bindings.length === 0 ) { + delete this.bindings[event]; + } + } + return this; +}; + +/** + * Emit an event. + * TODO: Should this be chainable? What is the usefulness of the boolean + * return value here? + * + * @method + * @param {string} event Type of event + * @param {Mixed} args First in a list of variadic arguments passed to event handler (optional) + * @returns {boolean} If event was handled by at least one listener + */ +oo.EventEmitter.prototype.emit = function ( event ) { + var i, len, binding, bindings, args; + + if ( event in this.bindings ) { + // Slicing ensures that we don't get tripped up by event handlers that add/remove bindings + bindings = this.bindings[event].slice(); + args = Array.prototype.slice.call( arguments, 1 ); + for ( i = 0, len = bindings.length; i < len; i++ ) { + binding = bindings[i]; + binding.callback.apply( + binding.context, + binding.args ? binding.args.concat( args ) : args + ); + } + return true; + } + return false; +}; + +/** + * Connect event handlers to an object. + * + * @method + * @param {Object} context Object to call methods on when events occur + * @param {Object.|Object.|Object.} methods List of + * event bindings keyed by event name containing either method names, functions or arrays containing + * method name or function followed by a list of arguments to be passed to callback before emitted + * arguments + * @chainable + */ +oo.EventEmitter.prototype.connect = function ( context, methods ) { + var method, callback, args, event; + + for ( event in methods ) { + method = methods[event]; + // Allow providing additional args + if ( Array.isArray( method ) ) { + args = method.slice( 1 ); + method = method[0]; + } else { + args = []; + } + // Allow callback to be a method name + if ( typeof method === 'string' ) { + // Validate method + if ( !context[method] || typeof context[method] !== 'function' ) { + throw new Error( 'Method not found: ' + method ); + } + // Resolve to function + callback = context[method]; + } else { + callback = method; + } + // Add binding + this.on.apply( this, [ event, callback, args, context ] ); + } + return this; +}; + +/** + * Disconnect event handlers from an object. + * + * @method + * @param {Object} context Object to disconnect methods from + * @param {Object.|Object.|Object.} [methods] List of + * event bindings keyed by event name containing either method names or functions + * @chainable + */ +oo.EventEmitter.prototype.disconnect = function ( context, methods ) { + var i, method, callback, event, bindings; + + if ( methods ) { + for ( event in methods ) { + method = methods[event]; + if ( typeof method === 'string' ) { + // Validate method + if ( !context[method] || typeof context[method] !== 'function' ) { + throw new Error( 'Method not found: ' + method ); + } + // Resolve to function + callback = context[method]; + } else { + callback = method; + } + bindings = this.bindings[event]; + i = bindings.length; + while ( i-- ) { + if ( bindings[i].context === context && bindings[i].callback === callback ) { + bindings.splice( i, 1 ); + } + } + if ( bindings.length === 0 ) { + delete this.bindings[event]; + } + } + } else { + for ( event in this.bindings ) { + bindings = this.bindings[event]; + i = bindings.length; + while ( i-- ) { + if ( bindings[i].context === context ) { + bindings.splice( i, 1 ); + } + } + if ( bindings.length === 0 ) { + delete this.bindings[event]; + } + } + } + + return this; +}; +/*jshint node:true */ +if ( typeof module !== 'undefined' && module.exports ) { + module.exports = oo; +} else { + global.OO = oo; +} +}( this ) ); diff --git a/modules/ve/test/index.php b/modules/ve/test/index.php index 2b3338561b..274e8e7146 100644 --- a/modules/ve/test/index.php +++ b/modules/ve/test/index.php @@ -33,6 +33,7 @@ + diff --git a/modules/ve/ve.EventEmitter.js b/modules/ve/ve.EventEmitter.js index 6b02c58184..869647c732 100644 --- a/modules/ve/ve.EventEmitter.js +++ b/modules/ve/ve.EventEmitter.js @@ -9,221 +9,6 @@ * Event emitter. * * @class - * @constructor - * @property {Object} bindings + * @extends OO.EventEmitter */ -ve.EventEmitter = function VeEventEmitter() { - // Properties - this.bindings = {}; -}; - -/* Methods */ - -/** - * Emit an event. - * - * @method - * @param {string} event Type of event - * @param {Mixed} args First in a list of variadic arguments passed to event handler (optional) - * @returns {boolean} If event was handled by at least one listener - */ -ve.EventEmitter.prototype.emit = function ( event ) { - var i, len, binding, bindings, args; - - if ( event in this.bindings ) { - // Slicing ensures that we don't get tripped up by event handlers that add/remove bindings - bindings = this.bindings[event].slice(); - args = Array.prototype.slice.call( arguments, 1 ); - for ( i = 0, len = bindings.length; i < len; i++ ) { - binding = bindings[i]; - binding.callback.apply( - binding.context, - binding.args ? binding.args.concat( args ) : args - ); - } - return true; - } - return false; -}; - -/** - * Add a listener to events of a specific event. - * - * @method - * @param {string} event Type of event to listen to - * @param {Function} callback Function to call when event occurs - * @param {Array} [args] Arguments to pass to listener, will be prepended to emitted arguments - * @param {Object} [context=null] Object to use as context for callback function or call method on - * @throws {Error} Listener argument is not a function or method name - * @chainable - */ -ve.EventEmitter.prototype.on = function ( event, callback, args, context ) { - // Validate callback - if ( typeof callback !== 'function' ) { - throw new Error( 'Invalid callback. Function or method name expected.' ); - } - - // Auto-initialize binding - if ( !( event in this.bindings ) ) { - this.bindings[event] = []; - } - - // Add binding - this.bindings[event].push( { - 'callback': callback, - 'args': args, - 'context': context || null - } ); - return this; -}; - -/** - * Remove a specific listener from a specific event. - * - * @method - * @param {string} event Type of event to remove listener from - * @param {Function} [callback] Listener to remove, omit to remove all - * @chainable - * @throws {Error} Listener argument is not a function - */ -ve.EventEmitter.prototype.off = function ( event, callback ) { - var i, bindings; - - if ( arguments.length === 1 ) { - // Remove all bindings for event - if ( event in this.bindings ) { - delete this.bindings[event]; - } - } else { - if ( typeof callback !== 'function' ) { - throw new Error( 'Invalid callback. Function expected.' ); - } - if ( !( event in this.bindings ) || !this.bindings[event].length ) { - // No matching bindings - return this; - } - // Remove matching handlers - bindings = this.bindings[event]; - i = bindings.length; - while ( i-- ) { - if ( bindings[i].callback === callback ) { - bindings.splice( i, 1 ); - } - } - // Cleanup if now empty - if ( bindings.length === 0 ) { - delete this.bindings[event]; - } - } - return this; -}; - -/** - * Connect event handlers to an object. - * - * @method - * @param {Object} context Object to call methods on when events occur - * @param {Object.|Object.|Object.} methods List of - * event bindings keyed by event name containing either method names, functions or arrays containing - * method name or function followed by a list of arguments to be passed to callback before emitted - * arguments - * @chainable - */ -ve.EventEmitter.prototype.connect = function ( context, methods ) { - var method, callback, args, event; - - for ( event in methods ) { - method = methods[event]; - // Allow providing additional args - if ( ve.isArray( method ) ) { - args = method.slice( 1 ); - method = method[0]; - } else { - args = []; - } - // Allow callback to be a method name - if ( typeof method === 'string' ) { - // Validate method - if ( !context[method] || typeof context[method] !== 'function' ) { - throw new Error( 'Method not found: ' + method ); - } - // Resolve to function - callback = context[method]; - } else { - callback = method; - } - // Add binding - this.on.apply( this, [ event, callback, args, context ] ); - } - return this; -}; - -/** - * Disconnect event handlers from an object. - * - * @method - * @param {Object} context Object to disconnect methods from - * @param {Object.|Object.|Object.} [methods] List of - * event bindings keyed by event name containing either method names or functions - * @chainable - */ -ve.EventEmitter.prototype.disconnect = function ( context, methods ) { - var i, method, callback, event, bindings; - - if ( methods ) { - for ( event in methods ) { - method = methods[event]; - if ( typeof method === 'string' ) { - // Validate method - if ( !context[method] || typeof context[method] !== 'function' ) { - throw new Error( 'Method not found: ' + method ); - } - // Resolve to function - callback = context[method]; - } else { - callback = method; - } - bindings = this.bindings[event]; - i = bindings.length; - while ( i-- ) { - if ( bindings[i].context === context && bindings[i].callback === callback ) { - bindings.splice( i, 1 ); - } - } - if ( bindings.length === 0 ) { - delete this.bindings[event]; - } - } - } else { - for ( event in this.bindings ) { - bindings = this.bindings[event]; - i = bindings.length; - while ( i-- ) { - if ( bindings[i].context === context ) { - bindings.splice( i, 1 ); - } - } - if ( bindings.length === 0 ) { - delete this.bindings[event]; - } - } - } - - return this; -}; - -/** - * Adds a one-time listener to a specific event. - * - * @method - * @param {string} event Type of event to listen to - * @param {Function} listener Listener to call when event occurs - * @chainable - */ -ve.EventEmitter.prototype.once = function ( event, listener ) { - var eventEmitter = this; - return this.on( event, function listenerWrapper() { - eventEmitter.off( event, listenerWrapper ); - listener.apply( eventEmitter, Array.prototype.slice.call( arguments, 0 ) ); - } ); -}; +ve.EventEmitter = OO.EventEmitter; diff --git a/modules/ve/ve.js b/modules/ve/ve.js index 128b60e8b0..d25e4cd002 100644 --- a/modules/ve/ve.js +++ b/modules/ve/ve.js @@ -5,7 +5,7 @@ * @license The MIT License (MIT); see LICENSE.txt */ -( function () { +( function ( oo ) { var ve, hasOwn; /** @@ -28,124 +28,23 @@ * Create an object that inherits from another object. * * @method - * @until ES5: Object.create * @param {Object} origin Object to inherit from * @return {Object} Empty object that inherits from origin */ - ve.createObject = Object.create || function ( origin ) { - function O() {} - O.prototype = origin; - var r = new O(); - - return r; - }; + ve.createObject = Object.create; /** - * Utility for common usage of ve.createObject for inheriting from one - * prototype to another. - * - * Beware: This redefines the prototype, call before setting your prototypes. - * Beware: This redefines the prototype, can only be called once on a function. - * If called multiple times on the same function, the previous prototype is lost. - * This is how prototypal inheritance works, it can only be one straight chain - * (just like classical inheritance in PHP for example). If you need to work with - * multiple constructors consider storing an instance of the other constructor in a - * property instead, or perhaps use a mixin (see ve.mixinClass). - * - * function Foo() {} - * Foo.prototype.jump = function () {}; - * - * function FooBar() {} - * ve.inheritClass( FooBar, Foo ); - * FooBar.prop.feet = 2; - * FooBar.prototype.walk = function () {}; - * - * function FooBarQuux() {} - * ve.inheritClass( FooBarQuux, FooBar ); - * FooBarQuux.prototype.jump = function () {}; - * - * FooBarQuux.prop.feet === 2; - * var fb = new FooBar(); - * fb.jump(); - * fb.walk(); - * fb instanceof Foo && fb instanceof FooBar && fb instanceof FooBarQuux; - * * @method - * @param {Function} targetFn - * @param {Function} originFn - * @throws {Error} If target already inherits from origin + * @inheritdoc OO#inheritClass */ - ve.inheritClass = function ( targetFn, originFn ) { - if ( targetFn.prototype instanceof originFn ) { - throw new Error( 'Target already inherits from origin' ); - } - - var targetConstructor = targetFn.prototype.constructor; - - targetFn.prototype = ve.createObject( originFn.prototype ); - - // Restore constructor property of targetFn - targetFn.prototype.constructor = targetConstructor; - - // Extend static properties - always initialize both sides - originFn.static = originFn.static || {}; - targetFn.static = ve.createObject( originFn.static ); - - // Copy mixin tracking - targetFn.mixins = originFn.mixins ? originFn.mixins.slice( 0 ) : []; - }; + ve.inheritClass = oo.inheritClass; /** - * Utility to copy over *own* prototype properties of a mixin. - * The 'constructor' (whether implicit or explicit) is not copied over. - * - * This does not create inheritance to the origin. If inheritance is needed - * use ve.inheritClass instead. - * - * Beware: This can redefine a prototype property, call before setting your prototypes. - * Beware: Don't call before ve.inheritClass. - * - * function Foo() {} - * function Context() {} - * - * // Avoid repeating this code - * function ContextLazyLoad() {} - * ContextLazyLoad.prototype.getContext = function () { - * if ( !this.context ) { - * this.context = new Context(); - * } - * return this.context; - * }; - * - * function FooBar() {} - * ve.inheritClass( FooBar, Foo ); - * ve.mixinClass( FooBar, ContextLazyLoad ); - * * @method - * @param {Function} targetFn - * @param {Function} originFn + * @inheritdoc OO#mixinClass */ ve.mixinClass = function ( targetFn, originFn ) { - var key; - - // Copy prototype properties - for ( key in originFn.prototype ) { - if ( key !== 'constructor' && hasOwn.call( originFn.prototype, key ) ) { - targetFn.prototype[key] = originFn.prototype[key]; - } - } - - // Copy static properties - always initialize both sides - targetFn.static = targetFn.static || {}; - if ( originFn.static ) { - for ( key in originFn.static ) { - if ( hasOwn.call( originFn.static, key ) ) { - targetFn.static[key] = originFn.static[key]; - } - } - } else { - originFn.static = {}; - } + oo.mixinClass( targetFn, originFn ); // Track mixins targetFn.mixins = targetFn.mixins || []; @@ -168,41 +67,43 @@ }; /** - * Create a new object that is an instance of the same - * constructor as the input, inherits from the same object - * and contains the same own properties. - * - * This makes a shallow non-recursive copy of own properties. - * To create a recursive copy of plain objects, use ve.copyObject. - * - * var foo = new Person( mom, dad ); - * foo.setAge( 21 ); - * var foo2 = ve.cloneObject( foo ); - * foo.setAge( 22 ); - * - * // Then - * foo2 !== foo; // true - * foo2 instanceof Person; // true - * foo2.getAge(); // 21 - * foo.getAge(); // 22 + * @method + * @inheritdoc OO#cloneObject + */ + ve.cloneObject = oo.cloneObject; + + /** + * @method + * @inheritdoc OO#cloneObject + */ + ve.getObjectValues = oo.getObjectValues; + + /** + * Gets an array of all property names in an object. * * @method - * @param {Object} origin - * @return {Object} Clone of origin + * @param {Object} Object to get properties from + * @returns {string[]} List of object keys */ - ve.cloneObject = function ( origin ) { - var key, r; + ve.getObjectKeys = Object.keys; - r = ve.createObject( origin.constructor.prototype ); + /** + * @method + * @inheritdoc OO#compare + */ + ve.compare = oo.compare; - for ( key in origin ) { - if ( hasOwn.call( origin, key ) ) { - r[key] = origin[key]; - } - } + /** + * @method + * @inheritdoc OO#copy + */ + ve.copyArray = oo.copy; - return r; - }; + /** + * @method + * @inheritdoc OO#copy + */ + ve.copyObject = oo.copy; /** * Check to see if an object is a plain object (created using "{}" or "new Object"). @@ -448,149 +349,6 @@ } }; - /** - * Gets an array of all property names in an object. - * - * This falls back to the native impelentation of Object.keys if available. - * Performance optimization: http://jsperf.com/object-keys-shim-perf#/fnHasown_fnForIfcallLength - * - * @method - * @until ES5 - * @param {Object} Object to get properties from - * @returns {string[]} List of object keys - */ - ve.getObjectKeys = Object.keys || function ( obj ) { - var key, keys; - - if ( Object( obj ) !== obj ) { - throw new TypeError( 'Called on non-object' ); - } - - keys = []; - for ( key in obj ) { - if ( hasOwn.call( obj, key ) ) { - keys[keys.length] = key; - } - } - - return keys; - }; - - /** - * Gets an array of all property values in an object. - * - * @method - * @param {Object} Object to get values from - * @returns {Array} List of object values - */ - ve.getObjectValues = function ( obj ) { - var key, values; - - if ( Object( obj ) !== obj ) { - throw new TypeError( 'Called on non-object' ); - } - - values = []; - for ( key in obj ) { - if ( hasOwn.call( obj, key ) ) { - values[values.length] = obj[key]; - } - } - - return values; - }; - - /** - * Recursively compares values between two objects or arrays. - * - * A false result may be caused by property inequality or by properties in one object missing - * from the other. An asymmetrical test may also be performed, which checks only that properties - * in the first object are present in the second object, but not the inverse. - * - * @method - * @param {Object} a First object to compare - * @param {Object} b Second object to compare - * @param {boolean} [asymmetrical] Whether to check only that b contains values from a - * @returns {boolean} If the objects contain the same values as each other - */ - ve.compare = function ( a, b, asymmetrical ) { - var aValue, bValue, aType, bType, k; - for ( k in a ) { - aValue = a[k]; - bValue = b[k]; - aType = typeof aValue; - bType = typeof bValue; - if ( aType !== bType || - ( ( aType === 'string' || aType === 'number' ) && aValue !== bValue ) || - ( aValue === Object( aValue ) && !ve.compare( aValue, bValue ) ) ) { - return false; - } - } - // If the check is not asymmetrical, recursing with the arguments swapped will verify our result - return asymmetrical ? true : ve.compare( b, a, true ); - }; - - /** - * Gets a deep copy of an array's string, number, array, plain-object and cloneable object contents. - * - * @method - * @param {Array} source Array to copy - * @param {Function} [callback] Applied to leaf values before they added to the clone - * @returns {Array} Copy of source array - */ - ve.copyArray = function ( source, callback ) { - var i, sourceValue, sourceType, - destination = []; - for ( i = 0; i < source.length; i++ ) { - sourceValue = source[i]; - sourceType = typeof sourceValue; - if ( ve.isPlainObject( sourceValue ) ) { - destination.push( ve.copyObject( sourceValue, callback ) ); - } else if ( ve.isArray( sourceValue ) ) { - destination.push( ve.copyArray( sourceValue, callback ) ); - } else if ( sourceValue && typeof sourceValue.clone === 'function' ) { - destination.push( callback ? callback( sourceValue.clone() ) : sourceValue.clone() ); - } else if ( sourceValue && typeof sourceValue.cloneNode === 'function' ) { - destination.push( callback ? callback( sourceValue.cloneNode( true ) ) : sourceValue.cloneNode( true ) ); - } else { - destination.push( callback ? callback( sourceValue ) : sourceValue ); - } - } - return destination; - }; - - /** - * Gets a deep copy of an object's string, number, array and plain-object properties. - * - * @method - * @param {Object} source Object to copy - * @param {Function} [callback] Applied to leaf values before they added to the clone - * @returns {Object} Copy of source object - */ - ve.copyObject = function ( source, callback ) { - var key, sourceValue, sourceType, - destination = {}; - if ( typeof source.clone === 'function' ) { - return source.clone(); - } - for ( key in source ) { - sourceValue = source[key]; - sourceType = typeof sourceValue; - if ( ve.isPlainObject( sourceValue ) ) { - destination[key] = ve.copyObject( sourceValue, callback ); - } else if ( ve.isArray( sourceValue ) ) { - destination[key] = ve.copyArray( sourceValue, callback ); - } else if ( sourceValue && typeof sourceValue.clone === 'function' ) { - destination[key] = callback ? callback( sourceValue.clone() ) : sourceValue.clone(); - } else if ( sourceValue && typeof sourceValue.cloneNode === 'function' ) { - destination[key] = callback ? callback( sourceValue.cloneNode( true ) ) : sourceValue.cloneNode( true ); - } else { - destination[key] = callback ? callback( sourceValue ) : sourceValue; - } - } - return destination; - }; - /** * Splice one array into another. * @@ -1163,4 +921,4 @@ // Expose window.ve = ve; -}() ); +}( OO ) );