oxjs/examples/ui/widget_design_patterns/js/example.js

457 lines
14 KiB
JavaScript
Raw Normal View History

2012-05-29 10:22:06 +00:00
/*
2012-06-12 12:32:49 +00:00
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
2012-05-29 10:22:06 +00:00
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`.
*/
2012-05-28 14:38:16 +00:00
'use strict';
/*
Load the UI module.
*/
2012-06-25 14:39:31 +00:00
Ox.load(['Image', 'UI'], function() {
2012-05-28 14:38:16 +00:00
/*
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:
2012-05-29 10:22:06 +00:00
It doesn't have to be called with `new`, and doesn't return an `instanceof`
2012-05-28 14:38:16 +00:00
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
2012-05-29 10:22:06 +00:00
shared private object.
2012-05-28 14:38:16 +00:00
*/
self = self || {};
/*
`that` is the widget itself, its public object, or, in JavaScript terms,
2012-05-29 10:22:06 +00:00
its `this`. Every widget "inherits" from another widget by simple
2012-05-28 14:38:16 +00:00
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
2012-05-29 10:22:06 +00:00
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.
2012-05-28 14:38:16 +00:00
*/
var that = Ox.Element({}, self)
.defaults({
color: [128, 128, 128],
2012-06-25 14:39:31 +00:00
size: [128, 128]
2012-05-28 14:38:16 +00:00
})
2012-05-28 16:15:16 +00:00
.options(options || {})
.update({
color: setColor,
size: setSize
2012-06-25 14:39:31 +00:00
})
.addClass('OxMyBox');
2012-05-28 14:38:16 +00:00
/*
2012-05-29 10:22:06 +00:00
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.
2012-05-28 14:38:16 +00:00
*/
2012-06-25 14:39:31 +00:00
self.minSize = 1;
self.maxSize = 256;
2012-05-28 14:38:16 +00:00
setColor();
setSize();
/*
2012-05-29 10:22:06 +00:00
Third, we declare the widget's private methods. These are just function
2012-05-28 14:38:16 +00:00
declarations, hoisted to the top of the "constructor".
*/
function setColor() {
/*
To interact with the DOM, Ox.Element (and any widget derived from
2012-05-29 10:22:06 +00:00
it) wraps jQuery. If you type Ox.Element() in the console, you
2012-05-28 14:38:16 +00:00
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() {
/*
2012-05-29 10:22:06 +00:00
Before setting the size, we make sure the value is between `minSize`
and `maxSize`.
2012-05-28 14:38:16 +00:00
*/
2012-06-25 14:39:31 +00:00
self.options.size = self.options.size.map(function(value) {
return Ox.limit(value, self.minSize, self.maxSize);
});
2012-05-28 14:38:16 +00:00
that.css({
2012-06-25 14:39:31 +00:00
width: self.options.size[0] + 'px',
height: self.options.size[1] + 'px'
2012-05-28 14:38:16 +00:00
});
}
/*
Next, we define the widgets public methods, as properties of `that`.
(Note that unlike private methods, they are not hoisted.)
*/
that.showOptions = function() {
/*
As there isn't much to do yet, this method just displays the
widget's options.
*/
that.html(JSON.stringify(self.options).replace(/([,:])/g, '$1 '));
/*
Public methods should return `that`, for chaining.
*/
return that;
};
/*
2012-05-29 10:22:06 +00:00
And finally, at the very end of the "constructor", we return `that`. And
2012-05-28 14:38:16 +00:00
that's it.
*/
return that;
};
2012-06-25 14:39:31 +00:00
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});
};
return that;
};
Ox.My.MetaBox = function(options, self) {
self = self || {};
var that = Ox.My.Box(options || {}, self)
.update({color: setColor});
self.inverting = false;
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]]
})
.update({
inverted: function() {
if (!self.inverting) {
self.inverting = true;
self.$boxes[y][x].invert();
that.invert();
return false;
}
}
})
.appendTo(that);
});
});
function setColor() {
self.$boxes.forEach(function(array, y) {
array.forEach(function($box, x) {
$box.options({color: self.options.color[y][x]});
});
});
}
that.invert = function() {
self.inverting = true;
self.$boxes.forEach(function(array) {
array.forEach(function($box) {
$box.invert();
});
});
self.inverting = false;
};
return that;
};
Ox.My.PixelBox = function(options, self) {
self = self || {};
var that = Ox.My.Box(options || {}, self)
.update({
color: setColor
});
self.$pixel = Ox.My.MetaBox(Ox.extend(options, {
color: getColor()
}), self)
.appendTo(that);
self.$pixel.invert = function() {
self.inverting = true;
self.$boxes.forEach(function(array) {
array.forEach(function($box, x) {
$box.options({
color: $box.options('color').map(function(value, i) {
return i == x ? 255 - value : value
})
});
});
});
self.inverting = false;
that.triggerEvent('invert');
};
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()});
}
that.invert = function() {
self.$pixel.invert();
};
return that;
};
Ox.My.ImageBox = function(options, self) {
self = self || {};
var that = Ox.My.Box(options || {}, self);
self.inverting = false;
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);
});
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]]
})
.bindEvent({
invert: function() {
if (!self.inverting) {
self.inverting = true;
self.$boxes[y][x].invert();
that.invert();
return false;
}
}
})
.appendTo(that);
});
});
});
that.invert = function() {
self.inverting = true;
self.$boxes.forEach(function(array) {
array.forEach(function($box) {
$box.invert();
});
});
self.inverting = false;
};
return that;
};
2012-05-28 14:38:16 +00:00
/*
2012-06-25 14:39:31 +00:00
This is left as an exercise for the reader ;)
2012-05-28 14:38:16 +00:00
*/
2012-06-25 14:39:31 +00:00
Ox.My.VideoBox = function(options, self) {
};
window.My = {};
My.$div = $('<div>')
.css({float: 'left', width: '256px', height: '256px'})
.appendTo(Ox.$body);
My.$box = Ox.My.Box({
color: [255, 0, 0]
}).appendTo(My.$div);
My.$invertibleBox = Ox.My.InvertibleBox({
color: [0, 0, 255]
}).appendTo(My.$div);
My.$metaBox = Ox.My.MetaBox({
color: [
[[255, 0, 0], [255, 255, 0], [0, 255, 0]],
[[0, 255, 255], [0, 0, 255], [255, 0, 255]]
]
}).appendTo(My.$div);
My.$pixelBox = Ox.My.PixelBox({
color: [255, 128, 0]
}).appendTo(My.$div);
setTimeout(function() {
My.$imageBox = Ox.My.ImageBox({
image: 'png/pandora32.png',
size: [256, 256]
}).appendTo(Ox.$body);
});
return;
2012-05-28 14:38:16 +00:00
/*
Now we create a new widget that subclasses `Ox.My.Box`.
*/
2012-05-28 16:15:16 +00:00
Ox.My.RoundedBox = function(options, self) {
self = self || {};
/*
This time `that` is an instance of `Ox.My.Box`
*/
2012-05-28 16:15:16 +00:00
var that = Ox.My.Box({}, self);
/*
`Ox.RoundedBox` has additional default properties,
we define this by overwriting the defaults.
Sice we do not want to overwrite the existing defaults,
we first get the defaults and extend them with our new values.
Here we also show an alternative way of getting updates.
Instead of passing an object `{key: function}`,
we can also pass one function that gets called with key, value.
*/
2012-05-28 16:15:16 +00:00
that.defaults(Ox.extend(that.defaults(), {
radius: 16
}))
.options(options || {})
.update(function(key, value) {
if (key == 'radius') {
setRadius();
}
});
setRadius();
function setRadius() {
that.css({
2012-06-15 09:36:21 +00:00
borderRadius: self.options.radius + 'px'
2012-05-28 16:15:16 +00:00
});
}
return that;
};
/*
<pre>
Ox.My.RoundedBox({
color: [255, 0, 0],
radius: 32
})
.appendTo(Ox.$body)
.options({size: 256})
.showOptions();
</pre>
*/
2012-05-28 16:15:16 +00:00
/*
Its also possible to pass objects or other elements as options
*/
2012-05-28 14:38:16 +00:00
Ox.My.MetaBox = function(options, self) {
self = self || {};
var that = Ox.My.Box({}, self);
that.defaults(Ox.extend(that.defaults(), {
boxes: []
}))
.options(options || {});
self.options.boxes.forEach(function(box) {
that.append(box);
});
return that;
};
/*
Now its time to create some boxes for this demo:
*/
window.boxes = Ox.My.MetaBox({
2012-05-28 14:38:16 +00:00
boxes: [
Ox.My.Box({
color: [64, 128, 255]
}),
Ox.My.RoundedBox({
color: [255, 128, 64]
})
],
size: 384
})
.appendTo(Ox.$body);
});