/*! * 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;