/* 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'; /* Create our own namespace. Not required, but if you wanted to create a module named `My`, this is how you would do it. */ Ox.My = {}; /*
var that = Ox.My.Box({}, self)
.defaults({
color: [128, 128, 128],
inverted: false,
size: [128, 128]
})
.options(options || ())
.update({
...
})
— but why repeat the defaults of `Ox.My.Box` if we can simply extend
them. (Just like `options()` returns all options of a widget, `defaults()`
returns all its defaults.)
*/
var that = Ox.My.Box({}, self);
that.defaults(Ox.extend(that.defaults(), {
inverted: false
}))
.options(options || {})
/*
Again, we add handlers that run when the widget's options are updated.
The original handlers of `Ox.My.Box` will run next, so we just add the
ones we need. We leave out `size`, so when the `size` option changes,
we'll get the original behavior.
*/
.update({
color: setColor,
inverted: setColor
})
/*
The same as `.css({cursor: 'pointer'})`.
*/
.addClass('OxMyInvertibleBox')
/*
Ox.Element and its descendants provide a number of public methods
(`bindEvent`, `bindEventOnce`, `triggerEvent` and `unbindEvent`) that
allow widgets to communicate via custom events. Here, we add a handler
for Ox.Element's `doubleclick` event. If we just wanted to handle a
`click` event, we could also use jQuery here:
.on({
click: function() {
that.invert();
}
})
*/
.bindEvent({
doubleclick: function() {
that.invert();
}
});
/*
The idea is that our widget's inverted state is separate from its color. If
the inverted option is set, then the color option stays the same, but has
the inverse effect. This means that when initializing the widget, we have
to call our custom `setColor` method if `self.options.inverted` is `true`.
*/
self.options.inverted && setColor();
/*
When `setColor` is invoked as an update handler, returning `false` signals
that no other handler should run. Otherwise, the original handler of
`Ox.My.Box` would run next, and revert any inversion we might have done
here.
*/
function setColor() {
that.css({backgroundColor: 'rgb(' + (
self.options.inverted ? self.options.color.map(function(value) {
return 255 - value;
}) : self.options.color
).join(', ') + ')'});
return false;
}
/*
The public `invert` method is added as a convenience for the users of our
widget, so that when they want to toggle its inverted state, they don't have
to write
$widget.options({
inverted: !$widget.options('inverted')
});
all the time.
Also, we trigger an `invert` event, that anyone can bind to via
$widget.bindEvent({
invert: function() { ... }
});
*/
that.invert = function() {
that.options({inverted: !self.options.inverted});
that.triggerEvent('invert');
return that;
};
/*
And again, we return `that`.
*/
return that;
};
/*
Ox.My.MetaBox({
color: [
[[255, 0, 0], [255, 255, 0], [0, 255, 0]],
[[0, 255, 255], [0, 255, 0], [255, 0, 255]]
]
});
*/
self = self || {};
var that = Ox.My.InvertibleBox({}, self)
.options(options || {})
.update({color: setColor});
/*
But we keep the default color of `Ox.My.InvertibleBox` (`[128, 128, 128]`)
as our own default color, and only here check if the color option is a
single value. In that case, we convert it to an array of one row and one
column. This way, whenever someone accidentally passes a single color value,
our MetaBox can handle it.
*/
if (Ox.isNumber(self.options.color[0])) {
self.options.color = [[self.options.color]];
}
/*
`self.sizes` holds the width of each column and the height of each row.
`self.options.color.length` is the number of rows,
`self.options.color[0].length` the number of columns, and Ox.splitInt(a, b)
"splits" an integer `a` into an array of `b` integers that sum up to `a`.
(We don't want fractional pixel sizes.)
*/
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` are the actual boxes. We use `Ox.My.InvertibleBox`, but
remove their `doubleclick` handlers, since our meta-box already has one.
(`unbindEvent(event)` removes all handlers, `unbindEvent(event, handler)`
removes a specific one.) Then we simply append each box to the meta-box.
*/
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);
});
});
/*
To set the color of a meta-box means to set the color of each box.
*/
function setColor() {
self.$boxes.forEach(function(array, y) {
array.forEach(function($box, x) {
$box.options({color: self.options.color[y][x]});
});
});
}
/*
This is the rare case of a shared private method. Its purpose will become
apparent a bit later. Otherwise, we could just have made a private function,
or an anonymous function in the loop below.
*/
self.invertBox = function($box) {
$box.invert();
};
/*
Here, we override the public `invert` method of `Ox.My.InvertibleBox`. When
inverting an `Ox.My.MetaBox`, we have to invert each of its boxes. (If we
wanted to keep the original method around, we could store it as
`that.superInvert` before.)
*/
that.invert = function() {
self.$boxes.forEach(function(array) {
array.forEach(self.invertBox);
});
that.triggerEvent('invert');
return that;
};
/*
And that's all it takes to make a meta-box.
*/
return that;
};
/*
someOtherObject.method.apply(this, args)
pattern that is common in JavaScript.
Note that we have to pass `self.options` too, otherwise our own size would
get overwritten by the MetaBox default size.
*/
that.invert = Ox.My.MetaBox(self.options, self).invert;
/*
But wait — where's our `doubleclick` handler?
*/
return that;
};
/*