/* The following examples explain the common design pattern for OxJS UI widgets: an inheritance model that is neither classical nor prototypal, but "parasitical" (a term coined by Douglas Crockford). In a nutshell, "instances" are created by augmenting other instances, but in addition to private members (`var foo`) and public members (`that.bar`), they can have shared private members (`self.baz`). `self` cannot be accessed from outside, but since `self` itself is an argument of the "constructor", an instance can inherit its parent's `self` by passing its own `self`. */ 'use strict'; /* Load the UI module. */ Ox.load(['Image', 'UI'], function() { /* Create our own namespace. */ Ox.My = {}; /* First, lets build the most basic Box widget. A widget is a "constructor" function that takes two (optional) arguments, `options` and `self`, and returns a widget object. It's not a constructor in JavaScript terms though: It doesn't have to be called with `new`, and doesn't return an `instanceof` anything. It just enhances another widget object and returns it. */ Ox.My.Box = function(options, self) { /* This is how every widget "constructor" begins. `self` is the widget's shared private object. */ self = self || {}; /* `that` is the widget itself, its public object, or, in JavaScript terms, its `this`. Every widget "inherits" from another widget by simple assignment. All public properties of the "super" widget, i.e. all properties of its `that`, will be present on our own `that`. In this case, we use Ox.Element, the "root" widget at the end of the inheritance chain, and pass an empty options object. But we always pass our own `self`, which means that any property that Ox.Element (or any other widget in the inheritance chain) adds to `self` will be present on our own `self`. Then we call the public `defaults`, `options` and `update` methods of Ox.Element. `defaults` assigns the defaults object to `self.defaults` and copies it to `self.options`, `options` extends `self.options` with the options object, and `update` adds one or more callbacks that are invoked whenever, by way of calling the `options` method, a property of `self.options` is modified or added. */ var that = Ox.Element({}, self) .defaults({ color: [128, 128, 128], size: [128, 128] }) .options(options || {}) .update({ color: setColor, size: setSize }) .addClass('OxMyBox'); /* The second part of the "constructor" function can be thought of as the "initializer", and contains everything needed to set up the "instance". In this case, we just define a minimum and maximum size and then set the widget's color and size. We could have used `var minSize` and `var maxSize` here, but by using `self` for private variables that we want to be accessible across all the widget's methods, we can be sure that inside such methods, any local `var` is actually local to the method. */ self.minSize = 1; self.maxSize = 256; setColor(); setSize(); /* Third, we declare the widget's private methods. These are just function declarations, hoisted to the top of the "constructor". */ function setColor() { /* To interact with the DOM, Ox.Element (and any widget derived from it) wraps jQuery. If you type Ox.Element() in the console, you will get something like `[
]`, and the widget's prototype has all the methods of a `$('
')`, with proper chaining. If you have `var $d = $('
'), $e = Ox.Element();`, then `$d.appendTo($e)` returns `$d`, and `$e.append($d)` returns `$e`. */ that.css({ backgroundColor: 'rgb(' + self.options.color.join(', ') + ')', }); } function setSize() { /* Before setting the size, we make sure the value is between `minSize` and `maxSize`. */ self.options.size = self.options.size.map(function(value) { return Ox.limit(value, self.minSize, self.maxSize); }); that.css({ width: self.options.size[0] + 'px', height: self.options.size[1] + 'px' }); } /* Next, we define the widgets public methods, as properties of `that`. (Note that unlike private methods, they are not hoisted.) */ that.displayText = function(text) { /* As there isn't much to do yet, this method just displays the widget's options. */ that.empty(); text && that.append($('
').addClass('OxMyText').html(text)); /* Public methods should return `that`, for chaining. */ return that; }; /* And finally, at the very end of the "constructor", we return `that`. And that's it. */ return that; }; Ox.My.InvertibleBox = function(options, self) { self = self || {}; var that = Ox.My.Box({}, self); that.defaults(Ox.extend(that.defaults(), { inverted: false })) .options(options || {}) .update({ color: setColor, inverted: setColor }) .addClass('OxMyInvertibleBox') .bindEvent({ doubleclick: function() { that.invert(); } }); self.options.inverted && setColor(); function getInvertedColor() { return self.options.color.map(function(value) { return 255 - value; }); } function setColor() { that.css({backgroundColor: 'rgb(' + ( self.options.inverted ? getInvertedColor() : self.options.color ).join(', ') + ')'}); return false; } that.invert = function() { that.options({inverted: !self.options.inverted}); that.triggerEvent('invert'); return that; }; return that; }; Ox.My.MetaBox = function(options, self) { self = self || {}; var that = Ox.My.Box({}, self); that.defaults(Ox.extend(that.defaults(), { color: [[[128, 128, 128]]] })) .options(options || {}) .update({color: setColor}) .bindEvent({ doubleclick: function() { that.invert(); } }); self.sizes = [ Ox.splitInt(self.options.size[0], self.options.color[0].length), Ox.splitInt(self.options.size[1], self.options.color.length) ]; self.$boxes = self.options.color.map(function(array, y) { return array.map(function(color, x) { return Ox.My.InvertibleBox({ color: color, size: [self.sizes[0][x], self.sizes[1][y]] }) .unbindEvent('doubleclick') .appendTo(that); }); }); function setColor() { self.$boxes.forEach(function(array, y) { array.forEach(function($box, x) { $box.options({color: self.options.color[y][x]}); }); }); } self.invertBox = function($box) { $box.invert(); }; that.invert = function() { self.$boxes.forEach(function(array) { array.forEach(self.invertBox); }); that.triggerEvent('invert'); return that; }; return that; }; Ox.My.PixelBox = function(options, self) { self = self || {}; self.options = Ox.extend(Ox.My.Box().defaults(), options || {}); var that = Ox.My.MetaBox(Ox.extend(self.options, { color: getColor() }), self) .update({ color: function() { setColor(); return false; } }); self.invertBox = function($box, x) { $box.options({ color: $box.options('color').map(function(value, i) { return i == x ? 255 - value : value }) }); }; function getColor() { return [[ [self.options.color[0], 0, 0], [0, self.options.color[1], 0], [0, 0, self.options.color[2]] ]]; } function setColor() { self.$pixel.options({color: getColor()}); } return that; }; Ox.My.ImageBox = function(options, self) { self = self || {}; var that = Ox.My.Box({}, self).displayText('Loading...') that.defaults({ image: null }) .options(options || {}); Ox.Image(self.options.image, function(image) { var size = image.getSize(); size = [size.width, size.height]; self.sizes = size.map(function(value, index) { return Ox.splitInt(self.options.size[index], value); }); that.displayText(); self.$boxes = Ox.range(size[1]).map(function(y) { return Ox.range(size[0]).map(function(x) { return Ox.My.PixelBox({ color: image.pixel(x, y).slice(0, 3), size: [self.sizes[0][x], self.sizes[1][y]] }) .unbindEvent('doubleclick') .appendTo(that); }); }); }); that.invert = Ox.My.MetaBox(self.options, self).invert; return that; }; /* This is left as an exercise for the reader ;) */ Ox.My.VideoBox = function(options, self) { }; (function() { var h = Ox.random(360), s = 1, l = 0.5; window.My = {}; My.$backgroundBox = Ox.My.Box({ size: [256, 256] }) .append( My.$box = Ox.My.Box({ color: Ox.rgb(h, s, l) }), My.$invertibleBox = Ox.My.InvertibleBox({ color: Ox.rgb(h + 120, s, l) }), My.$metaBox = Ox.My.MetaBox({ color: Ox.range(2).map(function(y) { return Ox.range(3).map(function(x) { return Ox.rgb(h + x * 60 + y * 180, s, l); }); }) }), My.$pixelBox = Ox.My.PixelBox({ color: Ox.rgb(h + 120, s, l) }) ) .appendTo(Ox.$body); My.$imageBox = Ox.My.ImageBox({ image: 'png/pandora32.png', size: [256, 256] }) .appendTo(Ox.$body); Ox.forEach(My, function($element, name) { $element.bindEvent({ invert: function() { My.$box.displayText(name[1].toUpperCase() + name.slice(2)); } }) }); }()); });