433 lines
13 KiB
JavaScript
433 lines
13 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.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;
|
|
};
|
|
|
|
/*
|
|
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});
|
|
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)
|
|
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);
|
|
});
|
|
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) {
|
|
|
|
};
|
|
|
|
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;
|
|
|
|
/*
|
|
Now we create a new widget that subclasses `Ox.My.Box`.
|
|
*/
|
|
Ox.My.RoundedBox = function(options, self) {
|
|
|
|
self = self || {};
|
|
/*
|
|
This time `that` is an instance of `Ox.My.Box`
|
|
*/
|
|
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.
|
|
*/
|
|
that.defaults(Ox.extend(that.defaults(), {
|
|
radius: 16
|
|
}))
|
|
.options(options || {})
|
|
.update(function(key, value) {
|
|
if (key == 'radius') {
|
|
setRadius();
|
|
}
|
|
});
|
|
|
|
setRadius();
|
|
|
|
function setRadius() {
|
|
that.css({
|
|
borderRadius: self.options.radius + 'px'
|
|
});
|
|
}
|
|
|
|
return that;
|
|
|
|
};
|
|
/*
|
|
<pre>
|
|
Ox.My.RoundedBox({
|
|
color: [255, 0, 0],
|
|
radius: 32
|
|
})
|
|
.appendTo(Ox.$body)
|
|
.options({size: 256})
|
|
.showOptions();
|
|
</pre>
|
|
*/
|
|
|
|
/*
|
|
Its also possible to pass objects or other elements as options
|
|
*/
|
|
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({
|
|
boxes: [
|
|
Ox.My.Box({
|
|
color: [64, 128, 255]
|
|
}),
|
|
Ox.My.RoundedBox({
|
|
color: [255, 128, 64]
|
|
})
|
|
],
|
|
size: 384
|
|
})
|
|
.appendTo(Ox.$body);
|
|
});
|