oxjs/examples/ui/widget_design_patterns/js/example.js
2012-06-25 20:40:14 +02:00

358 lines
12 KiB
JavaScript

/*
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 <a
href="http://www.crockford.com/javascript/inheritance.html">Douglas
Crockford</a>). 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 `[<div class="OxElement"></div>]`, and the
widget's prototype has all the methods of a `$('<div>')`, with
proper chaining. If you have `var $d = $('<div>'), $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($('<div>').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));
}
})
});
}());
});