/*!
* Copyright (c) 2008. All rights reserved.
* creator: lifesinger@gmail.com
*/
Unicorn = { version: '0.4.5' };
Unicorn.namespace = function() {
var o = null, i, j, parts;
for (i = 0; i < arguments.length; ++i) {
parts = arguments[i].split('.');
o = Unicorn;
// Unicorn is implied, so it is ignored if it is included
for (j = (parts[0] == 'Unicorn') ? 1 : 0; j < parts.length; ++j) {
o[parts[j]] = o[parts[j]] || {};
o = o[parts[j]];
}
}
return o;
};
Unicorn.namespace('env', 'lang', 'util', 'widget', 'example');
YAHOO.lang.augmentObject(Unicorn.env, YAHOO.env);
YAHOO.lang.augmentObject(Unicorn.lang, YAHOO.lang);
YAHOO.lang.augmentObject(Unicorn.util, YAHOO.util);
YAHOO.lang.augmentObject(Unicorn.widget, YAHOO.widget);
Unicorn.log = YAHOO.log; // for debug
/*
* languange utilites and extensions
* ref:
* - Ext/Ext.js 2.1
* - http://developer-test.mozilla.org/docs/Core_JavaScript_1.5_Reference
* - prototype-1.6.0.2.js
*
* creator: lifesinger@gmail.com
*/
/**
* Copies all the properties of config to obj.
* @param {Object} obj The receiver of the properties
* @param {Object} config The source of the properties
* @param {Object} defaults A different object that will also be applied for default values
* @return {Object} returns obj
*/
Unicorn.lang.apply = function(o, c, defaults) {
if(defaults) {
// no "this" reference for friendly out of scope calls
Unicorn.apply(o, defaults);
}
if(o && c && typeof c == 'object') {
for(var p in c) {
o[p] = c[p];
}
}
return o;
};
Unicorn.lang.apply(Unicorn.lang, {
/**
* Copies all the properties of config to obj if they don't already exist.
* @param {Object} obj The receiver of the properties
* @param {Object} config The source of the properties
* @return {Object} returns obj
*/
applyIf: function(o, c) {
if(o && c) {
for(var p in c) {
if(typeof o[p] == 'undefined') {
o[p] = c[p];
}
}
}
return o;
},
/**
* extend like Ext
*/
extend: function(subclass, superclass, overrides) {
if(arguments.length == 2 && typeof overrides == 'object') {
subclass = function() { };
}
YAHOO.lang.extend(subclass, superclass, overrides);
return subclass;
}
});
// shorthands
Unicorn.extend = Unicorn.lang.extend;
/**
* @class String
*/
Unicorn.lang.applyIf(String.prototype, {
/**
* Trims whitespace from either end of a string, leaving spaces within the string intact.
*
| var s = ' foo bar ';
| s.trim(); // -> 'foo bar'
*
* @return {String} The trimmed string
*/
trim: function() {
return this.replace(/^\s+|\s+$/g, '');
},
/**
* Strips a string of any HTML tag.
*
| var s = 'a link';
| s.stripTags(); // -> 'a link'
*
* @return {String} The stripped string
*/
stripTags: function() {
return this.replace(/<.*?>/g, '');
}
});
/**
* @class Array
*/
Unicorn.lang.applyIf(Array.prototype, {
/**
* Checks whether or not the specified object exists in the array.
* @param {Object} o The object to check for
* @return {Number} The index of o in the array (or -1 if it is not found)
*/
indexOf: function (o, fromIndex) {
if (fromIndex == null) {
fromIndex = 0;
} else if (fromIndex < 0) {
fromIndex = Math.max(0, this.length + fromIndex);
}
for (var i = fromIndex, len = this.length; i < len; ++i) {
if (this[i] === o)
return i;
}
return -1;
},
/**
* Ref: http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Objects:Array:forEach
*/
forEach: function(fun/*, thisp*/) {
var len = this.length;
if (typeof fun != "function") {
throw new TypeError();
}
var thisp = arguments[1];
for (var i = 0; i < len; ++i) {
if (i in this) fun.call(thisp, this[i], i, this);
}
}
});
/*
* DOM utilites
* creator: lifesinger@gmail.com
*/
Unicorn.util.Dom = { };
Unicorn.lang.augmentObject(Unicorn.util.Dom, YAHOO.util.Dom);
Unicorn.lang.apply(Unicorn.util.Dom, function() {
var L = Unicorn.lang,
D = Unicorn.util.Dom;
/* private memembers */
var propertyCache = { }, // for faster hyphen converts
patterns = { // regex cache
HYPHEN: /(-[a-z])/i // to normalize get/setStyle
};
var toCamel = function(property) {
if ( !patterns.HYPHEN.test(property) ) {
return property; // no hyphens
}
if (propertyCache[property]) { // already converted
return propertyCache[property];
}
var converted = property;
while( patterns.HYPHEN.exec(converted) ) {
converted = converted.replace(RegExp.$1,
RegExp.$1.substr(1).toUpperCase());
}
propertyCache[property] = converted;
return converted;
};
// A method for quickly swapping in/out CSS properties to get correct calculations
var cssSwap = function(elem, props, callback) {
var old = { };
// Remember the old values, and insert the new ones
for (var name in props) {
old[name] = elem.style[name];
elem.style[name] = props[name];
}
callback.call(elem);
// Revert the old values
for ( var name in props )
elem.style[name] = old[name];
};
/* end of private memembers */
return {
/**
* 切换元素的class
* 只有两个参数el和classA时,如果el有classA,则去掉classA,如果没有,则加上
* 有三个参数,当el有classA或classB时,将classA和classB互换;如果classA和classB在el中都没有,不进行任何操作
* Ref: jQuery 1.2.6
*
* @param {String | HTMLElement} el
* @param {String} classA
* @param {String} [optional] classB
*/
toggleClass: function(el, classA, classB) {
el = D.get(el);
if(!el || !classA) return;
if(D.hasClass(el, classA)) {
if(classB) {
D.replaceClass(el, classA, classB);
} else {
D.removeClass(el, classA);
}
} else {
if (classB && D.hasClass(el, classB)) {
D.replaceClass(el, classB, classA);
} else {
D.addClass(el, classA);
}
}
},
/**
* Get the computed style
* See: YUI 3.x getComputedStyle
* @param el 单个元素,不支持数组
* @param property
* 注意:只转换能够转换的单值,如marginTop,其它值不做转换,如 margin, 在ie和firefox下,margin的返回值不同
* YUI 3.x PR1里,node.getComputedStyle('margin') 返回空值,感觉不妥
*/
getComputedStyle: function(el, prop) {
el = D.get(el);
var val = D.getStyle(el, prop);
//var re = /^\d+(px)?$/i;
//if(re.test(val)) {
if(!val || val.indexOf('px') > -1) {
return val;
}
// convert to pixel
// font-size: 120% 百分比的转换目前不支持,在ie下不对
// YUI 3.x PR1 也存在这个bug
// 这里对于百分比,不做转换,直接返回
var reUnit = /^(\d[.\d]*)+(em|ex|px|gd|rem|vw|vh|vm|ch|mm|cm|in|pt|pc|deg|rad|ms|s|hz|khz){1}?$/i;
if(reUnit.test(val)) {
val = getPixel(el, prop, val);
}
// width/height 'auto'
// Ref: jquery 1.2.6 css function
if(val == 'auto' && (prop == 'width' || prop == 'height')) {
if (D.getStyle(el, 'display') != 'none') { // display为none时,clientWidth等属性全为0
val = getWH(el, prop);
} else {
cssSwap(el, { visibility: 'hidden', display:'block' }, function() {
val = getWH(el, prop);
});
}
}
return val;
// http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
function getPixel(element, property, value) {
if (document.defaultView && document.defaultView.getComputedStyle) { // W3C DOM method
var computed = element.ownerDocument.defaultView.getComputedStyle(element, '');
if (computed) { // test computed before touching for safari
value = parseFloat(computed[toCamel(property)]);
value = Math.round(value) + 'px';
}
}
else if(document.documentElement.currentStyle && Unicorn.env.ua.ie) { // IE method
// Remember the original values
var right = element.style.right;
// Put in the new values to get a computed value out
element.style.right = value;
value = element.style.pixelRight + 'px';
// Revert the changed values
element.style.right = right;
}
return value;
}
// 获取高宽的实际值
function getWH(element, property) {
var isWidth = (property == 'width');
var directs = isWidth ? ['Left', 'Right'] : ['Top', 'Bottom'];
var padding = 0, border = 0;
directs.forEach(function(direct) {
// 没有考虑ie下获取border时,有可能得到medium, thin, 这时统统归一为0
padding += parseFloat(D.getComputedStyle(element, 'padding' + direct)) || 0;
border += parseFloat(D.getComputedStyle(element, 'border' + direct + 'Width')) || 0;
});
var value = isWidth ? element.offsetWidth : element.offsetHeight;
value -= Math.round(padding + border);
return value + 'px';
// 不用clientWidth, 采用offsetWidth的理由: http://lifesinger.org/blog/?p=47
}
},
/**
* 根据ClassName获取节点的第一个Child
* 未获取成功时返回null
*/
getFirstChildByClassName: function(node, className) {
return D.getFirstChildBy(node, function(child) { return D.hasClass(child, className); });
}
};
}());
/*
* Event utilites
* creator: lifesinger@gmail.com
*/
Unicorn.util.Event = { };
Unicorn.lang.augmentObject(Unicorn.util.Event, YAHOO.util.Event);
Unicorn.lang.apply(Unicorn.util.Event, function() {
var L = Unicorn.lang,
U = Unicorn.util,
D = U.Dom;
return {
/**
* 说明:YAHOO.util.Event.on([], 'click', func) 会产生错误
* 这里包装一下,使得 E.on([], ...) 和 E.on(null, ...) 行为一致,不产生错误
* 等YUI改进了,这里直接删掉即可
*/
addListener: function(el, sType, fn, obj, override) {
if(L.isArray(el) && el.length == 0) return false;
return YAHOO.util.Event.addListener(el, sType, fn, obj, override);
},
/**
* 同 addListener
*/
on: function(el, sType, fn, obj, override) {
return this.addListener(el, sType, fn, obj, override);
}
};
}());
/*
* common utilites
* ref:
* - Ext/Ext.js 2.1
*
* creator: lifesinger@gmail.com
*/
Unicorn.lang.apply(Unicorn.util, function() {
var L = Unicorn.lang;
return {
/**
* Takes an object and converts it to an encoded URI-like query.
*
| {foo: 1, bar: 2} // -> 'foo=1&bar=2'
*
* Optionally, property values can be arrays, instead of keys
* and the resulting string that's returned will contain a name/value pair for each array value.
* @param {Object} o
* @return {String}
*/
encodeUriQuery: function(o) {
if(!o) return '';
var buf = [];
for(var key in o) {
var ov = o[key], k = encodeURIComponent(key);
var type = typeof ov;
if(type == 'undefined') {
buf.push(k, '=&');
}
else if(type != 'function' && type != 'object') {
buf.push(k, '=', encodeURIComponent(ov), '&');
}
else if(L.isArray(ov)) {
if (ov.length) {
for(var i = 0, len = ov.length; i < len; i++) {
var t = typeof ov[i];
if(t != 'function' && t != 'object') {
buf.push(k, '=', encodeURIComponent(ov[i] === undefined ? '' : ov[i]), '&');
}
}
}
else {
buf.push(k, '=&');
}
}
}
buf.pop();
return buf.join('');
},
/**
* Parses a URI-like query string and returns an object composed of parameter/value pairs.
* This method is realy targeted at parsing query strings (hence the default value of "&" for the separator argument).
* For this reason, it does not consider anything that is either before a question
* mark (which signals the beginning of a query string) or beyond the hash symbol ("#"),
* and runs decodeURIComponent() on each parameter/value pair.
* Note that parameters which do not have a specified value will be set to undefined.
*
| 'section=blog&id=45' // -> {section: 'blog', id: '45'}
|
| 'section=blog;id=45', false, ';' // -> {section: 'blog', id: '45'}
|
| 'http://www.example.com?section=blog&id=45#comments' // -> {section: 'blog', id: '45'}
|
| 'section=blog&tag=javascript&tag=prototype&tag=doc'
| // -> {section: 'blog', tag: ['javascript', 'prototype', 'doc']}
|
| 'tag=ruby%20on%20rails' // -> {tag: 'ruby on rails'}
|
| 'id=45&raw' // -> {id: '45', raw: undefined}
*
* @param {String} string
* @param {Boolean} overwrite (optional) Items of the same name will overwrite previous values instead of creating an an array (Defaults to false).
* @return {Object} A literal with members
*/
decodeUriQuery: function(string, overwrite, separator) {
if(!string || !string.length) return { };
var match = string.trim().match(/([^?#]*)(#.*)?$/);
if (!match) return { };
var obj = { };
var pairs = match[1].split(separator || '&');
var pair, name, value;
for(var i = 0, len = pairs.length; i < len; ++i) {
pair = pairs[i].split('=');
name = decodeURIComponent(pair[0]);
value = decodeURIComponent(pair[1]);
if(value === '' || value === 'undefined') value = undefined; // &k 和 &k= 都还原成 undefined
if(overwrite !== true) {
if(typeof obj[name] == 'undefined') {
obj[name] = value;
} else if(typeof obj[name] == 'string') {
obj[name] = [obj[name]];
obj[name].push(value);
} else {
obj[name].push(value);
}
} else {
obj[name] = value;
}
}
return obj;
}
};
}());
/*
* Effect utilites
* creator: lifesinger@gmail.com
*/
Unicorn.util.Effect = { };
Unicorn.lang.apply(Unicorn.util.Effect, function() {
var L = Unicorn.lang,
U = Unicorn.util,
D = U.Dom,
E = U.Event;
var slideAnim = [ ];
return {
/**
* 通过高度/宽度的变化来显示/隐藏元素
* @param el 元素
* @param directions 可取 'x', 'y', 'xy', '-x+y'等,-代表缩小,+代表增大
* @config duration 动画持续时间。单位为秒,缺省值为0.25秒
* @config easing 默认为 Easing.easeNone
* @config callback 在动画完成时执行的回调函数
* @config thisObj callback里的this object
* @param force 强制执行,在手风琴等效果里,需要采用jQuery风格的动画
*/
slide: function(el, directions, config, force) {
el = D.get(el);
if(!el) return;
// 采用Ext风格,当连续点击时,只有当当前动画完成之后的点击才生效
// 值得注意的是jQuery采取的风格是依次排序,多次连续点击后,所有动画依次执行
var key = el.id || D. generateId(el, 'unicorn-gen');
if( (force !== true) && (slideAnim[key] && slideAnim[key].isAnimated())) {
//YAHOO.log('isAnimated = ' + slideAnim[el].isAnimated(), 'info', 'Effect');
return;
}
directions = directions || '-y'; // 默认为-y
config = L.applyIf(config || { }, {
duration: 0.25,
easing: Unicorn.util.Easing.easeNone,
thisObj: el
});
// parse directions
// 0 代表此方向无动画
// 1 代表增大
// -1 代表缩小
var directs = { x: 0, y: 0 };
['x', 'y'].forEach(function(axis) {
if(directions.indexOf(axis) > -1) {
if(directions.indexOf('-' + axis) > -1) {
directs[axis] = -1;
} else { // x 或 +x
directs[axis] = 1;
}
}
});
// remember original style values
var originalStyle = {
width: el.style.width,
height: el.style.height,
overflow: el.style.overflow
};
// get computed size unit: px
var size = { width: D.getComputedStyle(el, 'width'), height: D.getComputedStyle(el, 'height') };
// set attributes
var attributes = { };
if(directs.x != 0) {
attributes.width = { to: (directs.x > 0) ? parseFloat(size.width) : 0 };
}
if(directs.y != 0) {
attributes.height = { to: (directs.y > 0) ? parseFloat(size.height) : 0 };
}
slideAnim[key] = new Unicorn.util.Anim(el, attributes, config.duration, config.easing);
slideAnim[key].onComplete.subscribe(function() {
// 防止padding等因素导致隐藏不完全
if(directs.x < 0 || directs.y < 0) D.setStyle(el, 'display', 'none');
// Revert the original style values
el.style.width = originalStyle.width;
el.style.height = originalStyle.height;
el.style.overflow = originalStyle.overflow;
// free
slideAnim[key] = null;
});
if(config.callback) slideAnim[key].onComplete.subscribe(config.callback, config.thisObj, true);
// Make sure that nothing sneaks out
if(directs.y < 0 && Unicorn.env.ua.ie) {
// ie7中,某些情况下(具体请参考 http://lifesinger.org/blog/?p=47),
// 设置D.setStyle(el, 'overflow', 'hidden');将导致offsetHeight的值不对
// 这将导致slideUp时,元素垂直方向突然变小
// jQuery 1.2.6中有这个bug
// 为了避免这种情况,在设置overflow hidden时,先设置好height
el.style.height = size.height; // 在动画最后会还原,属于无侵入
}
D.setStyle(el, 'overflow', 'hidden');
// 调整到动画开始状态
if(directs.x > 0) D.setStyle(el, 'width', '0');
if(directs.y > 0) D.setStyle(el, 'height', '0');
if(D.getStyle(el, 'display') == 'none') D.setStyle(el, 'display', '');
// go
slideAnim[key].animate();
},
slideToggle: function(el, direction, config, force) {
var minus = (D.getStyle(el, 'display') != 'none') ? '-' : '';
var directions = minus + (direction == 'x' ? 'x' : 'y');
this.slide(el, directions, config, force);
},
slideDown: function(el, config, force) {
this.slide(el, 'y', config, force);
},
slideUp: function(el, config, force) {
this.slide(el, '-y', config, force);
},
/**
* 通过透明度的变化来淡入/淡出元素
* @param el 元素
* @param opacity 0到1之间的数字
* @config duration 动画持续时间。单位为秒,缺省值为0.25秒
* @config easing 默认为 YAHOO.util.Easing.easeNone
* @config callback 在动画完成时执行的回调函数
* @config thisObj callback里的this object
*/
fadeTo: function(el, opacity, config) {
el = D.get(el);
if(!el) return;
opacity = L.isNumber(opacity) ? opacity : 1; // 默认为1
config = L.applyIf(config || { }, {
duration: 0.25,
thisObj: el
});
var attributes = { opacity: { to: opacity } };
var anim = new Unicorn.util.Anim(el, attributes, config.duration, config.easing);
if(config.callback) anim.onComplete.subscribe(config.callback, config.thisObj, true);
anim.animate();
},
fadeIn: function(el, config) {
this.fadeTo(el, 1, config);
},
fadeOut: function(el, config) {
this.fadeTo(el, 0, config);
}
};
}());
/**
* 实现widget时的一些辅助方法
*
* 注意:目前还未想清楚如何组织这些代码,属于私有类,外部请勿调用
* creator: lifesinger@gmail.com
*/
Unicorn.widget.WidgetHelper = { };
Unicorn.lang.apply(Unicorn.widget.WidgetHelper, function() {
var L = Unicorn.lang,
U = Unicorn.util,
D = U.Dom,
E = U.Event;
return {
/**
* Used to add events on object
* eg. addEvents.apply(this, ['onCollapse', 'beforeCollapse', 'onExpand', 'beforeExpand']);
*/
addEvents: function() {
if(!this.events) this.events = { };
var obj = this;
var eventNames = arguments;
for(var i = 0, len = eventNames.length; i < len; ++i) {
(function() {
var eventName = eventNames[i];
if(typeof eventName != 'string') return;
obj.events[eventName] = new Unicorn.util.CustomEvent(eventName, null, false, Unicorn.util.CustomEvent.FLAT);
if(obj.config && obj.config.events && obj.config.events[eventName]) {
obj.events[eventName].subscribe(obj.config.events[eventName]);
}
obj[eventName] = obj.events[eventName];
})();
}
},
/**
* 解析参数
*/
parseArguments: function(args, firstArgName) {
var validArgs = [];
for(var i = 0, len = args.length; i < len; ++i) {
if(typeof args[i] != 'undefined')
validArgs.push(args[i]);
}
args = validArgs;
var cfg1, cfg2;
firstArgName = firstArgName || 'container';
len = args.length;
if(len == 1) {
if(typeof args[0] == 'string' || args[0].nodeType == 1) { // new WidgetName(firstArgValue)
cfg1 = {};
cfg1[firstArgName] = args[0];
} else if(typeof args[0] == 'object') { // new WidgetName( {firstArgName: 'value', ... } )
cfg1 = args[0];
}
} else if(len == 2) {
// new WidgetName(firstArgValue, {secondArgName: 'value', ...} )
if((typeof args[0] == 'string' || args[0].nodeType == 1) && typeof args[1] == 'object') {
cfg1 = args[1];
cfg1[firstArgName] = args[0];
// new WidgetName(cfg1, cfg2)
} else if(typeof args[0] == 'object' && typeof args[1] == 'object') {
cfg1 = args[0];
cfg2 = args[1];
}
} else if(len == 3) {
// new WidgetName(firstArgValue, cfg1, cfg2)
if((typeof args[0] == 'string' || args[0].nodeType == 1) && typeof args[2] == 'object') {
cfg1 = args[1] || { }; // args[1]可以为null
cfg1[firstArgName] = args[0];
cfg2 = args[2];
}
}
return [cfg1, cfg2];
}
};
}());/**
* @class Unicorn.widget.InputMask
* creator: lifesinger@gmail.com
*/
/**
* members:
* this.config
* this.inputEl
* this.value // 当前有效值
* this.events: 'onRightInput', 'onErrorInput', 'onFinish', 'onPass', 'onError'
*/
Unicorn.widget.InputMask = function() {
var L = Unicorn.lang,
U = Unicorn.util,
D = U.Dom,
E = U.Event,
helper = Unicorn.widget.WidgetHelper;
// 默认配置
var defConfig = {
inputEl: null, // input 元素
formatMask: /^.*$/, // 输入的整个值需要满足的正则
keyMask: /./, // 输入的键值需要满足的正则
minLength: 0, // 仅在onblur时,做校验用
maxLength: -1, // 如果input中设置了maxlength, init时会将maxLength的默认值设为maxlength
chineseCharLength: 2, // 一个汉字默认算2个字符
events: { }
};
var InputMask = function() {
var cfgs = helper.parseArguments(arguments, 'inputEl');
// 获取默认配置
this.config = L.applyIf(cfgs[0] || {}, defConfig);
init.call(this);
};
function init(config) {
var cfg = this.config;
this.inputEl = D.get(cfg.inputEl);
if(!this.inputEl) return;
if(!(cfg.keyMask instanceof RegExp)) return; // 不是正则表达式,直接返回
if(!(cfg.formatMask instanceof RegExp)) return;
// 如果配置里没有指定maxLength, 则从input的maxlength属性中获取
if(cfg.maxLength == '-1' && this.inputEl.getAttribute('maxlength')) {
cfg.maxLength = this.inputEl.getAttribute('maxlength');
}
this.value = this.inputEl.value || '';
// 添加事件
helper.addEvents.apply(this, ['onRightInput', 'onErrorInput', 'onFinish', 'onPass', 'onError']);
// 考虑到输入法未打开时,通过keypress实现的阻止非法字符输入效果最好,
// 因此依旧保留对此事件的监听(其它方法阻止非法字符时,会先显示出来,再自动删掉)
E.on(this.inputEl, 'keypress', function(e) {
//Unicorn.log('keypress fired.', 'info', 'InputMask');
var keyCode = getKeyCode(e);
// 在Opera下,非打印字符也会触发keypress
// 通过isPrintable判断,使得Opera和其它浏览器行为一致
// isPrintable还可以将输入法开启时的输入拦截掉,统一到后面处理
if(!keyCode || !isPrintable(keyCode)) return;
// firefox中,ctrl + c/v/x/z/y等组合键会触发keypress,应过滤掉
// 否则无法使用快捷键来进行复制粘贴操作
if(e.ctrlKey || e.altKey) return;
var key = String.fromCharCode(keyCode);
//Unicorn.log('keypress: key = ' + key, 'info', 'InputMask');
if(!cfg.keyMask.test(key)) {
this.events['onErrorInput'].fire(this);
E.stopEvent(e);
}
}, null, this);
// 监听input/propertychange
var eventName = Unicorn.env.ua.ie ? 'propertychange' : 'input';
E.on(this.inputEl, eventName, function(e) {
//Unicorn.log(eventName + ' fired.', 'info', 'InputMask');
// 对于propertychange事件,只检测value值的改变
if(eventName == 'propertychange' && e.propertyName != 'value') return;
updateInputValue.apply(this);
}, null, this);
// 触发输入完成事件
E.on(this.inputEl, 'blur', function(e) {
//Unicorn.log('blur fired. Current input value = ' + this.inputEl.value, 'info', 'InputMask');
this.events['onFinish'].fire(this);
this.value = this.inputEl.value; // 更新value,可能在onFinish事件中有改动
// 最后验证是否满足format
if(cfg.formatMask instanceof RegExp) {
if(cfg.formatMask.test(this.value)
&& getValidLength(this.value, this.config.chineseCharLength) >= this.config.minLength) {
this.events['onPass'].fire(this);
} else {
this.events['onError'].fire(this);
}
}
}, null, this);
};
/* private members */
// regex cache
var patterns = {
CHINESE_CHAR: /[\u4e00-\u9fa5]/ // GBK编码范围
};
/**
* Returns true if the key is a printable character.
*/
function isPrintable(keyCode) {
return (keyCode >= 32 && keyCode < 127);
}
/**
* Returns the key code associated with the event.
*/
function getKeyCode(e) {
return window.event ? window.event.keyCode : (e ? e.which : null);
}
/**
* 将值中的非法字符去掉
*/
function getValidValue(val, regex) {
//Unicorn.log('getValidValue BEGIN: val = ' + val, 'info', 'InputMask');
var tchars = val.split('');
var chars = [];
// 直接遍历获取满足要求的字符
for(var i = 0, len = tchars.length; i < len; ++i) {
if(regex.test(tchars[i])) {
chars.push(tchars[i]);
}
}
val = chars.join('');
//Unicorn.log('getValidValue END: val = ' + val, 'info', 'InputMask');
return val;
}
/**
* 检查输入值的长度是否有效
*/
function checkLength(val, maxLength, chineseCharLength) {
if(maxLength == -1) return true; // -1说明长度无限制
if(!val || val.length == 0) return true; // 输入值为空
var len = getValidLength(val, chineseCharLength);
return len <= maxLength;
}
/**
* 获取有效长度
*/
function getValidLength(val, chineseCharLength) {
var arr = val.split('');
var len = 0;
for(var i = 0, l = arr.length; i < l; ++i) {
if(patterns.CHINESE_CHAR.test(arr[i])) {
len += chineseCharLength;
} else {
len += 1;
}
}
//Unicorn.log('getValidLength: length = ' + len, 'info', 'InputMask');
return len;
}
/**
* 更新输入值
*/
function updateInputValue() {
var inputValue = this.inputEl.value;
if(this.value == inputValue || inputValue.legnth == 0) return; // 没有变化,说明在keypress阶段已经成功阻止了非法输入,这里无需再处理
// 值有变化时,直接将值中的非法字符去掉(输入时的位置变化,粘贴等情况都没问题)
inputValue = getValidValue(inputValue, this.config.keyMask);
// 检查长度
var lengthIsOK = checkLength(inputValue, this.config.maxLength, this.config.chineseCharLength);
if(inputValue == this.inputEl.value // 新输入值不是非法字符
&& lengthIsOK) { // 小于等于最大长度
this.events['onRightInput'].fire(this);
} else {
// 定时器方法中,firefox下,输入法打开时,下面这句会导致inputEl的value置空
// 遗留问题:Safari下,输入法打开时,下面这句会导致input的焦点丢失
this.inputEl.value = lengthIsOK ? inputValue : this.value; // 超出长度时,直接还原
this.events['onErrorInput'].fire(this);
}
// 更新this.value
this.value = this.inputEl.value;
}
/* end of private members */
return InputMask;
}();
// 增加decorate静态方法
Unicorn.widget.InputMask.decorate = function(inputEl, config) {
return new Unicorn.widget.InputMask(inputEl, config);
};
/**
* @class Unicorn.widget.SimpleInput
* creator: lifesinger@gmail.com
*/
Unicorn.widget.SimpleInput = function() {
var L = Unicorn.lang,
U = Unicorn.util,
D = U.Dom,
E = U.Event,
helper = Unicorn.widget.WidgetHelper;
// SimpleInput的默认配置
// 和InputMask相同的配置项请参考InputMask.source.js
var defConfig = {
tipsCls: 'tips',
warningCls: 'warning',
passedCls: 'passed',
errorCls: 'error',
currentCls: 'current'
};
var SimpleInput = function() {
var cfgs = helper.parseArguments(arguments, 'inputEl');
// 获取默认配置
var config = L.applyIf(cfgs[0] || {}, defConfig);
// chain ctor
this.constructor.superclass.constructor.call(this, config);
init.call(this);
};
function init() {
var cfg = this.config;
var tips = D.getElementsByClassName('tips', '*', this.inputEl.parentNode)[0];
E.on(this.inputEl, 'focus', function() {
// 还原样式
D.removeClass(this.parentNode, cfg.errorCls);
D.removeClass(tips, cfg.warningCls);
D.removeClass(tips, cfg.passedCls);
D.addClass(this.parentNode, cfg.currentCls);
});
this.onRightInput.subscribe(function() {
//Unicorn.log('onRightInput handler fired.', 'pass', 'testCase');
D.removeClass(tips, cfg.warningCls);
});
this.onErrorInput.subscribe(function() {
//Unicorn.log('onErrorInput handler fired.', 'pass', 'testCase');
D.addClass(tips, cfg.warningCls);
});
this.onFinish.subscribe(function() {
//Unicorn.log('onBlur handler fired. inputEl.value = ' + inputEl.value, 'pass', 'testCase');
D.removeClass(this.inputEl.parentNode, cfg.currentCls);
}, null, this);
this.onError.subscribe(function() {
//Unicorn.log('onError handler fired.', 'pass', 'testCase');
D.addClass(this.inputEl.parentNode, cfg.errorCls);
D.addClass(tips, cfg.warningCls);
}, null, this);
this.onPass.subscribe(function() {
//Unicorn.log('onPass handler fired.', 'pass', 'testCase');
D.removeClass(this.inputEl.parentNode, cfg.errorCls);
D.removeClass(tips, cfg.warningCls);
D.addClass(tips, cfg.passedCls);
}, null, this);
};
Unicorn.extend(SimpleInput, Unicorn.widget.InputMask);
return SimpleInput;
}();
// 增加decorate静态方法
Unicorn.widget.SimpleInput.decorate = function(inputEl, config) {
return new Unicorn.widget.SimpleInput(inputEl, config);
};/**
* @class Unicorn.widget.Panel
* ref:
* - Ext/Ext.js 2.1
*
* creator: lifesinger@gmail.com
*/
/**
* members:
* this.container
* this.config
* this.head, this.body, this.foot
* this.collapsed, this.isAnimating
* this.events: onCollapse, beforeCollapse, onExpand, beforeExpand
* prototype: getDom(), collapse(), expand(), toggleCollapse()
*/
Unicorn.widget.Panel = function() {
var L = Unicorn.lang,
U = Unicorn.util,
D = U.Dom,
E = U.Event,
Ef = U.Effect,
helper = Unicorn.widget.WidgetHelper;
// 默认配置
var defConfig = {
container: null,
// 将优先根据cls来获取panel的head, body, foot
headCls: 'panel-hd',
bodyCls: 'panel-bd',
footCls: 'panel-ft',
collapsible: false, // 是否可以收缩,只有为true时,下面相关配置才生效
collapsed: false, // 初始是否处于收缩状态
collapsedCls: 'collapsed', // 处于收缩状态时,给container附加的cls
titleCollapse: false, // 整个title在点击时都触发collapse切换
disableTriggerClick: true, // 禁止触发器的点击事件
// 工具条上的cls
toolBaseCls: 'tool',
toolOverCls: 'over',
toolToggleCls: 'toggle',
animCollapse: L.isObject(U.Anim), // 收缩时,是否有动画效果。默认值取决于Anim有没有加载
animDuration: 0.25,
animEasing: U.Easing.easeNone,
/* 保持简单,可拖动性等暂时不考虑实现
draggable: false,
draggableCls: 'draggable-panel',
//shadow: null, // TODO
*/
events: { }
};
var Panel = function() {
var cfgs = helper.parseArguments(arguments);
// 获取默认配置
this.config = L.applyIf(cfgs[0] || {}, defConfig);
// 如果Anim没加载,animCollapse应该强制为false
if(!L.isObject(U.Anim)) {
this.config.animCollapse = false;
}
init.call(this);
};
function init() {
var cfg = this.config;
this.container = D.get(cfg.container);
if(!this.container) return;
// 初始化panel的各个部分
initParts.call(this);
// init collapse
// 限制:必须有头部时才能collapse,不考虑其它情况
if(cfg.collapsible && this.head) {
initCollapse.call(this);
}
// init DD
/*if(DD && cfg.draggable && this.head) { // 只支持头部的拖动
initDD.call(this);
}*/
};
// private members
/**
* 得到panel的head, body, foot
*/
function initParts() {
var cfg = this.config;
// 优先根据className来获取
this.head = D.getFirstChildByClassName(this.container, cfg.headCls);
this.body = D.getFirstChildByClassName(this.container, cfg.bodyCls);
this.foot = D.getFirstChildByClassName(this.container, cfg.footCls);
// 上面未获取到时,再根据默认规则来获取
// 原则:给与适当的灵活性
var children = D.getChildren(this.container);
var len = children.length;
// 含有两个子元素时,默认为head和body
if(!this.head && !this.body && len == 2) {
this.head = children[0];
this.body = children[1];
}
// 含有三个子元素时,默认依次赋值
if(!this.head && !this.body && !this.foot && len == 3) {
this.head = children[0];
this.body = children[1];
this.foot = children[2];
}
if(!this.body) { // body是必须有的
throw new Error('The Config of Panel has something wrong.');
}
}
/**
* init collapse
*/
function initCollapse() {
var cfg = this.config;
// 先从头部获取toggle,未获取到时则自动创建
var toggleCls = cfg.toolBaseCls + '-' + cfg.toolToggleCls;
var toolToggle = D.getFirstChildByClassName(this.head, toggleCls);
if(!toolToggle) { // 没有时,自动创建
toolToggle = document.createElement('div');
D.addClass(toolToggle, cfg.toolBaseCls);
D.addClass(toolToggle, toggleCls);
this.head.appendChild(toolToggle);
}
// init trigger
var toggleTrigger = cfg.titleCollapse ? this.head : toolToggle;
// 交给css去处理。在这里加上,会导致某些情况下不灵活(比如我的淘宝菜单)
//D.setStyle(toggleTrigger, 'cursor', 'pointer');
var normalCls = cfg.toolBaseCls + '-' + cfg.toolToggleCls;
var overCls = cfg.toolBaseCls + '-' + cfg.toolToggleCls + '-' + cfg.toolOverCls;
var toggleToolCls = function() {
D.toggleClass(toolToggle, normalCls, overCls);
};
E.on(toolToggle, 'mouseover', toggleToolCls);
E.on(toolToggle, 'mouseout', toggleToolCls);
E.on(toggleTrigger, 'click', function(e) {
if(cfg.disableTriggerClick !== true && E.getTarget(e).nodeName == 'A') return;
E.preventDefault(e);
this.toggleCollapse();
}, null, this);
// 添加自定义事件
helper.addEvents.apply(this, ['onCollapse', 'beforeCollapse', 'onExpand', 'beforeExpand']);
// 初始为collapsed状态时
this.collapsed = false;
if(cfg.collapsed) {
this.collapse(false);
}
}
/**
* init DD
*/
/*function initDD() {
var cfg = this.config;
var dd = new DD(cfg.container);
dd.setHandleElId(this.head);
D.addClass(this.container, cfg.draggableCls);
D.setStyle(this.head, 'cursor', 'move');
}*/
// end of private members
Panel.prototype = {
/**
* 获取与panel相关联的dom元素
*/
getDom: function() {
return this.container;
},
/**
* Collapses the panel body so that it becomes hidden.
* Fires the beforeCollapse event which will cancel the collapse action if it returns false.
*
* @param {Boolean} anmiate True to animate the transition, else false (defaults to the value of the animCollapse config)
*/
collapse: function(animate, force) {
// force表示强制执行
if((force !== true) && (this.isAnimating || this.collapsed)) return;
if(this.events['beforeCollapse'].fire(this) === false) return;
if(typeof animate != 'boolean') animate = this.config.animCollapse;
if(animate) {
var callback = function() {
this.collapsed = true;
D.addClass(this.getDom(), this.config.collapsedCls);
this.events['onCollapse'].fire(this);
this.isAnimating = false;
};
Ef.slideUp(this.body, { duration: this.config.animDuration, easing: this.config.animEasing, callback: callback, thisObj: this }, force);
} else {
D.setStyle(this.body, 'display', 'none');
this.collapsed = true;
D.addClass(this.getDom(), this.config.collapsedCls);
}
this.isAnimating = animate;
Unicorn.log(new Date().toLocaleTimeString() + ' collapse fired', 'info', 'Panel');
},
expand: function(animate, force) {
if((force !== true) && (this.isAnimating || !this.collapsed)) return;
if(this.events['beforeExpand'].fire(this) === false) return;
this.collapsed = false;
D.removeClass(this.getDom(), this.config.collapsedCls);
if(typeof animate != 'boolean') animate = this.config.animCollapse;
if(animate) {
var callback = function() {
this.events['onExpand'].fire(this);
this.isAnimating = false;
};
Ef.slideDown(this.body, { duration: this.config.animDuration, easing: this.config.animEasing, callback: callback, thisObj: this }, force);
} else {
D.setStyle(this.body, 'display', '');
}
this.isAnimating = animate;
Unicorn.log(new Date().toLocaleTimeString() + ' expand fired', 'info', 'Panel');
},
toggleCollapse: function(animate) {
this[this.collapsed ? 'expand' : 'collapse'](animate);
}
};
return Panel;
}();
// 增加decorate静态方法
Unicorn.widget.Panel.decorate = function(container, panelCfg) {
return new Unicorn.widget.Panel(container, panelCfg);
};
/**
* @class Unicorn.widget.PanelList
* creator: lifesinger@gmail.com
*/
/**
* members:
* this.container
* this.config, this.panelConfig
* this.panels
*/
Unicorn.widget.PanelList = function() {
var L = Unicorn.lang,
U = Unicorn.util,
D = U.Dom,
E = U.Event,
Ef = U.Effect,
helper = Unicorn.widget.WidgetHelper;
// 默认配置
var defConfig = {
container: null,
/**
* 1. recursive为false时,如果没指定panelCls,则获取所有子元素为panels;如果指定
* 了panelCls,则只获取class中有panelCls的子元素为panels
* 2. recursive为true时,必须指定panelCls,否则异常
*/
recursive: false, // 递归获取所有panels
panelCls: ''
};
var PanelList = function() {
var cfgs = helper.parseArguments(arguments);
// 获取默认配置
this.config = L.applyIf(cfgs[0] || {}, defConfig);
this.panelConfig = cfgs[1] || {};
// 初始化
init.call(this);
};
function init() {
var cfg = this.config;
this.container = D.get(cfg.container);
if(!this.container) return;
// init panels
var domPanels = [];
if(cfg.recursive && cfg.panelCls) {
domPanels = D.getElementsByClassName(cfg.panelCls, '*', this.container);
} else if(!cfg.recursive) {
domPanels = D.getChildrenBy(this.container, function(el) {
if(cfg.panelCls) {
return D.hasClass(el, cfg.panelCls);
} else {
// 只获取Element,对于其它node(如注释,style等),需要直接忽略
return (el.nodeType == 1 && ['STYLE', 'SCRIPT'].indexOf(el.nodeName) == -1);
}
});
} else {
throw new Error('The config of PanelList has something wrong.');
}
this.panels = [];
for(var i = 0, len = domPanels.length; i < len; ++i) {
this.panels.push(new Unicorn.widget.Panel(domPanels[i], this.panelConfig));
}
}
return PanelList;
}();
// 增加decorate静态方法
Unicorn.widget.PanelList.decorate = function(container, listCfg, panelCfg) {
return new Unicorn.widget.PanelList(container, listCfg, panelCfg);
};/**
* @class Unicorn.widget.FoldingList
* creator: lifesinger@gmail.com
*/
/**
* super: PanelList
* members:
* prototype: expandAll(), collapseAll()
*/
Unicorn.widget.FoldingList = function() {
var L = Unicorn.lang,
U = Unicorn.util,
D = U.Dom,
E = U.Event,
Ef = U.Effect,
helper = Unicorn.widget.WidgetHelper;
// FoldingList的默认配置
// 和PanelList相同的配置项请参考PanelList.source.js
var defConfig = {
multiExpand: true // true: 允许同时展开多个 false:同一时间只允许展开一个
};
var FoldingList = function() {
var cfgs = helper.parseArguments(arguments);
// 获取默认配置
var config = L.applyIf(cfgs[0] || {}, defConfig);
var panelConfig = cfgs[1] || {};
// 设置特定配置
panelConfig.collapsible = true; // 对于FoldingList来说,这个肯定是true,用户不能覆盖
// chain ctor
this.constructor.superclass.constructor.call(this, config, panelConfig);
// 初始化
init.call(this);
};
function init() {
if(!this.container) return;
// 处理单一展开
if(!this.config.multiExpand) {
processMultiExpand.call(this);
}
}
// private members
/*
* 处理单一展开
*/
function processMultiExpand() {
var panels = this.panels;
for(var i = 0, len = panels.length; i < len; ++i) {
var panel = panels[i];
panel.beforeExpand.subscribe(function() {
// 展开此panel前,先将其它panel收缩起来
for(var j = 0; j < len; ++j) {
// 只收缩同级的其它面板
var p = panels[j];
if(p.getDom().parentNode == this.getDom().parentNode
&& p != this && !p.collapsed) {
Unicorn.log('p.isAnimating = ' + p.isAnimating, 'info', 'FoldingList');
p.collapse(null, p.isAnimating ? true : false);
}
}
}, null, panel);
}
}
// end of private members
Unicorn.extend(FoldingList, Unicorn.widget.PanelList, {
expandAll: function() {
if(!this.config.multiExpand) return;
for(var i = 0, len = this.panels.length; i < len; ++i) {
this.panels.expand();
}
},
collapseAll: function() {
for(var i = 0, len = this.panels.length; i < len; ++i) {
this.panels.collapse();
}
}
});
return FoldingList;
}();
// 增加decorate静态方法
Unicorn.widget.FoldingList.decorate = function(container, listCfg, panelCfg) {
return new Unicorn.widget.FoldingList(container, listCfg, panelCfg);
};/**
* @class Unicorn.widget.TabView
* creator: lifesinger@gmail.com
*/
/**
* members:
* this.config
* this.container, this.triggers, this.panels
* this.panels[i].trigger, this.panels[i].hash
* this.events: onSwitch, beforeSwitch
* prototype: setActiveItem()
*/
Unicorn.widget.TabView = function() {
var L = Unicorn.lang,
U = Unicorn.util,
D = U.Dom,
E = U.Event,
helper = Unicorn.widget.WidgetHelper;
// 默认配置
var defConfig = {
container: null,
containerCls: 'tv-container',
navBarCls: 'tv-nav',
panelsWrapperCls: 'tv-wrapper',
eventType: 'click', // or 'mouse'
delay: 0.1,
disableTriggerClick: true, // 禁止触发器的点击事件
activeIndex: 0, // 为了避免闪烁,mackup的无js时默认激活的项,应该与此index一致
parseHash: false, // 从location.hash中解析出activeIndex. 为true时,配置项activeIndex无效
activeNavItemCls: 'current',
events: { }
};
var TabView = function() {
var cfgs = helper.parseArguments(arguments);
// 获取默认配置
this.config = L.applyIf(cfgs[0] || {}, defConfig);
init.call(this);
};
function init() {
var cfg = this.config;
this.container = D.get(cfg.container);
if(!this.container) return;
helper.addEvents.apply(this, ['onSwitch', 'beforeSwitch']);
this.parseMackup();
initTriggersAndPanels.call(this);
// 从location.hash中解析出activeIndex
if(cfg.parseHash && location.hash) {
var index = parseHash.call(this);
// 当需要激活时才激活,尽量懒
if(index != cfg.activeIndex) {
this.setActiveItem(index);
}
}
}
// private members
/**
* 初始化this.triggers和this.panels
*/
function initTriggersAndPanels() {
var cfg = this.config;
this.panels.forEach(function(panel, index) {
var trigger = this.triggers[index];
// init panel.trigger
trigger.setAttribute('rel', index);
// 添加触发器事件
attachTriggerEvent.call(this, trigger, index);
panel.trigger = trigger;
// init hash
if(cfg.parseHash) {
panel.hash = '#tab' + index;
}
// 阻止掉panel.trigger里面a的点击事件
if(cfg.disableTriggerClick === true) {
var anchor = D.getFirstChildBy(trigger, function(child) { return child.nodeName == 'A'; });
if(anchor) {
E.on(anchor, 'click', function(e) { E.preventDefault(e); });
}
}
}, this);
// 将非当前页隐藏掉
this.panels.forEach(function(panel) {
if(!D.hasClass(panel.trigger, cfg.activeNavItemCls)) {
D.setStyle(panel, 'display', 'none');
}
});
}
/**
* 给触发器添加事件
*/
function attachTriggerEvent(trigger, index) {
var timer, cfg = this.config;
if(this.config.eventType == 'mouse') {
E.on(trigger, 'mouseover', function() {
if(cfg.activeIndex == index) return; // 重复触发
timer = L.later(cfg.delay*1000, this, this.setActiveItem, index);
}, null, this);
E.on(trigger, 'mouseout', function() {
if(timer) timer.cancel();
});
} else { // 默认为'click'
E.on(trigger, 'click', function(e) {
if(cfg.activeIndex == index) return; // 重复触发
this.setActiveItem(index);
}, null, this);
}
}
/**
* 从location.hash中获取activeIndex
*/
function parseHash() {
for(var i = 0, len = this.panels.length; i < len; ++i) {
var hash = this.panels[i].hash;
if(hash == location.hash) {
return i;
}
}
}
function getHtmlElementsChildren(parent) {
return D.getChildrenBy(parent, function(el) {
// 只获取Element,对于其它node(如注释,style等),需要直接忽略
return (el.nodeType == 1 && ['STYLE', 'SCRIPT'].indexOf(el.nodeName) == -1);
});
}
// end of private members
TabView.prototype = {
/**
* private
* 从mackup中解析出this.triggers和this.panels
* 子类可以覆盖此方法,以适用不同的mackup
*/
parseMackup: function() {
// 解析mackup的顺序是:
// 1. 根据navBarCls和panelsWrapperCls来获取;
// 2. container下面包含navBar和panelsWrapper两个子元素,navBar可以
// 由navBarCls指定,或者自动获取第一个ul/ol子元素作为navBar.
// 原则:给与适当的灵活性
var cfg = this.config;
// 1. 根据Cls来获取
// 适用情况:container下面不仅包含navBar和panelsWrapper,还包含其它一些
// 元素(比如纯装饰性的透明层等)
var navBar = D.getFirstChildBy(this.container, function(child) {
return D.hasClass(child, cfg.navBarCls);
});
var panelsWrapper = D.getFirstChildBy(this.container, function(child) {
return D.hasClass(child, cfg.panelsWrapperCls);
});
// 上面的方法没获取全
if(!navBar || !panelsWrapper) {
// 2. container下面只包含两个子元素时
var elems = D.getChildren(this.container);
if(elems.length == 2) {
// 2.1 首先根据navBarCls来获取导航条
for(var i = 0; i < 2; ++i) {
if(D.hasClass(elems[i], this.config.navBarCls)) {
panelsWrapper = elems[1 - i];
navBar = elems[i];
break;
}
}
// 2.2 将子节点中的第一个ul/ol作为导航条
if(!navBar) {
for(var i = 0; i < 2; ++i) {
var nodeName = elems[i].nodeName.toUpperCase();
if(nodeName == 'UL' || nodeName == 'OL') {
panelsWrapper = elems[1 - i];
navBar = elems[i];
break;
}
}
}
}
}
// 初始化未成功,抛异常
if(!navBar || !panelsWrapper) {
throw new Error('Can not get navBar and panelsWrapper.');
}
// 成功解析,获取triggers和panels
this.panels = getHtmlElementsChildren(panelsWrapper);
this.triggers = getHtmlElementsChildren(navBar);
},
/**
* 设置激活项
*/
setActiveItem: function(index) {
//Unicorn.log('call setActiveItem of TabView', 'info', 'TabView');
var len = this.panels.length;
if(len < 1) return;
// 值非法时,默认选中第一项
if(!(index in this.panels)) index = 0;
var cfg = this.config;
var activePanel = this.panels[cfg.activeIndex]; // 当前激活项
var newActivePanel = this.panels[index]; // 即将激活的项
// 如果beforeSwitch返回false,终止进行
if(this.events['beforeSwitch'].fire(this) === false) {
return;
}
// 切换导航条上的样式
D.removeClass(activePanel.trigger, cfg.activeNavItemCls);
D.addClass(newActivePanel.trigger, cfg.activeNavItemCls);
// 更新内容
this.switchContent(activePanel, newActivePanel, index);
},
/**
* private
* 子类中可以覆盖此方法,以实现不同的切换效果
* 这里默认实现的是最简单的一种切换
*/
switchContent: function(activePanel, newActivePanel, index) {
var cfg = this.config;
// 最简单的隐藏/显示效果
D.setStyle(activePanel, 'display', 'none');
D.setStyle(newActivePanel, 'display', '');
// 更新activeIndex
cfg.activeIndex = index;
// 触发切换后事件
this.events['onSwitch'].fire(this);
// location响应变化,保证刷新时保留当前激活项
if(cfg.parseHash) {
var url = location.href.replace(location.hash, '');
location.replace(url + this.panels[index].hash);
}
}
};
return TabView;
}();
// 增加decorate静态方法
Unicorn.widget.TabView.decorate = function(container, config) {
return new Unicorn.widget.TabView(container, config);
};
/**
* @class Unicorn.widget.SlideView
* creator: lifesinger@gmail.com
* 想法:SlideView可以看作是TabView的变体,继承TabView,稍作变化即可
*/
/**
* super: TabView
* members:
* see TabView
* this.panelsWrapper (有些效果的实现需要panelsWrapper,故增加此属性)
* private members:
* this.effectType
* this.panelHeight, this.panelWidth
* this.slideAnim, this.autoPlayTimer, this.autoPlayIsPaused
*/
Unicorn.widget.SlideView = function() {
var L = Unicorn.lang,
U = Unicorn.util,
D = U.Dom,
E = U.Event,
helper = Unicorn.widget.WidgetHelper;
// SlideView的默认配置
// 和TabView相同的部分请参见TabView.source.js
var defConfig = {
containerCls: 'slide-container',
navBarCls: 'slide-nav',
panelsWrapperCls: 'slide-wrapper',
eventType: 'mouse', // 默认为'mouse'
autoPlay: true, // 是否自动播放
autoPlayInterval: 3, // 自动播放间隔时间,默认3秒
pauseOnMouseOver: true, // 当eventType为mouse时,当鼠标悬停在slide上时,是否暂停自动播放
effect: 'none', // 'scrollx', 'scrolly', 'fade', 'custom'
animDuration: 0.5, // 开启切换效果时,切换的时长
animEasing: Unicorn.util.Easing.easeOutStrong, // easing method
animAttributes: {}, // effect为'custom'时,通过animAttributes设定自定义动画属性 // TODO
panelSize: {} // 卡盘panel的宽高。一般不需要设定此值。
// 只有当无法正确获取高宽时(比如TabView的隐藏Tab中含有SlideView时),才需要设定
// 因为父级元素中有display: none时,无法获取到offsetWidth, offsetHeight
};
var SlideView = function() {
var cfgs = helper.parseArguments(arguments);
// 获取默认配置
var config = L.applyIf(cfgs[0] || {}, defConfig);
// chain ctor
this.constructor.superclass.constructor.call(this, config);
init.call(this);
};
function init() {
if(!this.container) return;
var cfg = this.config;
// 获取panelsWrapper
this.panelsWrapper = this.panels[0].parentNode; // 对于SlideView来说,mackup是固定模式的
// 根据effectType,调整初始状态
adjustStyle.call(this);
// 处理pauseOnMouseOver
if(cfg.eventType == 'mouse' && cfg.autoPlay && cfg.pauseOnMouseOver) {
pauseOnMouseOver.call(this);
}
// 设置自动播放
if(cfg.autoPlay) {
setAutoPlay.call(this);
}
}
// private members
/**
* 根据effectType,调整初始状态
*/
function adjustStyle() {
var cfg = this.config;
this.panelHeight = cfg.panelSize.height || this.panels[0].offsetHeight; // 所有panel的尺寸应该相同
this.panelWidth = cfg.panelSize.width || this.panels[0].offsetWidth;
// 最好指定第一张图片的width和height,因为Safari下,图片未加载时,读取的offsetHeight等值会不对
//Unicorn.log('panelHeight = ' + this.panelHeight, 'info', 'SlideView');
//Unicorn.log('panelWidth = ' + this.panelWidth, 'info', 'SlideView');
this.effectType = cfg.effect.toLowerCase();
if(['scrolly', 'scrollx'].indexOf(this.effectType) > -1) {
// 这些特效需要将panels都显示出来
this.panels.forEach(function(panel) { D.setStyle(panel, 'display', ''); });
// 设置最大高宽,以保证最后不会出现空白,以及float:left时,能水平排布
// 这里按理说应该交给css处理,但为了避免更换图片时,修改的麻烦,所以保留这里的自动设置还是有必要的
if(this.effectType == 'scrollx') {
D.setStyle(this.panelsWrapper, 'width', this.panelWidth * this.panels.length + 'px');
} else {
D.setStyle(this.panelsWrapper, 'height', this.panelHeight * this.panels.length + 'px');
}
} else if(this.effectType == 'fade') {
// 让所有panel初始时都处于透明状态
this.panels.forEach(function(panel) {
D.setStyle(panel, 'display', '');
setOpacity(panel, 0);
});
// 显示激活的panel
setOpacity(this.panels[cfg.activeIndex], 1);
}
}
/**
* 鼠标悬停时,停止自动播放
*/
function pauseOnMouseOver() {
navBar = this.triggers[0].parentNode; // navBar仅这里用到,就不弄个this.navBar了,遵从属性最少原理
E.on([this.panelsWrapper, navBar], 'mouseover', function() {
this.autoPlayIsPaused = true;
}, null, this);
E.on([this.panelsWrapper, navBar], 'mouseout', function() {
this.autoPlayIsPaused = false;
}, null, this);
}
/**
* 处理自动播放
*/
function setAutoPlay() {
var cfg = this.config;
var fn = function() {
var n = cfg.activeIndex;
n = (n < this.panels.length - 1) ? n + 1 : 0;
this.setActiveItem(n, true);
};
this.autoPlayTimer = L.later(cfg.autoPlayInterval * 1000, this, fn, null, true);
}
/**
* 设置Panel的透明度,val只能为0或1
*/
function setOpacity(panel, val) {
D.setStyle(panel, 'opacity', val);
D.setStyle(panel, 'filter', 'alpha(opacity=' + val * 100 + ')');
}
/**
* 动画完成时的事件
*/
function animCompleteHandler(activePanel, newActivePanel, index) {
// 触发切换后事件
this.events['onSwitch'].fire(activePanel, newActivePanel);
// free
this.slideAnim = null;
}
// end of private members
Unicorn.extend(SlideView, Unicorn.widget.TabView, {
/**
* 激活指定的Slide
*/
setActiveItem: function(index, auto) {
if(auto) { // 自动播放碰到暂停或动画尚未结束时,推迟到下一次
if(this.autoPlayIsPaused) return; // 暂停状态
if(this.slideAnim && this.slideAnim.isAnimated()) return; // 动画尚未结束
} else { // 用户触发的事件
if(this.slideAnim && this.slideAnim.isAnimated()) { // 立刻停止当前动画
this.slideAnim.stop();
}
}
//Unicorn.log('call setActiveItem of SlideView', 'info', 'SlideView');
this.constructor.superclass.setActiveItem.call(this, index);
},
// private
switchContent: function(activePanel, newActivePanel, index) {
var cfg = this.config;
if(['scrolly', 'scrollx'].indexOf(this.effectType) > -1) { // 垂直/水平滚动效果
var isX = (this.effectType == 'scrollx');
var diff = (isX ? this.panelWidth : this.panelHeight) * index;
var attributes = { };
attributes[isX ? 'left' : 'top'] = { to: -diff };
this.slideAnim = new U.Anim(this.panelsWrapper, attributes, cfg.animDuration, cfg.animEasing);
this.slideAnim.onComplete.subscribe(function(){
animCompleteHandler.call(this, activePanel, newActivePanel, index);
}, this, true);
// 更新activeIndex
cfg.activeIndex = index;
this.slideAnim.animate();
} else if(this.effectType == 'fade') { // 淡隐淡出效果
// 设置z-index
D.setStyle(newActivePanel, 'z-index', '8');
D.setStyle(activePanel, 'z-index', '9');
// 首先显示下一张
setOpacity(newActivePanel, 1);
this.slideAnim = new U.Anim(activePanel, { opacity: { to: 0 } }, cfg.animDuration, cfg.animEasing);
this.slideAnim.onComplete.subscribe(function(){
animCompleteHandler.call(this, activePanel, newActivePanel, index);
}, this, true);
// 更新activeIndex
cfg.activeIndex = index;
this.slideAnim.animate();
} else { // 最普通的显示/隐藏效果
this.constructor.superclass.switchContent.call(this, activePanel, newActivePanel, index);
}
}
});
return SlideView;
}();
// 增加decorate静态方法
Unicorn.widget.SlideView.decorate = function(container, config) {
return new Unicorn.widget.SlideView(container, config);
};
/**
* @class Unicorn.widget.LiquidView
* creator: lifesinger@gmail.com
* 想法:LiquidView可以看作是TabView的变体,继承TabView,稍作变化即可
*/
/**
* super: TabView
* members:
* see TabView
* private members:
* this.effectType
* this.effectAnims
*/
Unicorn.widget.LiquidView = function() {
var L = Unicorn.lang,
U = Unicorn.util,
D = U.Dom,
E = U.Event,
helper = Unicorn.widget.WidgetHelper;
// LiquidView的默认配置
// 和TabView相同的部分请参见TabView.source.js
var defConfig = {
eventType: 'mouse', // 默认为'mouse',且只能为'mouse',用户不能更改
disableTriggerClick: false, // 默认可以点击,因为一般都是一个图片链接
resetOnMouseOut: true, // 鼠标移出container时,是否还原到初始状态
activeIndex: -1, // 默认没有激活任何panels,取-1
effect: 'scrollx', // 'scrollx', 'scrolly'
animDuration: 0.5, // 开启切换效果时,切换的时长
animEasing: Unicorn.util.Easing.easeOutStrong,
// scrollx
maxWdith: '', // px
minWidth: '',
normalWidth: '',
// scrolly
maxHeight: '', // px
minHeight: '',
noramlHeight: ''
};
var LiquidView = function() {
var cfgs = helper.parseArguments(arguments);
// 获取默认配置
var config = L.applyIf(cfgs[0] || {}, defConfig);
// 设置特定属性
config.eventType = 'mouse';
// chain ctor
this.constructor.superclass.constructor.call(this, config);
init.call(this);
};
function init() {
if(!this.container) return;
var cfg = this.config;
this.effectType = cfg.effect.toLowerCase();
this.effectAnims = [];
// 取消父类里对panel的隐藏
this.panels.forEach(function(panel) { D.setStyle(panel, 'display', ''); });
// 添加重置事件
if(cfg.resetOnMouseOut === true) {
E.on(this.container, 'mouseout', function(e) {
// 移动到内部元素上时,也会触发container的mouseout
// 这显然不是我们想要的,应该检测出来不执行
if(D.isAncestor(this.container, E.getRelatedTarget(e))) return;
// 快速mouse over out时,因为触发有delay,所以over时可能还未触发动画,这时
// mouse out时,无需reset
if(cfg.activeIndex != -1) {
//Unicorn.log('this.setActiveItem(-1) fired.', 'info', 'LiquidView');
this.setActiveItem(-1);
}
}, this, true);
}
}
// private members
/**
* 执行动画
* @param index 表示当前要展开的panel. 当index为-1时,表示reset
*/
function doAnimate(index) {
var cfg = this.config;
if(['scrolly', 'scrollx'].indexOf(this.effectType) > -1) { // 垂直/水平滚动效果
var isX = (this.effectType == 'scrollx');
var prop = isX ? 'width' : 'height';
var toMax = {};
var toMin = {};
var toNormal = {};
toMax[prop] = { to: (isX ? cfg.maxWidth : cfg.maxHeight) };
toMin[prop] = { to: (isX ? cfg.minWidth : cfg.minHeight) };
toNormal[prop] = { to: (isX ? cfg.normalWidth : cfg.normalHeight) };
this.panels.forEach(function(panel, j) {
var attributes = toMin;
if(index == -1) attributes = toNormal;
else if(index == j) attributes = toMax;
if(index != -1 && cfg.activeIndex > -1) { // 已经有一个处于展开状态时
if(j != cfg.activeIndex && j != index) { // 仅需要两个panel执行动画即可
return;
}
}
this.effectAnims[j] = new U.Anim(panel, attributes, cfg.animDuration, cfg.animEasing);
this.effectAnims[j].onComplete.subscribe(function(){
// 触发切换后事件
this.events['onSwitch'].fire(this);
// free
this.effectAnims[j] = null;
}, this, true);
}, this);
// 更新activeIndex
cfg.activeIndex = index;
this.effectAnims.forEach(function(anim) { if(anim) anim.animate(); });
} else { // 无动画效果
throw new Error('Do not support this effectType: ' + this.effectType);
}
}
// end of private members
Unicorn.extend(LiquidView, Unicorn.widget.TabView, {
/**
* private
* 从mackup中解析出this.triggers和this.panels
*/
parseMackup: function() {
// triggers是container下面的子元素
this.triggers = D.getChildren(this.container);
// panel是trigger下面的子元素
this.panels = [];
this.triggers.forEach(function(trigger) {
this.panels.push(D.getFirstChild(trigger));
}, this);
},
/**
* 激活指定项
*/
setActiveItem: function(index) {
if(this.effectAnims) {
for(var i = 0, len = this.effectAnims.length; i < len; ++i) {
var anim = this.effectAnims[i];
if(anim && anim.isAnimated()) {
anim.stop(true); // 注意参数,一定要结束时跳到最后一帧,否则容易错位
}
}
}
// 如果beforeSwitch返回false,终止进行
if(this.events['beforeSwitch'].fire(this) === false) return;
//Unicorn.log('call setActiveItem of LiquidView', 'info', 'LiquidView');
var cfg = this.config;
var activePanel = this.panels[cfg.activeIndex]; // 当前激活项
var newActivePanel = this.panels[index]; // 即将激活的项
// 切换导航条上的样式
if(activePanel) D.removeClass(activePanel.trigger, cfg.activeNavItemCls);
if(newActivePanel) D.addClass(newActivePanel.trigger, cfg.activeNavItemCls);
// 执行动画
doAnimate.call(this, index);
}
});
return LiquidView;
}();
// 增加decorate静态方法
Unicorn.widget.LiquidView.decorate = function(container, config) {
return new Unicorn.widget.LiquidView(container, config);
};
/**
* for TAOBAO
*/
// alias
TAOBAO = Unicorn;