├── .gitignore ├── .travis.yml ├── Gruntfile.js ├── LICENSE ├── README.md ├── benchmarks ├── creation.js ├── definition.js ├── extension.js ├── libs │ └── Class.Resig.js └── super.js ├── package.json ├── source ├── Class.js └── examples │ ├── _shared │ └── css │ │ └── styles.css │ ├── amd │ ├── css │ │ └── logger.css │ ├── index.html │ └── modules │ │ ├── Logger.js │ │ └── main.js │ ├── logger │ ├── ColorLogger.js │ ├── Colorizer.js │ ├── Logger.js │ └── index.js │ └── widgets │ ├── css │ └── alert.css │ ├── index.html │ └── js │ ├── Alert.js │ ├── Base.js │ ├── Events.js │ └── Widget.js └── test ├── 1.creation.js ├── 2.identity.js ├── 3.extension.js ├── 4.lifecycle.js ├── 5.super.js ├── 6.staticProps.js └── 7.properties.js /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /node_modules/ 3 | /misc/ 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | script: grunt travis 5 | before_install: 6 | - 'npm install -g grunt-cli' 7 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.initConfig({ 3 | pkg: grunt.file.readJSON('package.json'), 4 | clean: { 5 | build: 'build/' 6 | }, 7 | copy: { 8 | js: { 9 | expand: true, 10 | flatten: true, 11 | isFile: true, 12 | src: ['source/*.js'], 13 | dest: 'build/' 14 | }, 15 | examples: { 16 | expand: true, 17 | cwd: 'source/examples/', 18 | src: ['**'], 19 | dest: 'build/examples/' 20 | } 21 | }, 22 | uglify: { 23 | options: { 24 | banner: '/*! <%= pkg.name %> - v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>\n' + 25 | '* <%= pkg.homepage %>/\n' + 26 | '* Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author %>; Licensed <%= pkg.license %> */\n', 27 | mangle: { 28 | except: ['_super'] 29 | } 30 | }, 31 | dist: { 32 | files: { 33 | 'build/Class.min.js': ['build/Class.js'] 34 | } 35 | } 36 | }, 37 | jshint: { 38 | files: ['Gruntfile.js', 'source/**/*.js'], 39 | options: { 40 | globals: { 41 | eqeqeq: true 42 | } 43 | } 44 | }, 45 | mochacov: { 46 | coverage: { 47 | options: { 48 | coveralls: true 49 | } 50 | }, 51 | test: { 52 | options: { 53 | reporter: 'spec', 54 | checkLeaks: true 55 | } 56 | }, 57 | options: { 58 | files: 'test/*.js' 59 | } 60 | }, 61 | watch: { 62 | files: ['source/**', 'test/**'], 63 | tasks: ['jshint', 'test'] 64 | }, 65 | bench: { 66 | all: { 67 | src: ['benchmarks/*.js'], 68 | dest: 'build/results/benchmark.csv' 69 | }, 70 | creation: { 71 | src: ['benchmarks/creation.js'], 72 | dest: 'build/results/benchmark_creation.csv' 73 | }, 74 | definition: { 75 | src: ['benchmarks/definition.js'], 76 | dest: 'build/results/benchmark_definition.csv' 77 | }, 78 | extension: { 79 | src: ['benchmarks/extension.js'], 80 | dest: 'build/results/benchmark_extension.csv' 81 | }, 82 | super: { 83 | src: ['benchmarks/super.js'], 84 | dest: 'build/results/benchmark_super.csv' 85 | } 86 | } 87 | }); 88 | 89 | grunt.loadNpmTasks('grunt-contrib-uglify'); 90 | grunt.loadNpmTasks('grunt-contrib-clean'); 91 | grunt.loadNpmTasks('grunt-contrib-jshint'); 92 | grunt.loadNpmTasks('grunt-contrib-watch'); 93 | grunt.loadNpmTasks('grunt-contrib-copy'); 94 | grunt.loadNpmTasks('grunt-mocha-cov'); 95 | grunt.loadNpmTasks('grunt-benchmark'); 96 | 97 | grunt.renameTask('benchmark', 'bench'); 98 | grunt.registerTask('benchmark', 'bench:all'); 99 | 100 | grunt.registerTask('travis', [ 'jshint', 'mochacov:coverage' ]); 101 | grunt.registerTask('test', [ 'jshint', 'mochacov:test' ]); 102 | grunt.registerTask('default', [ 'clean', 'test', 'copy', 'uglify' ]); 103 | }; 104 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2013, Lawrence Davis 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PseudoClass [![NPM version][npm-image]][npm-url] [![Build status][travis-image]][travis] [![Code coverage status][coveralls-image]][coveralls] 2 | > An OOP framework for Node.js and the browser 3 | 4 | #### Sweet syntactic sugar for prototypal inheritance. 5 | PseudoClass provides `construct()`, `destruct()`, `_super()`, and an `init()` method that runs after construction is complete. 6 | 7 | #### All the same under the hood. 8 | PseudoClass uses JavaScript constructors and prototypal inheritance under the hood. Monkey-patching, `instanceof`, and `instance.constructor` all work as expected. 9 | 10 | #### Not afraid to mix it up. 11 | Mixins can be added when a class is declared using the `mixins` option or after instantiation with the `instance.mixin()` method. 12 | 13 | #### Crushes boilerplate with a classy touch. 14 | Stay classy and boilerplate-free with string-based `toString` declarations and automatic chaining of `construct()` and `destruct()`. 15 | 16 | #### Define and override properties effortlessly. 17 | Make instance properties non-writable, non-enumerable, or employ setters & getters with the `properties` option, then inherit and override individually. 18 | 19 | 20 | ## Dependencies 21 | 22 | PseudoClass is completely standalone. All you need to stay classy is [`Class.js`][Class.min.js]. 23 | 24 | 25 | ## Compatibility 26 | 27 | As PseudoClass makes use of ECMAScript 5 features, it is only compatible with modern browsers. 28 | 29 | * IE 9+ 30 | * Firefox 4+ 31 | * Chrome 6+ 32 | * Safari 5+ 33 | * Opera 12+ 34 | * Node 0.8+ 35 | 36 | PseudoClass can be used in a Node, AMD, or browser environment out of the box. 37 | 38 | ## Usage 39 | 40 | PseudoClass empowers you without getting in your way. See the examples below to see how PseudoClass makes prototypal inheritance painless. 41 | 42 | 43 | ### Define a class 44 | 45 | ```javascript 46 | var Parent = Class({ 47 | toString: 'Parent', 48 | properties: { 49 | visible: { 50 | value: true, 51 | enumerable: true 52 | } 53 | }, 54 | construct: function() { 55 | console.log('Parent: Constructing'); 56 | }, 57 | destruct: function() { 58 | console.log('Parent: Destructing'); 59 | }, 60 | doStuff: function() { 61 | console.log('Parent: Doing stuff'); 62 | } 63 | }); 64 | ``` 65 | 66 | 67 | ### Define a mixin 68 | 69 | A mixin is a set methods you can plug into any class. Mixins can use `_super`, just like normal methods. 70 | 71 | ```javascript 72 | var stuffDoer = { 73 | doStuff: function() { 74 | this._super(); 75 | console.log('Mixin: Doing stuff'); 76 | } 77 | }; 78 | ``` 79 | 80 | 81 | ### Inherit from Parent and add a mixin to the class prototype 82 | 83 | Mixins added at declaration time become part of the prototype. 84 | 85 | ```javascript 86 | var Child = Parent.extend({ 87 | toString: 'Child', 88 | mixins: [stuffDoer], 89 | properties: { 90 | visible: { 91 | value: false // Only override the value 92 | } 93 | }, 94 | construct: function() { 95 | console.log(this+': Constructing'); 96 | }, 97 | destruct: function() { 98 | console.log(this+': Destructing'); 99 | }, 100 | doStuff: function() { 101 | this._super(); 102 | console.log(this+': Doing stuff'); 103 | } 104 | }); 105 | ``` 106 | 107 | 108 | ### Create an instance 109 | 110 | ``` 111 | var child = new Child(); 112 | /* Output: 113 | Parent: Constructing 114 | Child: Constructing 115 | */ 116 | ``` 117 | 118 | 119 | ### Call a method 120 | 121 | ```javascript 122 | child.doStuff(); 123 | /* Output: 124 | Parent: Doing stuff 125 | Child: Doing stuff 126 | Mixin: Doing stuff 127 | */ 128 | ``` 129 | 130 | 131 | ### Add a mixin to the instance 132 | 133 | Mixins added after instantiation become part of the instance. 134 | 135 | ```javascript 136 | child.mixin({ 137 | doMoreStuff: function() { 138 | console.log(this+': Doing more stuff') 139 | } 140 | }); 141 | 142 | child.doMoreStuff(); 143 | /* Output: 144 | Child: Doing more stuff 145 | */ 146 | ``` 147 | 148 | 149 | ### Check yo' self 150 | 151 | ```javascript 152 | console.log(child.instanceOf(Child)); // true 153 | console.log(child.instanceOf(Parent)); // true 154 | console.log(child.constructor === Child) // true 155 | console.log(child+''); // 'Child' 156 | ``` 157 | 158 | 159 | ### Wreck yo' self 160 | 161 | ```javascript 162 | child.destruct(); 163 | /* Output: 164 | Child: Destructing 165 | Parent: Destructing 166 | */ 167 | ``` 168 | 169 | ### No-conflict by default 170 | 171 | PseudoClass is always accessible as `PseudoClass`. If you're using another library that defines `Class`, you can still use PseudoClass by referencing `PseudoClass` instead. 172 | 173 | 174 | ## License 175 | 176 | PseudoClass is licensed under the permissive BSD license. 177 | 178 | 179 | [Class.min.js]: http://lazd.github.io/PseudoClass/build/Class.min.js 180 | 181 | [coveralls]: https://coveralls.io/r/lazd/PseudoClass?branch=master 182 | [coveralls-image]: https://img.shields.io/coveralls/lazd/PseudoClass.svg 183 | 184 | [travis]: http://travis-ci.org/lazd/PseudoClass 185 | [travis-image]: https://secure.travis-ci.org/lazd/PseudoClass.svg?branch=master 186 | 187 | [npm-url]: https://npmjs.org/package/pseudoclass 188 | [npm-image]: https://badge.fury.io/js/pseudoclass.svg 189 | -------------------------------------------------------------------------------- /benchmarks/creation.js: -------------------------------------------------------------------------------- 1 | var Class = require('../source/Class'); 2 | var ResigClass = require('./libs/Class.Resig'); 3 | 4 | var ClassA = Class.extend({ 5 | construct: function() { 6 | return 'test'; 7 | } 8 | }); 9 | 10 | var ResigA = ResigClass.extend({ 11 | init: function() { 12 | return 'test'; 13 | } 14 | }); 15 | 16 | var NativeA = function() { 17 | this.construct(); 18 | }; 19 | 20 | NativeA.prototype.construct = function() { 21 | return 'test'; 22 | }; 23 | 24 | module.exports = { 25 | name: 'Instance creation', 26 | tests: { 27 | 'PseudoClass': function() { 28 | var a = new ClassA(); 29 | }, 30 | 'Resig': function() { 31 | var a = new ResigA(); 32 | }, 33 | 'Native': function() { 34 | var a = new NativeA(); 35 | } 36 | } 37 | }; -------------------------------------------------------------------------------- /benchmarks/definition.js: -------------------------------------------------------------------------------- 1 | var Class = require('../source/Class'); 2 | var ResigClass = require('./libs/Class.Resig'); 3 | 4 | module.exports = { 5 | name: 'Class definition', 6 | tests: { 7 | 'PseudoClass': function() { 8 | var A = Class.extend({ 9 | method: function() { 10 | return 'test'; 11 | } 12 | }); 13 | }, 14 | 'Resig': function() { 15 | var A = ResigClass.extend({ 16 | method: function() { 17 | return 'test'; 18 | } 19 | }); 20 | }, 21 | 'Native': function() { 22 | var A = function() {}; 23 | A.prototype.method = function() { 24 | return 'test'; 25 | }; 26 | } 27 | } 28 | }; -------------------------------------------------------------------------------- /benchmarks/extension.js: -------------------------------------------------------------------------------- 1 | var Class = require('../source/Class'); 2 | var ResigClass = require('./libs/Class.Resig'); 3 | 4 | module.exports = { 5 | name: 'Class extension', 6 | tests: { 7 | 'PseudoClass': function() { 8 | var A = Class.extend({ 9 | method: function() { 10 | return 'test'; 11 | } 12 | }); 13 | 14 | var B = A.extend({ 15 | method1: function() { 16 | return 'test'; 17 | } 18 | }); 19 | }, 20 | 'Resig': function() { 21 | var A = ResigClass.extend({ 22 | method: function() { 23 | return 'test'; 24 | } 25 | }); 26 | 27 | var B = A.extend({ 28 | method1: function() { 29 | return 'test'; 30 | } 31 | }); 32 | }, 33 | 'Native': function() { 34 | var A = function() {}; 35 | A.prototype.method = function() { 36 | return 'test'; 37 | }; 38 | 39 | var B = function() {}; 40 | B.prototype = new A(); 41 | B.prototype.method1 = function() { 42 | return 'test'; 43 | }; 44 | } 45 | } 46 | }; -------------------------------------------------------------------------------- /benchmarks/libs/Class.Resig.js: -------------------------------------------------------------------------------- 1 | /* Simple JavaScript Inheritance 2 | * By John Resig http://ejohn.org/ 3 | * MIT Licensed. 4 | */ 5 | // Inspired by base2 and Prototype 6 | 7 | var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/; 8 | 9 | // The base Class implementation (does nothing) 10 | var Class = function(){}; 11 | 12 | // Create a new Class that inherits from this class 13 | Class.extend = function(prop) { 14 | var _super = this.prototype; 15 | 16 | // Instantiate a base class (but only create the instance, 17 | // don't run the init constructor) 18 | initializing = true; 19 | var prototype = new this(); 20 | initializing = false; 21 | 22 | // Copy the properties over onto the new prototype 23 | for (var name in prop) { 24 | // Check if we're overwriting an existing function 25 | prototype[name] = typeof prop[name] == "function" && 26 | typeof _super[name] == "function" && fnTest.test(prop[name]) ? 27 | (function(name, fn){ 28 | return function() { 29 | var tmp = this._super; 30 | 31 | // Add a new ._super() method that is the same method 32 | // but on the super-class 33 | this._super = _super[name]; 34 | 35 | // The method only need to be bound temporarily, so we 36 | // remove it when we're done executing 37 | var ret = fn.apply(this, arguments); 38 | this._super = tmp; 39 | 40 | return ret; 41 | }; 42 | })(name, prop[name]) : 43 | prop[name]; 44 | } 45 | 46 | // The dummy class constructor 47 | function Class() { 48 | // All construction is actually done in the init method 49 | if ( !initializing && this.init ) 50 | this.init.apply(this, arguments); 51 | } 52 | 53 | // Populate our constructed prototype object 54 | Class.prototype = prototype; 55 | 56 | // Enforce the constructor to be what we expect 57 | Class.prototype.constructor = Class; 58 | 59 | // And make this class extendable 60 | Class.extend = arguments.callee; 61 | 62 | return Class; 63 | }; 64 | 65 | module.exports = Class; -------------------------------------------------------------------------------- /benchmarks/super.js: -------------------------------------------------------------------------------- 1 | var Class = require('../source/Class'); 2 | var ResigClass = require('./libs/Class.Resig'); 3 | 4 | // PseudoClass 5 | var ClassA = Class.extend({ 6 | method: function() { 7 | return 'test'; 8 | } 9 | }); 10 | var ClassB = ClassA.extend({ 11 | method: function() { 12 | return this._super(); 13 | } 14 | }); 15 | var ClassC = ClassB.extend({ 16 | method: function() { 17 | return this._super(); 18 | } 19 | }); 20 | var ClassN = ClassC.extend({ 21 | method: function() { 22 | return this._super(); 23 | } 24 | }); 25 | 26 | // Resig 27 | var ResigA = ResigClass.extend({ 28 | method: function() { 29 | return 'test'; 30 | } 31 | }); 32 | var ResigB = ResigA.extend({ 33 | method: function() { 34 | return this._super(); 35 | } 36 | }); 37 | var ResigC = ResigB.extend({ 38 | method: function() { 39 | return this._super(); 40 | } 41 | }); 42 | var ResigN = ResigC.extend({ 43 | method: function() { 44 | return this._super(); 45 | } 46 | }); 47 | 48 | // Navtive 49 | var NativeA = function() {}; 50 | NativeA.prototype.method = function() { 51 | return 'test'; 52 | }; 53 | 54 | var NativeB = function() {}; 55 | NativeB.prototype = new NativeA(); 56 | NativeB.prototype.method = function() { 57 | return NativeA.prototype.method.apply(this, arguments); 58 | }; 59 | 60 | var NativeC = function() {}; 61 | NativeC.prototype = new NativeB(); 62 | NativeC.prototype.method = function() { 63 | return NativeB.prototype.method.apply(this, arguments); 64 | }; 65 | 66 | var NativeN = function() {}; 67 | NativeN.prototype = new NativeC(); 68 | NativeN.prototype.method = function() { 69 | return NativeC.prototype.method.apply(this, arguments); 70 | }; 71 | 72 | var nativeN = new NativeN(); 73 | var resigN = new ResigN(); 74 | var pseudoClassN = new ClassN(); 75 | 76 | module.exports = { 77 | name: 'Superclass methods', 78 | tests: { 79 | 'PseudoClass': function() { 80 | pseudoClassN.method(); 81 | }, 82 | 'Resig': function() { 83 | resigN.method(); 84 | }, 85 | 'Native': function() { 86 | nativeN.method(); 87 | } 88 | } 89 | }; 90 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pseudoclass", 3 | "version": "1.0.3", 4 | "homepage": "https://lazd.github.com/PseudoClass", 5 | "author": "Larry Davis ", 6 | "license": "BSD", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/lazd/PseudoClass.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/lazd/PseudoClass/issues" 13 | }, 14 | "devDependencies": { 15 | "grunt": "~0.4.2", 16 | "grunt-benchmark": "~0.2.0", 17 | "grunt-contrib-clean": "~0.5.0", 18 | "grunt-contrib-copy": "~0.5.0", 19 | "grunt-contrib-watch": "~0.5.3", 20 | "grunt-contrib-uglify": "~0.2.7", 21 | "grunt-contrib-jshint": "~0.8.0", 22 | "grunt-mocha-cov": "~0.3.0", 23 | "chai": "~1.8.1" 24 | }, 25 | "scripts": { 26 | "blanket": { 27 | "pattern": [ 28 | "source" 29 | ] 30 | }, 31 | "test": "grunt test" 32 | }, 33 | "main": "source/Class.js", 34 | "engines": { 35 | "node": ">=0.8" 36 | }, 37 | "files": [ 38 | "source/", 39 | "LICENSE", 40 | "README.md" 41 | ], 42 | "keywords": [ 43 | "class", 44 | "inheritance", 45 | "oop", 46 | "extend" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /source/Class.js: -------------------------------------------------------------------------------- 1 | /* 2 | PseudoClass - JavaScript inheritance 3 | 4 | Construction: 5 | Setup and construction should happen in the construct() method. 6 | The construct() method is automatically chained, so all construct() methods defined by superclass methods will be called first. 7 | 8 | Initialization: 9 | Initialziation that needs to happen after all construct() methods have been called should be done in the init() method. 10 | The init() method is not automatically chained, so you must call this._super() if you intend to call the superclass' init method. 11 | init() is not passed any arguments 12 | 13 | Destruction: 14 | Teardown and destruction should happen in the destruct() method. The destruct() method is also chained. 15 | 16 | Mixins: 17 | An array of mixins can be provided with the mixins[] property. An object or the prototype of a class should be provided, not a constructor. 18 | Mixins can be added at any time by calling this.mixin(properties) 19 | 20 | Usage: 21 | var MyClass = Class(properties); 22 | var MyClass = new Class(properties); 23 | var MyClass = Class.extend(properties); 24 | 25 | Credits: 26 | Inspired by Simple JavaScript Inheritance by John Resig http://ejohn.org/ 27 | 28 | Usage differences: 29 | construct() is used to setup instances and is chained so superclass construct() methods run automatically 30 | destruct() is used to tear down instances. destruct() is also chained 31 | init(), if defined, is called after construction is complete and is not chained 32 | toString() can be defined as a string or a function 33 | mixin() is provided to mix properties into an instance 34 | properties.mixins as an array results in each of the provided objects being mixed in (last object wins) 35 | this._super() is supported in mixins 36 | properties, if defined, should be a hash of property descriptors as accepted by Object.defineProperties 37 | */ 38 | (function(global) { 39 | // Extend the current context by the passed objects 40 | function extendThis() { 41 | var i, ni, objects, object, prop; 42 | objects = arguments; 43 | for (i = 0, ni = objects.length; i < ni; i++) { 44 | object = objects[i]; 45 | for (prop in object) { 46 | this[prop] = object[prop]; 47 | } 48 | } 49 | 50 | return this; 51 | } 52 | 53 | // Return a function that calls the specified method, passing arguments 54 | function makeApplier(method) { 55 | return function() { 56 | return this[method].apply(this, arguments); 57 | }; 58 | } 59 | 60 | // Merge and define properties 61 | function defineAndInheritProperties(Component, properties) { 62 | var constructor, 63 | descriptor, 64 | property, 65 | propertyDescriptors, 66 | propertyDescriptorHash, 67 | propertyDescriptorQueue; 68 | 69 | // Set properties 70 | Component.properties = properties; 71 | 72 | // Traverse the chain of constructors and gather all property descriptors 73 | // Build a queue of property descriptors for combination 74 | propertyDescriptorHash = {}; 75 | constructor = Component; 76 | do { 77 | if (constructor.properties) { 78 | for (property in constructor.properties) { 79 | propertyDescriptorQueue = propertyDescriptorHash[property] || (propertyDescriptorHash[property] = []); 80 | propertyDescriptorQueue.unshift(constructor.properties[property]); 81 | } 82 | } 83 | constructor = constructor.superConstructor; 84 | } 85 | while (constructor); 86 | 87 | // Combine property descriptors, allowing overriding of individual properties 88 | propertyDescriptors = {}; 89 | for (property in propertyDescriptorHash) { 90 | descriptor = propertyDescriptors[property] = extendThis.apply({}, propertyDescriptorHash[property]); 91 | 92 | // Allow setters to be strings 93 | // An additional wrapping function is used to allow monkey-patching 94 | // apply is used to handle cases where the setter is called directly 95 | if (typeof descriptor.set === 'string') { 96 | descriptor.set = makeApplier(descriptor.set); 97 | } 98 | if (typeof descriptor.get === 'string') { 99 | descriptor.get = makeApplier(descriptor.get); 100 | } 101 | } 102 | 103 | // Store option descriptors on the constructor 104 | Component.properties = propertyDescriptors; 105 | } 106 | 107 | // Used for default initialization methods 108 | var noop = function() {}; 109 | 110 | // Given a function, the superTest RE will match if _super is used in the function 111 | // The function will be serialized, then the serialized string will be searched for _super 112 | // If the environment isn't capable of function serialization, make it so superTest.test always returns true 113 | var superTest = /xyz/.test(function(){return 'xyz';}) ? /\._super\b/ : { test: function() { return true; } }; 114 | 115 | // Bind an overriding method such that it gets the overridden method as its first argument 116 | var superifyDynamic = function(name, func, superPrototype) { 117 | return function PseudoClass_setStaticSuper() { 118 | // Store the old super 119 | var previousSuper = this._super; 120 | 121 | // Use the method from the superclass' prototype 122 | // This strategy allows monkey patching (modification of superclass prototypes) 123 | this._super = superPrototype[name]; 124 | 125 | // Call the actual function 126 | var ret = func.apply(this, arguments); 127 | 128 | // Restore the previous value of super 129 | // This is required so that calls to methods that use _super within methods that use _super work 130 | this._super = previousSuper; 131 | 132 | return ret; 133 | }; 134 | }; 135 | 136 | var superifyStatic = function(name, func, object) { 137 | // Store a reference to the overridden function 138 | var _super = object[name]; 139 | 140 | return function PseudoClass_setDynamicSuper() { 141 | // Use the method stored at declaration time 142 | this._super = _super; 143 | 144 | // Call the actual function 145 | return func.apply(this, arguments); 146 | }; 147 | }; 148 | 149 | // Mix the provided properties into the current context with the ability to call overridden methods with _super() 150 | var mixin = function(properties, superPrototype) { 151 | // Use this instance's prototype if no prototype provided 152 | superPrototype = superPrototype || this.constructor && this.constructor.prototype; 153 | 154 | // Copy the properties onto the new prototype 155 | for (var name in properties) { 156 | var value = properties[name]; 157 | 158 | // Never mix construct or destruct 159 | if (name === 'construct' || name === 'destruct') 160 | continue; 161 | 162 | // Check if the property if a method that makes use of _super: 163 | // 1. The value should be a function 164 | // 2. The super prototype should have a function by the same name 165 | // 3. The function should use this._super somewhere 166 | var usesSuper = superPrototype && typeof value === 'function' && typeof superPrototype[name] === 'function' && superTest.test(value); 167 | 168 | if (usesSuper) { 169 | // Wrap the function such that this._super will be available 170 | if (this.hasOwnProperty(name)) { 171 | // Properties that exist directly on the object should be superified statically 172 | this[name] = superifyStatic(name, value, this); 173 | } 174 | else { 175 | // Properties that are part of the superPrototype should be superified dynamically 176 | this[name] = superifyDynamic(name, value, superPrototype); 177 | } 178 | } 179 | else { 180 | // Directly assign the property 181 | this[name] = value; 182 | } 183 | } 184 | }; 185 | 186 | // The base Class implementation acts as extend alias, with the exception that it can take properties.extend as the Class to extend 187 | var PseudoClass = function(properties) { 188 | // If a class-like object is passed as properties.extend, just call extend on it 189 | if (properties && properties.extend) 190 | return properties.extend.extend(properties); 191 | 192 | // Otherwise, just create a new class with the passed properties 193 | return PseudoClass.extend(properties); 194 | }; 195 | 196 | // Add the mixin method to all classes created with PseudoClass 197 | PseudoClass.prototype.mixin = mixin; 198 | 199 | // Creates a new PseudoClass that inherits from this class 200 | // Give the function a name so it can refer to itself without arguments.callee 201 | PseudoClass.extend = function extend(properties) { 202 | // The constructor handles creating an instance of the class, applying mixins, and calling construct() and init() methods 203 | function PseudoClass() { 204 | // Optimization: Requiring the new keyword and avoiding usage of Object.create() increases performance by 5x 205 | if (this instanceof PseudoClass === false) { 206 | throw new Error('Cannot create instance without new operator'); 207 | } 208 | 209 | // Set properties 210 | var propertyDescriptors = PseudoClass.properties; 211 | if (propertyDescriptors) { 212 | Object.defineProperties(this, propertyDescriptors); 213 | } 214 | 215 | // Optimization: Avoiding conditionals in constructor increases performance of instantiation by 2x 216 | this.construct.apply(this, arguments); 217 | 218 | this.init(); 219 | } 220 | 221 | var superConstructor = this; 222 | var superPrototype = this.prototype; 223 | 224 | // Store the superConstructor 225 | // It will be accessible on an instance as follows: 226 | // instance.constructor.superConstructor 227 | PseudoClass.superConstructor = superConstructor; 228 | 229 | // Add extend() as a static method on the constructor 230 | PseudoClass.extend = extend; 231 | 232 | // Create an object with the prototype of the superclass 233 | // Store the extended class' prototype as the prototype of the constructor 234 | var prototype = PseudoClass.prototype = Object.create(superPrototype); 235 | 236 | // Assign prototype.constructor to the constructor itself 237 | // This allows instances to refer to this.constructor.prototype 238 | // This also allows creation of new instances using instance.constructor() 239 | prototype.constructor = PseudoClass; 240 | 241 | // Store the superPrototype 242 | // It will be accessible on an instance as follows: 243 | // instance.superPrototype 244 | // instance.constructor.prototype.superPrototype 245 | prototype.superPrototype = superPrototype; 246 | 247 | if (properties) { 248 | // Set property descriptors aside 249 | // We'll first inherit methods, then we'll apply these 250 | var propertyDescriptors = properties.properties; 251 | delete properties.properties; 252 | 253 | // Mix the new properties into the class prototype 254 | // This does not copy construct and destruct 255 | mixin.call(prototype, properties, superPrototype); 256 | 257 | // Mix in all the mixins 258 | // This also does not copy construct and destruct 259 | if (Array.isArray(properties.mixins)) { 260 | for (var i = 0, ni = properties.mixins.length; i < ni; i++) { 261 | // Mixins should be _super enabled, with the methods defined in the prototype as the superclass methods 262 | mixin.call(prototype, properties.mixins[i], prototype); 263 | } 264 | } 265 | 266 | // Define properties from this class and its parent classes 267 | defineAndInheritProperties(PseudoClass, propertyDescriptors); 268 | 269 | // Chain the construct() method (supermost executes first) if necessary 270 | if (properties.construct) { 271 | var construct = properties.construct; 272 | if (superPrototype.construct) { 273 | prototype.construct = function() { 274 | superPrototype.construct.apply(this, arguments); 275 | construct.apply(this, arguments); 276 | }; 277 | } 278 | else { 279 | prototype.construct = construct; 280 | } 281 | } 282 | 283 | // Chain the destruct() method in reverse order (supermost executes last) if necessary 284 | if (properties.destruct) { 285 | var destruct = properties.destruct; 286 | if (superPrototype.destruct) { 287 | prototype.destruct = function() { 288 | destruct.apply(this, arguments); 289 | superPrototype.destruct.apply(this, arguments); 290 | }; 291 | } 292 | else { 293 | prototype.destruct = destruct; 294 | } 295 | } 296 | 297 | // Allow definition of toString as a string (turn it into a function) 298 | if (typeof properties.toString === 'string') { 299 | var className = properties.toString; 300 | prototype.toString = function() { return className; }; 301 | } 302 | } 303 | 304 | // Define construct and init as noops if undefined 305 | // This serves to avoid conditionals inside of the constructor 306 | if (typeof prototype.construct !== 'function') 307 | prototype.construct = noop; 308 | if (typeof prototype.init !== 'function') 309 | prototype.init = noop; 310 | 311 | return PseudoClass; 312 | }; 313 | 314 | if (typeof module !== 'undefined' && module.exports) { 315 | // Node.js Support 316 | module.exports = PseudoClass; 317 | } 318 | else if (typeof global.define === 'function') { 319 | (function(define) { 320 | // AMD Support 321 | define(function() { return PseudoClass; }); 322 | }(global.define)); 323 | } 324 | else { 325 | // Browser support 326 | global.PseudoClass = PseudoClass; 327 | 328 | // Don't blow away existing Class variable 329 | if (!global.Class) { 330 | global.Class = PseudoClass; 331 | } 332 | } 333 | }(this)); 334 | -------------------------------------------------------------------------------- /source/examples/_shared/css/styles.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-size: 16px; 3 | font-family: 'Lucida Sans Unicode', 'Trebuchet MS', Helvetica, sans-serif; 4 | } 5 | 6 | body { 7 | background: rgb(240, 240, 240); 8 | 9 | margin: 1.5rem; 10 | } 11 | 12 | .button { 13 | display: inline-block; 14 | box-sizing: border-box; 15 | min-width: 3rem; 16 | margin: 0.5rem 0.25rem; 17 | padding: 0 0.5rem; 18 | 19 | cursor: pointer; 20 | 21 | height: 1.8625rem; 22 | 23 | border: 0.0625rem solid rgba(60,85,32,0.25); 24 | box-shadow: inset -0.0625rem -0.0625rem 0 rgba(0, 0, 0, 0.1), 25 | inset 0.0625rem 0.0625rem 0 rgba(255, 255, 255, 0.35); 26 | 27 | background: rgb(125,200,64); 28 | background: linear-gradient(to bottom, rgb(125,200,64) 0%,rgb(112,167,67) 100%); 29 | 30 | font-family: 'Lucida Sans Unicode', 'Trebuchet MS', Helvetica, sans-serif; 31 | font-size: 16px; 32 | color: rgb(250, 250, 250); 33 | text-decoration: none; 34 | text-align: center; 35 | line-height: 1.75rem; 36 | font-weight: bold; 37 | text-shadow: 0 -0.0625rem 0 rgba(0,0,0,0.5); 38 | 39 | border-radius: 0.125rem; 40 | } 41 | 42 | .button.red { 43 | border: 0.0625rem solid rgba(85,60,32,0.25); 44 | background: linear-gradient(to bottom, rgb(249, 76, 57) 0%,rgb(197, 63, 49) 100%); 45 | } 46 | 47 | input { 48 | font-size: 16px; 49 | height: 1.8625rem; 50 | box-sizing: border-box; 51 | padding: 0 0.25rem; 52 | } 53 | -------------------------------------------------------------------------------- /source/examples/amd/css/logger.css: -------------------------------------------------------------------------------- 1 | .log { 2 | padding: 0 0 2rem 0; 3 | 4 | font-family: Andale Mono, Monaco, Monospace; 5 | font-size: 1rem; 6 | line-height: 2rem; 7 | letter-spacing: 0.125rem; 8 | color: rgb(0, 128, 0); 9 | } 10 | 11 | .log .error { 12 | color: rgb(225, 0, 0); 13 | } 14 | -------------------------------------------------------------------------------- /source/examples/amd/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Class Example: AMD 12 | 13 | 14 |

Class Example: AMD

15 |

16 | In this example, we'll configure RequireJS to find Class and use Class as an AMD module. 17 |

18 | 19 |
20 | 21 | 22 | 23 |
24 | 25 |

26 | 


--------------------------------------------------------------------------------
/source/examples/amd/modules/Logger.js:
--------------------------------------------------------------------------------
 1 | define(['Class'], function(Class) {
 2 | 	var Logger = Class({
 3 | 		toString: 'Logger',
 4 | 		construct: function(options) {
 5 | 			options = options || {};
 6 | 
 7 | 			// Pull an option from the options object
 8 | 			this.width = options.width || 40;
 9 | 			
10 | 			this.log(this+' constructed');
11 | 		},
12 | 		doLog: function(message, type, noNewline) {
13 | 			// Add some dots
14 | 			while(message.length < 40)
15 | 				message += '.';
16 | 			
17 | 			var mark = '✔';
18 | 			if (type === 'error')
19 | 				mark = '✖';
20 | 			
21 | 			// Add the checkmark/X
22 | 			message += mark;
23 | 			
24 | 			if (type === 'error')
25 | 				message = ''+message+'';
26 | 			
27 | 			// Add the message to the DOM
28 | 			document.getElementById('log').innerHTML += noNewline ? message : message + '\n';
29 | 		},
30 | 		log: function(message, noNewline) {
31 | 			this.doLog(message, 'log', noNewline);
32 | 		},
33 | 		error: function(message, noNewline) {
34 | 			this.doLog(message, 'error', noNewline);
35 | 		}
36 | 	});
37 | 	
38 | 	Logger.prototype.log('Class loaded with RequireJS');
39 | 	
40 | 	return Logger;
41 | });


--------------------------------------------------------------------------------
/source/examples/amd/modules/main.js:
--------------------------------------------------------------------------------
 1 | // Configure RequireJS
 2 | requirejs.config({
 3 | 	paths: {
 4 | 		Class: [
 5 | 			//'http://some.cdn.org/class/Class.js',	// A CDN URL can go here, with a local fallback URL below
 6 | 			'../../../Class'
 7 | 		]
 8 | 	}
 9 | });
10 | 
11 | // Load our logger module, which depends on Class
12 | require(['Logger'], function(Logger) {
13 | 	// Create an instance of our module
14 | 	var logger = new Logger();
15 | 
16 | 	// Verify that Class has not leaked globally
17 | 	if (typeof window.Class === 'undefined') {
18 | 		logger.log('window.Class is undefined');
19 | 	}
20 | 	else {
21 | 		// This will never happen
22 | 		logger.error('window.Class is defined');
23 | 	}
24 | 	
25 | 	// Application code goes here!
26 | 	logger.log('Application started');
27 | 	
28 | 	var logType = 'log';
29 | 	document.getElementById('log_error').onclick = function(evt) {
30 | 		logType = 'error';
31 | 	};
32 | 	
33 | 	document.getElementById('doLog').onsubmit = function(evt) {
34 | 		var message = document.getElementById('log_message').value || '(no message)';
35 | 		// Log the message
36 | 		logger[logType](message);
37 | 		
38 | 		// Reset log field and type
39 | 		document.getElementById('log_message').value = '';
40 | 		logType = 'log';
41 | 
42 | 		// Cancel form submit
43 | 		return false;
44 | 	};
45 | });


--------------------------------------------------------------------------------
/source/examples/logger/ColorLogger.js:
--------------------------------------------------------------------------------
 1 | var Class = require('../../Class');
 2 | 
 3 | // A simple logger that colorizes output for the shell
 4 | module.exports = Class({
 5 | 	toString: 'ColorLogger',
 6 | 	
 7 | 	// Extend logger
 8 | 	extend: require('./Logger'),
 9 | 	
10 | 	// Load the Colorizer mixin to gain the colorize method
11 | 	mixins: [require('./Colorizer')],
12 | 	
13 | 	doLog: function(message) {
14 | 		// Colorize the first argument if it's a string
15 | 		var args = this.args(arguments);
16 | 		var type = args[args.length-1];
17 | 		
18 | 		if (typeof args[0] === 'string') {
19 | 			if (type === 'warn')
20 | 				args[0] = ''+args[0]+'';
21 | 			if (type === 'error')
22 | 				args[0] = ''+args[0]+'';
23 | 		
24 | 			args[0] = this.colorize(args[0]);
25 | 		}
26 | 		
27 | 		// Apply the modified arguments to the doLog() method of the superclass
28 | 		this._super.apply(this, args);
29 | 	},
30 | 	
31 | 	getDate: function() {
32 | 		// Call the superclass' getDate() method, but wrap its output in bold tags
33 | 		return this.colorize(''+this._super()+'');
34 | 	}
35 | });
36 | 


--------------------------------------------------------------------------------
/source/examples/logger/Colorizer.js:
--------------------------------------------------------------------------------
 1 | var Class = require('../../Class');
 2 | 
 3 | var escape = '\033[';
 4 | 
 5 | var open = {
 6 | 	'black': 30,
 7 | 	'red': 31,
 8 | 	'green': 32,
 9 | 	'yellow': 33,
10 | 	'blue': 34,
11 | 	'magenta': 35,
12 | 	'cyan': 36,
13 | 	'white': 37,
14 | 	'grey': 90,
15 | 	'bg-black': 40,
16 | 	'bg-red': 41,
17 | 	'bg-green': 42,
18 | 	'bg-yellow': 43,
19 | 	'bg-blue': 44,
20 | 	'bg-magenta': 45,
21 | 	'bg-cyan': 46,
22 | 	'bg-white': 47,
23 | 	'bg-grey': 100,
24 | 	'b': 1,
25 | 	'i': 3,
26 | 	'u': 4,
27 | 	'blink': 5,
28 | 	'reset': 0
29 | };
30 | 
31 | var close = {
32 | 	'b': 22,
33 | 	'i': 23,
34 | 	'u': 24,
35 | 	'blink': 25,
36 | 	'reset': 0
37 | };
38 | 
39 | // A mixin that provides the colorize method
40 | module.exports = {
41 | 	colorize: function(message) {
42 | 		if (!message) return '';
43 | 		
44 | 		// maintain a stack of our current tags
45 | 		var stack = [];
46 | 	
47 | 		// replace each tag with the corresponding shell color code
48 | 		message = message.replace(/<([a-z\-]+)>|<\/([a-z\-]+)>/g, function(matchedString, openTag, closeTag) {
49 | 			var seq = '';
50 | 		
51 | 			if (open.hasOwnProperty(openTag) || open.hasOwnProperty(closeTag)) { // handle color tags
52 | 				if (openTag) {
53 | 					// Add open code for tag
54 | 					seq = escape+open[openTag]+'m';
55 | 			
56 | 					// Push the tag onto our stack
57 | 					stack.unshift(openTag);
58 | 				}
59 | 				else {
60 | 					// Colors just reset, so there is no close code
61 | 					var isColor = !close.hasOwnProperty(closeTag);
62 | 					var seqPart = isColor ? open.reset : close[closeTag];
63 | 				
64 | 					// Add close code for tag
65 | 					seq = escape+seqPart+'m';
66 | 			
67 | 					// Remove the closed tag from the stack if it's on the top
68 | 					if (stack[0] == closeTag) {
69 | 						stack.shift();
70 | 					}
71 | 			
72 | 					// If we reset the color, we need to re-add previous colors
73 | 					if (isColor) {
74 | 						for (var i = stack.length-1; i >= 0; i--) {
75 | 							seq += escape+open[stack[i]]+'m';
76 | 						}
77 | 					}
78 | 				}
79 | 			}
80 | 			else { // pass through unrecognized tags
81 | 				seq = matchedString;
82 | 			}
83 | 			
84 | 			return seq;
85 | 		});
86 | 	
87 | 		// Make sure we reset if there were unclosed tags
88 | 		if (stack.length) {
89 | 			message += escape+open.reset+'m';
90 | 		}
91 | 	
92 | 		return message;
93 | 	}
94 | };
95 | 


--------------------------------------------------------------------------------
/source/examples/logger/Logger.js:
--------------------------------------------------------------------------------
 1 | var Class = require('../../Class');
 2 | 
 3 | // A utility function used by getDate that is effectively private
 4 | // You could call zeroPad.call(this, num) to execute it as a private function in the context of the instance
 5 | var zeroPad = function(num) {
 6 | 	return ('0'+num).slice(-2);
 7 | };
 8 | 
 9 | // A simple logger that includes the current date and time with messages
10 | module.exports = Class({
11 | 	toString: 'Logger',
12 | 	
13 | 	// A default property
14 | 	// Since strings are mutable, it's ok to store them in the prototype
15 | 	type: 'log',
16 | 	
17 | 	// Store some options in our constructor
18 | 	// Normally, you might use something like _.extend({}, this.options, options) to set defaults
19 | 	construct: function(options) {
20 | 		this.type = options.type || this.type;
21 | 	},
22 | 	
23 | 	// Looks funny, but effectively does a console.log(date+message)
24 | 	doLog: function() {
25 | 		var args = this.args(arguments);
26 | 		args.unshift(this.getDate()+':');
27 | 		
28 | 		// Type is the last argument
29 | 		var type = args.pop();
30 | 		
31 | 		console[type].apply(console, args);
32 | 	},
33 | 	
34 | 	log: function() {
35 | 		this.doLog.apply(this, this.args(arguments).concat(this.type));
36 | 	},
37 | 	
38 | 	warn: function() {
39 | 		this.doLog.apply(this, this.args(arguments).concat('warn'));
40 | 	},
41 | 	
42 | 	error: function() {
43 | 		this.doLog.apply(this, this.args(arguments).concat('error'));
44 | 	},
45 | 	
46 | 	getDate: function() {
47 | 		var d = new Date();
48 | 		var dateStr = d.getUTCFullYear()+'-'+zeroPad(d.getUTCMonth()+1)+'-'+zeroPad(d.getUTCDate())+
49 | 					' '+zeroPad(d.getUTCHours())+':'+zeroPad(d.getUTCMinutes())+':'+zeroPad(d.getUTCSeconds())+'.'+zeroPad((d.getTime()/1000).toFixed(2));
50 | 		return dateStr;
51 | 	},
52 | 	
53 | 	args: function(obj) {
54 | 		return Array.prototype.slice.call(obj);
55 | 	}
56 | });
57 | 


--------------------------------------------------------------------------------
/source/examples/logger/index.js:
--------------------------------------------------------------------------------
 1 | var Class = require('../../Class');
 2 | var ColorLogger = require('./ColorLogger');
 3 | 
 4 | // Create a couple loggers
 5 | var message = new ColorLogger({
 6 | 	type: 'log'
 7 | });
 8 | 
 9 | var error = new ColorLogger({
10 | 	type: 'error'
11 | });
12 | 
13 | // Log some messages
14 | console.log(ColorLogger.prototype.colorize("Class Example: Logger"));
15 | console.log(ColorLogger.prototype.colorize("In this example, we'll use inheritance, mixins, this._super.apply(), default properties,\nprivate helper functions, and methods on the prototype of a Class without an instance.\n"));
16 | 
17 | // Log some messages
18 | message.warn('Starting...');
19 | 
20 | ([1,2,3,4,5]).forEach(function(num) {
21 | 	setTimeout(function() {
22 | 		message.log('Counting '+num);
23 | 	}, num*200);
24 | });
25 | 
26 | setTimeout(function() {
27 | 	error.log('Goodbye!');
28 | 	process.exit();
29 | }, 1000);


--------------------------------------------------------------------------------
/source/examples/widgets/css/alert.css:
--------------------------------------------------------------------------------
  1 | .alert {
  2 | 	position: fixed;
  3 | 	left: 50%;
  4 | 	top: 50%;
  5 | 	z-index: 2;
  6 | 	
  7 | 	width: 30rem;
  8 | 	height: 10rem;
  9 | 	
 10 | 	border: 0.25rem solid rgba(0,0,0,0.375);
 11 | 	border-radius: 0.25rem;
 12 | 	
 13 | 	box-shadow: 0 0 2.5rem rgba(0, 0, 0, 0.35);
 14 | }
 15 | 
 16 | .alert:before {
 17 | 	position: fixed;
 18 | 	left: 0;
 19 | 	right: 0;
 20 | 	top: 0;
 21 | 	bottom: 0;
 22 | 	z-index: 0;
 23 | 	
 24 | 	display: block;
 25 | 	content: " ";
 26 | 	
 27 | 	background: rgba(0,0,0,0.375);
 28 | }
 29 | 
 30 | .alert-heading {
 31 | 	position: absolute;
 32 | 	top: 0;
 33 | 	left: 0;
 34 | 	right: 0;
 35 | 	
 36 | 	height: 2.5rem;
 37 | 	
 38 | 	margin: 0;
 39 | 	padding: 0 0.625rem;
 40 | 	
 41 | 	border-bottom: 0.0625rem solid rgb(160, 160, 160);
 42 | 	
 43 | 	font-size: 1.5rem;
 44 | 	line-height: 2.5rem;
 45 | 	text-shadow: 0 0.0625rem 0 rgba(255, 255, 255, 1);
 46 | 	
 47 | 	background: rgb(210, 210, 210);
 48 | }
 49 | 
 50 | .alert-message {
 51 | 	position: absolute;
 52 | 	top: 2.5rem;
 53 | 	left: 0;
 54 | 	right: 0;
 55 | 	bottom: 2.5rem;
 56 | 	
 57 | 	padding: 1rem;
 58 | 	
 59 | 	background: rgb(230, 230, 230);
 60 | 	
 61 | 	line-height: 1.3;
 62 | 	text-shadow: 0 0.0625rem 0 rgba(255, 255, 255, 0.75);
 63 | }
 64 | 
 65 | .alert-buttons {
 66 | 	position: absolute;
 67 | 	bottom: 0;
 68 | 	left: 0;
 69 | 	right: 0;
 70 | 	
 71 | 	height: 2.875rem;
 72 | 	
 73 | 	background: rgb(230, 230, 230);
 74 | 	
 75 | 	text-align: right;
 76 | }
 77 | 
 78 | .alert-close {
 79 | 	display: block;
 80 | 	
 81 | 	position: absolute;
 82 | 	top: 0;
 83 | 	right: 0;
 84 | 	z-index: 1;
 85 | 	
 86 | 	width: 2.5rem;
 87 | 	height: 2.25rem;
 88 | 	padding: 0;
 89 | 	margin: 0;
 90 | 	
 91 | 	border: none;
 92 | 	background: none;
 93 | 	
 94 | 	text-decoration: none;
 95 | 	
 96 | 	cursor: pointer;
 97 | }
 98 | 
 99 | .alert-close:before {
100 | 	display: block;
101 | 
102 | 	content: "\00d7";
103 | 
104 | 	color: rgba(0, 0, 0, 0.5);
105 | 	font-family: Helvetica;
106 | 	font-size: 2rem;
107 | 	text-align: center;
108 | 	line-height: 2rem;
109 | 	text-shadow: 0 0.0625rem 0.0625rem rgba(255, 255, 255, 1);
110 | }
111 | 
112 | .alert-close:hover:before {
113 | 	color: rgb(0, 0, 0);
114 | }
115 | 


--------------------------------------------------------------------------------
/source/examples/widgets/index.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | 	
 4 | 	
 5 | 	
 6 | 
 7 | 	
 8 | 	
 9 | 
10 | 	
11 | 	
12 | 	
13 | 	
14 | 	
15 | 
16 | 	
17 | 	
18 | 
19 | 	Class Example: Widgets
20 | 
21 | 
22 | 

Class Example: Widgets

23 |

24 | In this example, we'll use 25 | a mixin, 26 | 3 levels of inheritance ( 27 | 1, 28 | 2, 29 | 3 30 | ), 31 | and some 32 | method override tricks 33 | coupled with 34 | this._super(). 35 |

36 | 37 |

Open your browser's console, then click the button below.

38 | 39 | Show Alert 40 | 41 | 49 | 50 | 72 | -------------------------------------------------------------------------------- /source/examples/widgets/js/Alert.js: -------------------------------------------------------------------------------- 1 | // A basic alert widget 2 | // We'll extend the Widget using the alternate Widget.extend() static method 3 | var Alert = Widget.extend({ 4 | toString: function() { 5 | // Call the superclass' toString() method 6 | return this._super()+'->Modal'; 7 | }, 8 | 9 | // We'll define an init function that uses our method to setup the alert 10 | init: function() { 11 | console.log(this+': Initializing...'); 12 | 13 | // init() isn't chained, so call the superclass init method 14 | this._super.apply(this, arguments); 15 | 16 | if (this.options.heading) 17 | this.setHeading(this.options.heading); 18 | 19 | if (this.options.message) 20 | this.setMessage(this.options.message); 21 | 22 | this.$el.on('click', '[data-close]', function(evt) { 23 | evt.preventDefault(); 24 | this.hide(); 25 | }.bind(this)); 26 | }, 27 | 28 | // Override the default show method to shake the alert when shown 29 | show: function() { 30 | console.log(this+': Showing'); 31 | 32 | // First call the superclass' show method 33 | this._super(); 34 | 35 | // Position in the center of the screen 36 | this.$el.css({ 37 | marginTop: -this.$el.outerHeight()/2, 38 | marginLeft: -this.$el.outerWidth()/2 39 | }); 40 | 41 | // Then shake the alert 42 | this.shake(); 43 | }, 44 | 45 | shake: function() { 46 | var by = 20; 47 | for (var i = 0; i < 4; i++) { 48 | this.$el.animate({ 49 | marginLeft: '+='+(by = -by)+'px' 50 | }, 35); 51 | } 52 | }, 53 | 54 | // Override the default hide method to fade the dialog out 55 | hide: function() { 56 | console.log(this+': Hiding'); 57 | 58 | this.$el.fadeOut(250); 59 | }, 60 | 61 | setHeading: function(heading) { 62 | this.$el.find('.alert-heading').html(heading); 63 | }, 64 | 65 | setMessage: function(message) { 66 | this.$el.find('.alert-message').html(message); 67 | } 68 | }); 69 | -------------------------------------------------------------------------------- /source/examples/widgets/js/Base.js: -------------------------------------------------------------------------------- 1 | // A useless base class that does nothing more than report when it is constructed or destructed 2 | var Base = Class({ 3 | toString: 'Base', 4 | construct: function(options) { 5 | console.log('Base: Constructing...'); 6 | 7 | // Store options 8 | this.options = options || {}; 9 | }, 10 | destruct: function() { 11 | console.log('Base: Destructing...'); 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /source/examples/widgets/js/Events.js: -------------------------------------------------------------------------------- 1 | // A very basic events mixin 2 | var Events = { 3 | on: function(name, func) { 4 | this._events = this._events || {}; 5 | this._events[name] = this._events[name] || []; 6 | this._events[name].push(func); 7 | }, 8 | off: function(name, func) { 9 | this._events = this._events || {}; 10 | var index = this._events[name] && this._events[name].indexOf(func) || -1; 11 | if (~index) 12 | this._events.splice(index, 1); 13 | }, 14 | trigger: function(name, properties) { 15 | if (!this._events || !this._events[name]) return; 16 | for (var i = 0; i < this._events[name].length; i++) { 17 | this._events[name][i].call(this, properties); 18 | } 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /source/examples/widgets/js/Widget.js: -------------------------------------------------------------------------------- 1 | // A widget class that handles showing, hiding, and removing an element from the DOM 2 | var Widget = Class({ 3 | // Identify ourself 4 | toString: function() { 5 | // Using the superclass, Base's toString() method 6 | return this._super()+'->Widget'; 7 | }, 8 | 9 | // Extend the Base class 10 | extend: Base, 11 | 12 | // Add the Events mixin 13 | mixins: [Events], 14 | 15 | // construct() is chained, so we will first see the log entries made by Base 16 | construct: function(options) { 17 | console.log(this.widgetToString()+': Constructing...'); 18 | 19 | this.$el = $(options.el); 20 | }, 21 | 22 | // destruct() is chained, but we'll see Widget's log entries first 23 | destruct: function() { 24 | console.log('Widget: Destructing...'); 25 | 26 | this.$el.remove(); 27 | }, 28 | 29 | // The init() method is called after all constructors have been executed 30 | init: function() { 31 | if (this.options.visible) { 32 | this.show(); 33 | } 34 | else { 35 | // Call Widget's hide() method, regardless of overrides by a sub-class 36 | Widget.prototype.hide.call(this); 37 | } 38 | }, 39 | 40 | widgetToString: function() { 41 | // Call Widget's toString() method, regardless of overrides by a sub-class 42 | return Widget.prototype.toString.call(this); 43 | }, 44 | 45 | show: function() { 46 | console.log(this.widgetToString()+': Showing'); 47 | 48 | this.trigger('shown'); 49 | 50 | this.$el.show(); 51 | }, 52 | 53 | hide: function() { 54 | console.log(this.widgetToString()+': Hiding'); 55 | 56 | this.trigger('hidden'); 57 | 58 | this.$el.hide(); 59 | } 60 | }); 61 | -------------------------------------------------------------------------------- /test/1.creation.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var Class = require('../source/Class'); 3 | 4 | describe('Class creation:', function() { 5 | var properties = { 6 | method1: function() { return 1; } 7 | }; 8 | 9 | function test_method1(A) { 10 | var a = new A(); 11 | expect(a.method1()).to.equal(1); 12 | } 13 | 14 | it('using Class.extend(properties)', function() { 15 | var A = Class.extend(properties); 16 | test_method1(A); 17 | }); 18 | 19 | it('using Class(properties)', function() { 20 | var A = Class(properties); 21 | test_method1(A); 22 | }); 23 | 24 | it('using new Class(properties)', function() { 25 | var A = new Class(properties); 26 | test_method1(A); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/2.identity.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var Class = require('../source/Class'); 3 | 4 | describe('Class identity:', function() { 5 | describe('constructor property', function() { 6 | it('should be set to the class that constructed the instance', function() { 7 | var A = Class(); 8 | 9 | var a = new A(); 10 | 11 | var B = A.extend(); 12 | 13 | var b = new B(); 14 | 15 | expect(a.constructor).to.equal(A); 16 | expect(b.constructor).to.equal(B); 17 | }); 18 | }); 19 | 20 | describe('instanceOf', function() { 21 | var A, B, C; 22 | 23 | beforeEach(function(done) { 24 | A = Class(); 25 | B = A.extend(); 26 | C = B.extend(); 27 | 28 | done(); 29 | }); 30 | 31 | it('should be correct for direct instances of a class', function() { 32 | var a = new A(); 33 | 34 | expect(a instanceof A).to.be.true; 35 | }); 36 | 37 | it('should be correct for instances of a childclass created with new', function() { 38 | var c = new C(); 39 | 40 | expect(c instanceof A).to.be.true; 41 | expect(c instanceof B).to.be.true; 42 | expect(c instanceof C).to.be.true; 43 | }); 44 | }); 45 | 46 | describe('toString', function() { 47 | function test_toString(A) { 48 | var a = new A(); 49 | 50 | expect(a+'').to.equal('A'); 51 | } 52 | 53 | it('as a function', function() { 54 | var A = Class({ 55 | toString: function() { 56 | return 'A'; 57 | } 58 | }); 59 | 60 | test_toString(A); 61 | }); 62 | 63 | 64 | it('as a string', function() { 65 | var A = Class({ 66 | toString: 'A' 67 | }); 68 | 69 | test_toString(A); 70 | }); 71 | }); 72 | 73 | 74 | describe('superPrototype', function() { 75 | var A, B, C; 76 | 77 | beforeEach(function(done) { 78 | A = Class({ 79 | val: 1 80 | }); 81 | B = A.extend({ 82 | val: 2 83 | }); 84 | C = B.extend({ 85 | val: 3 86 | }); 87 | 88 | done(); 89 | }); 90 | 91 | it('should allow walking the prototype chain', function() { 92 | var c = new C(); 93 | 94 | var expected = 3; 95 | 96 | // Start with our prototype 97 | var proto = c.constructor.prototype; 98 | 99 | while (expected > 0) { 100 | // Check for expected value 101 | expect(proto.val).to.equal(expected); 102 | 103 | // Move to the next prototype 104 | proto = proto.superPrototype; 105 | expected--; 106 | } 107 | }); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /test/3.extension.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var Class = require('../source/Class'); 3 | 4 | describe('Class extension:', function() { 5 | var A; 6 | 7 | var return1 = function() { 8 | return 1; 9 | }; 10 | var return2 = function() { 11 | return 2; 12 | }; 13 | var return3 = function() { 14 | return 3; 15 | }; 16 | 17 | function test_return1(X) { 18 | var x = new X(); 19 | expect(x.return1()).to.equal(1); 20 | } 21 | 22 | function test_return2(X) { 23 | var x = new X(); 24 | expect(x.return2()).to.equal(2); 25 | } 26 | 27 | function test_return3(X) { 28 | var x = new X(); 29 | expect(x.return3()).to.equal(3); 30 | } 31 | 32 | beforeEach(function(done) { 33 | // Start fresh by re-defining A before every test 34 | A = Class({ 35 | toString: function() { return 'A'; }, 36 | return1: return1 37 | }); 38 | 39 | done(); 40 | }); 41 | 42 | describe('inheritance', function() { 43 | it('using A.extend()', function() { 44 | var B = A.extend({ 45 | return2: return2 46 | }); 47 | 48 | var C = B.extend({ 49 | return3: return3 50 | }); 51 | 52 | test_return1(C); 53 | test_return2(C); 54 | test_return3(C); 55 | }); 56 | 57 | it('using Class({ extend: A })', function() { 58 | var B = Class({ 59 | extend: A, 60 | return2: return2 61 | }); 62 | 63 | var C = Class({ 64 | extend: B, 65 | return3: return3 66 | }); 67 | 68 | test_return1(C); 69 | test_return2(C); 70 | test_return3(C); 71 | }); 72 | 73 | it('should call overriding child class method', function() { 74 | var B = A.extend({ 75 | return1: function() { 76 | return 3; 77 | } 78 | }); 79 | var b = new B(); 80 | 81 | // Child class method should override and return 3 82 | expect(b.return1()).to.equal(3); 83 | }); 84 | 85 | it('should use method added to prototype after instantiation', function() { 86 | var A = Class(); 87 | 88 | var a = new A(); 89 | 90 | A.prototype.return1 = return1; 91 | 92 | expect(a.return1()).to.equal(1); 93 | }); 94 | }); 95 | 96 | describe('mixins', function() { 97 | var mixable = { 98 | return1: function() { 99 | return 1; 100 | }, 101 | return2: function() { 102 | return 1; 103 | } 104 | }; 105 | 106 | var mixable2 = { 107 | return2: function() { 108 | return 2; 109 | } 110 | }; 111 | 112 | it('with mixin() on an instance', function() { 113 | var A = Class(); 114 | var a = new A(); 115 | 116 | a.mixin(mixable); 117 | 118 | expect(a.return1()).to.equal(1); 119 | }); 120 | 121 | it('with properites.mixins at declaration time', function() { 122 | var A = Class({ 123 | mixins: [mixable, mixable2] 124 | }); 125 | var a = new A(); 126 | 127 | expect(a.return1()).to.equal(1); 128 | expect(a.return2()).to.equal(2); 129 | }); 130 | 131 | it('at declaration and instance time', function() { 132 | var A = Class({ 133 | mixins: [mixable] 134 | }); 135 | 136 | var a = new A(); 137 | 138 | a.mixin(mixable2); 139 | 140 | expect(a.return1()).to.equal(1); 141 | expect(a.return2()).to.equal(2); 142 | }); 143 | 144 | it('modifying the prototype should override mixins added at declaration time', function() { 145 | var A = Class({ 146 | return1: function() { 147 | return 'Original'; 148 | }, 149 | mixins: [{ 150 | return1: function() { 151 | return 'Mixed'; 152 | } 153 | }] 154 | }); 155 | 156 | A.prototype.return1 = function() { 157 | return 'New'; 158 | }; 159 | 160 | var a = new A(); 161 | 162 | expect(a.return1()).to.equal('New'); 163 | }); 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /test/4.lifecycle.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var Class = require('../source/Class'); 3 | 4 | describe('Class lifecycle:', function() { 5 | describe('instantiation', function() { 6 | it('should throw when attempting to instantiate without new', function() { 7 | expect(function() { 8 | var A = Class(); 9 | var a = A(); 10 | }).to.throw(Error); 11 | }); 12 | }); 13 | 14 | describe('constructors', function() { 15 | it('should be chained', function() { 16 | var A = Class({ 17 | construct: function() { 18 | this.A = true; 19 | } 20 | }); 21 | 22 | var B = A.extend({ 23 | construct: function() { 24 | this.B = true; 25 | } 26 | }); 27 | 28 | var b = new B(); 29 | 30 | expect(b.A).to.be.true; 31 | expect(b.B).to.be.true; 32 | }); 33 | 34 | it('should execute in the correct order (superclass first)', function() { 35 | var constructed = {}; 36 | function handleConstruct(name) { 37 | constructed[name] = true; 38 | 39 | if (name === 'B' && !constructed['A']) 40 | throw new Error('B constructed before A'); 41 | }; 42 | 43 | var A = Class({ 44 | construct: function() { 45 | handleConstruct('A'); 46 | } 47 | }); 48 | 49 | var B = A.extend({ 50 | construct: function() { 51 | handleConstruct('B'); 52 | } 53 | }); 54 | 55 | var b = new B(); 56 | }); 57 | 58 | it('should chain if class in chain does not have construct', function() { 59 | var aConstructCalled = false; 60 | 61 | var A = Class({ 62 | construct: function() { 63 | aConstructCalled = true; 64 | } 65 | }); 66 | 67 | var B = A.extend({ 68 | }); 69 | 70 | var C = A.extend({ 71 | construct: function() { 72 | } 73 | }); 74 | 75 | var c = new C(); 76 | 77 | expect(aConstructCalled).to.be.true; 78 | }); 79 | 80 | it('should work when called as instance.constructor()', function() { 81 | var A = Class(); 82 | 83 | var a = new A(); 84 | 85 | var a1 = new a.constructor(); 86 | 87 | expect(a1 instanceof A).to.be.true; 88 | }); 89 | }); 90 | 91 | describe('destructors', function() { 92 | it('should be chained', function() { 93 | var A = Class({ 94 | destruct: function() { 95 | this.A = true; 96 | } 97 | }); 98 | 99 | var B = A.extend({ 100 | destruct: function() { 101 | this.B = true; 102 | } 103 | }); 104 | 105 | var b = new B(); 106 | b.destruct(); 107 | 108 | expect(b.A).to.be.true; 109 | expect(b.B).to.be.true; 110 | }); 111 | 112 | it('should chain if class in chain does not have destruct', function() { 113 | var aDestructCalled = false; 114 | 115 | var A = Class({ 116 | destruct: function() { 117 | aDestructCalled = true; 118 | } 119 | }); 120 | 121 | var B = A.extend({ 122 | }); 123 | 124 | var C = A.extend({ 125 | destruct: function() { 126 | } 127 | }); 128 | 129 | var c = new C(); 130 | c.destruct(); 131 | 132 | expect(aDestructCalled).to.be.true; 133 | }); 134 | 135 | it('should execute in the correct order (childclass first)', function() { 136 | var destructed = {}; 137 | function handleDestruct(name) { 138 | if (name === 'A' && !destructed['B']) 139 | throw new Error('A destructed before B'); 140 | destructed[name] = true; 141 | }; 142 | 143 | var A = Class({ 144 | destruct: function() { 145 | handleDestruct('A'); 146 | } 147 | }); 148 | 149 | var B = A.extend({ 150 | destruct: function() { 151 | handleDestruct('B'); 152 | } 153 | }); 154 | 155 | var b = new B(); 156 | b.destruct(); 157 | }); 158 | }); 159 | 160 | describe('init methods', function() { 161 | it('should be called after all constructors', function() { 162 | var A = Class({ 163 | construct: function() { 164 | this.A = true; 165 | } 166 | }); 167 | 168 | var B = A.extend({ 169 | construct: function() { 170 | this.B = true; 171 | }, 172 | init: function() { 173 | if (!this.A || !this.B) 174 | throw new Error('init called before constructors'); 175 | } 176 | }); 177 | 178 | var b = new B(); 179 | }); 180 | 181 | it('should not be chained', function() { 182 | var A = Class({ 183 | init: function() { 184 | throw new Error('init was chained'); 185 | } 186 | }); 187 | 188 | var B = A.extend({ 189 | init: function() {} 190 | }); 191 | 192 | var b = new B(); 193 | }); 194 | 195 | it('should support use of _super()', function() { 196 | var A = Class({ 197 | init: function() { 198 | return 1; 199 | } 200 | }); 201 | 202 | var B = A.extend({ 203 | init: function() { 204 | expect(this._super.call(this)).to.equal(1); 205 | } 206 | }); 207 | 208 | var b = new B(); 209 | }); 210 | }); 211 | }); 212 | -------------------------------------------------------------------------------- /test/5.super.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var Class = require('../source/Class'); 3 | 4 | describe('Calling super methods:', function() { 5 | var A; 6 | 7 | beforeEach(function(done) { 8 | // Start fresh by re-defining A before every test 9 | A = Class({ 10 | toString: function() { return 'A'; }, 11 | method1: function() { 12 | return 1; 13 | }, 14 | method2: function() { 15 | return 2; 16 | } 17 | }); 18 | 19 | done(); 20 | }); 21 | 22 | it('should call superclass method with _super()', function() { 23 | var B = A.extend({ 24 | method1: function() { 25 | return this._super(); 26 | } 27 | }); 28 | 29 | var b = new B(); 30 | 31 | expect(b.method1()).to.equal(1); 32 | }); 33 | 34 | it('should call superclass method with _super() if intermediate class does not have method', function() { 35 | var B = A.extend({ 36 | }); 37 | 38 | var C = B.extend({ 39 | method1: function() { 40 | return this._super(); 41 | } 42 | }); 43 | 44 | var c = new C(); 45 | 46 | expect(c.method1()).to.equal(1); 47 | }); 48 | 49 | it('should throw when calling _super() in method that does not override', function() { 50 | var B = A.extend({ 51 | methodX: function() { 52 | return this._super(); 53 | } 54 | }); 55 | 56 | var b = new B(); 57 | 58 | expect(b.methodX).to.Throw(); 59 | }); 60 | 61 | it('should support _super() for methods called without an instance', function() { 62 | var B = A.extend({ 63 | method1: function() { 64 | return this._super()+1; 65 | } 66 | }); 67 | 68 | expect(B.prototype.method1()).to.equal(2); 69 | }); 70 | 71 | it('should support _super() with more than one argument', function() { 72 | var A = Class({ 73 | add: function(a, b) { 74 | return a + b; 75 | } 76 | }); 77 | 78 | var B = A.extend({ 79 | add: function(a, b) { 80 | return this._super(a, b); 81 | } 82 | }); 83 | 84 | var b = new B(); 85 | 86 | expect(b.add(2, 4)).to.equal(6); 87 | }); 88 | 89 | it('should support _super() asynchronously without manually setting context', function(done) { 90 | var B = A.extend({ 91 | method1: function() { 92 | var _super = this._super; 93 | 94 | // Delay calling super for 100ms 95 | setTimeout(function() { 96 | expect(_super()).to.equal(1); 97 | done(); 98 | }, 100); 99 | }, 100 | method2: function() { 101 | return this._super(); 102 | } 103 | }); 104 | 105 | var b = new B(); 106 | b.method1(); 107 | expect(b.method2()).to.equal(2); 108 | }); 109 | 110 | 111 | it('should call superclass method correctly after calling another method that uses _super()', function() { 112 | var B = A.extend({ 113 | method1: function() { 114 | this.method2(); 115 | return this._super(); 116 | }, 117 | method2: function() { 118 | return this._super(); 119 | } 120 | }); 121 | 122 | var b = new B(); 123 | 124 | expect(b.method1()).to.equal(1); 125 | }); 126 | 127 | 128 | it('should support _super() within a single mixin on a class', function() { 129 | var A = Class({ 130 | toString: 'MixedUp', 131 | method1: function() { 132 | return 'Original'; 133 | }, 134 | mixins: [ 135 | { 136 | method1: function() { 137 | // Should call the method of the class 138 | return this._super()+'Mixed'; 139 | } 140 | } 141 | ] 142 | }); 143 | 144 | var a = new A(); 145 | 146 | expect(a.method1()).to.equal('OriginalMixed'); 147 | }); 148 | 149 | it('should support _super() within multiple mixins on class', function() { 150 | var A = Class({ 151 | toString: 'MixedUp', 152 | method1: function() { 153 | return 'Original'; 154 | }, 155 | mixins: [ 156 | { 157 | method1: function() { 158 | // Should call the method of the class 159 | return this._super()+'Mixed'; 160 | } 161 | }, 162 | { 163 | method1: function() { 164 | // Should call the method of the previous mixin 165 | return this._super()+'Again'; 166 | } 167 | }, 168 | { 169 | method1: function() { 170 | // Should call the method of the previous mixin 171 | return this._super()+'Twice'; 172 | } 173 | } 174 | ] 175 | }); 176 | 177 | var a = new A(); 178 | 179 | expect(a.method1()).to.equal('OriginalMixedAgainTwice'); 180 | }); 181 | 182 | 183 | it('should support _super() within a single mixin on an instance', function() { 184 | var A = Class({ 185 | toString: 'MixedUp', 186 | method1: function() { 187 | return 'Original'; 188 | } 189 | }); 190 | 191 | var a = new A(); 192 | 193 | a.mixin({ 194 | method1: function() { 195 | // Should call the method of the class 196 | return this._super()+'Mixed'; 197 | } 198 | }); 199 | 200 | expect(a.method1()).to.equal('OriginalMixed'); 201 | }); 202 | 203 | it('should support _super() within multiple mixins on an instance', function() { 204 | var A = Class({ 205 | toString: 'MixedUp', 206 | method1: function() { 207 | return 'Original'; 208 | } 209 | }); 210 | 211 | var a = new A(); 212 | 213 | a.mixin({ 214 | method1: function() { 215 | // Should call the method of the class 216 | return this._super()+'Mixed'; 217 | } 218 | }); 219 | 220 | a.mixin({ 221 | method1: function() { 222 | // Should call the method of the previous mixin 223 | return this._super()+'Again'; 224 | } 225 | }); 226 | 227 | a.mixin({ 228 | method1: function() { 229 | // Should call the method of the previous mixin 230 | return this._super()+'Twice'; 231 | } 232 | }); 233 | 234 | expect(a.method1()).to.equal('OriginalMixedAgainTwice'); 235 | }); 236 | 237 | it('should support _super() with mixins on class and instance', function() { 238 | var A = Class({ 239 | mixins: [{ 240 | method1: function() { 241 | return 'MixedFirst' 242 | } 243 | }] 244 | }); 245 | 246 | var a = new A(); 247 | 248 | a.mixin({ 249 | method1: function() { 250 | return this._super()+'MixedLater'; 251 | } 252 | }); 253 | 254 | expect(a.method1()).to.equal('MixedFirstMixedLater'); 255 | }); 256 | 257 | it('should call the new function when using _super() in a class mixin method after prototype chain modified', function() { 258 | var A = Class({ 259 | method1: function() { 260 | return 'Original'; 261 | } 262 | }); 263 | 264 | var B = A.extend({ 265 | method1: function() { 266 | return this._super(); 267 | } 268 | }); 269 | 270 | A.prototype.method1 = function() { 271 | return 'New'; 272 | }; 273 | 274 | var b = new B(); 275 | 276 | expect(b.method1()).to.equal('New'); 277 | 278 | b.mixin({ 279 | method1: function() { 280 | return 'MixedAgain'+this._super(); 281 | } 282 | }); 283 | 284 | expect(b.method1()).to.equal('MixedAgainNew'); 285 | }); 286 | 287 | it('should call the new function when using _super() in an instance mixin method after prototype chain modified', function() { 288 | var A = Class({ 289 | method1: function() { 290 | return 'Original'; 291 | } 292 | }); 293 | 294 | var a = new A(); 295 | 296 | A.prototype.method1 = function() { 297 | return 'New'; 298 | }; 299 | 300 | a.mixin({ 301 | method1: function() { 302 | return 'Mixed'+this._super(); 303 | } 304 | }); 305 | 306 | expect(a.method1()).to.equal('MixedNew'); 307 | }); 308 | 309 | it('should pass correct arguments when using _super.apply()', function() { 310 | var A = Class({ 311 | method1: function(num) { 312 | expect(num).to.equal(5); 313 | } 314 | }); 315 | 316 | var B = A.extend({ 317 | method1: function(number) { 318 | return this._super.apply(this, arguments); 319 | } 320 | }); 321 | 322 | var b = new B(); 323 | 324 | b.method1(5); 325 | }); 326 | 327 | it('should pass correct arguments when using _super.apply() with mixins on a class', function() { 328 | var A = Class({ 329 | method1: function(num) { 330 | expect(num).to.equal(5); 331 | return num; 332 | } 333 | }); 334 | 335 | var B = A.extend({ 336 | method1: function() { 337 | return this._super.apply(this, arguments)+1; 338 | }, 339 | mixins: [ 340 | { 341 | method1: function() { 342 | return this._super.apply(this, arguments)+1; 343 | } 344 | }, 345 | { 346 | method1: function() { 347 | return this._super.apply(this, arguments)+1; 348 | } 349 | } 350 | ] 351 | }); 352 | 353 | var b = new B(); 354 | 355 | expect(b.method1(5)).to.equal(8); 356 | }); 357 | 358 | it('should pass correct arguments when using _super.apply() with mixins on an instance', function() { 359 | var A = Class({ 360 | method1: function(num) { 361 | expect(num).to.equal(5); 362 | return num; 363 | } 364 | }); 365 | 366 | var B = A.extend({ 367 | method1: function() { 368 | return this._super.apply(this, arguments)+1; 369 | } 370 | }); 371 | 372 | var b = new B(); 373 | 374 | b.mixin({ 375 | method1: function() { 376 | return this._super.apply(this, arguments)+1; 377 | } 378 | }); 379 | 380 | b.mixin({ 381 | method1: function() { 382 | return this._super.apply(this, arguments)+1; 383 | } 384 | }); 385 | 386 | expect(b.method1(5)).to.equal(8); 387 | }); 388 | 389 | 390 | it('should have this set to the correct value when using _super()', function() { 391 | var A = Class({ 392 | method1: function(num) { 393 | expect(this).to.equal(b); 394 | return num; 395 | } 396 | }); 397 | 398 | var B = A.extend({ 399 | method1: function() { 400 | expect(this).to.equal(b); 401 | return this._super(); 402 | } 403 | }); 404 | 405 | var b = new B(); 406 | 407 | b.method1(); 408 | }); 409 | 410 | 411 | 412 | it('should have this set to the correct value when using _super.apply(this)', function() { 413 | var A = Class({ 414 | method1: function(num) { 415 | expect(this).to.equal(b); 416 | return num; 417 | } 418 | }); 419 | 420 | var B = A.extend({ 421 | method1: function() { 422 | expect(this).to.equal(b); 423 | return this._super.apply(this, arguments); 424 | } 425 | }); 426 | 427 | var b = new B(); 428 | 429 | b.method1(); 430 | }); 431 | 432 | it('should have this set to the correct value when using _super.apply(this) and a single mixin', function() { 433 | var A = Class({ 434 | method1: function(num) { 435 | expect(this).to.equal(b); 436 | return num; 437 | } 438 | }); 439 | 440 | var B = A.extend({ 441 | method1: function() { 442 | expect(this).to.equal(b); 443 | return this._super.apply(this, arguments); 444 | } 445 | }); 446 | 447 | var b = new B(); 448 | 449 | b.mixin({ 450 | method1: function() { 451 | expect(this).to.equal(b); 452 | return this._super.apply(this, arguments); 453 | } 454 | }); 455 | 456 | b.mixin({ 457 | method1: function() { 458 | expect(this).to.equal(b); 459 | return this._super.apply(this, arguments); 460 | } 461 | }); 462 | 463 | b.method1(); 464 | }); 465 | 466 | it('should have this set to the correct value when using _super.apply(this) and multiple mixins', function() { 467 | var A = Class({ 468 | method1: function(num) { 469 | expect(this).to.equal(b); 470 | return num; 471 | } 472 | }); 473 | 474 | var B = A.extend({ 475 | method1: function() { 476 | expect(this).to.equal(b); 477 | return this._super.apply(this, arguments); 478 | } 479 | }); 480 | 481 | var b = new B(); 482 | 483 | b.mixin({ 484 | method1: function() { 485 | expect(this).to.equal(b); 486 | return this._super.apply(this, arguments); 487 | } 488 | }); 489 | 490 | b.mixin({ 491 | method1: function() { 492 | expect(this).to.equal(b); 493 | return this._super.apply(this, arguments); 494 | } 495 | }); 496 | 497 | b.method1(); 498 | }); 499 | 500 | }); 501 | -------------------------------------------------------------------------------- /test/6.staticProps.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var Class = require('../source/Class'); 3 | 4 | describe('Class statics:', function() { 5 | describe('methods', function() { 6 | it('using Class.staticMethod', function() { 7 | var A = Class.extend(); 8 | A.staticMethod = function() { 9 | return 'Static method'; 10 | }; 11 | 12 | expect(A.staticMethod()).to.equal('Static method'); 13 | }); 14 | 15 | it('using instance.constructor.staticMethod', function() { 16 | var A = Class.extend(); 17 | A.staticMethod = function() { 18 | return 'Static method'; 19 | }; 20 | 21 | var a = new A(); 22 | 23 | expect(a.constructor.staticMethod()).to.equal('Static method'); 24 | }); 25 | 26 | it('of a parent class using Class.superConstructor', function() { 27 | var A = Class.extend(); 28 | A.staticMethod = function() { 29 | return 'Static method'; 30 | }; 31 | 32 | var B = A.extend(); 33 | 34 | expect(B.superConstructor.staticMethod()).to.equal('Static method'); 35 | }); 36 | 37 | it('of a parent class using instance.constructor.superConstructor', function() { 38 | var A = Class.extend(); 39 | A.staticMethod = function() { 40 | return 'Static method'; 41 | }; 42 | 43 | var B = A.extend(); 44 | 45 | var b = new B(); 46 | 47 | expect(b.constructor.superConstructor.staticMethod()).to.equal('Static method'); 48 | }); 49 | 50 | it('chained across multiple parent classes on a class', function() { 51 | var A = Class.extend(); 52 | A.staticMethod = function() { 53 | return 'Static method'; 54 | }; 55 | 56 | var B = A.extend(); 57 | B.staticMethod = function() { 58 | return this.superConstructor.staticMethod(); 59 | }; 60 | 61 | var C = B.extend(); 62 | C.staticMethod = function() { 63 | return this.superConstructor.staticMethod(); 64 | }; 65 | 66 | expect(C.staticMethod()).to.equal('Static method'); 67 | }); 68 | 69 | it('chained across multiple parent classes on an instance', function() { 70 | var A = Class.extend(); 71 | A.staticMethod = function() { 72 | return 'Static method'; 73 | }; 74 | 75 | var B = A.extend(); 76 | B.staticMethod = function() { 77 | return this.superConstructor.staticMethod(); 78 | }; 79 | 80 | var C = B.extend(); 81 | C.staticMethod = function() { 82 | return this.superConstructor.staticMethod(); 83 | }; 84 | 85 | var c = new C(); 86 | 87 | expect(c.constructor.staticMethod()).to.equal('Static method'); 88 | }); 89 | }); 90 | 91 | describe('properties', function() { 92 | it('using Class.staticProperty', function() { 93 | var A = Class.extend(); 94 | A.staticProperty = 'Static property'; 95 | 96 | expect(A.staticProperty).to.equal('Static property'); 97 | }); 98 | 99 | it('using constructor.staticMethod', function() { 100 | var A = Class.extend(); 101 | A.staticProperty = 'Static property'; 102 | 103 | var a = new A(); 104 | 105 | expect(a.constructor.staticProperty).to.equal('Static property'); 106 | }); 107 | 108 | it('of a parent class using Class.superConstructor', function() { 109 | var A = Class.extend(); 110 | A.staticProperty = 'Static property'; 111 | 112 | var B = A.extend(); 113 | 114 | expect(B.superConstructor.staticProperty).to.equal('Static property'); 115 | }); 116 | 117 | it('of a parent class using instance.constructor.superConstructor', function() { 118 | var A = Class.extend(); 119 | A.staticProperty = 'Static property'; 120 | 121 | var B = A.extend(); 122 | 123 | var b = new B(); 124 | 125 | expect(b.constructor.superConstructor.staticProperty).to.equal('Static property'); 126 | }); 127 | 128 | it('of a grandparent class using chained superConstructor on a class', function() { 129 | var A = Class.extend(); 130 | A.staticProperty = 'Static property'; 131 | 132 | var B = A.extend(); 133 | 134 | var C = B.extend(); 135 | 136 | expect(C.superConstructor.superConstructor.staticProperty).to.equal('Static property'); 137 | }); 138 | 139 | it('of a grandparent class using chained superConstructor on an instance', function() { 140 | var A = Class.extend(); 141 | A.staticProperty = 'Static property'; 142 | 143 | var B = A.extend(); 144 | 145 | var C = B.extend(); 146 | 147 | var c = new C(); 148 | 149 | expect(c.constructor.superConstructor.superConstructor.staticProperty).to.equal('Static property'); 150 | }); 151 | }); 152 | }); 153 | -------------------------------------------------------------------------------- /test/7.properties.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var Class = require('../source/Class'); 3 | 4 | describe('Instance properties:', function() { 5 | describe('property definition', function() { 6 | it('basic properties', function() { 7 | var A = Class.extend({ 8 | properties: { 9 | name: { 10 | value: 'PseudoClass', 11 | writable: false 12 | } 13 | } 14 | }); 15 | 16 | var a = new A(); 17 | 18 | expect(a.name).to.equal('PseudoClass'); 19 | }); 20 | 21 | it('properties with setters and getters', function() { 22 | var A = Class.extend({ 23 | properties: { 24 | name: { 25 | set: function(name) { 26 | this._name = name; 27 | }, 28 | get: function() { 29 | return this._name; 30 | } 31 | } 32 | } 33 | }); 34 | 35 | var a = new A(); 36 | a.name = 'PseudoClass'; 37 | 38 | expect(a.name).to.equal('PseudoClass'); 39 | expect(a._name).to.equal('PseudoClass'); 40 | }); 41 | 42 | it('properties with setters and getters as method strings', function() { 43 | var A = Class.extend({ 44 | properties: { 45 | name: { 46 | set: 'setName', 47 | get: 'getName' 48 | } 49 | }, 50 | setName: function(name) { 51 | this._name = name; 52 | }, 53 | getName: function() { 54 | return this._name; 55 | } 56 | }); 57 | 58 | var a = new A(); 59 | a.name = 'PseudoClass'; 60 | 61 | expect(a.name).to.equal('PseudoClass'); 62 | expect(a._name).to.equal('PseudoClass'); 63 | }); 64 | 65 | it('properties with setters and getters as method strings with monkey-patching', function() { 66 | var A = Class.extend({ 67 | properties: { 68 | name: { 69 | set: 'setName', 70 | get: 'getName' 71 | } 72 | } 73 | }); 74 | 75 | var a = new A(); 76 | 77 | // Add methods after instantiation 78 | A.prototype.setName = function(name) { 79 | this._name = name; 80 | }; 81 | A.prototype.getName = function() { 82 | return this._name; 83 | }; 84 | 85 | a.name = 'PseudoClass'; 86 | 87 | expect(a.name).to.equal('PseudoClass'); 88 | expect(a._name).to.equal('PseudoClass'); 89 | }); 90 | 91 | it('should not put properties on the prototype', function() { 92 | var A = Class.extend({ 93 | properties: { 94 | name: { 95 | value: 'PseudoClass', 96 | writable: false 97 | } 98 | } 99 | }); 100 | 101 | expect(A.properties).to.be.truthy; 102 | expect(A.prototype.properties).to.not.be.truthy; 103 | }); 104 | }); 105 | 106 | describe('property extension', function() { 107 | it('basic property extension', function() { 108 | var A = Class.extend({ 109 | properties: { 110 | name: { 111 | value: 'PseudoClass', 112 | writable: false 113 | } 114 | } 115 | }); 116 | 117 | var B = A.extend({ 118 | properties: { 119 | name: { 120 | value: 'PseudoChildClass' 121 | } 122 | } 123 | }); 124 | 125 | var b = new B(); 126 | 127 | expect(b.name).to.equal('PseudoChildClass'); 128 | 129 | // Test that lack of writability was preserved 130 | b.name = 'NewName'; 131 | 132 | expect(b.name).to.equal('PseudoChildClass'); 133 | }); 134 | 135 | it('property setter override', function() { 136 | var A = Class.extend({ 137 | properties: { 138 | name: { 139 | set: function(name) { 140 | this._name = name; 141 | }, 142 | get: function() { 143 | return this._name; 144 | } 145 | } 146 | } 147 | }); 148 | 149 | var B = A.extend({ 150 | properties: { 151 | name: { 152 | set: function(name) { 153 | this._otherName = name; 154 | }, 155 | get: function() { 156 | return this._otherName; 157 | } 158 | } 159 | } 160 | }); 161 | 162 | var b = new B(); 163 | b.name = 'PseudoClass'; 164 | 165 | expect(b.name).to.equal('PseudoClass'); 166 | expect(b._otherName).to.equal('PseudoClass'); 167 | expect(b._name).to.be.undefined; 168 | }); 169 | 170 | it('should not modify parent properities during extension', function() { 171 | var A = Class.extend({ 172 | properties: { 173 | name: { 174 | writable: false, 175 | value: 'Original' 176 | } 177 | } 178 | }); 179 | 180 | var B = A.extend({ 181 | properties: { 182 | name: { 183 | value: 'New' 184 | } 185 | } 186 | }); 187 | 188 | var b = new B(); 189 | expect(A.properties.name.writable).to.equal(false); 190 | expect(A.properties.name.value).to.equal('Original'); 191 | expect(B.properties.name.writable).to.equal(false); 192 | expect(B.properties.name.value).to.equal('New'); 193 | }); 194 | 195 | it('should inherit properties from parent if no properties are defined on the child', function() { 196 | var Parent = Class.extend({ 197 | properties: { 198 | value: { 199 | set: function(value) { this._value = value; }, 200 | get: function(value) { return this._value; } 201 | } 202 | } 203 | }); 204 | 205 | var Child = Parent.extend({ properties: {} }); 206 | 207 | var child = new Child(); 208 | 209 | child.value = 1; 210 | 211 | expect(child._value).to.equal(1); 212 | expect(child.value).to.equal(1); 213 | }); 214 | }); 215 | }); 216 | --------------------------------------------------------------------------------