Skip to content

Instantly share code, notes, and snippets.

@shouvik
Created October 25, 2010 10:16
Show Gist options
  • Save shouvik/644722 to your computer and use it in GitHub Desktop.
Save shouvik/644722 to your computer and use it in GitHub Desktop.
Click to touch
// packager build Custom-Event/* Mobile/* Class-Extras/* History/* DynamicMatcher/* EventStack/* Form-AutoGrow/* Form-Placeholder/* Tree/* ScrollLoader/* Interface/*
/*
---
name: Element.defineCustomEvent
description: Allows to create custom events based on other custom events.
authors: Christoph Pojer (@cpojer)
license: MIT-style license.
requires: [Core/Element.Event]
provides: Element.defineCustomEvent
...
*/
(function(){
[Element, Window, Document].invoke('implement', {hasEvent: function(event){
var events = this.retrieve('events'),
list = (events && events[event]) ? events[event].values : null;
if (list){
for (var i = list.length; i--;) if (i in list){
return true;
}
}
return false;
}});
var wrap = function(custom, method, extended, name){
method = custom[method];
extended = custom[extended];
return function(fn, customName){
if (!customName) customName = name;
if (extended && !this.hasEvent(customName)) extended.call(this, fn, customName);
if (method) method.call(this, fn, customName);
};
};
var inherit = function(custom, base, method, name){
return function(fn, customName){
base[method].call(this, fn, customName || name);
custom[method].call(this, fn, customName || name);
};
};
var events = Element.Events;
Element.defineCustomEvent = function(name, custom){
var base = events[custom.base];
custom.onAdd = wrap(custom, 'onAdd', 'onSetup', name);
custom.onRemove = wrap(custom, 'onRemove', 'onTeardown', name);
events[name] = base ? Object.append({}, custom, {
base: base.base,
condition: function(event){
return (!base.condition || base.condition.call(this, event)) &&
(!custom.condition || custom.condition.call(this, event));
},
onAdd: inherit(custom, base, 'onAdd', name),
onRemove: inherit(custom, base, 'onRemove', name)
}) : custom;
return this;
};
var loop = function(name){
var method = 'on' + name.capitalize();
Element[name + 'CustomEvents'] = function(){
Object.each(events, function(event, name){
if (event[method]) event[method].call(event, name);
});
};
return loop;
};
loop('enable')('disable');
})();
/*
---
name: Browser.Mobile
description: Provides useful information about the browser environment
authors: Christoph Pojer (@cpojer)
license: MIT-style license.
requires: [Core/Browser]
provides: Browser.Mobile
...
*/
(function(){
Browser.Device = {
name: 'other'
};
if (Browser.Platform.ios){
var device = navigator.userAgent.toLowerCase().match(/(ip(ad|od|hone))/)[0];
Browser.Device[device] = true;
Browser.Device.name = device;
}
if (this.devicePixelRatio == 2)
Browser.hasHighResolution = true;
Browser.isMobile = !['mac', 'linux', 'win'].contains(Browser.Platform.name);
})();
/*
---
name: Browser.Features.Touch
description: Checks whether the used Browser has touch events
authors: Christoph Pojer (@cpojer)
license: MIT-style license.
requires: [Core/Browser]
provides: Browser.Features.Touch
...
*/
Browser.Features.Touch = (function(){
try {
document.createEvent('TouchEvent').initTouchEvent('touchstart');
return true;
} catch (exception){}
return false;
})();
// Chrome 5 thinks it is touchy!
// Android doesn't have a touch delay and dispatchEvent does not fire the handler
Browser.Features.iOSTouch = (function(){
var name = 'cantouch', // Name does not matter
html = document.html,
hasTouch = false;
var handler = function(){
html.removeEventListener(name, handler, true);
hasTouch = true;
};
try {
html.addEventListener(name, handler, true);
var event = document.createEvent('TouchEvent');
event.initTouchEvent(name);
html.dispatchEvent(event);
return hasTouch;
} catch (exception){}
handler(); // Remove listener
return false;
})();
/*
---
name: Mouse
description: Maps mouse events to their touch counterparts
authors: Christoph Pojer (@cpojer)
license: MIT-style license.
requires: [Custom-Event/Element.defineCustomEvent, Browser.Features.Touch]
provides: Mouse
...
*/
if (!Browser.Features.Touch) (function(){
var condition = function(event){
event.targetTouches = [];
event.changedTouches = event.touches = [{
pageX: event.page.x, pageY: event.page.y,
clientX: event.client.x, clientY: event.client.y
}];
return true;
};
Element.defineCustomEvent('touchstart', {
base: 'mousedown',
condition: condition
}).defineCustomEvent('touchmove', {
base: 'mousemove',
condition: condition
}).defineCustomEvent('touchend', {
base: 'mouseup',
condition: condition
});
})();
/*
---
name: Touch
description: Provides a custom touch event on mobile devices
authors: Christoph Pojer (@cpojer)
license: MIT-style license.
requires: [Core/Element.Event, Custom-Event/Element.defineCustomEvent, Browser.Features.Touch]
provides: Touch
...
*/
(function(){
var preventDefault = function(event){
event.preventDefault();
};
var disabled;
Element.defineCustomEvent('touch', {
base: 'touchend',
condition: function(event){
if (disabled || event.targetTouches.length != 0) return false;
var touch = event.changedTouches[0],
target = document.elementFromPoint(touch.clientX, touch.clientY);
do {
if (target == this) return true;
} while ((target = target.parentNode) && target);
return false;
},
onSetup: function(){
this.addEvent('touchstart', preventDefault);
},
onTeardown: function(){
this.removeEvent('touchstart', preventDefault);
},
onEnable: function(){
disabled = false;
},
onDisable: function(){
disabled = true;
}
});
})();
/*
---
name: Click
description: Provides a replacement for click events on mobile devices
authors: Christoph Pojer (@cpojer)
license: MIT-style license.
requires: [Touch]
provides: Click
...
*/
if (Browser.Features.iOSTouch) (function(){
var name = 'click';
delete Element.NativeEvents[name];
Element.defineCustomEvent(name, {
base: 'touch'
});
})();
/*
---
name: Pinch
description: Provides a custom pinch event for touch devices
authors: Christopher Beloch (@C_BHole), Christoph Pojer (@cpojer)
license: MIT-style license.
requires: [Core/Element.Event, Custom-Event/Element.defineCustomEvent, Browser.Features.Touch]
provides: Pinch
...
*/
if (Browser.Features.Touch) (function(){
var name = 'pinch',
thresholdKey = name + ':threshold',
disabled, active;
var events = {
touchstart: function(event){
if (event.targetTouches.length == 2) active = true;
},
touchmove: function(event){
event.preventDefault();
if (disabled || !active) return;
var threshold = this.retrieve(thresholdKey, 0.5);
if (event.scale < (1 + threshold) && event.scale > (1 - threshold)) return;
active = false;
event.pinch = (event.scale > 1) ? 'in' : 'out';
this.fireEvent(name, event);
}
};
Element.defineCustomEvent(name, {
onSetup: function(){
this.addEvents(events);
},
onTeardown: function(){
this.removeEvents(events);
},
onEnable: function(){
disabled = false;
},
onDisable: function(){
disabled = true;
}
});
})();
/*
---
name: Swipe
description: Provides a custom swipe event for touch devices
authors: Christopher Beloch (@C_BHole), Christoph Pojer (@cpojer), Ian Collins (@3n)
license: MIT-style license.
requires: [Core/Element.Event, Custom-Event/Element.defineCustomEvent, Browser.Features.Touch]
provides: Swipe
...
*/
(function(){
var name = 'swipe',
distanceKey = name + ':distance',
cancelKey = name + ':cancelVertical',
dflt = 50;
var start = {}, disabled, active;
var clean = function(){
active = false;
};
var events = {
touchstart: function(event){
if (event.touches.length > 1) return;
var touch = event.touches[0];
active = true;
start = {x: touch.pageX, y: touch.pageY};
},
touchmove: function(event){
event.preventDefault();
if (disabled || !active) return;
var touch = event.changedTouches[0];
var end = {x: touch.pageX, y: touch.pageY};
if (this.retrieve(cancelKey) && Math.abs(start.y - end.y) > Math.abs(start.x - end.x)){
active = false;
return;
}
var distance = this.retrieve(distanceKey, dflt),
diff = end.x - start.x,
isLeftSwipe = diff < -distance,
isRightSwipe = diff > distance;
if (!isRightSwipe && !isLeftSwipe)
return;
active = false;
event.direction = (isLeftSwipe ? 'left' : 'right');
event.start = start;
event.end = end;
this.fireEvent(name, event);
},
touchend: clean,
touchcancel: clean
};
Element.defineCustomEvent(name, {
onSetup: function(){
this.addEvents(events);
},
onTeardown: function(){
this.removeEvents(events);
},
onEnable: function(){
disabled = false;
},
onDisable: function(){
disabled = true;
clean();
}
});
})();
/*
---
name: Touchhold
description: Provides a custom touchhold event for touch devices
authors: Christoph Pojer (@cpojer)
license: MIT-style license.
requires: [Core/Element.Event, Custom-Event/Element.defineCustomEvent, Browser.Features.Touch]
provides: Touchhold
...
*/
(function(){
var name = 'touchhold',
delayKey = name + ':delay',
disabled, timer;
var clear = function(e){
clearTimeout(timer);
};
var events = {
touchstart: function(event){
if (event.touches.length > 1){
clear();
return;
}
timer = (function(){
this.fireEvent(name, event);
}).delay(this.retrieve(delayKey) || 750, this);
},
touchmove: clear,
touchcancel: clear,
touchend: clear
};
Element.defineCustomEvent(name, {
onSetup: function(){
this.addEvents(events);
},
onTeardown: function(){
this.removeEvents(events);
},
onEnable: function(){
disabled = false;
},
onDisable: function(){
disabled = true;
clear();
}
});
})();
/*
---
name: Class.Binds
description: Alternate Class.Binds Implementation
authors: Scott Kyle (@appden), Christoph Pojer (@cpojer)
license: MIT-style license.
requires: [Core/Class, Core/Function]
provides: Class.Binds
...
*/
Class.Binds = new Class({
$bound: {},
bound: function(name){
return this.$bound[name] ? this.$bound[name] : this.$bound[name] = this[name].bind(this);
}
});
/*
---
name: Class.Instantiate
description: Simple Wrapper for Mass-Class-Instantation
authors: Christoph Pojer (@cpojer)
license: MIT-style license.
requires: [Core/Class]
provides: Class.Instantiate
...
*/
Class.Instantiate = function(klass, options){
var create = function(object){
new klass(object, options);
};
return function(objects){
objects.each(create);
};
};
/*
---
name: Class.Singleton
description: Beautiful Singleton Implementation that is per-context or per-object/element
authors: Christoph Pojer (@cpojer)
license: MIT-style license.
requires: [Core/Class]
provides: Class.Singleton
...
*/
(function(){
var storage = {
storage: {},
store: function(key, value){
this.storage[key] = value;
},
retrieve: function(key){
return this.storage[key] || null;
}
};
Class.Singleton = function(){
this.$className = String.uniqueID();
};
Class.Singleton.prototype.check = function(item){
if (!item) item = storage;
var instance = item.retrieve('single:' + this.$className);
if (!instance) item.store('single:' + this.$className, this);
return instance;
};
var gIO = function(klass){
var name = klass.prototype.$className;
return name ? this.retrieve('single:' + name) : null;
};
if (('Element' in this) && Element.implement) Element.implement({getInstanceOf: gIO});
Class.getInstanceOf = gIO.bind(storage);
}).call(this);
/*
---
name: History
description: History Management via popstate or hashchange.
authors: Christoph Pojer (@cpojer)
license: MIT-style license.
requires: [Core/Events, Core/Element.Event, Class-Extras/Class.Binds]
provides: History
...
*/
(function(){
var events = Element.NativeEvents,
location = window.location,
history = window.history,
hasPushState = ('pushState' in history),
event = hasPushState ? 'popstate' : 'hashchange';
this.History = new new Class({
Implements: [Class.Binds, Events],
initialize: hasPushState ? function(){
events[event] = 2;
window.addEvent(event, this.bound('pop'));
} : function(){
events[event] = 1;
window.addEvent(event, this.bound('pop'));
this.hash = location.hash;
if ('onhashchange' in window) return;
this.timer = this.periodical.periodical(200, this);
},
push: hasPushState ? function(url, title, state){
history.pushState(state || null, title || null, url);
this.onChange(url, state);
} : function(url){
location.hash = url;
},
replace: hasPushState ? function(url, title, state){
history.replaceState(state || null, title || null, url);
} : function(url){
this.hash = '#' + url;
this.push(url);
},
pop: hasPushState ? function(event){
var url = location.pathname;
this.onChange(url, event.event.state);
} : function(){
var hash = location.hash;
if (this.hash == hash) return;
this.hash = hash;
this.onChange(hash.substr(1));
},
onChange: function(url, state){
this.fireEvent('change', [url, state]);
},
back: function(){
history.back();
},
forward: function(){
history.forward();
},
getPath: function(){
return hasPushState ? location.pathname : location.hash.substr(1);
},
hasPushState: function(){
return hasPushState;
},
periodical: function(){
if (this.hash != location.hash) this.pop();
}
});
})();
/*
---
name: DynamicMatcher
description: Searches elements via complex selectors and executes functions on them
authors: Christoph Pojer (@cpojer)
license: MIT-style license.
requires: [Core/Events, Core/Element]
provides: DynamicMatcher
...
*/
(function(){
this.DynamicMatcher = new Class({
Implements: Events,
initialize: function(){
this.expressions = [];
this.handlers = [];
},
register: function(expression, fn){
var index = this.handlers.indexOf(fn);
if (index != -1 && this.expressions[index] == expression) return this;
this.expressions.push(expression);
this.handlers.push(fn);
return this;
},
unregister: function(expression, fn){
var handlers = this.handlers,
expressions = this.expressions;
for (var i = 0, l = handlers.length; i < l; i++) if (expression == expressions[i] && fn == handlers[i]){
delete handlers[i];
delete expressions[i];
break;
}
return this;
},
update: function(element){
element = document.id(element) || document;
var isDocument = (element == document),
handlers = this.handlers,
expressions = this.expressions;
for (var i = 0, l = handlers.length; i < l; i++){
var expression = expressions[i];
if (!expression) continue;
var elements = element.getElements(expression);
if (!isDocument && element.match(expression)) elements.push(element);
if (elements.length) handlers[i](elements);
}
this.fireEvent('update', [element]);
return this;
}
});
})();
/*
---
name: EventStack
description: Helps you Escape.
authors: Christoph Pojer (@cpojer)
license: MIT-style license.
requires: [Core/Class.Extras, Core/Element.Event, Class-Extras/Class.Binds]
provides: EventStack
...
*/
(function(){
this.EventStack = new Class({
Implements: [Options, Class.Binds],
options: {
event: 'keyup',
condition: function(event){
return (event.key == 'esc');
}
},
initialize: function(options){
this.setOptions(options);
this.stack = [];
this.data = [];
document.addEvent(this.options.event, this.bound('condition'));
},
condition: function(event){
if (this.options.condition.call(this, event, this.data.getLast()))
this.pop(event);
},
erase: function(fn){
this.data.erase(this.data[this.stack.indexOf(fn)]);
this.stack.erase(fn);
return this;
},
push: function(fn, data){
this.erase(fn);
this.data.push(data || null);
this.stack.push(fn);
return this;
},
pop: function(event){
var fn = this.stack.pop(),
data = this.data.pop();
if (fn) fn.call(this, event, data);
return this;
}
});
})();
/*
---
name: Form.AutoGrow
description: Automatically resizes textareas based on their content.
authors: Christoph Pojer (@cpojer)
credits: Based on a script by Gary Glass (www.bookballoon.com)
license: MIT-style license.
requires: [Core/Class.Extras, Core/Element, Core/Element.Event, Core/Element.Style, Core/Element.Dimensions, Class-Extras/Class.Binds, Class-Extras/Class.Singleton]
provides: Form.AutoGrow
...
*/
(function(){
var wrapper = new Element('div').setStyles({
overflowX: 'hidden',
position: 'absolute',
top: 0,
left: -9999
});
var escapeHTML = function(string){
return string.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
};
if (!this.Form) this.Form = {};
var AutoGrow = this.Form.AutoGrow = new Class({
Implements: [Options, Class.Singleton, Class.Binds],
options: {
minHeightFactor: 2,
margin: 0
},
initialize: function(element, options){
this.setOptions(options);
element = this.element = document.id(element);
return this.check(element) || this.setup();
},
setup: function(){
this.attach().focus().resize();
},
toElement: function(){
return this.element;
},
attach: function(){
this.element.addEvents({
focus: this.bound('focus'),
keydown: this.bound('keydown'),
scroll: this.bound('scroll')
});
return this;
},
detach: function(){
this.element.removeEvents({
focus: this.bound('focus'),
keydown: this.bound('keydown'),
scroll: this.bound('scroll')
});
return this;
},
focus: function(){
wrapper.setStyles(this.element.getStyles('fontSize', 'fontFamily', 'width', 'lineHeight', 'padding')).inject(document.body);
this.minHeight = (wrapper.set('html', 'A').getHeight() + this.options.margin) * this.options.minHeightFactor;
return this;
},
keydown: function(){
this.resize.delay(15, this);
},
resize: function(){
var element = this.element,
html = escapeHTML(element.get('value')).replace(/\n|\r\n/g, '<br/>A');
if (wrapper.get('html') == html) return this;
wrapper.set('html', html);
var height = wrapper.getHeight() + this.options.margin;
if (element.getHeight() != height){
element.setStyle('height', this.minHeight.max(height));
AutoGrow.fireEvent('resize', [this]);
}
return this;
},
scroll: function(){
this.element.scrollTo(0, 0);
}
});
AutoGrow.extend(new Events);
})();
/*
---
name: Form.Placeholder
description: Provides a fallback for the placeholder property on input elements for older browsers.
authors: Christoph Pojer (@cpojer)
license: MIT-style license.
requires: [Core/Class.Extras, Core/Element, Core/Element.Event, Core/Element.Style, Class-Extras/Class.Binds, Class-Extras/Class.Singleton]
provides: Form.Placeholder
...
*/
(function(){
if (!this.Form) this.Form = {};
var supportsPlaceholder = ('placeholder' in document.createElement('input'));
if (!('supportsPlaceholder' in this) && this.supportsPlaceholder !== false && supportsPlaceholder) return;
var wrapSet = function(self, set){
return function(key, value){
if (key == 'value' && !value){
self.reset();
return this;
}
return set.apply(this, arguments);
};
};
var wrapGet = function(get, self){
return function(key){
if (key == 'value' && !self.hasValue())
return '';
return get.apply(this, arguments);
};
};
this.Form.Placeholder = new Class({
Implements: [Class.Singleton, Class.Binds, Options],
options: {
color: '#777'
},
initialize: function(element, options){
this.setOptions(options);
element = this.element = document.id(element);
return this.check(element) || this.setup();
},
setup: function(){
var element = this.element;
this.defaultValue = element.get('placeholder') || element.value;
this.color = element.getStyle('color');
element.erase('placeholder');
element.set = wrapSet(element.set, this);
element.get = wrapGet(element.get, this);
this.attach();
if (!this.hasValue()) this.reset();
},
attach: function(){
this.element.addEvents({
focus: this.bound('focus'),
blur: this.bound('blur')
});
return this;
},
detach: function(){
this.element.removeEvents({
focus: this.bound('focus'),
blur: this.bound('blur')
});
return this;
},
clear: function(){
var element = this.element;
element.setStyle('color', this.color);
if (element.value == this.defaultValue) element.value = '';
return this;
},
reset: function(){
this.element.setStyle('color', this.options.color).value = this.defaultValue;
return this;
},
focus: function(){
this.clear();
},
blur: function(){
if (!this.element.value) this.reset();
},
hasValue: function(){
var value = this.element.value;
return (value && value != this.defaultValue);
}
});
})();
/*
---
name: Tree
description: Provides a way to sort and reorder a tree via drag&drop.
authors: Christoph Pojer (@cpojer)
license: MIT-style license.
requires: [Core/Events, Core/Element.Event, Core/Element.Style, Core/Element.Dimensions, Core/Fx.Tween, More/Drag.Move, More/Element.Delegation, Class-Extras/Class.Binds, Class-Extras/Class.Singleton]
provides: Tree
...
*/
(function(){
this.Tree = new Class({
Implements: [Options, Events, Class.Binds, Class.Singleton],
options: {
/*onChange: function(){},*/
indicatorOffset: 0,
cloneOffset: {x: 16, y: 16},
cloneOpacity: 0.8,
checkDrag: Function.from(true),
checkDrop: Function.from(true)
},
initialize: function(element, options){
this.setOptions(options);
element = this.element = document.id(element);
return this.check(element) || this.setup();
},
setup: function(){
this.indicator = new Element('div.treeIndicator');
var self = this;
this.handler = function(e){
self.mousedown(this, e);
};
this.attach();
},
attach: function(){
this.element.addEvent('mousedown:relay(li)', this.handler);
document.addEvent('mouseup', this.bound('mouseup'));
return this;
},
detach: function(){
this.element.removeEvent('mousedown:relay(li)', this.handler);
document.removeEvent('mouseup', this.bound('mouseup'));
return this;
},
mousedown: function(element, event){
event.preventDefault();
this.padding = (this.element.getElement('li ul li') || this.element.getElement('li')).getLeft() - this.element.getLeft() + this.options.indicatorOffset;
if (this.collapse === undefined && typeof Collapse != 'undefined')
this.collapse = this.element.getInstanceOf(Collapse);
if(!this.options.checkDrag.call(this, element)) return;
if (this.collapse && Slick.match(event.target, this.collapse.options.selector)) return;
this.current = element;
this.clone = element.clone().setStyles({
left: event.page.x + this.options.cloneOffset.x,
top: event.page.y + this.options.cloneOffset.y,
opacity: this.options.cloneOpacity
}).addClass('drag').inject(document.body);
this.clone.makeDraggable({
droppables: this.element.getElements('li span'),
onLeave: this.bound('hideIndicator'),
onDrag: this.bound('onDrag'),
onDrop: this.bound('onDrop')
}).start(event);
},
mouseup: function(){
if (this.clone) this.clone.destroy();
},
onDrag: function(el, event){
clearTimeout(this.timer);
if (this.previous) this.previous.fade(1);
this.previous = null;
if (!event || !event.target) return;
var droppable = (event.target.get('tag') == 'li') ? event.target : event.target.getParent('li');
if (!droppable || this.element == droppable || !this.element.contains(droppable)) return;
if (this.collapse) this.expandCollapsed(droppable);
var coords = droppable.getCoordinates(),
marginTop = + droppable.getStyle('marginTop').toInt(),
center = coords.top + marginTop + (coords.height / 2),
isSubnode = (event.page.x > coords.left + this.padding),
position = {
x: coords.left + (isSubnode ? this.padding : 0),
y: coords.top + coords.height + marginTop + droppable.getStyle('marginBottom').toInt()
};
var drop;
if ([droppable, droppable.getParent('li')].contains(this.current)){
this.drop = {};
} else if (event.page.y >= center){
if (isSubnode) position.y -= coords.height;
drop = {
target: droppable,
where: 'after',
isSubnode: isSubnode
};
if (!this.options.checkDrop.call(this, droppable, drop)) return;
this.setDropTarget(drop);
} else if (event.page.y < center){
position.y -= coords.height;
position.x = coords.left;
drop = {
target: droppable,
where: 'before'
};
if (!this.options.checkDrop.call(this, droppable, drop)) return;
this.setDropTarget(drop);
}
if (this.drop.target) this.showIndicator(position);
else this.hideIndicator();
},
onDrop: function(el){
el.destroy();
this.hideIndicator();
var drop = this.drop,
current = this.current;
if (!drop || !drop.target) return;
var previous = current.getParent('li');
if (drop.isSubnode) current.inject(drop.target.getElement('ul') || new Element('ul').inject(drop.target), 'bottom');
else current.inject(drop.target, drop.where || 'after');
if (this.collapse){
if (previous) this.collapse.updateElement(previous);
this.collapse.updateElement(drop.target);
}
this.fireEvent('change');
},
setDropTarget: function(drop){
this.drop = drop;
},
showIndicator: function(position){
this.indicator.setStyles({
left: position.x + this.options.indicatorOffset,
top: position.y
}).inject(document.body);
},
hideIndicator: function(){
this.indicator.dispose();
},
expandCollapsed: function(element){
var child = element.getElement('ul');
if (!child || !this.collapse.isCollapsed(child)) return;
element.set('tween', {duration: 150}).fade(0.5);
this.previous = element;
this.timer = (function(){
element.fade(1);
this.collapse.expand(element);
}).delay(300, this);
},
serialize: function(fn, base){
if (!base) base = this.element;
if (!fn) fn = function(el){
return el.get('id');
};
var result = {};
base.getChildren('li').each(function(el){
var child = el.getElement('ul');
result[fn(el)] = child ? this.serialize(fn, child) : true;
}, this);
return result;
}
});
})();
/*
---
name: Collapse
description: Allows to expand or collapse a list or a tree.
authors: Christoph Pojer (@cpojer)
license: MIT-style license.
requires: [Core/Events, Core/Element.Event, Core/Element.Style, Core/Element.Dimensions, Core/Fx, More/Element.Delegation, Class-Extras/Class.Singleton]
provides: Collapse
...
*/
(function(){
this.Collapse = new Class({
Implements: [Options, Class.Singleton],
options: {
animate: true,
fadeOpacity: 0.5,
className: 'collapse',
selector: 'a.expand',
listSelector: 'li',
childSelector: 'ul'
},
initialize: function(element, options){
this.setOptions(options);
element = this.element = document.id(element);
return this.check(element) || this.setup();
},
setup: function(){
var self = this;
this.handler = function(e){
self.toggle(this, e);
};
this.mouseover = function(){
if (self.hasChildren(this)) this.getElement(self.options.selector).fade(1);
};
this.mouseout = function(){
if (self.hasChildren(this)) this.getElement(self.options.selector).fade(self.options.fadeOpacity);
};
this.prepare().attach();
},
attach: function(){
var element = this.element;
element.addEvent('click:relay(' + this.options.selector + ')', this.handler);
if (this.options.animate){
element.addEvent('mouseover:relay(' + this.options.listSelector + ')', this.mouseover);
element.addEvent('mouseout:relay(' + this.options.listSelector + ')', this.mouseout);
}
return this;
},
detach: function(){
this.element.removeEvent('click:relay(' + this.options.selector + ')', this.handler)
.removeEvent('mouseover:relay(' + this.options.listSelector + ')', this.mouseover)
.removeEvent('mouseout:relay(' + this.options.listSelector + ')', this.mouseout);
return this;
},
prepare: function(){
this.prepares = true;
this.element.getElements(this.options.listSelector).each(this.updateElement, this);
this.prepares = false;
return this;
},
updateElement: function(element){
var child = element.getElement(this.options.childSelector),
icon = element.getElement(this.options.selector);
if (!this.hasChildren(element)){
if (!this.options.animate || this.prepares) icon.set('opacity', 0);
else icon.fade(0);
return;
}
if (this.options.animate) icon.fade(this.options.fadeOpacity);
else icon.set('opacity', this.options.fadeOpacity);
if (this.isCollapsed(child)) icon.removeClass('collapse');
else icon.addClass('collapse');
},
hasChildren: function(element){
var child = element.getElement(this.options.childSelector);
return (child && child.getChildren().length);
},
isCollapsed: function(element){
return (element.getStyle('display') == 'none');
},
toggle: function(element, event){
if (event) event.preventDefault();
if (!element.match(this.options.listSelector)) element = element.getParent(this.options.listSelector);
if (this.isCollapsed(element.getElement(this.options.childSelector))) this.expand(element);
else this.collapse(element);
return this;
},
expand: function(element){
element.getElement(this.options.childSelector).setStyle('display', 'block');
element.getElement(this.options.selector).addClass(this.options.className);
return this;
},
collapse: function(element){
element.getElement(this.options.childSelector).setStyle('display', 'none');
element.getElement(this.options.selector).removeClass(this.options.className);
return this;
}
});
})();
/*
---
name: Collapse.Cookie
description: Automatically saves the collapsed/expanded state in a Cookie.
authors: Christoph Pojer (@cpojer)
license: MIT-style license.
requires: [Core/Cookie, Core/JSON, Collapse]
provides: Collapse.Cookie
...
*/
(function(){
this.Collapse.Cookie = new Class({
Extends: this.Collapse,
options: {
getAttribute: function(element){
return element.get('id');
},
getIdentifier: function(element){
return 'collapse_' + element.get('id') + '_' + element.get('class').split(' ').join('_');
}
},
setup: function(){
this.cookie = this.options.getIdentifier.call(this, this.element);
this.parent();
},
prepare: function(){
var obj = this.getCookieData();
this.element.getElements(this.options.listSelector).each(function(element){
if (!element.getElement(this.options.childSelector)) return;
var state = obj[this.options.getAttribute.call(this, element)];
if (state == 1) this.expand(element);
else if (state == 0) this.collapse(element);
}, this);
return this.parent();
},
getCookieData: function(){
var self = this;
return Function.attempt(function(){
return JSON.decode(Cookie.read(self.cookie));
}) || {};
},
update: function(element, state){
var obj = this.getCookieData();
obj[this.options.getAttribute.call(this, element)] = state;
Cookie.write(this.cookie, JSON.encode(obj), {duration: 30});
return this;
},
expand: function(element){
this.parent(element);
this.update(element, 1);
return this;
},
collapse: function(element){
this.parent(element);
this.update(element, 0);
return this;
}
});
})();
/*
---
name: ScrollLoader
description: Fires an event when the user reaches a certain boundary.
authors: Christoph Pojer (@cpojer)
license: MIT-style license.
requires: [Core/Events, Core/Options, Core/Element.Event, Core/Element.Dimension, Class-Extras/Class.Binds]
provides: ScrollLoader
...
*/
(function(){
this.ScrollLoader = new Class({
Implements: [Options, Events, Class.Binds],
options: {
/*onScroll: fn,*/
area: 50,
mode: 'vertical',
container: null
},
initialize: function(options){
this.setOptions(options);
this.element = document.id(this.options.container) || window;
this.attach();
},
attach: function(){
this.element.addEvent('scroll', this.bound('scroll'));
return this;
},
detach: function(){
this.element.removeEvent('scroll', this.bound('scroll'));
return this;
},
scroll: function(){
var z = (this.options.mode == 'vertical') ? 'y' : 'x';
var element = this.element,
size = element.getSize()[z],
scroll = element.getScroll()[z],
scrollSize = element.getScrollSize()[z];
if (scroll + size < scrollSize - this.options.area) return;
this.fireEvent('scroll');
}
});
})();
/*
---
name: Interface
description: Interfaces for Class to ensure certain properties are defined.
authors: Christoph Pojer (@cpojer), Luis Merino (@Rendez)
license: MIT-style license.
requires: [Core/Type, Core/Class]
provides: Interface
...
*/
(function(context){
this.Interface = new Type('Interface', function(object){
if (object.Implements){
Array.from(object.Implements).each(function(item){
Object.append(this, item);
}, this);
delete object.Implements;
}
return Object.append(this, object);
});
Class.Mutators.initialize = function(fn){
return this.prototype.Interface ? function(){
var result = fn.apply(this, arguments);
if (!this.Interface) return result;
var interfaces = Array.from(this.Interface);
for (var i = 0; i < interfaces.length; i++){
var iface = interfaces[i];
for (var key in iface){
if (key.charAt(0) == '$') continue; // Skip Internal
if (!(key in this)) throw new Error('Instance does not implement "' + key + '"');
var item = this[key],
object = iface[key];
if (object == null) continue;
var type = typeOf(item),
oType = typeOf(object);
// Needs to be same datatype OR instance of the provided object
if (type != oType && !instanceOf(item, object)){
var proto = object.prototype,
name = (proto && proto.$family) ? proto.$family().capitalize() : object.displayName;
throw new Error('Property "' + key + '" is implemented but not an instance of ' + (name ? '"' + name + '"' : 'the expected type'));
}
if (oType == 'function' && item.$origin.length < object.length)
throw new Error('Property "' + key + '" does not implement at least ' + object.length + ' parameter' + (object.length != 1 ? 's' : ''));
}
}
return result;
} : fn;
};
}).call(typeof exports != 'undefined' ? exports : this);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment