Archives Posts
July 16th, 2008 by kangax
One of the easiest ways to inspect an object is to type convert it to a string:
function foo() {
return 'foo';
}
foo + ''; // "function foo(){ return 'foo'; }"
// or
foo.toString();
// or
String(foo);
When using Class.create from prototype.js, I often get annoyed by a meaningless result of constructor’s toString:
var Person = Class.create({
initialize: function(name) {
this.name = name;
},
speak: function(msg) {
return this.name + ' says: ' + msg;
}
})
Person + ''; // "function klass() { this.initialize.apply(this, arguments); }"
var Employee = Class.create(Person, {
initialize: function($super, dept) {
$super();
this.dept = dept;
}
})
Employee + ''; // "function klass() { this.initialize.apply(this, arguments); }"
As you can see, constructor’s default toString tells little about what’s going on under the hood. In fact, Class.create returns a generic constructor-function which only calls initialize method of a prototype. In other words it acts as a proxy. One of the ways to see actual constructor’s code is to inspect “initialize” method directly:
// nothing new here
Person + ''; // "function klass() { this.initialize.apply(this, arguments); }"
// and the actual code
Person.prototype.initialize + ''; // "function (name) { this.name = name; }"
This is quite verbose, don’t you think? Let’s monkey patch Class.create a little:
Class.create = (function(original) {
var fn = function() {
var result = original.apply(null, arguments);
result.toString = function() { return result.prototype.initialize.toString() };
return result;
};
fn.toString = function(){ return original.toString() };
return fn;
})(Class.create);
We simply redefine original Class.create with an “enhanced” version. New version “binds” toString of prototype.initialize as toString of constructor-function. As a bonus, it also takes care of a “wrapped” Class.create.toString itself - trying to be as unobtrusive as possible:
// monkey-patch Class.create first...
// Person.toString now yields much more informative result
Person + ''; // "function (name) { this.name = name; }"
// Class.create code seems to be unchanged
Class.create + ''; // outputs actual Class.create code
Archives Posts
September 11th, 2007 by kangax
It looks like the “Lazy load” plugin for jQuery (released about a week ago) got quite of attention. I personally don’t find it much useful but the idea is pretty cool. Can we do something like this with prototype? Easy! Let’s see how.
One of the things I found to be challenging was calculating whether element is positioned within viewport. Prototype does not have such method (as far as I know) but it does provide us with something that makes it quite trivial. Let’s look at ingredients:
Note: The following snippets require prototype 1.6rc0 or higher
// returns viewport dimensions
// alternatively there are explicit getHeight() and getWidth() methods
document.viewport.getDimensions(); // {width: 1024, height: 768}
// returns scroll offsets of viewport (how much the page was scrolled)
document.viewport.getScrollOffsets(); // [0, 10]
// returns element's position relative to a PAGE
element.cumulativeOffset(); // [120, 560]
// getDimensions() - alternative for getHeight/getWidth (in the end I'll explain why use one over another):
element.getDimensions(); // {width: 100, height: 310}
OK. Those only look scary, but are actually quite simple to use.
I decided to wrap the entire script into “Proto” namespace to be nice with window object (you are wrapping your stuff into namespace, aren’t you?) and define “Lazy” class in that namespace. Two actions will trigger image loading - window’s scroll and resize events (I’m not sure if original plugin checks window for resize but I think it makes more sense this way). Alternatively, if “event: ‘click’” option is set, pictures will be revealed on click (ignoring window’s events). Here’s a short breakdown of what’s going on:
- Iterate over all images on a page.
- If image is NOT within viewport at the moment, empty its “src” attribute and store it as a custom property of an element (this prevents it from being loaded).
- If event: ‘click’ is set, attach click handler to reveal image else:
- Attach event handlers to window’s scroll and resize events
- Once any of window events occur (scrolled or resized) check whether image is within viewport and if so, put its original “src” attribute back from custom property, delete that property.
That’s all there is to it, 40 lines of happyness…
if (Object.isUndefined(Proto)) {var Proto = {}}
Proto.Lazy = Class.create({
initialize: function(options) {
this.options = options || {};
$$('img').each(function(el){
if (!this.withinViewport(el)) {
el._src = el.src;
el.src = this.options.placeHolder || '';
if (this.options.event === 'click') {
el.observe('click', function(){
if (this._src) { this.src = this._src; delete this._src }
})
}
}
}.bind(this));
if (this.options.event !== 'click') {
Event.observe(window, 'scroll', this.load.bind(this));
Event.observe(window, 'resize', this.load.bind(this));
}
},
load: function(el) {
$$('img').each(function(el){
if (el._src && this.withinViewport(el)) { el.src = el._src; delete el._src }
}.bind(this))
},
withinViewport: function(el) {
var elOffset = el.cumulativeOffset(),
vpOffset = document.viewport.getScrollOffsets(),
elDim = el.getDimensions(),
vpDim = document.viewport.getDimensions();
if (elOffset[1] + elDim.height < vpOffset[1] || elOffset[1] > vpOffset[1] + vpDim.height ||
elOffset[0] + elDim.width < vpOffset[0] || elOffset[0] > vpOffset[0] + vpDim.width) {
return false;
}
return true;
}
})
After including Proto.Lazy.js, all that’s left to do is instantiate object (preferably before images are loaded which is right after ‘contentloaded’ event is fired):
document.observe('contentloaded', function(){
new Proto.Lazy();
// new Proto.Lazy({event: 'click'})
// new Proto.Lazy({placeHolder: 'images/defaul